Files
cthulhu/internal/atspi/adapter_event_queue.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;
}
}