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,151 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user