// 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 }