152 lines
5.0 KiB
Go
152 lines
5.0 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package atspi
|
|
|
|
/*
|
|
#include "adapter_native.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
func native_mnemonic(source *C.AtspiAccessible) string {
|
|
action := C.atspi_accessible_get_action_iface(source)
|
|
if action == nil {
|
|
return ""
|
|
}
|
|
defer C.g_object_unref(C.gpointer(action))
|
|
if C.atspi_action_get_n_actions(action, nil) <= 0 {
|
|
return ""
|
|
}
|
|
binding := C.atspi_action_get_key_binding(action, 0, nil)
|
|
defer C.g_free(C.gpointer(binding))
|
|
parts := strings.Split(C.GoString(binding), ";")
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
// AT-SPI's first field is the mnemonic. Empty mnemonic fields must not
|
|
// turn an unrelated accelerator into a fabricated mnemonic.
|
|
return sanitizeSnapshotField(parts[0])
|
|
}
|
|
|
|
func native_misspelled(text *C.AtspiText, offset int) bool {
|
|
run := native_text_attribute_run(text, offset)
|
|
return TextAttributesMisspelled(run.Values)
|
|
}
|
|
|
|
func native_text_attribute_run(text *C.AtspiText, offset int) TextAttributeRun {
|
|
if offset < 0 {
|
|
return TextAttributeRun{}
|
|
}
|
|
var start, end C.gint
|
|
attributes := C.atspi_text_get_attribute_run(text, C.gint(offset), 0, &start, &end, nil)
|
|
if attributes == nil {
|
|
return TextAttributeRun{}
|
|
}
|
|
defer C.g_hash_table_unref(attributes)
|
|
run := TextAttributeRun{Start: int(start), End: int(end), Values: make(map[string]string)}
|
|
for _, key := range spokenCharacterAttributeKeys {
|
|
name := C.CString(key)
|
|
value := C.g_hash_table_lookup(attributes, C.gconstpointer(unsafe.Pointer(name)))
|
|
C.free(unsafe.Pointer(name))
|
|
if value != nil {
|
|
run.Values[key] = sanitizeSnapshotField(C.GoString((*C.char)(value)))
|
|
}
|
|
}
|
|
return run
|
|
}
|
|
|
|
// Each provider request can block. Stop after a bounded number of runs and a
|
|
// short deadline; the caret run is still available if a provider is slow.
|
|
func native_text_attribute_runs(text *C.AtspiText, start, end int, caret TextAttributeRun) []TextAttributeRun {
|
|
if start < 0 || end <= start {
|
|
return nil
|
|
}
|
|
deadline := time.Now().Add(60 * time.Millisecond)
|
|
var runs []TextAttributeRun
|
|
for offset := start; offset < end && len(runs) < 32 && time.Now().Before(deadline); {
|
|
run := caret
|
|
if offset < run.Start || offset >= run.End {
|
|
run = native_text_attribute_run(text, offset)
|
|
}
|
|
if run.End <= offset {
|
|
break
|
|
}
|
|
runs = append(runs, run)
|
|
offset = run.End
|
|
}
|
|
return runs
|
|
}
|
|
|
|
// Queries stay on the GLib owner. This copies only container metadata, bounded
|
|
// by depth and time, and never scans children or retains native references.
|
|
func native_speech_context(source *C.AtspiAccessible) ([]Snapshot, []Snapshot) {
|
|
result := make([]Snapshot, 0)
|
|
brailleContext := make([]Snapshot, 0)
|
|
speechOpen := true
|
|
current := C.atspi_accessible_get_parent(source, nil)
|
|
deadline := time.Now().Add(100 * time.Millisecond)
|
|
for depth := 0; current != nil && depth < 16 && time.Now().Before(deadline); depth++ {
|
|
roleName := C.atspi_accessible_get_role_name(current, nil)
|
|
role := C.GoString(roleName)
|
|
C.g_free(C.gpointer(roleName))
|
|
if role == "application" || role == "frame" || role == "window" || role == "dialog" {
|
|
speechOpen = false
|
|
}
|
|
id := nativeObjectID(current)
|
|
item := Snapshot{BusName: id.BusName, Path: id.Path, Role: role}
|
|
name := C.atspi_accessible_get_name(current, nil)
|
|
item.Name = sanitizeSnapshotField(C.GoString(name))
|
|
C.g_free(C.gpointer(name))
|
|
if role == "application" {
|
|
brailleContext = append(brailleContext, item)
|
|
break
|
|
}
|
|
if speechOpen {
|
|
switch role {
|
|
case "block quote", "blockquote", "list", "list box", "table", "tree table", "panel", "grouping", "landmark", "form", "section", "document spreadsheet", "link":
|
|
if (role == "list" || role == "list box") && time.Now().Before(deadline) {
|
|
var childError *C.GError
|
|
count := C.atspi_accessible_get_child_count(current, &childError)
|
|
item.ChildCount, item.ChildCountKnown = int(count), childError == nil && count >= 0
|
|
if childError != nil {
|
|
C.g_error_free(childError)
|
|
}
|
|
}
|
|
if role == "section" || role == "form" || role == "landmark" || role == "table" {
|
|
attributes := C.atspi_accessible_get_attributes(current, nil)
|
|
if attributes != nil {
|
|
for _, field := range []struct {
|
|
key string
|
|
target *string
|
|
}{{"xml-roles", &item.XMLRoles}, {"tag", &item.HTMLTag}} {
|
|
key := C.CString(field.key)
|
|
*field.target = sanitizeSnapshotField(C.GoString((*C.char)(C.g_hash_table_lookup(attributes, C.gconstpointer(unsafe.Pointer(key))))))
|
|
C.free(unsafe.Pointer(key))
|
|
}
|
|
key := C.CString("layout-guess")
|
|
item.LayoutTable = C.GoString((*C.char)(C.g_hash_table_lookup(attributes, C.gconstpointer(unsafe.Pointer(key))))) == "true"
|
|
C.free(unsafe.Pointer(key))
|
|
C.g_hash_table_unref(attributes)
|
|
}
|
|
}
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
brailleContext = append(brailleContext, item)
|
|
parent := C.atspi_accessible_get_parent(current, nil)
|
|
C.g_object_unref(C.gpointer(current))
|
|
current = parent
|
|
}
|
|
if current != nil {
|
|
C.g_object_unref(C.gpointer(current))
|
|
}
|
|
return result, brailleContext
|
|
}
|