Files
cthulhu/internal/atspi/adapter_event_target_native.go
2026-11-01 22:01:32 -05:00

97 lines
4.5 KiB
Go

// 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"
// resolveNativeEventTarget runs on the GLib owner. The returned menu item 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, returnedMenuItem *C.AtspiAccessible) (*C.AtspiAccessible, bool) {
target := returnedMenuItem
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
}