Files
cthulhu/internal/atspi/libreoffice_native.go
2026-10-15 01:10:43 -04:00

178 lines
6.3 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 "unsafe"
// libreOfficeFocusTarget resolves a Calc editor's direct text children and
// preserves the sheet source for selection events. F2 focuses its temporary
// panel; the paragraph can appear later, on typing.
// The callback owns and releases the returned reference. No sheet is walked.
func (adapter *Adapter) libreOfficeFocusTarget(event *C.AtspiEvent, eventType string) *C.AtspiAccessible {
if eventType != FocusEvent && eventType != FocusedStateEvent && eventType != SelectionChangedEvent ||
!IsLibreOfficeProcess(adapter.applicationProcess(event.source)) {
return nil
}
role := C.atspi_accessible_get_role(event.source, nil)
if eventType == SelectionChangedEvent {
// Keep the sheet as the selection source. Its first selected cell
// is not necessarily the active cell (and can be far offscreen).
if role == C.ATSPI_ROLE_TABLE {
parent := C.atspi_accessible_get_parent(event.source, nil)
if parent != nil {
spreadsheet := C.atspi_accessible_get_role(parent, nil) == C.ATSPI_ROLE_DOCUMENT_SPREADSHEET
C.g_object_unref(C.gpointer(parent))
if spreadsheet {
return (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(event.source)))
}
}
}
return nil
}
if role == C.ATSPI_ROLE_TABLE {
parent := C.atspi_accessible_get_parent(event.source, nil)
if parent != nil {
if C.atspi_accessible_get_role(parent, nil) == C.ATSPI_ROLE_DOCUMENT_SPREADSHEET {
// Orca clears stale table state around Calc focus transitions.
C.atspi_accessible_clear_cache_single(event.source)
}
C.g_object_unref(C.gpointer(parent))
}
return nil
}
if role != C.ATSPI_ROLE_PANEL && role != C.ATSPI_ROLE_EXTENDED {
return nil
}
parent := C.atspi_accessible_get_parent(event.source, nil)
if parent == nil {
return nil
}
spreadsheet := C.atspi_accessible_get_role(parent, nil) == C.ATSPI_ROLE_DOCUMENT_SPREADSHEET
C.g_object_unref(C.gpointer(parent))
if !spreadsheet {
return nil
}
count := min(int(C.atspi_accessible_get_child_count(event.source, nil)), 4)
for index := 0; index < count; index++ {
child := C.atspi_accessible_get_child_at_index(event.source, C.gint(index), nil)
if child == nil {
continue
}
states := C.atspi_accessible_get_state_set(child)
focused := states != nil && C.atspi_state_set_contains(states, C.ATSPI_STATE_FOCUSED) != 0 &&
C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) == 0
if states != nil {
C.g_object_unref(C.gpointer(states))
}
if focused && C.atspi_accessible_get_role(child, nil) == C.ATSPI_ROLE_PARAGRAPH {
return child
}
C.g_object_unref(C.gpointer(child))
}
return nil
}
func captureLibreOfficeContext(source *C.AtspiAccessible, snapshot *Snapshot) {
if !IsLibreOfficeProcess(snapshot.ApplicationProcess) || snapshot.Defunct {
return
}
switch snapshot.Role {
case "table", "table cell", "paragraph", "panel", "extended":
default:
return
}
context := LibreOfficeContext{}
parentRole := C.AtspiRole(C.ATSPI_ROLE_INVALID)
grandparentRole := C.AtspiRole(C.ATSPI_ROLE_INVALID)
current := (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
for depth := 0; current != nil && depth < 32; depth++ {
role := C.atspi_accessible_get_role(current, nil)
if depth == 1 {
parentRole = role
context.EditorParent = nativeObjectID(current)
}
if depth == 2 {
grandparentRole = role
}
if role == C.ATSPI_ROLE_TABLE {
states := C.atspi_accessible_get_state_set(current)
if states != nil {
context.ContainerFocused = C.atspi_state_set_contains(states, C.ATSPI_STATE_FOCUSED) != 0
C.g_object_unref(C.gpointer(states))
}
}
if role == C.ATSPI_ROLE_DOCUMENT_SPREADSHEET {
context.InSpreadsheet = true
C.g_object_unref(C.gpointer(current))
current = nil
break
}
if role == C.ATSPI_ROLE_FRAME || role == C.ATSPI_ROLE_DIALOG || role == C.ATSPI_ROLE_APPLICATION {
C.g_object_unref(C.gpointer(current))
current = nil
break
}
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))
}
if !context.InSpreadsheet {
return
}
context.Cell = snapshot.Role == "table cell"
context.CellEditor = (snapshot.Role == "panel" || snapshot.Role == "extended") &&
parentRole == C.ATSPI_ROLE_DOCUMENT_SPREADSHEET ||
snapshot.Role == "paragraph" && (parentRole == C.ATSPI_ROLE_PANEL || parentRole == C.ATSPI_ROLE_EXTENDED) &&
grandparentRole == C.ATSPI_ROLE_DOCUMENT_SPREADSHEET
if context.Cell {
context.Coordinates = snapshot.Name
// Orca requests uncached attributes: editing can replace a formula
// without replacing the accessible cell.
C.atspi_accessible_clear_cache_single(source)
attributes := C.atspi_accessible_get_attributes(source, nil)
if attributes != nil {
columnKey, rowKey := C.CString("colindextext"), C.CString("rowindextext")
column := C.GoString((*C.gchar)(C.g_hash_table_lookup(attributes, C.gconstpointer(columnKey))))
row := C.GoString((*C.gchar)(C.g_hash_table_lookup(attributes, C.gconstpointer(rowKey))))
C.free(unsafe.Pointer(columnKey))
C.free(unsafe.Pointer(rowKey))
if column != "" && row != "" {
context.Coordinates = sanitizeSnapshotField(column + row)
}
// Current providers use lowercase; Orca also accepts Formula.
for _, key := range []string{"formula", "Formula"} {
name := C.CString(key)
value := C.g_hash_table_lookup(attributes, C.gconstpointer(name))
C.free(unsafe.Pointer(name))
if value != nil {
context.Formula = sanitizeLibreOfficeFormula(C.GoString((*C.gchar)(value)))
break
}
}
C.g_hash_table_unref(attributes)
}
text := C.atspi_accessible_get_text_iface(source)
if text != nil {
// Calc's value is Text, while Name is the coordinate. Cell caret
// offsets can be invalid, so capture contents independently.
length := max(0, min(int(C.atspi_text_get_character_count(text, nil)), maxSnapshotFieldLength))
content := C.atspi_text_get_text(text, 0, C.gint(length), nil)
context.CellText = sanitizeTextContext(C.GoString(content))
C.g_free(C.gpointer(content))
C.g_object_unref(C.gpointer(text))
}
}
snapshot.Spreadsheet = true
snapshot.LibreOffice = &context
}