96 lines
3.1 KiB
Go
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
|
|
}
|