87 lines
2.7 KiB
Go
87 lines
2.7 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"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
func (adapter *Adapter) read_native_focused_line(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
|
|
snapshot := snapshotFromNativeWithCaret(source, false, false, false, false)
|
|
if snapshot.Role == "password text" {
|
|
return DocumentContent{}, ErrNoDocumentCaret
|
|
}
|
|
content := DocumentContent{Snapshot: snapshot, Text: snapshot.Name}
|
|
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()
|
|
}
|