Files
cthulhu/internal/atspi/libreoffice_headers.go

110 lines
3.4 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
import (
"errors"
"sync"
)
// Choices contain only identities and coordinates, never provider objects or
// cell text. They survive focus/lock/tab changes and expire on document/window
// destruction, sheet topology/name changes and shutdown. A fixed limit bounds
// missed deaths. Calc recreates its table objects on every sheet switch.
type libreOfficeHeaderKey struct {
window, document ObjectID
sheet string
}
type libreOfficeHeaderChoice struct {
row, column int
rowSet, columnSet bool
}
type libreOfficeHeaderStore struct {
mutex sync.Mutex
choices map[libreOfficeHeaderKey]libreOfficeHeaderChoice
}
func libreOfficeHeaderIdentity(snapshot Snapshot, window ObjectID) (libreOfficeHeaderKey, bool) {
context := snapshot.LibreOffice
key := libreOfficeHeaderKey{window: window}
if context != nil {
key.document = context.Document
key.sheet = context.SheetName
}
valid := IsLibreOfficeProcess(snapshot.ApplicationProcess) && !snapshot.Defunct &&
snapshot.Role == "table cell" && context != nil && context.InSpreadsheet && context.Cell &&
snapshot.TablePositionKnown && snapshot.TableRowIndex >= 0 && snapshot.TableColumnIndex >= 0 &&
key.window.Valid() && key.document.Valid() && key.sheet != "" && snapshot.TablePath != "" &&
key.window.BusName == snapshot.BusName && key.document.BusName == snapshot.BusName
return key, valid
}
// ConfigureLibreOfficeHeader is called only for the event loop's owned cell.
// It copies a choice without querying AT-SPI or waiting for the GLib thread.
// columnHeaders selects the row that supplies column headings; otherwise the
// current column supplies row headings. clear removes only that axis.
func (adapter *Adapter) ConfigureLibreOfficeHeader(snapshot Snapshot, window ObjectID, columnHeaders, clear bool) error {
key, valid := libreOfficeHeaderIdentity(snapshot, window)
if !valid || adapter.secure.Load() {
return errors.New("Not in a Calc cell.")
}
store := &adapter.libreOfficeHeaders
store.mutex.Lock()
defer store.mutex.Unlock()
choice, exists := store.choices[key]
if !exists && !clear && len(store.choices) >= 64 {
return errors.New("Header choices are full. Clear a sheet's headers first.")
}
if columnHeaders {
choice.row, choice.rowSet = snapshot.TableRowIndex, !clear
} else {
choice.column, choice.columnSet = snapshot.TableColumnIndex, !clear
}
if !choice.rowSet && !choice.columnSet {
delete(store.choices, key)
return nil
}
if store.choices == nil {
store.choices = make(map[libreOfficeHeaderKey]libreOfficeHeaderChoice)
}
store.choices[key] = choice
return nil
}
func (store *libreOfficeHeaderStore) get(key libreOfficeHeaderKey) libreOfficeHeaderChoice {
store.mutex.Lock()
defer store.mutex.Unlock()
return store.choices[key]
}
func (store *libreOfficeHeaderStore) forget(id ObjectID) {
store.mutex.Lock()
defer store.mutex.Unlock()
for key := range store.choices {
if id == key.window || id == key.document {
delete(store.choices, key)
}
}
}
func (store *libreOfficeHeaderStore) hasBus(bus string) bool {
store.mutex.Lock()
defer store.mutex.Unlock()
for key := range store.choices {
if key.window.BusName == bus {
return true
}
}
return false
}
func (store *libreOfficeHeaderStore) reset() {
store.mutex.Lock()
defer store.mutex.Unlock()
store.choices = nil
}