Files

96 lines
3.1 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package atspi
import (
"context"
"errors"
)
var ErrNoReviewWindow = errors.New("focused object has no review window")
// ReviewSnapshot owns one window's nodes and their generation-checked
// references. Keeping references alongside nodes prevents a reused object ID
// from inheriting an independent review position from a removed object.
type ReviewSnapshot struct {
Nodes []TreeNode
References map[ObjectID]ObjectReference
}
// ReviewWindow copies the focused menu, dialog, or window in tree order under
// one read lock. A consumer must not assemble layout from separately locked
// getters: replacement between getters could mix two different captures.
// Stale descendants fail the whole operation rather than yielding partial
// screen content. Call this on the review worker, never the input/event loop.
func (tree *AccessibleTree) ReviewWindow(ctx context.Context, focus ObjectID) (ReviewSnapshot, error) {
tree.mu.RLock()
defer tree.mu.RUnlock()
if err := ctx.Err(); err != nil {
return ReviewSnapshot{}, err
}
entry, ok := tree.nodes[focus]
if !ok {
return ReviewSnapshot{}, ErrObjectNotFound
}
root := focus
for {
switch entry.snapshot.Role {
case "menu", "popup menu", "dialog", "alert", "frame", "window":
goto found
}
if !entry.hasParent {
return ReviewSnapshot{}, ErrNoReviewWindow
}
root = entry.parent
entry = tree.nodes[root]
}
found:
var nodes []TreeNode
references := make(map[ObjectID]ObjectReference)
pending := []ObjectID{root}
for len(pending) > 0 {
if err := ctx.Err(); err != nil {
return ReviewSnapshot{}, err
}
id := pending[len(pending)-1]
pending = pending[:len(pending)-1]
entry, ok := tree.nodes[id]
if !ok {
return ReviewSnapshot{}, ErrStaleObject
}
if entry.childrenInvalid {
return ReviewSnapshot{}, ErrIncompleteTree
}
if entry.geometryInvalid {
return ReviewSnapshot{}, ErrStaleGeometry
}
if entry.textInvalid {
return ReviewSnapshot{}, ErrStaleText
}
text := entry.text
text.Ranges = append([]TextRange(nil), text.Ranges...)
nodes = append(nodes, TreeNode{Snapshot: entry.snapshot, Geometry: entry.geometry,
Text: text, Children: append([]ObjectID(nil), entry.children...)})
references[id] = newObjectReference(id, entry)
for i := len(entry.children) - 1; i >= 0; i-- {
pending = append(pending, entry.children[i])
}
}
return ReviewSnapshot{Nodes: nodes, References: references}, nil
}