119 lines
4.6 KiB
Go
119 lines
4.6 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/presentation"
|
|
)
|
|
|
|
// Owned by the event loop, with one bounded snapshot for the focused sheet.
|
|
// Retired on sheet/application focus changes and the ordinary focus reset
|
|
// (including lock, destruction, foreground loss, and shutdown). No timers or
|
|
// provider references survive a selection event.
|
|
type libreOfficeSelectionState struct {
|
|
sheet atspi.ObjectID
|
|
previous *atspi.LibreOfficeSelection
|
|
}
|
|
|
|
func calcSheet(snapshot atspi.Snapshot) atspi.ObjectID {
|
|
if !atspi.IsLibreOfficeProcess(snapshot.ApplicationProcess) || snapshot.LibreOffice == nil || !snapshot.LibreOffice.InSpreadsheet {
|
|
return atspi.ObjectID{}
|
|
}
|
|
if snapshot.Role == "table" {
|
|
return snapshot.ID()
|
|
}
|
|
if snapshot.LibreOffice.Cell {
|
|
return atspi.NewObjectID(snapshot.BusName, snapshot.TablePath)
|
|
}
|
|
return atspi.ObjectID{}
|
|
}
|
|
|
|
func (handler *accessibilityEventHandler) handleLibreOfficeSelection(event atspi.Event) {
|
|
state := handler.state
|
|
if isFocusGainEvent(event) && state.libreOfficeSelection.sheet != calcSheet(event.Source) {
|
|
state.libreOfficeSelection = libreOfficeSelectionState{}
|
|
}
|
|
context := event.Source.LibreOffice
|
|
if event.Type != atspi.SelectionChangedEvent || context == nil || context.Selection == nil ||
|
|
event.Source.Role != "table" || event.Source.Defunct || !context.InSpreadsheet ||
|
|
!atspi.IsLibreOfficeProcess(event.Source.ApplicationProcess) ||
|
|
handler.focusGate.Suspended() || !state.owner.window.Valid() ||
|
|
state.owner.window != eventWindow(event) || state.owner.applicationBus != event.Source.BusName ||
|
|
calcSheet(state.current) != event.Source.ID() {
|
|
return
|
|
}
|
|
history := &state.libreOfficeSelection
|
|
if history.sheet != event.Source.ID() {
|
|
*history = libreOfficeSelectionState{sheet: event.Source.ID()}
|
|
}
|
|
selection := context.Selection
|
|
handler.logger.Debug("received Calc selection snapshot", "kind", selection.Kind,
|
|
"count", selection.Count, "count_limited", selection.CountLimited,
|
|
"complete", selection.Complete, "cells", selection.Cells,
|
|
"first", selection.First, "last", selection.Last)
|
|
previous := history.previous
|
|
history.previous = selection
|
|
if previous != nil && previous.Kind == selection.Kind && previous.Count == selection.Count &&
|
|
previous.CountLimited == selection.CountLimited && previous.Complete == selection.Complete &&
|
|
previous.First == selection.First && previous.Last == selection.Last && slices.Equal(previous.Cells, selection.Cells) {
|
|
return
|
|
}
|
|
reading := state.presentationOptions.Reading
|
|
if handler.live != nil {
|
|
settings := handler.live.Load()
|
|
reading = &settings.Reading
|
|
}
|
|
if reading != nil && reading.OnlySpeakDisplayedText {
|
|
return
|
|
}
|
|
var messages []string
|
|
if previous != nil && (previous.Count != 0 || previous.Kind == "all") {
|
|
if selection.Count == 0 && selection.Kind != "all" {
|
|
messages = append(messages, "Selection cleared.")
|
|
} else if previous.Kind == selection.Kind && previous.Kind != "all" && previous.Complete && selection.Complete {
|
|
removed := *previous
|
|
removed.Cells = calcSelectionDifference(previous.Cells, selection.Cells)
|
|
removed.Count = len(removed.Cells)
|
|
if text := presentation.LibreOfficeSelectionText(removed, "unselected"); text != "" {
|
|
messages = append(messages, text)
|
|
}
|
|
}
|
|
}
|
|
added := *selection
|
|
if selection.Complete && (selection.Kind == "cells" || previous != nil && previous.Kind == selection.Kind) {
|
|
if previous != nil && previous.Complete && previous.Kind == selection.Kind {
|
|
added.Cells = calcSelectionDifference(selection.Cells, previous.Cells)
|
|
}
|
|
if selection.Kind == "cells" && state.current.TablePositionKnown {
|
|
// Orca leaves the anchor to ordinary active-cell presentation.
|
|
anchor := atspi.LibreOfficeCell{Row: state.current.TableRowIndex, Column: state.current.TableColumnIndex}
|
|
added.Cells = calcSelectionDifference(added.Cells, []atspi.LibreOfficeCell{anchor})
|
|
}
|
|
added.Count = len(added.Cells)
|
|
}
|
|
if text := presentation.LibreOfficeSelectionText(added, "selected"); text != "" {
|
|
messages = append(messages, text)
|
|
}
|
|
if text := strings.Join(messages, " "); text != "" {
|
|
state.pending.clear()
|
|
handler.speech.Interrupt()
|
|
handler.speech.Enqueue(speechRequest{Text: text, Role: "system", Cause: "calc-selection"})
|
|
}
|
|
}
|
|
|
|
func calcSelectionDifference(first, second []atspi.LibreOfficeCell) []atspi.LibreOfficeCell {
|
|
var result []atspi.LibreOfficeCell
|
|
for _, cell := range first {
|
|
if !slices.Contains(second, cell) {
|
|
result = append(result, cell)
|
|
}
|
|
}
|
|
return result
|
|
}
|