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
@@ -8,28 +8,43 @@ import "git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
|
||||
// Orca recovers removed caret contexts from the surviving parent. Here the
|
||||
// bounded case is one plain text leaf between the same adjacent siblings.
|
||||
// Retain only its parent and two generation-checked boundary references;
|
||||
// labels and child indices alone cannot establish the old reading position.
|
||||
// Retain its parent and two generation-checked boundary references. When a
|
||||
// ticking page recreates every child at once, those boundaries retire too;
|
||||
// the child's index and the parent's child count, checked against the same
|
||||
// generation-checked parent, then distinguish recreation from insertion.
|
||||
// Labels alone still cannot establish the old reading position, and neither
|
||||
// recovery is ever used for activation.
|
||||
func remember_text_parent(doc atspi.DocumentSnapshot, place *documentPlace) {
|
||||
place.textParent = atspi.ObjectReference{}
|
||||
place.textPrevious, place.textNext = atspi.ObjectReference{}, atspi.ObjectReference{}
|
||||
place.textChildIndex, place.textChildCount = 0, 0
|
||||
place.parent = atspi.ObjectReference{}
|
||||
place.parentPrevious, place.parentNext = atspi.ObjectReference{}, atspi.ObjectReference{}
|
||||
place.parentChildIndex, place.parentChildCount = 0, 0
|
||||
place.recoveryParents = nil
|
||||
node, ok := doc.Node(place.object.ID())
|
||||
if !ok || node.Reference != place.object {
|
||||
return
|
||||
}
|
||||
plainText := replacement_text_leaf(node)
|
||||
firstParent := true
|
||||
for parentID := node.Parent; parentID.Valid(); {
|
||||
parent, ok := doc.Node(parentID)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if firstParent {
|
||||
firstParent = false
|
||||
remember_child_slot(doc, place, parent, &place.parent, &place.parentPrevious, &place.parentNext,
|
||||
&place.parentChildIndex, &place.parentChildCount)
|
||||
}
|
||||
if len(place.recoveryParents) == 0 && plainText && replacement_text_parent(parent) {
|
||||
place.textParent = parent.Reference
|
||||
for childIndex, id := range parent.Children {
|
||||
if id != place.object.ID() {
|
||||
continue
|
||||
}
|
||||
place.textChildIndex, place.textChildCount = childIndex, len(parent.Children)
|
||||
if childIndex > 0 {
|
||||
if sibling, ok := doc.Node(parent.Children[childIndex-1]); ok {
|
||||
place.textPrevious = sibling.Reference
|
||||
@@ -42,6 +57,7 @@ func remember_text_parent(doc atspi.DocumentSnapshot, place *documentPlace) {
|
||||
}
|
||||
if childIndex > 0 && !place.textPrevious.Valid() || childIndex+1 < len(parent.Children) && !place.textNext.Valid() {
|
||||
place.textParent = atspi.ObjectReference{}
|
||||
place.textChildIndex, place.textChildCount = 0, 0
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -73,9 +89,9 @@ func survivingDocumentParent(doc atspi.DocumentSnapshot, saved documentPlace) (a
|
||||
return atspi.DocumentNode{}, false
|
||||
}
|
||||
|
||||
func replacement_text_node(doc atspi.DocumentSnapshot, saved documentPlace) (atspi.DocumentNode, bool) {
|
||||
func replacement_text_node(doc atspi.DocumentSnapshot, saved documentPlace) (atspi.DocumentNode, string, bool) {
|
||||
if saved.document != doc.Context.TopLevel || !saved.textParent.Valid() || saved.origin != CaretNavigation {
|
||||
return atspi.DocumentNode{}, false
|
||||
return atspi.DocumentNode{}, "", false
|
||||
}
|
||||
byID := make(map[atspi.ObjectID]*atspi.DocumentNode, len(doc.Nodes))
|
||||
for index := range doc.Nodes {
|
||||
@@ -104,11 +120,132 @@ func replacement_text_node(doc atspi.DocumentSnapshot, saved documentPlace) (ats
|
||||
}
|
||||
child := byID[id]
|
||||
if previous == saved.textPrevious && next == saved.textNext && child != nil && replacement_text_leaf(*child) {
|
||||
return *child, true
|
||||
return *child, "replaced-text-child", true
|
||||
}
|
||||
}
|
||||
// Every sibling reference retired together: the page recreated the
|
||||
// parent's children in place. Same child count and index under the
|
||||
// same generation-checked parent is the same navigation line. A
|
||||
// changed count stays ambiguous and falls back to the parent, as
|
||||
// does any saved sibling that still lives: a reorder or a single
|
||||
// replaced neighbor is a structural change, not a recreation.
|
||||
siblingsRetired := true
|
||||
for _, sibling := range []atspi.ObjectReference{saved.textPrevious, saved.textNext} {
|
||||
if !sibling.Valid() {
|
||||
continue
|
||||
}
|
||||
if node, ok := byID[sibling.ID()]; ok && node.Reference == sibling {
|
||||
siblingsRetired = false
|
||||
}
|
||||
}
|
||||
if siblingsRetired && saved.textChildCount > 0 && len(parent.Children) == saved.textChildCount &&
|
||||
saved.textChildIndex >= 0 && saved.textChildIndex < len(parent.Children) {
|
||||
if child := byID[parent.Children[saved.textChildIndex]]; child != nil && replacement_text_leaf(*child) {
|
||||
return *child, "replaced-text-child-index", true
|
||||
}
|
||||
}
|
||||
return atspi.DocumentNode{}, "", false
|
||||
}
|
||||
return atspi.DocumentNode{}, false
|
||||
return atspi.DocumentNode{}, "", false
|
||||
}
|
||||
|
||||
// remember_child_slot records a child's slot under its immediate parent:
|
||||
// the generation-checked parent, adjacent sibling references, and index
|
||||
// and count. Any unresolved boundary clears the slot; indices alone are
|
||||
// never enough evidence on their own.
|
||||
func remember_child_slot(doc atspi.DocumentSnapshot, place *documentPlace, parent atspi.DocumentNode,
|
||||
slotParent, slotPrevious, slotNext *atspi.ObjectReference, slotIndex, slotCount *int) {
|
||||
for childIndex, id := range parent.Children {
|
||||
if id != place.object.ID() {
|
||||
continue
|
||||
}
|
||||
*slotParent = parent.Reference
|
||||
*slotIndex, *slotCount = childIndex, len(parent.Children)
|
||||
if childIndex > 0 {
|
||||
if sibling, ok := doc.Node(parent.Children[childIndex-1]); ok {
|
||||
*slotPrevious = sibling.Reference
|
||||
}
|
||||
}
|
||||
if childIndex+1 < len(parent.Children) {
|
||||
if sibling, ok := doc.Node(parent.Children[childIndex+1]); ok {
|
||||
*slotNext = sibling.Reference
|
||||
}
|
||||
}
|
||||
if childIndex > 0 && !slotPrevious.Valid() || childIndex+1 < len(parent.Children) && !slotNext.Valid() {
|
||||
*slotParent = atspi.ObjectReference{}
|
||||
*slotIndex, *slotCount = 0, 0
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// replacement_child_node generalizes the replaced-text-child recovery to
|
||||
// any child (recreated button rows, containers) under the same bounded
|
||||
// evidence: either the generation-checked sibling boundaries still match,
|
||||
// or every sibling retired together with the child count unchanged, so
|
||||
// the same index under the same surviving parent is the recreation. Orca
|
||||
// recovers removed children the same way (parent plus removed index);
|
||||
// activation never uses this fallback.
|
||||
func replacement_child_node(doc atspi.DocumentSnapshot, saved documentPlace) (atspi.DocumentNode, string, bool) {
|
||||
if saved.document != doc.Context.TopLevel || !saved.parent.Valid() || saved.origin == NativeNavigation {
|
||||
return atspi.DocumentNode{}, "", false
|
||||
}
|
||||
// Plain text leaves keep their stricter replacement rules above; the
|
||||
// generic slot must not widen them for the same parent.
|
||||
if saved.textParent.Valid() && saved.parent == saved.textParent {
|
||||
return atspi.DocumentNode{}, "", false
|
||||
}
|
||||
byID := make(map[atspi.ObjectID]*atspi.DocumentNode, len(doc.Nodes))
|
||||
for index := range doc.Nodes {
|
||||
node := &doc.Nodes[index]
|
||||
byID[node.Reference.ID()] = node
|
||||
}
|
||||
parent, ok := doc.Node(saved.parent.ID())
|
||||
if !ok || parent.Reference != saved.parent || len(parent.Children) == 0 ||
|
||||
parent.Hidden || parent.ParentEditable || parent.Snapshot.Editable ||
|
||||
parent.Snapshot.Focusable || parent.Snapshot.SupportsClick {
|
||||
return atspi.DocumentNode{}, "", false
|
||||
}
|
||||
usable := func(child *atspi.DocumentNode) bool {
|
||||
return child != nil && !child.Hidden && !child.ParentEditable && !child.Snapshot.Editable &&
|
||||
child.Snapshot.Role != "password text"
|
||||
}
|
||||
for index, id := range parent.Children {
|
||||
var previous, next atspi.ObjectReference
|
||||
if index > 0 {
|
||||
sibling := byID[parent.Children[index-1]]
|
||||
if sibling == nil {
|
||||
continue
|
||||
}
|
||||
previous = sibling.Reference
|
||||
}
|
||||
if index+1 < len(parent.Children) {
|
||||
sibling := byID[parent.Children[index+1]]
|
||||
if sibling == nil {
|
||||
continue
|
||||
}
|
||||
next = sibling.Reference
|
||||
}
|
||||
if child := byID[id]; previous == saved.parentPrevious && next == saved.parentNext && usable(child) {
|
||||
return *child, "replaced-child", true
|
||||
}
|
||||
}
|
||||
siblingsRetired := true
|
||||
for _, sibling := range []atspi.ObjectReference{saved.parentPrevious, saved.parentNext} {
|
||||
if !sibling.Valid() {
|
||||
continue
|
||||
}
|
||||
if node, ok := byID[sibling.ID()]; ok && node.Reference == sibling {
|
||||
siblingsRetired = false
|
||||
}
|
||||
}
|
||||
if siblingsRetired && saved.parentChildCount > 0 && len(parent.Children) == saved.parentChildCount &&
|
||||
saved.parentChildIndex >= 0 && saved.parentChildIndex < len(parent.Children) {
|
||||
if child := byID[parent.Children[saved.parentChildIndex]]; usable(child) {
|
||||
return *child, "replaced-child-index", true
|
||||
}
|
||||
}
|
||||
return atspi.DocumentNode{}, "", false
|
||||
}
|
||||
|
||||
func replacement_text_parent(node atspi.DocumentNode) bool {
|
||||
|
||||
Reference in new issue
Block a user