// 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; } }