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:
1 parent
8ce5e5cdad
commit
7eef6e8be6
30 files changed
+1484
-174
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user