190 lines
8.4 KiB
Go
190 lines
8.4 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 (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func nativeError(operation string, nativeError *C.GError) error {
|
|
if nativeError == nil {
|
|
return fmt.Errorf("%s failed", operation)
|
|
}
|
|
return fmt.Errorf("%s: %s", operation, C.GoString(nativeError.message))
|
|
}
|
|
|
|
func snapshotFromNative(source *C.AtspiAccessible) Snapshot {
|
|
return snapshotFromNativeWithCaret(source, true, false, false, true)
|
|
}
|
|
|
|
func snapshotFromNativeWithCaret(source *C.AtspiAccessible, text, caretCharacter, documentMetadata, soundGeometry bool, captureAttributes ...bool) Snapshot {
|
|
return snapshotFromNativeWithOptions(source, text, caretCharacter, documentMetadata, soundGeometry, true, captureAttributes...)
|
|
}
|
|
|
|
func snapshotFromNativeWithOptions(source *C.AtspiAccessible, text, caretCharacter, documentMetadata, soundGeometry, applicationName bool, captureAttributes ...bool) Snapshot {
|
|
captureText := C.gboolean(0)
|
|
if text {
|
|
captureText = 1
|
|
}
|
|
captureCaretCharacter := C.gboolean(0)
|
|
if caretCharacter {
|
|
captureCaretCharacter = 1
|
|
}
|
|
captureDocumentMetadata := C.gboolean(0)
|
|
if documentMetadata {
|
|
captureDocumentMetadata = 1
|
|
}
|
|
captureSoundGeometry := C.gboolean(0)
|
|
if soundGeometry {
|
|
captureSoundGeometry = 1
|
|
}
|
|
captureApplicationName := C.gboolean(0)
|
|
if applicationName {
|
|
captureApplicationName = 1
|
|
}
|
|
captured := C.cthulhu_capture_snapshot(source, captureText, captureCaretCharacter, captureDocumentMetadata, captureSoundGeometry, captureApplicationName)
|
|
textContent := C.GoString(captured.text)
|
|
snapshot := NewSnapshotWithCaretCharacter(
|
|
C.GoString(captured.path),
|
|
C.GoString(captured.name),
|
|
C.GoString(captured.role),
|
|
C.GoString(captured.description),
|
|
textContent,
|
|
C.GoString(captured.caretCharacter),
|
|
)
|
|
snapshot.TextCaretKnown = captured.text != nil && captured.textStartOffset >= 0
|
|
if text && snapshot.Role != "password text" {
|
|
switch snapshot.Role {
|
|
case "button", "push button", "check box", "radio button", "toggle button", "menu item", "check menu item", "radio menu item":
|
|
snapshot.Mnemonic = native_mnemonic(source)
|
|
}
|
|
}
|
|
if snapshot.TextCaretKnown {
|
|
captureBrailleLine(&snapshot, textContent, C.GoString(captured.textBeforeCaret), int(captured.textStartOffset))
|
|
if text := C.atspi_accessible_get_text(source); text != nil {
|
|
snapshot.TextAttributeRun = native_text_attribute_run(text, snapshot.BrailleCaret)
|
|
snapshot.Misspelled = TextAttributesMisspelled(snapshot.TextAttributeRun.Values)
|
|
if len(captureAttributes) != 0 && captureAttributes[0] {
|
|
start := snapshot.BrailleLineStart
|
|
snapshot.TextAttributeRuns = native_text_attribute_runs(text, start, start+len([]rune(snapshot.BrailleLine)), snapshot.TextAttributeRun)
|
|
}
|
|
if C.atspi_text_get_n_selections(text, nil) > 0 {
|
|
if selection := C.atspi_text_get_selection(text, 0, nil); selection != nil {
|
|
snapshot.SelectionStart, snapshot.SelectionEnd = int(selection.start_offset), int(selection.end_offset)
|
|
if snapshot.Role != "password text" && snapshot.SelectionEnd > snapshot.SelectionStart {
|
|
end := min(snapshot.SelectionEnd, snapshot.SelectionStart+maxSnapshotFieldLength)
|
|
selected := C.atspi_text_get_text(text, C.gint(snapshot.SelectionStart), C.gint(end), nil)
|
|
snapshot.SelectionText = strings.ToValidUTF8(C.GoString(selected), "�")
|
|
C.g_free(C.gpointer(selected))
|
|
}
|
|
C.g_free(C.gpointer(selection))
|
|
}
|
|
}
|
|
C.g_object_unref(C.gpointer(text))
|
|
}
|
|
}
|
|
if caretCharacter && captured.editable == 0 && snapshot.Role != "terminal" && strings.ContainsRune(textContent, '\ufffc') {
|
|
budget := embeddedTextBudget{nodes: 32, characters: maxSnapshotFieldLength, deadline: time.Now().Add(100 * time.Millisecond)}
|
|
// Hypertext offsets refer to raw characters, before whitespace folding.
|
|
snapshot.Text = sanitizeSnapshotField(expandNativeEmbeddedText(source, textContent, int(captured.textStartOffset), 0, &budget))
|
|
}
|
|
snapshot.BusName = sanitizeSnapshotField(C.GoString(captured.busName))
|
|
if snapshot.Role == "radio button" {
|
|
group := C.cthulhu_radio_group_name(source)
|
|
snapshot.RadioGroupName = sanitizeSnapshotField(C.GoString(group))
|
|
C.g_free(C.gpointer(group))
|
|
}
|
|
snapshot.ApplicationName = sanitizeSnapshotField(C.GoString(captured.applicationName))
|
|
snapshot.Visible = captured.visible != 0
|
|
snapshot.Showing = captured.showing != 0
|
|
snapshot.StatesKnown = captured.statesKnown != 0
|
|
snapshot.Active = captured.active != 0
|
|
snapshot.Autocomplete = captured.autocomplete != 0 && snapshot.Role == "entry"
|
|
snapshot.Focusable = captured.focusable != 0
|
|
snapshot.Focused = captured.focused != 0
|
|
snapshot.HasPopup = captured.hasPopup != 0
|
|
snapshot.Checked = captured.checked != 0
|
|
snapshot.Defunct = captured.defunct != 0
|
|
snapshot.Editable = captured.editable != 0
|
|
snapshot.Expandable = captured.expandable != 0
|
|
snapshot.Expanded = captured.expanded != 0
|
|
snapshot.Indeterminate = captured.indeterminate != 0
|
|
snapshot.Invalid = captured.invalid != 0
|
|
snapshot.Multiselectable = captured.multiselectable != 0
|
|
snapshot.ReadOnly = captured.readOnly != 0
|
|
snapshot.Required = captured.required != 0
|
|
snapshot.Selected = captured.selected != 0
|
|
snapshot.Sensitive = captured.sensitive != 0
|
|
snapshot.SensitiveKnown = captured.sensitiveKnown != 0
|
|
snapshot.ValueKnown = captured.valueKnown != 0
|
|
snapshot.Visited = captured.visited != 0
|
|
snapshot.ExplicitName = captured.explicitName != 0
|
|
snapshot.HeadingLevel = int(captured.headingLevel)
|
|
snapshot.PositionInSet = int(captured.positionInSet)
|
|
snapshot.SetSize = int(captured.setSize)
|
|
snapshot.SupportsClick = captured.supportsClick != 0
|
|
snapshot.LiveRegion = captured.liveRegion != 0
|
|
snapshot.LayoutTable = captured.layoutTable != 0
|
|
snapshot.ChildCount = int(captured.childCount)
|
|
snapshot.ChildCountKnown = captured.childCountKnown != 0
|
|
snapshot.TableRowCount = int(captured.tableRowCount)
|
|
snapshot.TableColumnCount = int(captured.tableColumnCount)
|
|
snapshot.TableRowIndex = int(captured.tableRowIndex)
|
|
snapshot.TableColumnIndex = int(captured.tableColumnIndex)
|
|
snapshot.TableRowSpan = int(captured.tableRowSpan)
|
|
snapshot.TableColumnSpan = int(captured.tableColumnSpan)
|
|
snapshot.TablePositionKnown = captured.tablePositionKnown != 0
|
|
snapshot.TableRowHeaders = sanitizeSnapshotField(C.GoString(captured.tableRowHeaders))
|
|
snapshot.TableColumnHeaders = sanitizeSnapshotField(C.GoString(captured.tableColumnHeaders))
|
|
C.g_free(C.gpointer(captured.tableRowHeaders))
|
|
C.g_free(C.gpointer(captured.tableColumnHeaders))
|
|
snapshot.CharacterCount = int(captured.characterCount)
|
|
snapshot.EmbeddedObjectCount = int(captured.embeddedObjectCount)
|
|
snapshot.XMLRoles = sanitizeSnapshotField(C.GoString(captured.xmlRoles))
|
|
snapshot.HTMLTag = sanitizeSnapshotField(C.GoString(captured.htmlTag))
|
|
snapshot.ElementID = sanitizeSnapshotField(C.GoString(captured.elementID))
|
|
snapshot.CurrentValue = float64(captured.currentValue)
|
|
snapshot.MaximumValue = float64(captured.maximumValue)
|
|
snapshot.MinimumValue = float64(captured.minimumValue)
|
|
snapshot.MinimumIncrement = float64(captured.minimumIncrement)
|
|
snapshot.ValueText = sanitizeSnapshotField(C.GoString(captured.valueText))
|
|
snapshot.TextBeforeCaret = sanitizeTextContext(C.GoString(captured.textBeforeCaret))
|
|
snapshot.SoundGeometry = soundGeometryFromNative(captured)
|
|
C.g_free(C.gpointer(captured.applicationName))
|
|
C.g_free(C.gpointer(captured.name))
|
|
C.g_free(C.gpointer(captured.role))
|
|
C.g_free(C.gpointer(captured.description))
|
|
C.g_free(C.gpointer(captured.text))
|
|
C.g_free(C.gpointer(captured.textBeforeCaret))
|
|
C.g_free(C.gpointer(captured.caretCharacter))
|
|
C.g_free(C.gpointer(captured.valueText))
|
|
C.g_free(C.gpointer(captured.xmlRoles))
|
|
C.g_free(C.gpointer(captured.htmlTag))
|
|
C.g_free(C.gpointer(captured.elementID))
|
|
return snapshot
|
|
}
|
|
|
|
func soundGeometryFromNative(captured C.cthulhu_snapshot) SoundGeometry {
|
|
return SoundGeometry{
|
|
Object: Rectangle{X: int32(captured.objectX), Y: int32(captured.objectY), Width: int32(captured.objectWidth), Height: int32(captured.objectHeight)},
|
|
Reference: Rectangle{X: int32(captured.referenceX), Y: int32(captured.referenceY), Width: int32(captured.referenceWidth), Height: int32(captured.referenceHeight)},
|
|
ObjectKnown: captured.objectGeometryKnown != 0, ReferenceKnown: captured.referenceGeometryKnown != 0,
|
|
}
|
|
}
|
|
|
|
func nativeObjectID(source *C.AtspiAccessible) ObjectID {
|
|
return NewObjectID(
|
|
C.GoString(C.cthulhu_accessible_bus_name(source)),
|
|
C.GoString(C.cthulhu_accessible_path(source)),
|
|
)
|
|
}
|