// Cthulhu screen reader. // Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "strings" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/presentation" ) // handleBrowserTab presents a selected tab while leaving native focus in chrome. func (handler *accessibilityEventHandler) handleBrowserTab(event atspi.Event, activity input.Activity) bool { state := handler.state browser := handler.browser objectReview := handler.objectReview speech := handler.speech brailleOutput := handler.brailleOutput live := handler.live if event.Source.BrowserTab && (event.Type == atspi.SelectedStateEvent || event.Type == atspi.SelectionChangedEvent) { // Python's web page-switch handler presents the tab without moving // the locus of focus. Repeated Ctrl+T can keep the same address // control focused and produce no further focus events at all. selected := event.Type == atspi.SelectedStateEvent && event.Detail1 != 0 || event.Type == atspi.SelectionChangedEvent && event.Source.Selected && strings.EqualFold(event.Source.ToolkitName, "Gecko") if !selected || event.Source.StatesKnown && !event.Source.Selected || event.Source.Defunct || !state.owner.containsLoad(event) || event.Source.ID() == state.selectedBrowserTab { return true } state.selectedBrowserTab = event.Source.ID() state.tabFocusWindow, state.tabFocusSequence = eventWindow(event), activity.Sequence state.pending.caret.clear() state.documentLineFocus, state.documentLineRequest, state.documentLineEvent = atspi.ObjectID{}, 0, atspi.Event{} state.loadingDocument, state.loadedDocument = atspi.ObjectID{}, atspi.ObjectID{} state.chromeLoadedDocument, state.chromeLoadedWindow = atspi.ObjectID{}, atspi.ObjectID{} if browser != nil { browser.clear() } if objectReview != nil { focus := atspi.ObjectID{} if state.current.Autocomplete { focus = state.current.ID() } objectReview.ResetDocument(focus) } spoken := presentation.FocusSpeechWithOptions(event.Source, state.presentationOptions) text := spoken.Text if text != "" { speech.Enqueue(speechRequest{Text: text, Chunks: spoken.Chunks, Role: event.Source.Role, Cause: event.Type}) } if brailleOutput != nil { brailleOptions := config.DefaultSettings().Braille textAttributes := config.DefaultTextAttributes() if live != nil { settings := live.Load() brailleOptions, textAttributes = settings.Braille, settings.TextAttributes } brailleOutput.EnqueueFrame(brailleFocusFrameWithOptions(event.Source, brailleOptions, textAttributes)) } return true } if event.Source.Autocomplete && eventWindow(event) == state.tabFocusWindow && activity.Sequence != 0 && activity.Sequence == state.tabFocusSequence && (event.Type == atspi.CaretEvent || event.Type == "object:state-changed:expanded") { return true // Automatic address reset must not cut off the selected tab. } return false }