Flat review improvements.

This commit is contained in:
Storm Dragon committed 2026-10-21 11:49:25 -04:00
1 parent ce5f196956
commit 4643f8e47f
14 files changed
+1291 -35

No files matched your search

+49 -12
View File
@@ -88,6 +88,9 @@ type nativeTreeCache struct {
// focused application's subtree. The latter keeps flat review independent of
// unrelated AT-SPI providers that can be slow or transiently unavailable.
func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusName string, document, documentText, captureSound bool, retained map[ObjectID]*C.AtspiAccessible, cache nativeTreeCache) (ObjectID, []TreeNode, error) {
trace := capture_diagnostics(ctx)
trace.begin_stage("tree-setup", ObjectID{}, "")
reviewOnly := !document && reviewBusName != ""
rootID := nativeObjectID(root)
if !rootID.Valid() {
C.g_object_unref(C.gpointer(root))
@@ -122,17 +125,21 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
item := pending[last]
source := item.source
pending = pending[:last]
cached, known := cache.nodes[nativeObjectID(source)]
sourceID := nativeObjectID(source)
cached, known := cache.nodes[sourceID]
refresh := !known || cached.snapshotInvalid || cached.textInvalid || cached.geometryInvalid || cached.childrenInvalid
snapshot := cached.snapshot
trace.begin_stage("snapshot", sourceID, snapshot.Role)
if refresh {
// Only changed/new objects require synchronous provider queries.
// Reusing live siblings matches Python's event-driven web cache and
// prevents a small insertion from repeatedly timing out on a big page.
// New document nodes already have libatspi's event-maintained cache.
// Clear only entries our tree marked dirty; flushing every new node
// forces redundant provider queries across large catalogs.
if !document || known {
// New document and flat-review nodes already have libatspi's
// event-maintained cache. Clearing it forces another child-count
// request even for a cached leaf, which can fail on a busy provider.
// Known dirty entries still need a fresh provider snapshot. Preserve
// the existing full refresh for desktop/object capture.
if known || (!document && !reviewOnly) {
C.atspi_accessible_clear_cache_single(source)
}
applicationName, knownApplication := applicationNames[nativeObjectID(source).BusName]
@@ -143,13 +150,14 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
snapshot = snapshotFromNativeWithCaret(source, !document, false, document, document && captureSound)
}
}
trace.progress(snapshot.Role, len(nodes), len(pending)+1, textBytes)
if _, knownApplication := applicationNames[snapshot.BusName]; !knownApplication {
applicationNames[snapshot.BusName] = snapshot.ApplicationName
}
if snapshot.Defunct {
if document && item.parentIndex >= 0 {
// Chromium can retire a descendant after exposing it in its
// parent's child list. Drop that edge and keep the live page.
if (document || reviewOnly) && item.parentIndex >= 0 {
// Providers can retire a descendant after exposing it in its
// parent's child list. Drop that edge and keep the live window.
children := nodes[item.parentIndex].Children
for index, child := range children {
if child == snapshot.ID() {
@@ -168,6 +176,7 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
retained[snapshot.ID()] = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
}
trace.begin_stage("geometry", snapshot.ID(), snapshot.Role)
geometry := cached.geometry
var geometryErr error
if refresh {
@@ -196,6 +205,7 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
// can be the displayed page below it. Preserve the parent viewport.
childClip, childClipped = item.clip, item.clipped
}
trace.begin_stage("visible-text", snapshot.ID(), snapshot.Role)
text := VisibleText{Limited: true}
if (!document || documentText) && textBytes < maxTreeTextBytes {
if known && !cached.textInvalid && !cached.geometryInvalid && !item.clipChanged {
@@ -220,11 +230,11 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
}
}
trace.begin_stage("table-viewport", snapshot.ID(), snapshot.Role)
// Visual review cannot use descendants of hidden controls. Inactive
// notebook pages and closed GTK combo menus can expose thousands of
// children; traversing them can exhaust the entire capture deadline.
// Keep semantic document and desktop/object capture complete.
reviewOnly := !document && reviewBusName != ""
captureChildren := !reviewOnly || !geometry.VisibilityKnown ||
(geometry.Visible && geometry.Showing) || snapshot.Role == "application" || snapshot.Role == "desktop frame"
// A visibility change can reveal children omitted by an earlier review
@@ -238,15 +248,36 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
}
var childCountError *C.GError
childCount := len(cached.children)
var tableChildren []*C.AtspiAccessible
tableViewport := false
if reviewOnly && captureChildren {
var tableErr error
tableChildren, tableViewport, tableErr = native_review_table_children(ctx, source, clip)
if tableErr != nil {
C.g_object_unref(C.gpointer(source))
releasePending()
return ObjectID{}, nil, fmt.Errorf("%w (bus %q path %q role %q)", tableErr, snapshot.BusName, snapshot.Path, snapshot.Role)
}
// The pending stack takes its own reference to each admitted cell.
defer func(children []*C.AtspiAccessible) {
for _, child := range children {
C.g_object_unref(C.gpointer(child))
}
}(tableChildren)
}
trace.begin_stage("child-count", snapshot.ID(), snapshot.Role)
if !captureChildren {
childCount = 0
} else if tableViewport {
childCount = len(tableChildren)
} else if !cachedChildren {
// Snapshot refresh above already cleared dirty entries. Clearing
// again discards the state and interface data just fetched.
childCount = int(C.atspi_accessible_get_child_count(source, &childCountError))
}
if childCountError != nil {
err := nativeError("query AT-SPI child count", childCountError)
err := fmt.Errorf("%w (bus %q path %q role %q captured_nodes %d)",
nativeError("query AT-SPI child count", childCountError), snapshot.BusName, snapshot.Path, snapshot.Role, len(nodes))
C.g_error_free(childCountError)
C.g_object_unref(C.gpointer(source))
releasePending()
@@ -255,7 +286,8 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
if childCount < 0 {
C.g_object_unref(C.gpointer(source))
releasePending()
return ObjectID{}, nil, errors.New("query AT-SPI child count: negative result")
return ObjectID{}, nil, fmt.Errorf("query AT-SPI child count: negative result (bus %q path %q role %q captured_nodes %d)",
snapshot.BusName, snapshot.Path, snapshot.Role, len(nodes))
}
if reviewBusName == "" && len(scheduled)+childCount > maxTreeNodes {
C.g_object_unref(C.gpointer(source))
@@ -263,6 +295,7 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
return ObjectID{}, nil, fmt.Errorf("%w: native tree exceeds limit %d", ErrInvalidTree, maxTreeNodes)
}
trace.begin_stage("child-enumeration", snapshot.ID(), snapshot.Role)
children := make([]ObjectID, 0, childCount)
for index := 0; index < childCount; index++ {
if err := ctx.Err(); err != nil {
@@ -272,7 +305,9 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
}
var childError *C.GError
var child *C.AtspiAccessible
if cachedChildren {
if tableViewport {
child = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(tableChildren[index])))
} else if cachedChildren {
child = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(cache.sources[cached.children[index]])))
} else {
child = C.atspi_accessible_get_child_at_index(source, C.gint(index), &childError)
@@ -328,6 +363,7 @@ func captureNativeTree(ctx context.Context, root *C.AtspiAccessible, reviewBusNa
snapshot.TextChildren = nativeTextChildren(ctx, source, snapshot.CharacterCount, children)
}
nodes = append(nodes, TreeNode{Snapshot: snapshot, Children: children, Geometry: geometry, Text: text})
trace.progress(snapshot.Role, len(nodes), len(pending), textBytes)
C.g_object_unref(C.gpointer(source))
}
return rootID, nodes, nil
@@ -344,6 +380,7 @@ func cthulhuAtspiTreeCaptureCallback(userData unsafe.Pointer) {
return
}
request := <-adapter.treeRequests
capture_diagnostics(request.ctx).begin_stage("callback-setup", request.reviewFocus, "")
adapter.applySecureReset()
if request.ctx.Err() != nil {
request.result <- treeCaptureResult{err: request.ctx.Err()}