Better handling of some qt and gtk applications. Used Audacious for testing.

This commit is contained in:
Storm Dragon committed 2026-11-01 21:37:01 -05:00
1 parent 300f4a7504
commit bbe931335f
15 files changed
+507 -16

No files matched your search

+17 -3
View File
@@ -65,7 +65,7 @@ gchar *cthulhu_table_context(AtspiAccessible *source, gint *rows, gint *columns,
AtspiAccessible *tableAccessible = NULL;
gint row = -1;
gint column = -1;
if (atspi_accessible_get_role(source, NULL) == ATSPI_ROLE_TABLE) {
if (cthulhu_is_table_container(source)) {
tableAccessible = g_object_ref(source);
} else if (atspi_accessible_get_role(source, NULL) == ATSPI_ROLE_TABLE_CELL) {
AtspiTableCell *cell = atspi_accessible_get_table_cell(source);
@@ -76,8 +76,7 @@ gchar *cthulhu_table_context(AtspiAccessible *source, gint *rows, gint *columns,
}
if (tableAccessible == NULL) {
AtspiAccessible *parent = atspi_accessible_get_parent(source, NULL);
if (parent != NULL && (atspi_accessible_get_role(parent, NULL) == ATSPI_ROLE_TABLE ||
cthulhu_is_gtk_list_table(parent))) {
if (parent != NULL && cthulhu_is_table_container(parent)) {
tableAccessible = parent;
gint index = atspi_accessible_get_index_in_parent(source, NULL);
AtspiTable *parentTable = atspi_accessible_get_table_iface(parent);
@@ -125,6 +124,21 @@ gchar *cthulhu_table_context(AtspiAccessible *source, gint *rows, gint *columns,
for (gint column = 0; column < *columns; column++) {
AtspiAccessible *item = atspi_table_get_accessible_at(table, row, column, NULL);
if (item == NULL) continue;
// Qt keeps hidden columns in the Table interface. As in Orca's
// get_showing_cells_in_same_row, GUI full-row reading includes
// only showing cells. Document/spreadsheet capture keeps its
// existing policy. Query fresh state after column visibility changes.
if (*guiTable) {
atspi_accessible_clear_cache_single(item);
AtspiStateSet *states = atspi_accessible_get_state_set(item);
gboolean showing = states != NULL &&
atspi_state_set_contains(states, ATSPI_STATE_SHOWING);
if (states != NULL) g_object_unref(states);
if (!showing) {
g_object_unref(item);
continue;
}
}
gchar *name = atspi_accessible_get_name(item, NULL);
if ((name == NULL || *name == '\0') && remaining != 0 &&
atspi_accessible_get_role(item, NULL) == ATSPI_ROLE_TABLE_CELL) {
@@ -52,13 +52,13 @@ func selectNativeEventTarget(event *C.AtspiEvent, eventType string) (*C.AtspiAcc
// A table's first selected child is its active child only when the
// selection is unique. Multi-selection changes use their own events.
targetRole := C.atspi_accessible_get_role(target, nil)
listTable := targetRole == C.ATSPI_ROLE_LIST && C.cthulhu_is_gtk_list_table(target) != 0
tableContainer := C.cthulhu_is_table_container(target) != 0
// GTK notebooks report container focus before the active tab when an
// X11 key grab ends. Resolve that container to its selected tab, as
// with menus, so ordinary duplicate-focus handling preserves review.
if targetRole == C.ATSPI_ROLE_MENU || targetRole == C.ATSPI_ROLE_PAGE_TAB_LIST || targetRole == C.ATSPI_ROLE_TABLE || listTable {
if targetRole == C.ATSPI_ROLE_MENU || targetRole == C.ATSPI_ROLE_PAGE_TAB_LIST || tableContainer {
var child *C.AtspiAccessible
if targetRole == C.ATSPI_ROLE_TABLE || listTable {
if tableContainer {
child = C.cthulhu_table_selected_child(target)
} else {
child = C.cthulhu_selected_child(target)
@@ -93,7 +93,7 @@ func selectNativeEventTarget(event *C.AtspiEvent, eventType string) (*C.AtspiAcc
// child list. Tab creation/removal makes that list stale.
C.atspi_accessible_clear_cache_single(event.source)
}
if C.atspi_accessible_get_role(event.source, nil) == C.ATSPI_ROLE_TABLE || C.cthulhu_is_gtk_list_table(event.source) != 0 {
if C.cthulhu_is_table_container(event.source) != 0 {
target = C.cthulhu_table_selected_child(event.source)
} else {
target = C.cthulhu_selected_child(event.source)
@@ -433,6 +433,13 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) {
adapter.forgetWindowDocuments(originID)
}
invalidated := adapter.applyTreeInvalidation(eventType, event)
returnedMenuItem := adapter.returned_menu_item(event, eventType, originID)
if returnedMenuItem != nil {
defer C.g_object_unref(C.gpointer(returnedMenuItem))
// Keep the original showing event and popup origin in diagnostics.
// Only fresh focus on its parent supplies this normalized focus event.
eventType, invalidated = FocusEvent, false
}
if selection := adapter.window_switcher_event(event, eventType); selection != nil {
adapter.enqueueSecureEvent(*selection, secureEpoch)
return
@@ -500,8 +507,12 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) {
return
}
snapshotStarted := time.Now()
target := adapter.libreOfficeFocusTarget(event, eventType)
ownedTarget := target != nil
target := returnedMenuItem
ownedTarget := false
if target == nil {
target = adapter.libreOfficeFocusTarget(event, eventType)
ownedTarget = target != nil
}
if target == nil {
target, ownedTarget = selectNativeEventTarget(event, eventType)
}
+49 -3
View File
@@ -157,20 +157,66 @@ gboolean cthulhu_is_gtk_list_table(AtspiAccessible *source) {
return matches;
}
// Qt item views use TREE even for flat, multi-column playlists. Require the
// Table interface before applying row-selection policy to a tree.
gboolean cthulhu_is_table_container(AtspiAccessible *source) {
AtspiRole role = atspi_accessible_get_role(source, NULL);
if (role == ATSPI_ROLE_TABLE) return TRUE;
if (role == ATSPI_ROLE_LIST) return cthulhu_is_gtk_list_table(source);
if (role != ATSPI_ROLE_TREE && role != ATSPI_ROLE_TREE_TABLE) return FALSE;
AtspiTable *table = atspi_accessible_get_table_iface(source);
if (table == NULL) return FALSE;
g_object_unref(table);
return TRUE;
}
// Selection counts cells, so a single GTK row with two columns already has
// two selected children. Query the table's selected rows before treating its
// first selected cell as the focus target.
AtspiAccessible *cthulhu_table_selected_child(AtspiAccessible *source) {
// A menu/dialog can return focus without refreshing the cached child list.
atspi_accessible_clear_cache_single(source);
AtspiTable *table = atspi_accessible_get_table_iface(source);
if (table == NULL) return cthulhu_selected_child(source);
GError *error = NULL;
gint count = atspi_table_get_n_selected_rows(table, &error);
g_object_unref(table);
gboolean ambiguous = error == NULL && count > 1;
if (error != NULL) {
g_error_free(error);
g_clear_error(&error);
}
return ambiguous ? NULL : cthulhu_selected_child(source);
AtspiAccessible *child = ambiguous ? NULL : cthulhu_selected_child(source);
// Qt selects every cell in a row, including an unnamed status column.
// Prefer the provider's focused cell in that same uniquely selected row
// so returning from a menu restores a useful Braille/review target too.
// Keep the existing target when no such evidence is available; never
// search other rows or guess a focus target in a multiple selection.
gint columns = atspi_table_get_n_columns(table, NULL);
if (!ambiguous && count == 1 && columns > 0 && columns <= 32) {
GArray *rows = atspi_table_get_selected_rows(table, NULL);
if (rows != NULL && rows->len == 1) {
gint row = g_array_index(rows, gint, 0);
for (gint column = 0; column < columns; column++) {
AtspiAccessible *item = atspi_table_get_accessible_at(table, row, column, NULL);
if (item == NULL) continue;
atspi_accessible_clear_cache_single(item);
AtspiStateSet *states = atspi_accessible_get_state_set(item);
gboolean focused = states != NULL &&
atspi_state_set_contains(states, ATSPI_STATE_FOCUSED) &&
atspi_state_set_contains(states, ATSPI_STATE_SHOWING) &&
!atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
if (states != NULL) g_object_unref(states);
if (focused) {
if (child != NULL) g_object_unref(child);
child = item;
break;
}
g_object_unref(item);
}
}
if (rows != NULL) g_array_unref(rows);
}
g_object_unref(table);
return child;
}
// Resolve the focused descendant at a window-activation boundary. Providers
+27
View File
@@ -10,6 +10,33 @@
static const gchar *menu_checkability_key = "cthulhu-menu-checkability";
enum { CHECKABILITY_INFERRED = 1, CHECKABILITY_EXPLICIT = 2 };
// Qt can hide a submenu on Left/Escape without emitting focus-gained for
// its parent action. Require fresh provider focus and visibility on that
// exact parent; closing an entire menu or switching away grants no focus.
AtspiAccessible *cthulhu_returned_menu_item(AtspiAccessible *menu) {
atspi_accessible_clear_cache_single(menu);
AtspiStateSet *states = atspi_accessible_get_state_set(menu);
gboolean hidden = states != NULL &&
!atspi_state_set_contains(states, ATSPI_STATE_SHOWING) &&
!atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
if (states != NULL) g_object_unref(states);
if (!hidden) return NULL;
AtspiAccessible *parent = atspi_accessible_get_parent(menu, NULL);
if (parent == NULL) return NULL;
atspi_accessible_clear_cache_single(parent);
AtspiRole role = atspi_accessible_get_role(parent, NULL);
states = atspi_accessible_get_state_set(parent);
gboolean focused = states != NULL &&
atspi_state_set_contains(states, ATSPI_STATE_FOCUSED) &&
atspi_state_set_contains(states, ATSPI_STATE_SHOWING) &&
!atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
if (states != NULL) g_object_unref(states);
if (focused && (role == ATSPI_ROLE_MENU_ITEM || role == ATSPI_ROLE_CHECK_MENU_ITEM ||
role == ATSPI_ROLE_RADIO_MENU_ITEM)) return parent;
g_object_unref(parent);
return NULL;
}
void cthulhu_forget_menu_checkability(AtspiAccessible *source) {
if (source != NULL)
g_object_set_data(G_OBJECT(source), menu_checkability_key, NULL);
@@ -0,0 +1,39 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
/*
#include "adapter_native.h"
*/
import "C"
// A hiding submenu can return focus only from its most recently focused
// direct item. Queries and the returned reference belong to the GLib owner;
// the callback releases the reference after the ordinary foreground gates.
func (adapter *Adapter) returned_menu_item(event *C.AtspiEvent, eventType string, origin ObjectID) *C.AtspiAccessible {
if eventType != ShowingStateEvent || event.detail1 != 0 || len(adapter.nativeFocus) == 0 {
return nil
}
role := C.atspi_accessible_get_role(event.source, nil)
if role != C.ATSPI_ROLE_MENU && role != C.ATSPI_ROLE_POPUP_MENU {
return nil
}
current := adapter.nativeFocus[len(adapter.nativeFocus)-1].source
switch C.atspi_accessible_get_role(current, nil) {
case C.ATSPI_ROLE_MENU_ITEM, C.ATSPI_ROLE_CHECK_MENU_ITEM, C.ATSPI_ROLE_RADIO_MENU_ITEM:
default:
return nil
}
parent := C.atspi_accessible_get_parent(current, nil)
if parent == nil {
return nil
}
match := nativeObjectID(parent) == origin
C.g_object_unref(C.gpointer(parent))
if !match {
return nil
}
return C.cthulhu_returned_menu_item(event.source)
}
+2
View File
@@ -123,6 +123,8 @@ AtspiAccessible *cthulhu_event_active_descendant(AtspiEvent *event);
AtspiAccessible *cthulhu_selected_child(AtspiAccessible *source);
AtspiAccessible *cthulhu_table_selected_child(AtspiAccessible *source);
gboolean cthulhu_is_gtk_list_table(AtspiAccessible *source);
gboolean cthulhu_is_table_container(AtspiAccessible *source);
AtspiAccessible *cthulhu_returned_menu_item(AtspiAccessible *menu);
void cthulhu_refresh_frame_children(AtspiAccessible *window);
AtspiAccessible *cthulhu_focused_descendant(AtspiAccessible *window);
AtspiAccessible *cthulhu_showing_web_document(AtspiAccessible *window);