Share native current-line speech with Remote
This commit is contained in:
1 parent
103fbfd81a
commit
ee3d91af23
11 files changed
+289
-16
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReadFocusedLine reads only the line containing the current native caret.
|
||||
@@ -21,7 +22,7 @@ func (adapter *Adapter) ReadFocusedLine(ctx context.Context, focus ObjectID) (Do
|
||||
return result.content, err
|
||||
}
|
||||
|
||||
func (adapter *Adapter) read_native_focused_line(ctx context.Context, focus ObjectID) (DocumentContent, error) {
|
||||
func (adapter *Adapter) read_native_focused_line(ctx context.Context, focus ObjectID) (content DocumentContent, err error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return DocumentContent{}, err
|
||||
}
|
||||
@@ -29,11 +30,17 @@ func (adapter *Adapter) read_native_focused_line(ctx context.Context, focus Obje
|
||||
return DocumentContent{}, ErrStaleObject
|
||||
}
|
||||
source := adapter.nativeFocus[len(adapter.nativeFocus)-1].source
|
||||
C.atspi_accessible_clear_cache_single(source)
|
||||
snapshot := snapshotFromNativeWithCaret(source, false, false, false, false)
|
||||
if snapshot.Role == "password text" {
|
||||
return DocumentContent{}, ErrNoDocumentCaret
|
||||
}
|
||||
content := DocumentContent{Snapshot: snapshot, Text: snapshot.Name}
|
||||
content = DocumentContent{Snapshot: snapshot, Text: snapshot.Name}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
content.SourceContext = native_focused_line_context(ctx, source)
|
||||
}
|
||||
}()
|
||||
text := C.atspi_accessible_get_text_iface(source)
|
||||
if text == nil {
|
||||
return content, ctx.Err()
|
||||
@@ -84,3 +91,57 @@ func (adapter *Adapter) read_native_focused_line(ctx context.Context, focus Obje
|
||||
content.Start, content.End, content.Text, content.RawText = line.Start, line.End, line.Text, line.Text
|
||||
return content, ctx.Err()
|
||||
}
|
||||
|
||||
// Privacy capture is separate from presentation context: include every parent,
|
||||
// even unnamed containers and parents outside the window. Keep it on the GLib
|
||||
// owner and bound the walk without scanning children or retaining native refs.
|
||||
// Failed/truncated capture must not turn a successful local read into an error.
|
||||
func native_focused_line_context(ctx context.Context, source *C.AtspiAccessible) []Snapshot {
|
||||
deadline, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer cancel()
|
||||
current := (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
|
||||
defer func() {
|
||||
if current != nil {
|
||||
C.g_object_unref(C.gpointer(current))
|
||||
}
|
||||
}()
|
||||
var result []Snapshot
|
||||
seen := make(map[ObjectID]bool)
|
||||
for current != nil && len(result) < 64 && deadline.Err() == nil {
|
||||
C.atspi_accessible_clear_cache_single(current)
|
||||
id := nativeObjectID(current)
|
||||
if !id.Valid() || seen[id] {
|
||||
return nil
|
||||
}
|
||||
seen[id] = true
|
||||
var nativeErr *C.GError
|
||||
roleName := C.atspi_accessible_get_role_name(current, &nativeErr)
|
||||
role := C.GoString(roleName)
|
||||
C.g_free(C.gpointer(roleName))
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
return nil
|
||||
}
|
||||
states := C.atspi_accessible_get_state_set(current)
|
||||
if states == nil {
|
||||
return nil
|
||||
}
|
||||
defunct := C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) != 0
|
||||
C.g_object_unref(C.gpointer(states))
|
||||
result = append(result, Snapshot{BusName: id.BusName, Path: id.Path, Role: role, Defunct: defunct})
|
||||
if role == "application" {
|
||||
if deadline.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
parent := C.atspi_accessible_get_parent(current, &nativeErr)
|
||||
C.g_object_unref(C.gpointer(current))
|
||||
current = parent
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -55,6 +55,7 @@ func TestNativeCurrentLine(t *testing.T) {
|
||||
{"text", "Final\n", 6, "", false},
|
||||
{"password", "private test text", 5, "", true},
|
||||
{"button", "", 0, "Example button", false},
|
||||
{"deep", "Deep local line", 3, "Deep local line", false},
|
||||
} {
|
||||
command, _ := json.Marshal(map[string]any{"action": "focus", "control": probe.control, "text": probe.text, "offset": probe.offset})
|
||||
fmt.Fprintln(stdin, string(command))
|
||||
@@ -64,7 +65,7 @@ func TestNativeCurrentLine(t *testing.T) {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
if event.Type == "object:state-changed:focused" && event.Detail1 == 1 &&
|
||||
(event.Source.Name == map[string]string{"entry": "Entry", "text": "Text", "password": "Password", "button": "Example button"}[probe.control]) {
|
||||
(event.Source.Name == map[string]string{"entry": "Entry", "text": "Text", "password": "Password", "button": "Example button", "deep": "Deep entry"}[probe.control]) {
|
||||
focus = event.Source.ID()
|
||||
goto focused
|
||||
}
|
||||
@@ -85,6 +86,33 @@ func TestNativeCurrentLine(t *testing.T) {
|
||||
if err != nil || strings.TrimRight(content.Text, "\r\n") != probe.want {
|
||||
t.Fatalf("%s offset %d: %+v %v, want %q", probe.control, probe.offset, content, err, probe.want)
|
||||
}
|
||||
if probe.control == "deep" {
|
||||
if len(content.SourceContext) != 0 {
|
||||
t.Fatal("truncated native ancestry authorized export")
|
||||
}
|
||||
} else if len(content.SourceContext) == 0 || content.SourceContext[0].ID() != focus ||
|
||||
content.SourceContext[len(content.SourceContext)-1].Role != "application" {
|
||||
t.Fatalf("native report lost complete source ancestry: %+v", content.SourceContext)
|
||||
}
|
||||
if probe.control == "entry" && probe.want != "" {
|
||||
for _, role := range []struct{ atk, want string }{{"PASSWORD_TEXT", "password text"}, {"UNKNOWN", "unknown"}, {"INVALID", "invalid"}} {
|
||||
fmt.Fprintf(stdin, "{\"action\":\"privacy\",\"role\":%q}\n", role.atk)
|
||||
ready()
|
||||
private, err := adapter.ReadFocusedLine(ctx, focus)
|
||||
if err != nil || private.Text != content.Text {
|
||||
t.Fatal("ancestor privacy changed local text", err)
|
||||
}
|
||||
found := false
|
||||
for _, ancestor := range private.SourceContext {
|
||||
found = found || ancestor.Role == role.want
|
||||
}
|
||||
if len(private.SourceContext) != 0 && !found {
|
||||
t.Fatal("native capture omitted a private ancestor", role.want)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(stdin, "{\"action\":\"privacy\",\"role\":\"FILLER\"}")
|
||||
ready()
|
||||
}
|
||||
if probe.control != "button" {
|
||||
fmt.Fprintf(stdin, "{\"action\":\"caret\",\"control\":%q}\n", probe.control)
|
||||
if !scanner.Scan() || scanner.Text() != fmt.Sprint(probe.offset) {
|
||||
|
||||
@@ -15,6 +15,9 @@ type DocumentContent struct {
|
||||
End int
|
||||
Text string
|
||||
RawText string
|
||||
// SourceContext is the focused-line source followed by its ancestors through
|
||||
// the application. Nil means privacy capture failed; local text stays usable.
|
||||
SourceContext []Snapshot
|
||||
// Separator is display spacing before this object, outside its native range.
|
||||
Separator string
|
||||
Bounds Rectangle
|
||||
|
||||
Reference in new issue
Block a user