Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,142 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in new issue
Block a user