// 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" import ( "net/url" "runtime/cgo" "strings" "time" "unsafe" ) func shouldSpeakEvent(eventType string, detail1 int) bool { return eventType != FocusedStateEvent || detail1 == 1 } // Every completion that can acquire document focus needs a retained native // source for the subsequent context lookup, even without load-complete. func shouldRetainNativeFocus(eventType string, detail1 int, chromeFocus ObjectID) bool { switch eventType { case FocusEvent, FocusedStateEvent, WindowActivatedEvent, ActiveDescendantEvent, SelectionChangedEvent, CaretEvent: return true case DocumentLoadCompleteEvent, DocumentLoadStoppedEvent: return !chromeFocus.Valid() case BusyStateEvent: return detail1 == 0 && !chromeFocus.Valid() default: return false } } // selectNativeEventTarget returns a borrowed event source or a retained target. // The callback releases a retained target after all native queries finish. func selectNativeEventTarget(event *C.AtspiEvent, eventType string) (*C.AtspiAccessible, bool) { target := event.source ownedTarget := false switch eventType { case FocusEvent, FocusedStateEvent: target = C.cthulhu_combo_ancestor(event.source) if target == nil { target = event.source } else { ownedTarget = true } // 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) 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 || tableContainer { var child *C.AtspiAccessible if tableContainer { child = C.cthulhu_table_selected_child(target) } else { child = C.cthulhu_selected_child(target) } if child != nil { if ownedTarget { C.g_object_unref(C.gpointer(target)) } target = child ownedTarget = true } } case WindowActivatedEvent: C.cthulhu_refresh_frame_children(event.source) target = C.cthulhu_popup_menu_target(event.source) if target == nil { target = C.cthulhu_focused_descendant(event.source) } if target == nil { target = C.cthulhu_showing_web_document(event.source) } if target != nil { ownedTarget = true } else { target = event.source } case ActiveDescendantEvent: target = C.cthulhu_event_active_descendant(event) case SelectionChangedEvent: if C.atspi_accessible_get_role(event.source, nil) == C.ATSPI_ROLE_PAGE_TAB_LIST { // libatspi resolves the selected child's index through its cached // child list. Tab creation/removal makes that list stale. C.atspi_accessible_clear_cache_single(event.source) } 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) } ownedTarget = target != nil } return target, ownedTarget } // captureNativeEventSnapshot enriches the target without changing event ownership. // It runs on the GLib owner and retains no native references. func (adapter *Adapter) captureNativeEventSnapshot(event *C.AtspiEvent, target *C.AtspiAccessible, eventType string, originID ObjectID, live nativeLiveProperties) (Snapshot, ObjectID) { // Document focus/load presentation needs only the title. Real caret // events still capture text, including carets hosted on document roots. captureCaret := !isDocumentLifecycleEvent(eventType) && eventType != BusyStateEvent if eventType == FocusEvent || eventType == FocusedStateEvent { role := C.atspi_accessible_get_role(target, nil) captureCaret = role != C.ATSPI_ROLE_DOCUMENT_WEB && role != C.ATSPI_ROLE_DOCUMENT_FRAME && role != C.ATSPI_ROLE_EMBEDDED } snapshot := snapshotFromNativeWithCaret(target, captureCaret, eventType == CaretEvent, false, adapter.captureSound.Load(), adapter.captureTextAttributes.Load()) if eventType == CaretEvent { if combo := C.cthulhu_read_only_combo(target); combo != nil { snapshot.ReadOnlyCombo = nativeObjectID(combo) C.g_object_unref(C.gpointer(combo)) } } if snapshot.Editable && (eventType == CaretEvent || eventType == TextInsertedEvent || eventType == TextDeletedEvent || eventType == TextSelectionChangedEvent) { if editor := C.cthulhu_editable_root(target); editor != nil { snapshot.EditableRoot = nativeObjectID(editor) C.g_object_unref(C.gpointer(editor)) } } snapshot.ApplicationProcess = adapter.applicationProcess(target) if snapshot.Role == "slider" || snapshot.Role == "combo box" && (snapshot.Name == "Key echo speech synthesizer" || snapshot.Name == "Key echo person") { snapshot.ApplicationPID = int(C.atspi_accessible_get_process_id(target, nil)) } if snapshot.Role == "combo box" && strings.EqualFold(snapshot.ApplicationProcess, "firefox") { snapshot.Autocomplete = snapshot.Autocomplete || isFirefoxChromeAutocomplete(snapshot, C.cthulhu_in_active_document(target) != 0) } if eventType == TextInsertedEvent || eventType == ChildrenAddedEvent { snapshot.LivePoliteness = live.politeness snapshot.LiveRegion = live.politeness != "" && C.cthulhu_in_active_document(event.source) != 0 if snapshot.LiveRegion { snapshot.LiveAtomic = live.atomic if eventType == TextInsertedEvent && snapshot.LiveAtomic { text := C.cthulhu_atomic_live_content(event.source) snapshot.Text = sanitizeSnapshotField(C.GoString(text)) C.g_free(C.gpointer(text)) } if eventType == ChildrenAddedEvent { if child := C.cthulhu_event_active_descendant(event); child != nil { text := C.cthulhu_live_content(child) if snapshot.LiveAtomic { C.g_free(C.gpointer(text)) text = C.cthulhu_atomic_live_content(event.source) } snapshot.Text = sanitizeSnapshotField(C.GoString(text)) C.g_free(C.gpointer(text)) } } } } if snapshot.BusName == "" { snapshot.BusName = originID.BusName } if eventType == CaretEvent { adapter.repairNativeCaretLine(target, &snapshot, int(C.cthulhu_event_detail1(event))) } if eventType == AccessibleNameEvent || snapshot.Role == "page tab" { toolkit := C.atspi_accessible_get_toolkit_name(target, nil) snapshot.ToolkitName = sanitizeSnapshotField(C.GoString(toolkit)) C.g_free(C.gpointer(toolkit)) } if snapshot.Role == "page tab" { document := C.cthulhu_document_ancestor(target) snapshot.BrowserTab = isBrowserChromeTab(snapshot, document != nil) if document != nil { C.g_object_unref(C.gpointer(document)) } } if (eventType == WindowCreateEvent || eventType == WindowActivatedEvent) && snapshot.Role == "frame" { if labels := C.cthulhu_collect_simple_prompt_text(target); labels != nil { snapshot.Text = sanitizeSnapshotField(C.GoString(labels)) C.g_free(C.gpointer(labels)) } } if eventType == WindowCreateEvent { if snapshot.Role == "alert" { if labels := C.cthulhu_collect_notification_text(target); labels != nil { snapshot.Text = sanitizeSnapshotField(C.GoString(labels)) C.g_free(C.gpointer(labels)) } } switch strings.ToLower(snapshot.ApplicationName) { case "notification-daemon", "notify-osd", "xfce4-notifyd", "mate-notification-daemon": if labels := C.cthulhu_collect_notification_text(target); labels != nil { snapshot.Text = sanitizeSnapshotField(C.GoString(labels)) C.g_free(C.gpointer(labels)) } } } if snapshot.Autocomplete && (eventType == TextInsertedEvent || eventType == TextDeletedEvent || eventType == FocusEvent || eventType == FocusedStateEvent) { text := C.atspi_accessible_get_text(target) if text != nil && C.atspi_text_get_n_selections(text, nil) > 0 { selection := C.atspi_text_get_selection(text, 0, nil) if selection != nil { snapshot.SelectionStart, snapshot.SelectionEnd = int(selection.start_offset), int(selection.end_offset) if selection.start_offset > 0 && selection.end_offset == C.atspi_text_get_character_count(text, nil) { prefix := C.atspi_text_get_text(text, selection.start_offset-1, selection.start_offset, nil) snapshot.CompletionPrefixCharacter = sanitizeCaretCharacter(C.GoString(prefix)) C.g_free(C.gpointer(prefix)) } C.g_free(C.gpointer(selection)) } } if text != nil { C.g_object_unref(C.gpointer(text)) } } // A read-only combo child only mirrors the chosen value. Qt 6.9 can stop // answering accessibility requests after its word-at-caret query; no word // feedback is needed for this non-writable selection proxy. if eventType == CaretEvent && snapshot.Role != "password text" && !snapshot.ReadOnlyCombo.Valid() { word := C.cthulhu_capture_caret_unit(target, C.ATSPI_TEXT_GRANULARITY_WORD) snapshot.CaretWord = sanitizeSnapshotField(C.GoString(word)) C.g_free(C.gpointer(word)) } var popupFor ObjectID if snapshot.Role == "list item" || snapshot.Role == "list box" || snapshot.Role == "menu item" || snapshot.Role == "button" || snapshot.Role == "push button" || snapshot.Role == "toggle button" { if popupTarget := C.cthulhu_autocomplete_popup_target(target); popupTarget != nil { popupFor = nativeObjectID(popupTarget) snapshot.AutocompletePopup = popupFor.Valid() C.g_object_unref(C.gpointer(popupTarget)) } } return snapshot, popupFor } // nativeWindowDescription runs on the GLib owner for both toolkit events and // foreground recovery. A table can acquire focus before its frame cache is ready. func nativeWindowDescription(window *C.AtspiAccessible, eventType, role string) string { description := C.atspi_accessible_get_description(window, nil) if (description == nil || C.GoString(description) == "") && (role == "table" || role == "table cell") && (eventType == WindowActivatedEvent || eventType == FocusEvent || eventType == FocusedStateEvent) { C.g_free(C.gpointer(description)) C.atspi_accessible_clear_cache_single(window) description = C.atspi_accessible_get_description(window, nil) } result := sanitizeSnapshotField(C.GoString(description)) C.g_free(C.gpointer(description)) return result } // enrichNativeWindowEvent queries and releases the window on the GLib owner. // It applies foreground, tab/document, table, and prompt state in query order. func (adapter *Adapter) enrichNativeWindowEvent(event *C.AtspiEvent, target *C.AtspiAccessible, eventType string, snapshot Snapshot, focusEvent *Event) { webNotification := focusEvent.WebNotification window := C.cthulhu_window_ancestor(event.source) var restoredDocument ObjectID if window == nil && (eventType == FocusedStateEvent || eventType == FocusEvent || eventType == CaretEvent) { window, restoredDocument = adapter.detachedDocumentWindow(event.source, eventType, focusEvent.Detail1) } focusEvent.Window = nativeObjectID(window) if webNotification != nil { webNotification.WindowPath = focusEvent.Window.Path if adapter.webNotificationRoots == nil || len(adapter.webNotificationRoots) >= 64 { adapter.webNotificationRoots = make(map[ObjectID]bool) } adapter.webNotificationRoots[NewObjectID(webNotification.BusName, webNotification.RootPath)] = true } focusEvent.WindowRecovered = restoredDocument.Valid() if eventType == WindowActivatedEvent && (snapshot.Role == "tree table" || snapshot.Role == "table cell") && window != nil { if labels := C.cthulhu_collect_simple_prompt_text(window); labels != nil { focusEvent.WindowMessage = sanitizeSnapshotField(C.GoString(labels)) C.g_free(C.gpointer(labels)) } if focusEvent.WindowMessage == "" && focusEvent.Window.Valid() { adapter.promptRefreshWindow = focusEvent.Window } } if eventType == WindowActivatedEvent && window != nil { name := C.atspi_accessible_get_name(window, nil) focusEvent.WindowTitle = sanitizeSnapshotField(C.GoString(name)) C.g_free(C.gpointer(name)) } focusEvent.Foreground = adapter.Foreground() focusedMenuItem := (eventType == FocusEvent || eventType == FocusedStateEvent) && (snapshot.Role == "menu item" || snapshot.Role == "check menu item" || snapshot.Role == "radio menu item") && snapshot.Focused if eventType == WindowActivatedEvent { focusEvent.ForegroundMismatch = !adapter.matchForeground(window, focusEvent.Window, &focusEvent.ForegroundCheck, false) } else { focusEvent.ForegroundMismatch = !adapter.matchForeground(window, focusEvent.Window, nil, focusedMenuItem) } if !focusEvent.ForegroundMismatch && window != nil && (eventType == WindowActivatedEvent || snapshot.Role == "table" || snapshot.Role == "table cell") { focusEvent.WindowDescription = nativeWindowDescription(window, eventType, snapshot.Role) } if !focusEvent.ForegroundMismatch && (eventType == WindowActivatedEvent || eventType == FocusEvent || eventType == FocusedStateEvent || eventType == SelectionChangedEvent || eventType == ActiveDescendantEvent || eventType == AccessibleNameEvent && snapshot.Role == "table cell") && (snapshot.Role == "table" || snapshot.Role == "table cell") { var rows, columns C.gint var tableName, tablePath *C.gchar var rowIndex, columnIndex C.gint var guiTable C.gboolean rowText := C.cthulhu_table_context(target, &rows, &columns, &tableName, &tablePath, &rowIndex, &columnIndex, &guiTable) snapshot.TableRowCount = int(rows) snapshot.TableColumnCount = int(columns) snapshot.TableName = sanitizeSnapshotField(C.GoString(tableName)) snapshot.TablePath = sanitizeSnapshotField(C.GoString(tablePath)) snapshot.TableRowIndex = int(rowIndex) snapshot.TableColumnIndex = int(columnIndex) snapshot.TablePositionKnown = rowIndex >= 0 && columnIndex >= 0 snapshot.TableRowText = sanitizeSnapshotField(C.GoString(rowText)) snapshot.GUITable = guiTable != 0 snapshot.TableRowText = mumbleTableRowText(snapshot) C.g_free(C.gpointer(tableName)) C.g_free(C.gpointer(tablePath)) C.g_free(C.gpointer(rowText)) focusEvent.Source = snapshot } if !focusEvent.ForegroundMismatch { captureLibreOfficeContext(target, &snapshot) adapter.captureLibreOfficeHeaders(target, &snapshot, focusEvent.Window) if eventType == SelectionChangedEvent && snapshot.Role == "table" && snapshot.LibreOffice != nil && snapshot.LibreOffice.InSpreadsheet { snapshot.LibreOffice.Selection = captureLibreOfficeSelection(target) } focusEvent.Source = snapshot if eventType == FocusEvent || eventType == FocusedStateEvent && focusEvent.Detail1 != 0 || eventType == CaretEvent || eventType == ActiveDescendantEvent || eventType == SelectionChangedEvent { snapshot.SpeechContext, snapshot.BrailleContext = native_speech_context(target) for _, ancestor := range snapshot.SpeechContext { if ancestor.Role == "document spreadsheet" { snapshot.Spreadsheet = true } } focusEvent.Source = snapshot } if snapshot.BrowserTab && (eventType == SelectedStateEvent && focusEvent.Detail1 != 0 && snapshot.Selected || eventType == SelectionChangedEvent && snapshot.Selected && strings.EqualFold(snapshot.ToolkitName, "Gecko")) { adapter.selectedTabWindow = focusEvent.Window adapter.activeDocument = ObjectID{} adapter.deferredLiveRegions = nil } if IsWebDocumentRole(snapshot.Role) && (eventType == WindowActivatedEvent || eventType == FocusEvent || eventType == FocusedStateEvent && focusEvent.Detail1 != 0 || eventType == DocumentLoadCompleteEvent) { adapter.rememberDocumentWindow(snapshot.ID(), window) adapter.activeDocument = snapshot.ID() adapter.selectedTabWindow = ObjectID{} } else if restoredDocument.Valid() { adapter.activeDocument = restoredDocument } } if (eventType == WindowCreateEvent || eventType == WindowActivatedEvent) && snapshot.Role == "frame" { adapter.promptRefreshWindow = ObjectID{} if !focusEvent.ForegroundMismatch && snapshot.Text == "" && focusEvent.Window.Valid() { adapter.promptRefreshWindow = focusEvent.Window } if eventType == WindowActivatedEvent && !focusEvent.ForegroundMismatch { adapter.promptScannedWindow = focusEvent.Window adapter.promptScannedEpoch = focusEvent.Foreground.Epoch } } if eventType == WindowActivatedEvent && snapshot.Role == "button" && !focusEvent.ForegroundMismatch && window != nil && focusEvent.Window.Valid() && (adapter.promptScannedWindow != focusEvent.Window || adapter.promptScannedEpoch != focusEvent.Foreground.Epoch) { adapter.promptScannedWindow = focusEvent.Window adapter.promptScannedEpoch = focusEvent.Foreground.Epoch if labels := C.cthulhu_collect_simple_prompt_text(window); labels != nil { focusEvent.WindowMessage = sanitizeSnapshotField(C.GoString(labels)) C.g_free(C.gpointer(labels)) } if focusEvent.WindowMessage == "" { adapter.promptRefreshWindow = focusEvent.Window } else { adapter.promptRefreshWindow = ObjectID{} } } if !focusEvent.ForegroundMismatch && window != nil && (eventType == FocusEvent || eventType == FocusedStateEvent && focusEvent.Detail1 == 1) && (snapshot.Role == "button" || snapshot.Role == "tree table" || snapshot.Role == "table cell") && focusEvent.Window == adapter.promptRefreshWindow { if labels := C.cthulhu_collect_simple_prompt_text(window); labels != nil { focusEvent.WindowMessage = sanitizeSnapshotField(C.GoString(labels)) C.g_free(C.gpointer(labels)) if focusEvent.WindowMessage != "" { adapter.promptRefreshWindow = ObjectID{} } } } if window != nil { C.g_object_unref(C.gpointer(window)) } } //export cthulhuAtspiEventCallback func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) { dispatch_native_event(event, userData, false, 0) } //export cthulhuAtspiSelectionCallback func cthulhuAtspiSelectionCallback(event *C.AtspiEvent, userData unsafe.Pointer, epoch C.uint64_t) { dispatch_native_event(event, userData, true, uint64(epoch)) } func dispatch_native_event(event *C.AtspiEvent, userData unsafe.Pointer, deferredSelection bool, selectionEpoch uint64) { if event == nil || userData == nil { return } callbackHandle := cgo.Handle(C.cthulhu_callback_value((*C.cthulhu_callback_data)(userData))) value := callbackHandle.Value() adapter, validAdapter := value.(*Adapter) if !validAdapter { return } secureEpoch := adapter.secureEpoch.Load() if deferredSelection && (selectionEpoch != secureEpoch || adapter.secure.Load()) { return } // Retire copied header identities even while locked. Tab lifecycle only // checks roles; it never captures text or presents output. nativeEventType := C.GoString(C.cthulhu_event_type(event)) if strings.HasPrefix(nativeEventType, MouseButtonEvent+":") { if !adapter.secure.Load() && strings.HasSuffix(nativeEventType, "p") { // Like Orca, observe the shared AT-SPI pointer signal. No object // capture or pointer coordinates are needed for navigation handoff. clear(adapter.navigationEcho) adapter.enqueueSecureEvent(Event{Type: MouseButtonEvent}, secureEpoch) } return } eventType := canonicalObjectChangeEventType(nativeEventType) originID := nativeObjectID(event.source) adapter.retireEchoFocus(eventType, int(C.cthulhu_event_detail1(event)), originID) // Retired GTK cells can emit hundreds of obsolete focus/selection claims // during a rebuild. Leave lifecycle retirement on its established path, // but avoid all snapshot and foreground queries for known-dead targets. switch eventType { case FocusEvent, FocusedStateEvent, SelectedStateEvent, SelectionChangedEvent, ActiveDescendantEvent: if C.cthulhu_accessible_known_dead(event.source) != 0 || eventType == ActiveDescendantEvent && C.cthulhu_accessible_known_dead(C.cthulhu_event_active_descendant(event)) != 0 { return } } if eventType == AccessibleNameEvent || eventType == DefunctStateEvent && C.cthulhu_event_detail1(event) != 0 { C.cthulhu_forget_menu_checkability(event.source) } if eventType != DefunctStateEvent || C.cthulhu_event_detail1(event) != 0 { adapter.retireLibreOfficeHeaders(event.source, eventType, originID) } if adapter.secure.Load() { return } if eventType == "object:state-changed:checked" && C.atspi_accessible_get_role(event.source, nil) == C.ATSPI_ROLE_MENU_ITEM { C.cthulhu_note_menu_checkability(event.source) } adapter.applySecureReset() gtkSelection := eventType == SelectionChangedEvent && C.cthulhu_gtk_table(event.source, 0) != 0 // Structural events which only invalidate caches do not need a captured // selection between every removed row. A structural event that survives // invalidation below still flushes before its ordinary presentation path. selectionBoundary := eventType != DefunctStateEvent && !strings.HasPrefix(eventType, ChildrenChangedEvent) if !deferredSelection && selectionBoundary { if gtkSelection { C.cthulhu_flush_other_selection(adapter.callbackData, event.source) } else { C.cthulhu_flush_selection(adapter.callbackData) } } adapter.refreshForeground() if isDocumentLifecycleEvent(eventType) || eventType == DefunctStateEvent && C.cthulhu_event_detail1(event) != 0 { adapter.deferredLiveRegions.take(originID) adapter.deferredLiveRegions.forget_document(originID) } if eventType == DefunctStateEvent && C.cthulhu_event_detail1(event) != 0 { adapter.forgetDocumentWindow(originID) } else if eventType == WindowDestroyedEvent { adapter.forgetWindowDocuments(originID) } invalidated := adapter.applyTreeInvalidation(eventType, event) returnedTarget := adapter.returned_menu_item(event, eventType, originID) if returnedTarget != nil { defer C.g_object_unref(C.gpointer(returnedTarget)) // 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 } if eventType == DefunctStateEvent && C.cthulhu_event_detail1(event) != 0 && (adapter.applicationProcesses[originID.BusName] == "pidgin" || adapter.applicationProcesses[originID.BusName] == "mumble") { // Read-only transcripts need not own native focus. Forward their // retirement before the ordinary dead-object early return, without // querying a destroyed accessible or broadening focus ownership. retired := NewEvent(eventType, Snapshot{BusName: originID.BusName, Path: originID.Path, ApplicationProcess: adapter.applicationProcesses[originID.BusName], Defunct: true}) retired.Detail1 = 1 adapter.enqueueSecureEvent(retired, secureEpoch) return } if eventType == DefunctStateEvent && C.cthulhu_event_detail1(event) != 0 && adapter.webNotificationRoots[originID] { delete(adapter.webNotificationRoots, originID) // Retire a known toast before the normal dead-object early return. // Do not query or retain the accessible which has just been destroyed. retired := NewEvent(eventType, Snapshot{BusName: originID.BusName, Path: originID.Path, Defunct: true}) retired.Detail1 = 1 adapter.enqueueSecureEvent(retired, secureEpoch) return } // Showing/child-added events normally finish at invalidation. A matched // notification script needs their semantics before that early return, // including when the provider is outside foreground ownership. webNotification := adapter.webNotification(event) message := adapter.pidginMessage(event) if message == nil { message = adapter.mumbleMessage(event) } if message != nil { // A transcript insertion is a passive announcement. Keep it out of // native focus capture, selected-descendant resolution and review. roleName := C.atspi_accessible_get_role_name(event.source, nil) chatEvent := NewEvent(TextInsertedEvent, Snapshot{BusName: message.Provider, Path: message.Conversation, Role: C.GoString(roleName), ApplicationProcess: adapter.applicationProcesses[originID.BusName]}) C.g_free(C.gpointer(roleName)) chatEvent.ChatMessage = message chatEvent.Window = NewObjectID(message.Provider, message.Window) chatEvent.Foreground = adapter.Foreground() adapter.enqueueSecureEvent(chatEvent, secureEpoch) return } if invalidated && webNotification == nil && eventType != ChildrenAddedEvent && (eventType != DefunctStateEvent || !adapter.nativeFocusIs(originID)) { return } if !shouldSpeakEvent(eventType, int(C.cthulhu_event_detail1(event))) { return } if adapter.filterNativeBackgroundEvent(event, eventType, originID, webNotification != nil) { adapter.backgroundEventsFiltered.Add(1) return } // Seed an unknown toolkit identity only after foreground filtering, so a // background provider never delays this path with a toolkit lookup. if !deferredSelection && eventType == SelectionChangedEvent && (gtkSelection || C.cthulhu_gtk_table(event.source, 1) != 0) { C.cthulhu_defer_selection(adapter.callbackData, event, C.uint64_t(secureEpoch)) return } if tab := addedSelectedBrowserTab(event, eventType); tab != nil { defer C.g_object_unref(C.gpointer(tab)) returnedTarget = tab eventType, invalidated = SelectionChangedEvent, false } var live nativeLiveProperties if eventType == TextInsertedEvent || eventType == ChildrenAddedEvent || eventType == BusyStateEvent { live = capture_live_properties(event.source) } if invalidated && webNotification == nil && eventType == ChildrenAddedEvent && (live.politeness == "" || live.politeness == "off") { return } if !deferredSelection && !selectionBoundary { C.cthulhu_flush_selection(adapter.callbackData) } deferLive, completeLive := adapter.prepare_live_region(event, eventType, originID, live) if deferLive { return } if completeLive { eventType = TextInsertedEvent } if (isDocumentLifecycleEvent(eventType) || eventType == BusyStateEvent) && C.cthulhu_active_document(event.source) == 0 { return } snapshotStarted := time.Now() target, ownedTarget := adapter.resolveNativeEventTarget(event, eventType, returnedTarget) if target == nil { return } if (eventType == FocusEvent || eventType == FocusedStateEvent || eventType == SelectedStateEvent || eventType == ActiveDescendantEvent || eventType == SelectionChangedEvent) && C.cthulhu_accessible_known_dead(target) != 0 { if ownedTarget { C.g_object_unref(C.gpointer(target)) } return } refreshNativeEventTarget(event, target, eventType) snapshot, popupFor, payload := adapter.captureNativeEventContent(event, target, eventType, originID, live, completeLive) eventType, inlineMenuID := normalizeNativeMenuFocus(event, target, eventType, originID, &snapshot) // Chromium can drop ACTIVE from its visible main frame while a tiny // same-process sibling claims it. On X11, the correlated foreground // window is the stronger ownership check performed below. Keep the // AT-SPI state filter when no X11 ownership data is available. progressUpdate := snapshot.Role == "progress bar" && (eventType == ValueChangedEvent || eventType == AccessibleValueEvent) inactiveProgress := progressUpdate && !adapter.Foreground().Client.Known && C.cthulhu_in_inactive_window(event.source) != 0 if webNotification == nil && !progressUpdate && !adapter.Foreground().Client.Known && eventType != WindowCreateEvent && eventType != WindowActivatedEvent && eventType != WindowDeactivatedEvent && eventType != WindowDestroyedEvent && eventType != AnnouncementEvent && !snapshot.AutocompletePopup && C.cthulhu_in_inactive_window(event.source) != 0 { if ownedTarget { C.g_object_unref(C.gpointer(target)) } return } var chromeFocus ObjectID if isDocumentLifecycleEvent(eventType) || eventType == BusyStateEvent { chromeFocus = adapter.focusedChrome(snapshot.ID()) } focusEvent := NewEventWithPayload( eventType, snapshot, payload, ) if eventType != nativeEventType { focusEvent.NativeType = nativeEventType } focusEvent.Detail1 = int(C.cthulhu_event_detail1(event)) focusEvent.WebNotification = webNotification focusEvent.PopupFor = popupFor adapter.enrichNativeWindowEvent(event, target, eventType, snapshot, &focusEvent) if inactiveProgress { focusEvent.ForegroundMismatch = true } if !focusEvent.Window.Valid() && (eventType == WindowActivatedEvent || eventType == WindowDeactivatedEvent || eventType == WindowDestroyedEvent) { focusEvent.Window = originID } if !focusEvent.ForegroundMismatch && !snapshot.Defunct && snapshot.Role != "invalid" { if shouldRetainNativeFocus(eventType, focusEvent.Detail1, chromeFocus) && !snapshot.ReadOnlyCombo.Valid() && !(eventType == SelectionChangedEvent && focusEvent.Source.LibreOffice != nil && focusEvent.Source.LibreOffice.InSpreadsheet) { adapter.rememberNativeFocus(target, snapshot.ID()) } } focusEvent.FocusOutsideDocument = chromeFocus if eventType == DocumentLoadCompleteEvent { uri := C.cthulhu_document_uri(event.source) if uri != nil { if parsed, err := url.Parse(C.GoString(uri)); err == nil { focusEvent.DocumentFragment = sanitizeSnapshotField(parsed.Fragment) } C.g_free(C.gpointer(uri)) } } focusEvent.SnapshotDuration = time.Since(snapshotStarted) bit := documentNavigationEchoBit(focusEvent, adapter.navigationCaretOffset, adapter.navigationSelectionOffsets) if adapter.navigationEcho[snapshot.ID()]&bit != 0 { focusEvent.NavigationEcho = true adapter.navigationEcho[snapshot.ID()] &^= bit } else if eventType == CaretEvent || eventType == FocusEvent || eventType == FocusedStateEvent { clear(adapter.navigationEcho) } focusEvent.OriginPath = sanitizeSnapshotField(C.GoString(C.cthulhu_accessible_path(event.source))) if eventType == ActiveDescendantEvent && inlineMenuID.Valid() { focusEvent.OriginPath = inlineMenuID.Path } if ownedTarget { C.g_object_unref(C.gpointer(target)) } adapter.enqueueSecureEvent(focusEvent, secureEpoch) }