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,238 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (adapter *Adapter) clearMouseApplication() {
|
||||
if adapter.mouseApplication != nil {
|
||||
C.g_object_unref(C.gpointer(adapter.mouseApplication))
|
||||
}
|
||||
adapter.mouseApplication = nil
|
||||
adapter.mouseApplicationPID = 0
|
||||
}
|
||||
|
||||
func (adapter *Adapter) mouseApplicationForPID(ctx context.Context, pid int) *C.AtspiAccessible {
|
||||
if pid <= 0 || ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
if adapter.mouseApplicationPID == pid && adapter.mouseApplication != nil {
|
||||
return adapter.mouseApplication
|
||||
}
|
||||
adapter.clearMouseApplication()
|
||||
desktop := C.atspi_get_desktop(0)
|
||||
if desktop == nil {
|
||||
return nil
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(desktop))
|
||||
count := min(int(C.atspi_accessible_get_child_count(desktop, nil)), 128)
|
||||
for i := 0; i < count && ctx.Err() == nil; i++ {
|
||||
app := C.atspi_accessible_get_child_at_index(desktop, C.gint(i), nil)
|
||||
if app == nil {
|
||||
continue
|
||||
}
|
||||
if int(C.atspi_accessible_get_process_id(app, nil)) == pid {
|
||||
adapter.mouseApplication, adapter.mouseApplicationPID = app, pid
|
||||
return app
|
||||
}
|
||||
C.g_object_unref(C.gpointer(app))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nativeContainsPoint(source *C.AtspiAccessible, x, y int) bool {
|
||||
component := C.atspi_accessible_get_component_iface(source)
|
||||
if component == nil {
|
||||
return false
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(component))
|
||||
var nativeErr *C.GError
|
||||
inside := C.atspi_component_contains(component, C.gint(x), C.gint(y), C.ATSPI_COORD_TYPE_SCREEN, &nativeErr) != 0
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
return false
|
||||
}
|
||||
return inside
|
||||
}
|
||||
|
||||
func nativePointArea(source *C.AtspiAccessible) int64 {
|
||||
component := C.atspi_accessible_get_component_iface(source)
|
||||
if component == nil {
|
||||
return 1 << 62
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(component))
|
||||
var nativeErr *C.GError
|
||||
rect := C.atspi_component_get_extents(component, C.ATSPI_COORD_TYPE_SCREEN, &nativeErr)
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
}
|
||||
if rect == nil {
|
||||
return 1 << 62
|
||||
}
|
||||
defer C.g_free(C.gpointer(rect))
|
||||
if rect.width <= 0 || rect.height <= 0 {
|
||||
return 1 << 62
|
||||
}
|
||||
return int64(rect.width) * int64(rect.height)
|
||||
}
|
||||
|
||||
func nativeAccessibleAtPoint(source *C.AtspiAccessible, x, y int) *C.AtspiAccessible {
|
||||
component := C.atspi_accessible_get_component_iface(source)
|
||||
if component == nil {
|
||||
return nil
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(component))
|
||||
var nativeErr *C.GError
|
||||
child := C.atspi_component_get_accessible_at_point(component, C.gint(x), C.gint(y), C.ATSPI_COORD_TYPE_SCREEN, &nativeErr)
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
||||
func nativePointText(ctx context.Context, source, window *C.AtspiAccessible, x, y int) (word, line string, lineStart, nativeOffset int) {
|
||||
if C.atspi_accessible_get_role(source, nil) == C.ATSPI_ROLE_PASSWORD_TEXT {
|
||||
return "", "", -1, -1
|
||||
}
|
||||
text := C.atspi_accessible_get_text_iface(source)
|
||||
if text == nil {
|
||||
return "", "", -1, -1
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(text))
|
||||
var nativeErr *C.GError
|
||||
offset := C.atspi_text_get_offset_at_point(text, C.gint(x), C.gint(y), C.ATSPI_COORD_TYPE_SCREEN, &nativeErr)
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
nativeErr = nil
|
||||
}
|
||||
if offset < 0 {
|
||||
component := C.atspi_accessible_get_component_iface(window)
|
||||
if component != nil {
|
||||
rect := C.atspi_component_get_extents(component, C.ATSPI_COORD_TYPE_SCREEN, &nativeErr)
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
nativeErr = nil
|
||||
}
|
||||
if rect != nil {
|
||||
offset = C.atspi_text_get_offset_at_point(text, C.gint(x)-rect.x, C.gint(y)-rect.y, C.ATSPI_COORD_TYPE_WINDOW, &nativeErr)
|
||||
C.g_free(C.gpointer(rect))
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
nativeErr = nil
|
||||
}
|
||||
}
|
||||
C.g_object_unref(C.gpointer(component))
|
||||
}
|
||||
}
|
||||
if offset < 0 {
|
||||
// GTK text entries can report -1 at points inside displayed glyphs.
|
||||
// Inspect a bounded number of character rectangles as a fallback.
|
||||
count := min(int(C.atspi_text_get_character_count(text, nil)), 128)
|
||||
for i := 0; i < count && ctx.Err() == nil; i++ {
|
||||
rect := C.atspi_text_get_character_extents(text, C.gint(i), C.ATSPI_COORD_TYPE_SCREEN, &nativeErr)
|
||||
if nativeErr != nil {
|
||||
C.g_error_free(nativeErr)
|
||||
nativeErr = nil
|
||||
}
|
||||
if rect == nil {
|
||||
continue
|
||||
}
|
||||
inside := C.gint(x) >= rect.x && C.gint(x) < rect.x+rect.width &&
|
||||
C.gint(y) >= rect.y && C.gint(y) < rect.y+rect.height
|
||||
C.g_free(C.gpointer(rect))
|
||||
if inside {
|
||||
offset = C.gint(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if offset < 0 {
|
||||
return "", "", -1, -1
|
||||
}
|
||||
read := func(granularity C.AtspiTextGranularity) (string, int) {
|
||||
var err *C.GError
|
||||
value := C.atspi_text_get_string_at_offset(text, offset, granularity, &err)
|
||||
if err != nil {
|
||||
C.g_error_free(err)
|
||||
}
|
||||
if value == nil {
|
||||
return "", -1
|
||||
}
|
||||
defer C.g_free(C.gpointer(value))
|
||||
defer C.g_free(C.gpointer(value.content))
|
||||
return sanitizeSnapshotField(C.GoString(value.content)), int(value.start_offset)
|
||||
}
|
||||
word, _ = read(C.ATSPI_TEXT_GRANULARITY_WORD)
|
||||
line, lineStart = read(C.ATSPI_TEXT_GRANULARITY_LINE)
|
||||
return strings.TrimSpace(word), strings.TrimSpace(line), lineStart, int(offset)
|
||||
}
|
||||
|
||||
// nativeReviewAtPoint runs on the GLib owner. It retains only the current
|
||||
// application; all window and descendant references are released per query.
|
||||
func (adapter *Adapter) nativeReviewAtPoint(ctx context.Context, x, y, pid int) (PointReview, error) {
|
||||
var result PointReview
|
||||
app := adapter.mouseApplicationForPID(ctx, pid)
|
||||
if app == nil || ctx.Err() != nil {
|
||||
return result, ctx.Err()
|
||||
}
|
||||
var window *C.AtspiAccessible
|
||||
bestArea := int64(1 << 62)
|
||||
count := min(int(C.atspi_accessible_get_child_count(app, nil)), 64)
|
||||
for i := 0; i < count && ctx.Err() == nil; i++ {
|
||||
candidate := C.atspi_accessible_get_child_at_index(app, C.gint(i), nil)
|
||||
if candidate == nil {
|
||||
continue
|
||||
}
|
||||
if nativeContainsPoint(candidate, x, y) {
|
||||
area := nativePointArea(candidate)
|
||||
if window == nil || area <= bestArea {
|
||||
if window != nil {
|
||||
C.g_object_unref(C.gpointer(window))
|
||||
}
|
||||
window, bestArea = candidate, area
|
||||
continue
|
||||
}
|
||||
}
|
||||
C.g_object_unref(C.gpointer(candidate))
|
||||
}
|
||||
if window == nil || ctx.Err() != nil {
|
||||
if window != nil {
|
||||
C.g_object_unref(C.gpointer(window))
|
||||
}
|
||||
return result, ctx.Err()
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(window))
|
||||
result.Window = nativeObjectID(window)
|
||||
current := (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(window)))
|
||||
defer func() { C.g_object_unref(C.gpointer(current)) }()
|
||||
for depth := 0; depth < 24 && ctx.Err() == nil; depth++ {
|
||||
child := nativeAccessibleAtPoint(current, x, y)
|
||||
if child == nil {
|
||||
break
|
||||
}
|
||||
if nativeObjectID(child) == nativeObjectID(current) || !nativeContainsPoint(child, x, y) {
|
||||
C.g_object_unref(C.gpointer(child))
|
||||
break
|
||||
}
|
||||
C.g_object_unref(C.gpointer(current))
|
||||
current = child
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return PointReview{}, ctx.Err()
|
||||
}
|
||||
result.Snapshot = snapshotFromNativeWithOptions(current, false, false, false, false, true)
|
||||
if result.Snapshot.Defunct || result.Snapshot.Role == "password text" {
|
||||
return result, nil
|
||||
}
|
||||
result.Word, result.Line, result.LineStart, result.Offset = nativePointText(ctx, current, window, x, y)
|
||||
return result, nil
|
||||
}
|
||||
Reference in new issue
Block a user