Bring the fragmented-line browser fixture into agreement with current browse navigation. Rich editors are read through their text descendants, so Down from the preceding heading must read "Review the code." directly. Remove the stale empty editor-control stop while retaining forward and reverse line navigation, character and word movement, Read All, and inline Braille-routing assertions. Fix Firefox's missing announcement when creating a second tab. Firefox can emit selection events before the new tab enters its accessible child list, causing the native selected-child lookup to return nothing. Use the subsequent child-added event only when its target is a live, freshly selected browser-chrome tab with the emitting tab list as its direct parent. Keep document-authored tabs and unselected background additions excluded, retain foreground filtering and tab-identity deduplication, and release native references on the GLib owner. Normalize child-added origin suffixes such as :system before event dispatch, preserving the original native type in diagnostics. Rename the shared mutation normalizer to reflect its expanded scope and update its callers, including the Pidgin insertion check. Extend the normalization regression to cover child additions and reject unrelated event names. Firefox keeps its normal animations and all existing tab-announcement and address-editing assertions. Make private-session teardown tolerate the document portal's asynchronous FUSE unmount after D-Bus exits. Retry only ENOTCONN and EBUSY cleanup failures for at most five seconds. Keep unrelated errors and persistent mount failures visible. Add regressions for transient teardown, preservation of a failing child's exit status, unrelated cleanup errors, and the cleanup deadline. Runtime directories and accessibility buses remain private, with no init-system dependency. Update browser verification notes and the parity ledger with the fixes, evidence, regression risks, and concrete live acceptance procedures. Validation: - All 33 Chromium browser proofs passed in the final combined run. - Firefox baseline and tab checks passed repeatedly with normal animations. - All eight private-session isolation and cleanup regressions passed. - AT-SPI, app, and review package race suites passed during development; focused race regressions passed again after the final event-normalization change. - Go vet passed for internal/atspi, internal/app, and internal/review. - A separate reader binary built successfully. - Parity-ledger validation passed for 75 rows; git diff --check passed.
144 lines
6.1 KiB
Go
144 lines
6.1 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"
|
|
|
|
// 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
|
|
}
|