Make web document navigation survive live-updating pages

Recreated-node caret recovery, tolerant caret validation, per-root retained document trees and cursors across tab switches, document warm-up, coalesced structural moves, and hung-provider fast-fail with an 'Application is not responding' announcement.
This commit is contained in:
hjozwiak committed 2026-11-03 10:34:55 -05:00
1 parent 8ce5e5cdad
commit 7eef6e8be6
30 files changed
+1484 -174

No files matched your search

+43 -10
View File
@@ -22,14 +22,19 @@ func (adapter *Adapter) captureDesktop(ctx context.Context, reviewFocus ObjectID
trace.begin_stage("root-selection", reviewFocus, "")
root, err := adapter.nativeDocumentRoot(ctx, reviewFocus)
adapter.treeMutex.Lock()
previousTree := adapter.documentTree
retainedRoots := make(map[ObjectID]bool, 1+len(adapter.stashedDocumentIDs))
if adapter.documentSlotRoot.Valid() && adapter.documentTree != nil {
retainedRoots[adapter.documentSlotRoot] = true
}
for _, id := range adapter.stashedDocumentIDs {
retainedRoots[id] = true
}
adapter.treeMutex.Unlock()
if (errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrStaleObject)) && previousTree != nil && len(adapter.nativeFocus) != 0 {
if (errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrStaleObject)) && len(retainedRoots) != 0 && len(adapter.nativeFocus) != 0 {
// Dynamic pages can remove the browse cursor without sending a new
// focus event. Recover only from the newest retained native focus in
// the same document. A chrome or other-tab focus is not a fallback.
currentRoot, rootErr := previousTree.Root()
for i := len(adapter.nativeFocus) - 1; rootErr == nil && i >= 0; i-- {
for i := len(adapter.nativeFocus) - 1; i >= 0; i-- {
latest := adapter.nativeFocus[i]
if latest.id.BusName != reviewFocus.BusName {
break
@@ -52,14 +57,16 @@ func (adapter *Adapter) captureDesktop(ctx context.Context, reviewFocus ObjectID
}
candidate, candidateErr := adapter.nativeDocumentRoot(ctx, latest.id)
if candidateErr != nil {
continue
}
if retainedRoots[nativeObjectID(candidate)] {
root, err = candidate, nil
break
}
if nativeObjectID(candidate) == currentRoot.ID() {
root, err = candidate, nil
} else {
C.g_object_unref(C.gpointer(candidate))
}
break
// The newest live focus can sit in browser chrome or a tab
// whose document was never retained; older focus history in
// the retained document may still resolve.
C.g_object_unref(C.gpointer(candidate))
}
}
if err != nil {
@@ -67,6 +74,10 @@ func (adapter *Adapter) captureDesktop(ctx context.Context, reviewFocus ObjectID
}
trace.set_root(nativeObjectID(root), "document")
trace.begin_stage("cache-preparation", nativeObjectID(root), "")
adapter.rotateDocumentSlots(nativeObjectID(root))
adapter.treeMutex.Lock()
previousTree := adapter.documentTree
adapter.treeMutex.Unlock()
cache := nativeTreeCache{}
if !documentText && previousTree != nil {
cache.nodes = previousTree.nativeRefresh(nativeObjectID(root))
@@ -333,6 +344,25 @@ func (adapter *Adapter) focusedChrome(document ObjectID) ObjectID {
// Return an owned reference to the outermost document containing focus.
// Parent traversal is bounded and never scans other windows or tabs.
// retainedDocumentSource returns a new reference to a retained native
// source from the active or a stashed document capture. Capture root
// selection needs this when the focus identity (often the document root
// itself after a load, or a saved document used as capture anchor) never
// received a focus event and so is absent from the focus history.
func (adapter *Adapter) retainedDocumentSource(id ObjectID) *C.AtspiAccessible {
adapter.treeMutex.Lock()
defer adapter.treeMutex.Unlock()
if source := adapter.nativeDocument[id]; source != nil {
return (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
}
for _, root := range adapter.stashedDocumentIDs {
if source := adapter.stashedDocuments[root].sources[id]; source != nil {
return (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
}
}
return nil
}
func (adapter *Adapter) nativeDocumentRoot(ctx context.Context, focus ObjectID) (*C.AtspiAccessible, error) {
var source *C.AtspiAccessible
for _, item := range adapter.nativeFocus {
@@ -341,6 +371,9 @@ func (adapter *Adapter) nativeDocumentRoot(ctx context.Context, focus ObjectID)
break
}
}
if source == nil {
source = adapter.retainedDocumentSource(focus)
}
if source == nil {
return nil, fmt.Errorf("%w: document focus bus %q path %q", ErrObjectNotFound, focus.BusName, focus.Path)
}