239 lines
7.2 KiB
Go
239 lines
7.2 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 (
|
|
"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
|
|
}
|