91 lines
4.1 KiB
C
91 lines
4.1 KiB
C
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "adapter_native.h"
|
|
|
|
extern void cthulhuAtspiSelectionCallback(AtspiEvent *event, void *userData, uint64_t epoch);
|
|
|
|
// This check uses only libatspi's cache. Unknown metadata retains the normal
|
|
// query path; a stale non-FOCUSED state alone never rejects an explicit claim.
|
|
gboolean cthulhu_accessible_known_dead(AtspiAccessible *source) {
|
|
if (source == NULL) return TRUE;
|
|
if (source->states != NULL && atspi_state_set_contains(source->states, ATSPI_STATE_DEFUNCT)) return TRUE;
|
|
return (source->cached_properties & ATSPI_CACHE_ROLE) && source->role == ATSPI_ROLE_INVALID;
|
|
}
|
|
|
|
gboolean cthulhu_gtk_table(AtspiAccessible *source, gboolean queryToolkit) {
|
|
if (source == NULL || !(source->cached_properties & ATSPI_CACHE_ROLE)) return FALSE;
|
|
if (source->role != ATSPI_ROLE_TABLE && source->role != ATSPI_ROLE_TREE_TABLE &&
|
|
source->role != ATSPI_ROLE_LIST && source->role != ATSPI_ROLE_TREE) return FALSE;
|
|
AtspiApplication *application = ATSPI_OBJECT(source)->app;
|
|
// libatspi caches toolkit identity per application. Seed that cache on
|
|
// the first table selection if focus capture has not needed it yet.
|
|
if (queryToolkit && application != NULL && application->toolkit_name == NULL) {
|
|
gchar *toolkit = atspi_accessible_get_toolkit_name(source, NULL);
|
|
g_free(toolkit);
|
|
}
|
|
if (application == NULL || g_ascii_strcasecmp(application->toolkit_name != NULL ? application->toolkit_name : "", "gtk") != 0) return FALSE;
|
|
if (source->role == ATSPI_ROLE_TABLE || source->role == ATSPI_ROLE_TREE_TABLE) return TRUE;
|
|
if ((source->role != ATSPI_ROLE_LIST && source->role != ATSPI_ROLE_TREE) ||
|
|
!(source->cached_properties & ATSPI_CACHE_INTERFACES)) return FALSE;
|
|
AtspiTable *table = atspi_accessible_get_table_iface(source);
|
|
if (table == NULL) return FALSE;
|
|
g_object_unref(table);
|
|
return TRUE;
|
|
}
|
|
|
|
// All queue state and boxed events belong exclusively to the GLib owner.
|
|
// Only one table can be pending. A different table or another live event
|
|
// flushes it first, preserving ordering across focus/lifecycle boundaries.
|
|
void cthulhu_flush_selection(cthulhu_callback_data *data) {
|
|
if (data->selectionIdle != 0) {
|
|
g_source_remove(data->selectionIdle);
|
|
data->selectionIdle = 0;
|
|
}
|
|
AtspiEvent *event = data->pendingSelection;
|
|
uint64_t epoch = data->selectionEpoch;
|
|
data->pendingSelection = NULL;
|
|
if (event == NULL) return;
|
|
cthulhuAtspiSelectionCallback(event, data, epoch);
|
|
g_boxed_free(ATSPI_TYPE_EVENT, event);
|
|
}
|
|
|
|
void cthulhu_flush_other_selection(cthulhu_callback_data *data, AtspiAccessible *source) {
|
|
if (data->pendingSelection != NULL && data->pendingSelection->source != source) {
|
|
cthulhu_flush_selection(data);
|
|
}
|
|
}
|
|
|
|
static gboolean cthulhu_selection_idle(gpointer userData) {
|
|
cthulhu_callback_data *data = userData;
|
|
data->selectionIdle = 0;
|
|
cthulhu_flush_selection(data);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
void cthulhu_defer_selection(cthulhu_callback_data *data, AtspiEvent *event, uint64_t epoch) {
|
|
cthulhu_flush_other_selection(data, event->source);
|
|
if (data->pendingSelection != NULL) g_boxed_free(ATSPI_TYPE_EVENT, data->pendingSelection);
|
|
data->pendingSelection = g_boxed_copy(ATSPI_TYPE_EVENT, event);
|
|
data->selectionEpoch = epoch;
|
|
if (data->selectionIdle == 0) {
|
|
// No timer/debounce delay: read the latest selection when the ready
|
|
// provider signals have drained. Ordinary arrows still flush on their
|
|
// following live focus/descendant event, or at the next idle turn.
|
|
data->selectionIdle = g_idle_add(cthulhu_selection_idle, data);
|
|
}
|
|
}
|
|
|
|
void cthulhu_clear_selection(cthulhu_callback_data *data) {
|
|
if (data == NULL) return;
|
|
if (data->selectionIdle != 0) {
|
|
g_source_remove(data->selectionIdle);
|
|
data->selectionIdle = 0;
|
|
}
|
|
if (data->pendingSelection != NULL) {
|
|
g_boxed_free(ATSPI_TYPE_EVENT, data->pendingSelection);
|
|
data->pendingSelection = NULL;
|
|
}
|
|
}
|