162 lines
5.0 KiB
Go
162 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 (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
type AccessibleAction struct{ Name, Label string }
|
|
type ActionTarget struct {
|
|
Object, Window, Focus ObjectID
|
|
Epoch uint64
|
|
Role string
|
|
}
|
|
type ActionSet struct {
|
|
Target ActionTarget
|
|
Actions []AccessibleAction
|
|
}
|
|
|
|
func (adapter *Adapter) AccessibleActions(ctx context.Context, object ObjectID) (ActionSet, error) {
|
|
result, err := adapter.documentOperation(ctx, treeCaptureRequest{listActions: true, reviewFocus: object})
|
|
return result.actions, err
|
|
}
|
|
func (adapter *Adapter) PerformAccessibleAction(ctx context.Context, target ActionTarget, name string) error {
|
|
_, err := adapter.documentOperation(ctx, treeCaptureRequest{actionTarget: &target, actionName: name})
|
|
return err
|
|
}
|
|
|
|
// Native action work stays on the GLib owner, using retained review/document
|
|
// handles as well as focus. Never redirect a stale action to a new focus.
|
|
func (adapter *Adapter) native_action_source(object ObjectID) (*C.AtspiAccessible, ActionTarget, error) {
|
|
var source *C.AtspiAccessible
|
|
for _, item := range adapter.nativeFocus {
|
|
if item.id == object {
|
|
source = item.source
|
|
break
|
|
}
|
|
}
|
|
if source == nil {
|
|
source = adapter.nativeReview[object]
|
|
}
|
|
if source == nil {
|
|
source = adapter.nativeDocument[object]
|
|
}
|
|
if source == nil {
|
|
return nil, ActionTarget{}, ErrObjectNotFound
|
|
}
|
|
C.atspi_accessible_clear_cache_single(source)
|
|
states := C.atspi_accessible_get_state_set(source)
|
|
if states == nil {
|
|
return nil, ActionTarget{}, ErrStaleObject
|
|
}
|
|
defunct := C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) != 0
|
|
C.g_object_unref(C.gpointer(states))
|
|
if defunct {
|
|
return nil, ActionTarget{}, ErrStaleObject
|
|
}
|
|
window := C.cthulhu_window_ancestor(source)
|
|
if window == nil {
|
|
return nil, ActionTarget{}, ErrStaleObject
|
|
}
|
|
defer C.g_object_unref(C.gpointer(window))
|
|
adapter.refreshForeground()
|
|
windowID := nativeObjectID(window)
|
|
if !adapter.matchesForeground(window, windowID) {
|
|
return nil, ActionTarget{}, ErrStaleObject
|
|
}
|
|
role := C.atspi_accessible_get_role_name(source, nil)
|
|
defer C.g_free(C.gpointer(role))
|
|
target := ActionTarget{Object: object, Window: windowID, Epoch: adapter.Foreground().Epoch, Role: C.GoString(role)}
|
|
if len(adapter.nativeFocus) != 0 {
|
|
target.Focus = adapter.nativeFocus[len(adapter.nativeFocus)-1].id
|
|
}
|
|
return source, target, nil
|
|
}
|
|
|
|
func (adapter *Adapter) native_actions(ctx context.Context, object ObjectID, expected *ActionTarget, name string) (ActionSet, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return ActionSet{}, err
|
|
}
|
|
source, target, err := adapter.native_action_source(object)
|
|
if err != nil {
|
|
return ActionSet{}, err
|
|
}
|
|
if expected != nil && target != *expected {
|
|
return ActionSet{}, ErrStaleObject
|
|
}
|
|
action := C.atspi_accessible_get_action_iface(source)
|
|
if action == nil {
|
|
return ActionSet{Target: target}, nil
|
|
}
|
|
defer C.g_object_unref(C.gpointer(action))
|
|
var nativeErr *C.GError
|
|
count := int(C.atspi_action_get_n_actions(action, &nativeErr))
|
|
if nativeErr != nil {
|
|
C.g_error_free(nativeErr)
|
|
return ActionSet{}, errors.New("could not read accessible actions")
|
|
}
|
|
if count < 0 || count > 256 {
|
|
return ActionSet{}, errors.New("invalid accessible action count")
|
|
}
|
|
result := ActionSet{Target: target}
|
|
seen := map[string]bool{}
|
|
for index := 0; index < count; index++ {
|
|
if err := ctx.Err(); err != nil {
|
|
return ActionSet{}, err
|
|
}
|
|
rawName := C.atspi_action_get_name(action, C.gint(index), nil)
|
|
actionName := sanitizeSnapshotField(C.GoString(rawName))
|
|
C.g_free(C.gpointer(rawName))
|
|
if actionName == "" || seen[actionName] {
|
|
continue
|
|
}
|
|
seen[actionName] = true
|
|
if expected != nil && actionName == name {
|
|
if adapter.secure.Load() || ctx.Err() != nil {
|
|
return ActionSet{}, ErrStaleObject
|
|
}
|
|
// Recheck ownership immediately before the external side effect.
|
|
_, current, checkErr := adapter.native_action_source(object)
|
|
if checkErr != nil || current != target {
|
|
return ActionSet{}, ErrStaleObject
|
|
}
|
|
if C.atspi_action_do_action(action, C.gint(index), &nativeErr) == 0 {
|
|
if nativeErr != nil {
|
|
C.g_error_free(nativeErr)
|
|
}
|
|
return ActionSet{}, errors.New("accessible action failed")
|
|
}
|
|
if nativeErr != nil {
|
|
C.g_error_free(nativeErr)
|
|
return ActionSet{}, errors.New("accessible action failed")
|
|
}
|
|
return result, nil
|
|
}
|
|
labelRaw := C.atspi_action_get_localized_name(action, C.gint(index), nil)
|
|
label := sanitizeSnapshotField(C.GoString(labelRaw))
|
|
C.g_free(C.gpointer(labelRaw))
|
|
if label == "" {
|
|
description := C.atspi_action_get_description(action, C.gint(index), nil)
|
|
label = sanitizeSnapshotField(C.GoString(description))
|
|
C.g_free(C.gpointer(description))
|
|
}
|
|
if label == "" {
|
|
label = actionName
|
|
}
|
|
result.Actions = append(result.Actions, AccessibleAction{Name: actionName, Label: label})
|
|
}
|
|
if expected != nil {
|
|
return ActionSet{}, ErrStaleObject
|
|
}
|
|
return result, nil
|
|
}
|