Files
cthulhu/internal/app/event_browser_tabs.go

75 lines
3.0 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 (
"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
}