143 lines
4.6 KiB
Go
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)
|
|
}
|