168 lines
5.5 KiB
Go
168 lines
5.5 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package atspi
|
|
|
|
/*
|
|
#cgo pkg-config: atspi-2 glib-2.0 gobject-2.0
|
|
#include <atspi/atspi.h>
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ReadFocusedLine reads only the line containing the current native caret.
|
|
func (adapter *Adapter) ReadFocusedLine(ctx context.Context, focus ObjectID) (DocumentContent, error) {
|
|
result, err := adapter.documentOperation(ctx, treeCaptureRequest{reviewFocus: focus, readFocusedLine: true})
|
|
return result.content, err
|
|
}
|
|
|
|
// ReadFocusedSnapshot captures presentation and privacy metadata together on
|
|
// the GLib owner. It neither moves focus nor changes the native caret.
|
|
func (adapter *Adapter) ReadFocusedSnapshot(ctx context.Context, focus ObjectID) (DocumentContent, error) {
|
|
result, err := adapter.documentOperation(ctx, treeCaptureRequest{reviewFocus: focus, readFocusedSnapshot: true})
|
|
return result.content, err
|
|
}
|
|
|
|
func (adapter *Adapter) read_native_focused_snapshot(ctx context.Context, focus ObjectID) (DocumentContent, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return DocumentContent{}, err
|
|
}
|
|
if !adapter.nativeFocusIs(focus) {
|
|
return DocumentContent{}, ErrStaleObject
|
|
}
|
|
source := adapter.nativeFocus[len(adapter.nativeFocus)-1].source
|
|
C.atspi_accessible_clear_cache_single(source)
|
|
snapshot := snapshotFromNativeWithCaret(source, true, false, false, false)
|
|
return DocumentContent{Snapshot: snapshot, SourceContext: native_focused_line_context(ctx, source)}, ctx.Err()
|
|
}
|
|
|
|
func (adapter *Adapter) read_native_focused_line(ctx context.Context, focus ObjectID) (content DocumentContent, err error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return DocumentContent{}, err
|
|
}
|
|
if !adapter.nativeFocusIs(focus) {
|
|
return DocumentContent{}, ErrStaleObject
|
|
}
|
|
source := adapter.nativeFocus[len(adapter.nativeFocus)-1].source
|
|
C.atspi_accessible_clear_cache_single(source)
|
|
snapshot := snapshotFromNativeWithCaret(source, false, false, false, false)
|
|
if snapshot.Role == "password text" {
|
|
return DocumentContent{}, ErrNoDocumentCaret
|
|
}
|
|
content = DocumentContent{Snapshot: snapshot, Text: snapshot.Name}
|
|
defer func() {
|
|
if err == nil {
|
|
content.SourceContext = native_focused_line_context(ctx, source)
|
|
}
|
|
}()
|
|
text := C.atspi_accessible_get_text_iface(source)
|
|
if text == nil {
|
|
return content, ctx.Err()
|
|
}
|
|
defer C.g_object_unref(C.gpointer(text))
|
|
reader := nativeReviewText{text: text, terminal: snapshot.Role == "terminal"}
|
|
count, err := reader.count(ctx)
|
|
if err != nil {
|
|
return DocumentContent{}, err
|
|
}
|
|
content.Text = ""
|
|
if count == 0 {
|
|
return content, ctx.Err()
|
|
}
|
|
var nativeErr *C.GError
|
|
offset := int(C.atspi_text_get_caret_offset(text, &nativeErr))
|
|
if nativeErr != nil {
|
|
err := nativeError("read current line caret", nativeErr)
|
|
C.g_error_free(nativeErr)
|
|
return DocumentContent{}, err
|
|
}
|
|
// A text object without a caret reads from its first line, as NVDA does.
|
|
offset = min(max(0, offset), count)
|
|
if offset == count {
|
|
last := C.atspi_text_get_text(text, C.gint(count-1), C.gint(count), &nativeErr)
|
|
if last != nil {
|
|
defer C.g_free(C.gpointer(last))
|
|
}
|
|
if nativeErr != nil {
|
|
err := nativeError("read current line end", nativeErr)
|
|
C.g_error_free(nativeErr)
|
|
return DocumentContent{}, err
|
|
}
|
|
// A trailing newline puts the caret on an empty final line.
|
|
if strings.HasSuffix(C.GoString(last), "\n") {
|
|
content.Start, content.End = count, count
|
|
return content, ctx.Err()
|
|
}
|
|
offset--
|
|
}
|
|
line, err := reader.line(ctx, offset)
|
|
if err != nil {
|
|
return DocumentContent{}, err
|
|
}
|
|
if line.Start < 0 || line.Start > offset || line.End <= offset || line.End > count {
|
|
return DocumentContent{}, errors.New("read current line: invalid text range")
|
|
}
|
|
content.Start, content.End, content.Text, content.RawText = line.Start, line.End, line.Text, line.Text
|
|
return content, ctx.Err()
|
|
}
|
|
|
|
// Privacy capture is separate from presentation context: include every parent,
|
|
// even unnamed containers and parents outside the window. Keep it on the GLib
|
|
// owner and bound the walk without scanning children or retaining native refs.
|
|
// Failed/truncated capture must not turn a successful local read into an error.
|
|
func native_focused_line_context(ctx context.Context, source *C.AtspiAccessible) []Snapshot {
|
|
deadline, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
|
defer cancel()
|
|
current := (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
|
|
defer func() {
|
|
if current != nil {
|
|
C.g_object_unref(C.gpointer(current))
|
|
}
|
|
}()
|
|
var result []Snapshot
|
|
seen := make(map[ObjectID]bool)
|
|
for current != nil && len(result) < 64 && deadline.Err() == nil {
|
|
C.atspi_accessible_clear_cache_single(current)
|
|
id := nativeObjectID(current)
|
|
if !id.Valid() || seen[id] {
|
|
return nil
|
|
}
|
|
seen[id] = true
|
|
var nativeErr *C.GError
|
|
roleName := C.atspi_accessible_get_role_name(current, &nativeErr)
|
|
role := C.GoString(roleName)
|
|
C.g_free(C.gpointer(roleName))
|
|
if nativeErr != nil {
|
|
C.g_error_free(nativeErr)
|
|
return nil
|
|
}
|
|
states := C.atspi_accessible_get_state_set(current)
|
|
if states == nil {
|
|
return nil
|
|
}
|
|
defunct := C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) != 0
|
|
C.g_object_unref(C.gpointer(states))
|
|
result = append(result, Snapshot{BusName: id.BusName, Path: id.Path, Role: role, Defunct: defunct})
|
|
if role == "application" {
|
|
if deadline.Err() != nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
parent := C.atspi_accessible_get_parent(current, &nativeErr)
|
|
C.g_object_unref(C.gpointer(current))
|
|
current = parent
|
|
if nativeErr != nil {
|
|
C.g_error_free(nativeErr)
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|