Files

143 lines
4.6 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
/*
#cgo pkg-config: atspi-2 glib-2.0 gobject-2.0
#include "adapter_native.h"
*/
import "C"
import (
"context"
"unicode/utf8"
)
// BrailleRoute refers to the exact source span which produced the display.
// Revalidate it on the GLib thread so edits or focus changes cannot route a
// delayed hardware key into different content.
type BrailleRoute struct {
Focus ObjectID
Position ObjectReference // Valid only for document browse routing.
Line string
Start, Offset int
}
func (adapter *Adapter) RouteBraille(ctx context.Context, route BrailleRoute) error {
_, err := adapter.documentOperation(ctx, treeCaptureRequest{brailleRoute: &route})
return err
}
func validBrailleRoute(snapshot Snapshot, route BrailleRoute) bool {
return route.Focus.Valid() && snapshot.ID() == route.Focus && !snapshot.Defunct && snapshot.StatesKnown && snapshot.Focused &&
snapshot.Role != "password text" && snapshot.TextCaretKnown &&
snapshot.BrailleLineStart == route.Start && snapshot.BrailleLine == route.Line &&
route.Start >= 0 && route.Offset >= route.Start &&
route.Offset-route.Start <= utf8.RuneCountInString(route.Line)
}
func (adapter *Adapter) routeNativeBraille(ctx context.Context, route BrailleRoute) error {
if err := ctx.Err(); err != nil {
return err
}
if route.Position.Valid() {
return adapter.route_document_braille(ctx, route)
}
if !adapter.nativeFocusIs(route.Focus) {
return ErrStaleObject
}
source := adapter.nativeFocus[len(adapter.nativeFocus)-1].source
// Request caret capture even for read-only text. Ordinary focus snapshots
// deliberately avoid querying non-editable text on the event hot path.
snapshot := snapshotFromNativeWithCaret(source, true, true, false, false)
if !validBrailleRoute(snapshot, route) {
return ErrStaleObject
}
text := C.atspi_accessible_get_text_iface(source)
if text == nil {
return ErrNoDocumentCaret
}
defer C.g_object_unref(C.gpointer(text))
if err := ctx.Err(); err != nil {
return err
}
var nativeErr *C.GError
placed := C.atspi_text_set_caret_offset(text, C.gint(route.Offset), &nativeErr)
if nativeErr != nil {
err := nativeError("route braille caret", nativeErr)
C.g_error_free(nativeErr)
return err
}
if placed == 0 {
return ErrNoDocumentCaret
}
return nil
}
func (adapter *Adapter) route_document_braille(ctx context.Context, route BrailleRoute) error {
adapter.treeMutex.Lock()
tree := adapter.documentTree
adapter.treeMutex.Unlock()
if tree == nil || len(adapter.nativeFocus) == 0 {
return ErrStaleObject
}
snapshot, err := tree.Resolve(route.Position)
if err != nil || snapshot.Role == "password text" || snapshot.Defunct {
return ErrStaleObject
}
if err = documentPositionMatchesFocus(tree, route.Position, route.Focus); err != nil {
return err
}
latest := adapter.nativeFocus[len(adapter.nativeFocus)-1].id
if err = documentPositionMatchesFocus(tree, route.Position, latest); err != nil {
return err
}
source := adapter.nativeDocument[route.Position.ID()]
if source == nil {
return ErrStaleObject
}
// Recheck the protected state and foreground before reading fresh content.
C.atspi_accessible_clear_cache_single(source)
fresh := snapshotFromNativeWithCaret(source, false, false, false, false)
if fresh.Role != snapshot.Role || fresh.Defunct || fresh.Role == "password text" {
return ErrStaleObject
}
window := C.cthulhu_window_ancestor(source)
if window == nil {
return ErrStaleObject
}
foreground := adapter.matchesForeground(window, nativeObjectID(window))
C.g_object_unref(C.gpointer(window))
if !foreground {
return ErrStaleObject
}
text := C.atspi_accessible_get_text_iface(source)
if text == nil {
return ErrNoDocumentCaret
}
defer C.g_object_unref(C.gpointer(text))
length := utf8.RuneCountInString(route.Line)
if route.Start < 0 || route.Offset < route.Start || route.Offset > route.Start+length || length == 0 {
return ErrStaleObject
}
count := int(C.atspi_text_get_character_count(text, nil))
if route.Start+length > count {
return ErrStaleObject
}
var nativeErr *C.GError
content := C.atspi_text_get_text(text, C.gint(route.Start), C.gint(route.Start+length), &nativeErr)
if nativeErr != nil {
err := nativeError("validate document braille text", nativeErr)
C.g_error_free(nativeErr)
return err
}
unchanged := C.GoString(content) == route.Line
C.g_free(C.gpointer(content))
if !unchanged {
return ErrStaleObject
}
return adapter.setNativeDocumentPosition(ctx, route.Position, route.Focus, route.Offset, true)
}