Files
cthulhu/internal/atspi/adapter_event_callback_native.go

660 lines
29 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"
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
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" {
// 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: "pidgin", 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)
if message := adapter.pidginMessage(event); message != nil {
// A transcript insertion is a passive announcement. Keep it out of
// native focus capture, selected-descendant resolution and review.
chatEvent := NewEvent(TextInsertedEvent, Snapshot{BusName: message.Provider,
Path: message.Conversation, Role: "text", ApplicationProcess: "pidgin"})
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)
}