Files
2026-10-15 01:10:43 -04:00

106 lines
4.9 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/braille"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/presentation"
)
// prepareLibreOfficeEvent follows current Orca's soffice focus/caret policy.
// A Calc active cell is the logical focus even when editing destroyed the old
// focus or a sheet switch changed its managed container (LO bug 161444).
func prepareLibreOfficeEvent(event atspi.Event, current atspi.Snapshot, owner windowOwner) applicationEventResult {
result := applicationEventResult{event: event}
context := event.Source.LibreOffice
if !atspi.IsLibreOfficeProcess(event.Source.ApplicationProcess) ||
event.Source.Defunct || !event.Source.ID().Valid() ||
owner.window.Valid() && (eventWindow(event) != owner.window || event.Source.BusName != owner.applicationBus) {
return result
}
if isFocusGainEvent(event) && event.Type != atspi.WindowActivatedEvent &&
current.LibreOffice != nil && current.LibreOffice.InSpreadsheet {
// LibreOffice also claims focus for ancestors of the real focused
// object. Orca ignores these; copied ancestry is enough to identify
// them without another native tree query.
for _, ancestor := range current.BrailleContext {
if ancestor.ID().Valid() && ancestor.ID() == event.Source.ID() {
result.suppress = true
return result
}
}
}
if context != nil && context.InSpreadsheet && context.Cell && event.Source.Role == "table cell" &&
event.Type == atspi.ActiveDescendantEvent &&
(event.Source.Focused || context.ContainerFocused) {
// Preserve NativeType/OriginPath for diagnostics. Ordinary focus
// handling now owns review, duplicate suppression, and Braille.
if result.event.NativeType == "" {
result.event.NativeType = event.Type
}
result.event.Type = atspi.FocusEvent
}
if context != nil && context.InSpreadsheet && event.Type == atspi.SelectionChangedEvent {
// Selection can name the first cell of a range rather than the
// active cell. Dedicated selection presentation consumes copied
// sheet metadata without changing logical focus or review.
result.suppress = true
}
if context != nil && context.InSpreadsheet && event.Source.Role == "table" &&
isFocusGainEvent(event) && event.Type != atspi.WindowActivatedEvent &&
current.LibreOffice != nil &&
(current.LibreOffice.CellEditor || current.LibreOffice.Cell && current.TablePath == event.Source.Path) {
// Orca treats this as the sheet reclaiming focus after editing.
// Own it silently; the following active cell supplies presentation.
result.adoptFocus, result.suppress = true, true
}
if context != nil && context.Cell && isFocusGainEvent(result.event) && current.ID() != event.Source.ID() {
// Returning to the cell after typing must speak it even if its
// value is unchanged; typing uses a separate presentation history.
result.resetPresentation = true
}
if context != nil && context.CellEditor && event.Source.Role == "paragraph" &&
(isTextChangeEvent(event.Type) || event.Type == atspi.CaretEvent) &&
owner.window.Valid() && event.Source.ID() != current.ID() &&
(event.Source.Focused || context.EditorParent == current.ID() && current.LibreOffice != nil && current.LibreOffice.CellEditor) {
// Empty-cell F2 can focus a panel before its paragraph exists.
// Admit that editor's first mutation before generic text filtering.
result.adoptFocus = true
}
if event.Type == atspi.CaretEvent && current.LibreOffice != nil && current.LibreOffice.Cell &&
(context == nil || !context.CellEditor) {
// Formula-bar/background caret events must not displace the cell.
result.suppress = true
}
return result
}
func libreOfficeBrailleFrame(snapshot atspi.Snapshot, options config.BrailleOptions) (braille.Frame, bool) {
context := snapshot.LibreOffice
if !atspi.IsLibreOfficeProcess(snapshot.ApplicationProcess) || snapshot.Role != "table cell" ||
context == nil || !context.InSpreadsheet || !context.Cell {
return braille.Frame{}, false
}
// Orca presents the value followed by its cell address. This is a
// component, not an editable-text route; cell focus has no text caret.
return braille.Frame{Text: brailleJoin(context.CellText, context.Coordinates), Cursor: 0,
WordWrap: options.WordWrap}, true
}
// The event loop owns the accepted focus snapshot. Formula commands share its
// foreground/lock gates and output queue, with no separate application state.
func (handler *eventReviewHandler) presentCellFormula() {
if handler.focusGate.Locked() || handler.focusGate.Suspended() {
return
}
handler.syncForeground()
handler.state.pending.clear()
text := presentation.LibreOfficeFormulaText(handler.state.current)
handler.speech.Interrupt()
handler.speech.Enqueue(speechRequest{Text: text, Role: "system", Cause: "cell-formula"})
}