119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package atspi
|
|
|
|
/*
|
|
#include <atspi/atspi.h>
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
func (adapter *Adapter) readNativeDocumentLine(ctx context.Context, id ObjectID, snapshot Snapshot, text *C.AtspiText, offset, count int) (DocumentCaretText, error) {
|
|
source := nativeReviewText{text: text}
|
|
line, err := source.line(ctx, offset)
|
|
if err != nil {
|
|
return DocumentCaretText{}, err
|
|
}
|
|
result := DocumentCaretText{Start: line.Start, End: line.End, Text: line.Text, CharacterCount: count}
|
|
if snapshot.Editable || snapshot.Role == "password text" || snapshot.HTMLTag == "pre" || snapshot.HTMLTag == "code" || count > maxReviewLineCharacters ||
|
|
len(strings.Fields(line.Text)) > 1 || line.Start == 0 && line.End == count {
|
|
return result, nil
|
|
}
|
|
if cached, ok := adapter.documentLineCache[id]; ok && cached.CharacterCount == count {
|
|
if cached.End > 0 {
|
|
return cached, nil
|
|
}
|
|
return result, nil
|
|
}
|
|
adapter.treeMutex.Lock()
|
|
tree := adapter.documentTree
|
|
adapter.treeMutex.Unlock()
|
|
if tree != nil {
|
|
if ref, err := tree.Reference(id); err == nil {
|
|
if owner, err := tree.DocumentContext(ref); err == nil {
|
|
for _, ancestor := range owner.Ancestors {
|
|
if ancestor.HTMLTag == "pre" || ancestor.HTMLTag == "code" || strings.Contains(" "+ancestor.XMLRoles+" ", " code ") {
|
|
return result, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var nativeErr *C.GError
|
|
value := C.atspi_text_get_text(text, 0, C.gint(count), &nativeErr)
|
|
if value != nil {
|
|
defer C.g_free(C.gpointer(value))
|
|
}
|
|
if nativeErr != nil {
|
|
C.g_error_free(nativeErr)
|
|
return result, nil
|
|
}
|
|
content := strings.ToValidUTF8(C.GoString(value), "�")
|
|
if utf8.RuneCountInString(content) != count {
|
|
return result, nil
|
|
}
|
|
probe, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
|
repair, probeErr := documentLinesAreTokens(probe, source, content)
|
|
cancel()
|
|
if probeErr != nil {
|
|
return result, ctx.Err()
|
|
}
|
|
if adapter.documentLineCache == nil || len(adapter.documentLineCache) >= 64 {
|
|
adapter.documentLineCache = make(map[ObjectID]DocumentCaretText)
|
|
}
|
|
cached := DocumentCaretText{CharacterCount: count}
|
|
if repair {
|
|
cached.End, cached.Text = count, content
|
|
result = cached
|
|
}
|
|
adapter.documentLineCache[id] = cached
|
|
return result, nil
|
|
}
|
|
|
|
// Repair only owned web text, on the GLib owner. Native selections use the
|
|
// same logical line as virtual browsing, including braille cursor placement.
|
|
func (adapter *Adapter) repairNativeCaretLine(source *C.AtspiAccessible, snapshot *Snapshot, offset int) {
|
|
if !snapshot.TextCaretKnown || snapshot.Editable || snapshot.Role == "password text" || offset < 0 {
|
|
return
|
|
}
|
|
adapter.treeMutex.Lock()
|
|
tree := adapter.documentTree
|
|
adapter.treeMutex.Unlock()
|
|
if tree == nil {
|
|
return
|
|
}
|
|
ref, err := tree.Reference(snapshot.ID())
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, err = tree.DocumentContext(ref); err != nil {
|
|
return
|
|
}
|
|
text := C.atspi_accessible_get_text_iface(source)
|
|
if text == nil {
|
|
return
|
|
}
|
|
defer C.g_object_unref(C.gpointer(text))
|
|
count := int(C.atspi_text_get_character_count(text, nil))
|
|
if count <= 0 || offset > count {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
line, err := adapter.readNativeDocumentLine(ctx, snapshot.ID(), *snapshot, text, min(offset, count-1), count)
|
|
if err != nil || line.Start < 0 || line.End <= line.Start {
|
|
return
|
|
}
|
|
snapshot.Text = sanitizeSnapshotField(line.Text)
|
|
snapshot.BrailleLine, snapshot.BrailleLineStart, snapshot.BrailleCaret = line.Text, line.Start, offset
|
|
snapshot.CharacterCount = count
|
|
}
|