106 lines
4.9 KiB
Go
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"})
|
|
}
|