178 lines
6.3 KiB
Go
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
|
|
}
|