// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "fmt" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" ) type libreOfficeHeaderConfigurer interface { ConfigureLibreOfficeHeader(atspi.Snapshot, atspi.ObjectID, bool, bool) error } func (handler *eventReviewHandler) configureCalcHeader(command input.Command) { if handler.focusGate.Locked() || handler.focusGate.Suspended() { return } handler.syncForeground() state := handler.state snapshot := state.current context := snapshot.LibreOffice text := "Not in a Calc cell." if handler.calcHeaders != nil && context != nil && context.Cell && context.InSpreadsheet && !snapshot.Defunct && atspi.IsLibreOfficeProcess(snapshot.ApplicationProcess) && state.owner.window.Valid() && state.owner.applicationBus == snapshot.BusName && state.owner.focus == snapshot.ID() && snapshot.TablePositionKnown { columnHeaders := command == input.SetColumnHeadersRow || command == input.ClearColumnHeadersRow clear := command == input.ClearColumnHeadersRow || command == input.ClearRowHeadersColumn if err := handler.calcHeaders.ConfigureLibreOfficeHeader(snapshot, state.owner.window, columnHeaders, clear); err != nil { text = err.Error() } else if columnHeaders { // The command changes this axis immediately. Its text is captured // again on the next cell event, never queried by the event loop. state.current.TableColumnHeaders = "" text = fmt.Sprintf("Row %d set as column headers.", snapshot.TableRowIndex+1) if clear { text = "Column headers cleared." } } else { state.current.TableRowHeaders = "" text = fmt.Sprintf("Column %s set as row headers.", calcColumnLabel(snapshot.TableColumnIndex)) if clear { text = "Row headers cleared." } } if handler.brailleOutput != nil { options := config.DefaultSettings().Braille if handler.live != nil { options = handler.live.Load().Braille } state.brailleSnapshot = state.current handler.brailleOutput.EnqueueFrame(brailleFocusFrameWithOptions(state.current, options)) } } state.pending.clear() handler.speech.Enqueue(speechRequest{Text: text, Role: "system", Cause: "calc-headers"}) } func calcColumnLabel(column int) string { label := "" for column >= 0 { label = string(rune('A'+column%26)) + label column = column/26 - 1 } return label } // Use existing table presentation history, which belongs to the event loop. // Keep the full snapshot for Where Am I and Braille; only automatic speech // omits headings for an unchanged axis, following current Orca. func (state *eventLoopState) calcHeaderSpeechEvent(event atspi.Event) atspi.Event { snapshot := event.Source if snapshot.LibreOffice == nil || !snapshot.LibreOffice.Cell || !atspi.IsLibreOfficeProcess(snapshot.ApplicationProcess) || !snapshot.TablePositionKnown || !state.lastTableCell.Valid() || state.lastTable != calcSheet(snapshot) { return event } if snapshot.TableRowIndex == state.lastTableRow { event.Source.TableRowHeaders = "" } if snapshot.TableColumnIndex == state.lastTableColumn { event.Source.TableColumnHeaders = "" } return event }