// 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 } }