Files
cthulhu/internal/app/gui_table_presentation.go

100 lines
4.2 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/presentation"
)
type guiTablePresentation struct {
options presentation.Options
table atspi.ObjectID
sequence uint64
newDescription bool
skip bool
}
func (state *eventLoopState) prepareGUITable(event atspi.Event, sequence uint64) guiTablePresentation {
if event.Type == atspi.WindowActivatedEvent {
state.tableDescriptionWindow = atspi.ObjectID{}
state.tableDescriptionText = ""
state.lastTable = atspi.ObjectID{}
state.lastTableCell = atspi.ObjectID{}
} else if isFocusGainEvent(event) && event.Source.Role != "table" && event.Source.Role != "table cell" {
state.lastTable = atspi.ObjectID{}
state.lastTableCell = atspi.ObjectID{}
}
result := guiTablePresentation{
options: state.presentationOptions,
table: atspi.NewObjectID(event.Source.BusName, event.Source.TablePath),
sequence: sequence,
}
result.newDescription = state.presentationOptions.SpeakDescription && event.Window.Valid() &&
(event.Source.Role == "table" || event.Source.Role == "table cell") &&
event.WindowDescription != "" && event.WindowDescription != event.Source.Description &&
(state.tableDescriptionWindow != event.Window || state.tableDescriptionText != event.WindowDescription)
sameRow := event.Source.Role == "table cell" &&
event.Source.TablePositionKnown && result.table.Valid() && result.table == state.lastTable &&
event.Source.TableRowIndex == state.lastTableRow
if sameRow {
unchangedRow := presentation.FocusTextWithOptions(event.Source, state.presentationOptions) == state.lastTableText
if unchangedRow || event.Source.ID() != state.lastTableCell {
result.options.ReadFullRowInGUITable = false
if result.options.Reading != nil {
reading := *result.options.Reading
reading.ReadFullRowInDocumentTable, reading.ReadFullRowInSpreadSheet = false, false
result.options.Reading = &reading
}
}
sameAction := sequence != 0 && sequence == state.lastTableSequence ||
sequence == 0 && time.Since(state.lastTableAt) < 75*time.Millisecond
result.skip = sameAction && !result.newDescription && unchangedRow &&
(state.lastTableFullRow || event.Source.TableColumnIndex == state.lastTableColumn)
}
return result
}
func (state *eventLoopState) decorateGUITable(event atspi.Event, spoken presentation.Speech, result guiTablePresentation) (presentation.Speech, bool) {
if event.Source.Role == "table cell" && event.Source.GUITable && result.table.Valid() &&
result.table != state.lastTable && event.Source.TableName != "" &&
(isFocusGainEvent(event) || isManagedDescendantEvent(event.Type)) &&
event.Type != atspi.WindowActivatedEvent {
spoken = spoken.Prepend(event.Source.TableName, ", ")
}
appendFocus := isFocusGainEvent(event) && event.Type != atspi.WindowActivatedEvent &&
state.tableDescriptionWindow.Valid() && event.Window == state.tableDescriptionWindow &&
time.Since(state.tableDescriptionAt) < 300*time.Millisecond
if result.newDescription {
if event.Type != atspi.WindowActivatedEvent {
spoken = spoken.Append(event.WindowDescription, ", ")
}
state.tableDescriptionWindow = event.Window
state.tableDescriptionText = event.WindowDescription
state.tableDescriptionAt = time.Now()
}
return spoken, appendFocus
}
func (state *eventLoopState) rememberGUITable(event atspi.Event, result guiTablePresentation) {
if event.Source.Role == "table cell" && event.Source.TablePositionKnown && result.table.Valid() {
state.lastTable = result.table
state.lastTableCell = event.Source.ID()
state.lastTableText = presentation.FocusTextWithOptions(event.Source, state.presentationOptions)
state.lastTableRow = event.Source.TableRowIndex
state.lastTableColumn = event.Source.TableColumnIndex
state.lastTableSequence = result.sequence
state.lastTableAt = time.Now()
state.lastTableFullRow = presentation.FullRowEnabled(event.Source, result.options) && event.Source.TableRowText != ""
} else if event.Source.Role == "table" && isFocusGainEvent(event) {
state.lastTable = event.Source.ID()
state.lastTableCell = atspi.ObjectID{}
state.lastTableRow = -1
}
}