// 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" // addedSelectedBrowserTab handles providers which emit selection before the new // tab enters their accessible child list. The later child-added event supplies // the actual target; only a freshly selected browser chrome tab qualifies. // The caller releases the retained target on the GLib owner. func addedSelectedBrowserTab(event *C.AtspiEvent, eventType string) *C.AtspiAccessible { if eventType != ChildrenAddedEvent || C.atspi_accessible_get_role(event.source, nil) != C.ATSPI_ROLE_PAGE_TAB_LIST { return nil } child := C.cthulhu_event_active_descendant(event) if child == nil || C.atspi_accessible_get_role(child, nil) != C.ATSPI_ROLE_PAGE_TAB { return nil } parent := C.atspi_accessible_get_parent(child, nil) if parent == nil { return nil } matchesParent := nativeObjectID(parent) == nativeObjectID(event.source) C.g_object_unref(C.gpointer(parent)) if !matchesParent { return nil } C.atspi_accessible_clear_cache_single(child) states := C.atspi_accessible_get_state_set(child) if states == nil { return nil } selected := C.atspi_state_set_contains(states, C.ATSPI_STATE_SELECTED) != 0 && C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) == 0 C.g_object_unref(C.gpointer(states)) if !selected { return nil } toolkit := C.atspi_accessible_get_toolkit_name(child, nil) snapshot := Snapshot{Role: "page tab", ToolkitName: C.GoString(toolkit)} C.g_free(C.gpointer(toolkit)) document := C.cthulhu_document_ancestor(child) if document != nil { C.g_object_unref(C.gpointer(document)) return nil } if !isBrowserChromeTab(snapshot, false) { return nil } C.g_object_ref(C.gpointer(child)) return child } // resolveNativeEventTarget runs on the GLib owner. The override target is // borrowed from the callback; LibreOffice and selected descendants can return // owned references. The caller releases an owned target after event capture. func (adapter *Adapter) resolveNativeEventTarget(event *C.AtspiEvent, eventType string, overrideTarget *C.AtspiAccessible) (*C.AtspiAccessible, bool) { target := overrideTarget ownedTarget := false if target == nil { target = adapter.libreOfficeFocusTarget(event, eventType) ownedTarget = target != nil } if target == nil { target, ownedTarget = selectNativeEventTarget(event, eventType) } return target, ownedTarget } // Refresh only the established provider caches, in query order, before capture. // Both accessibles are borrowed and stay on the GLib owner thread. func refreshNativeEventTarget(event *C.AtspiEvent, target *C.AtspiAccessible, eventType string) { if (eventType == FocusEvent || eventType == FocusedStateEvent) && C.atspi_accessible_get_role(target, nil) == C.ATSPI_ROLE_MENU_ITEM { // Qt menus can change their checked state without a state-change // event. Reopening the menu must capture the current provider state. C.atspi_accessible_clear_cache_single(target) } if (eventType == SelectedStateEvent || eventType == SelectionChangedEvent) && C.atspi_accessible_get_role(target, nil) == C.ATSPI_ROLE_PAGE_TAB { // Firefox can emit selected-state events from the previous tab; // query the current state/name before using either event path. C.atspi_accessible_clear_cache_single(target) } if eventType == WindowDeactivatedEvent { // Match Python's key-grab churn check against fresh window state rather // than an AT-SPI cache entry from before the deactivation event. C.atspi_accessible_clear_cache_single(event.source) } } // captureNativeEventContent borrows event/target and owns all payload buffers. // A completed atomic live region replaces the event payload before copying it. // No native references escape this helper; all calls remain on the GLib owner. func (adapter *Adapter) captureNativeEventContent(event *C.AtspiEvent, target *C.AtspiAccessible, eventType string, originID ObjectID, live nativeLiveProperties, completeLive bool) (Snapshot, ObjectID, string) { payload := C.cthulhu_event_payload(event) defer func() { C.g_free(C.gpointer(payload)) }() snapshot, popupFor := adapter.captureNativeEventSnapshot(event, target, eventType, originID, live) if completeLive { text := C.cthulhu_live_content(event.source) snapshot.Text = sanitizeSnapshotField(C.GoString(text)) snapshot.LiveAtomic = true C.g_free(C.gpointer(payload)) payload = text } return snapshot, popupFor, C.GoString(payload) } // normalizeNativeMenuFocus applies provider evidence to this event's snapshot. // It borrows target and releases the temporary inline-menu reference locally. func normalizeNativeMenuFocus(event *C.AtspiEvent, target *C.AtspiAccessible, eventType string, originID ObjectID, snapshot *Snapshot) (string, ObjectID) { // Shared Qt actions can report focus through their original, hidden menu // even while another menu emits focus-gained for that very accessible. // The explicit event establishes focus; the queried state may disagree. // Do not transfer this evidence to a redirected descendant or a dead item. if snapshot.ID() == originID && !snapshot.Defunct && (eventType == FocusEvent || eventType == FocusedStateEvent && C.cthulhu_event_detail1(event) == 1) { switch snapshot.Role { case "menu item", "check menu item", "radio menu item": snapshot.Focused = true } } var inlineMenuID ObjectID if eventType == SelectedStateEvent && C.cthulhu_event_detail1(event) == 1 && !snapshot.Defunct && (snapshot.Role == "button" || snapshot.Role == "push button" || snapshot.Role == "toggle button" || snapshot.Role == "menu item" || snapshot.Role == "check menu item" || snapshot.Role == "radio menu item") { if menu := C.cthulhu_inline_menu_container(target); menu != nil { inlineMenuID = nativeObjectID(menu) C.g_object_unref(C.gpointer(menu)) if inlineMenuID.Valid() && inlineMenuID.BusName == snapshot.BusName { // The event's detail is authoritative even when libatspi still // caches the previous SELECTED state on the first visit. eventType, snapshot.Selected = ActiveDescendantEvent, true } } } return eventType, inlineMenuID }