525 lines
22 KiB
Go
525 lines
22 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)
|
|
if targetRole == C.ATSPI_ROLE_MENU || targetRole == C.ATSPI_ROLE_TABLE {
|
|
var child *C.AtspiAccessible
|
|
if targetRole == C.ATSPI_ROLE_TABLE {
|
|
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.atspi_accessible_get_role(event.source, nil) == C.ATSPI_ROLE_TABLE {
|
|
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) (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())
|
|
snapshot.ApplicationProcess = adapter.applicationProcess(target)
|
|
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 {
|
|
key := C.CString("container-live")
|
|
live := C.cthulhu_live_attribute(event.source, key)
|
|
C.free(unsafe.Pointer(key))
|
|
snapshot.LivePoliteness = sanitizeSnapshotField(C.GoString(live))
|
|
snapshot.LiveRegion = snapshot.LivePoliteness != "" && C.cthulhu_in_active_document(event.source) != 0
|
|
C.g_free(C.gpointer(live))
|
|
if snapshot.LiveRegion {
|
|
key = C.CString("container-atomic")
|
|
atomic := C.cthulhu_live_attribute(event.source, key)
|
|
C.free(unsafe.Pointer(key))
|
|
snapshot.LiveAtomic = C.GoString(atomic) == "true"
|
|
C.g_free(C.gpointer(atomic))
|
|
if eventType == TextInsertedEvent && snapshot.LiveAtomic {
|
|
text := C.cthulhu_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_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))
|
|
}
|
|
}
|
|
|
|
if eventType == CaretEvent && snapshot.Role != "password text" {
|
|
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
|
|
}
|
|
|
|
// 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.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") {
|
|
description := C.atspi_accessible_get_description(window, nil)
|
|
if (description == nil || C.GoString(description) == "") &&
|
|
(snapshot.Role == "table" || snapshot.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)
|
|
}
|
|
focusEvent.WindowDescription = sanitizeSnapshotField(C.GoString(description))
|
|
C.g_free(C.gpointer(description))
|
|
}
|
|
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)
|
|
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{}
|
|
}
|
|
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) {
|
|
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 adapter.secure.Load() {
|
|
return
|
|
}
|
|
|
|
adapter.applySecureReset()
|
|
adapter.refreshForeground()
|
|
nativeEventType := C.GoString(C.cthulhu_event_type(event))
|
|
eventType := canonicalTextChangeEventType(nativeEventType)
|
|
originID := nativeObjectID(event.source)
|
|
if eventType == DefunctStateEvent && C.cthulhu_event_detail1(event) != 0 {
|
|
adapter.forgetDocumentWindow(originID)
|
|
} else if eventType == WindowDestroyedEvent {
|
|
adapter.forgetWindowDocuments(originID)
|
|
}
|
|
invalidated := adapter.applyTreeInvalidation(eventType, event)
|
|
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 invalidated && webNotification == nil && (eventType != DefunctStateEvent || !adapter.nativeFocusIs(originID)) {
|
|
return
|
|
}
|
|
if !shouldSpeakEvent(eventType, int(C.cthulhu_event_detail1(event))) {
|
|
return
|
|
}
|
|
if (isDocumentLifecycleEvent(eventType) || eventType == BusyStateEvent) && C.cthulhu_active_document(event.source) == 0 {
|
|
return
|
|
}
|
|
snapshotStarted := time.Now()
|
|
target := adapter.libreOfficeFocusTarget(event, eventType)
|
|
ownedTarget := target != nil
|
|
if target == nil {
|
|
target, ownedTarget = selectNativeEventTarget(event, eventType)
|
|
}
|
|
if target == nil {
|
|
return
|
|
}
|
|
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)
|
|
}
|
|
payload := C.cthulhu_event_payload(event)
|
|
snapshot, popupFor := adapter.captureNativeEventSnapshot(event, target, eventType, originID)
|
|
// 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 && !snapshot.AutocompletePopup &&
|
|
C.cthulhu_in_inactive_window(event.source) != 0 {
|
|
C.g_free(C.gpointer(payload))
|
|
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,
|
|
C.GoString(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) &&
|
|
!(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 := uint8(0)
|
|
switch eventType {
|
|
case FocusEvent:
|
|
bit = 1
|
|
case FocusedStateEvent:
|
|
bit = 2
|
|
case CaretEvent:
|
|
if focusEvent.Detail1 == adapter.navigationCaretOffset {
|
|
bit = 4
|
|
}
|
|
}
|
|
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)))
|
|
C.g_free(C.gpointer(payload))
|
|
if ownedTarget {
|
|
C.g_object_unref(C.gpointer(target))
|
|
}
|
|
|
|
adapter.enqueueSecureEvent(focusEvent, secureEpoch)
|
|
}
|