// 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/input" ) const browserButtonNameDelay = 350 * time.Millisecond // The event loop retains only the latest focused web button name. Focus and // Braille update immediately; speech waits for the label to settle. Document // generation and input identity prevent a delayed name from interrupting a // later action, including leaving and returning to the same object. type pendingBrowserButtonName struct { pendingPresentation document atspi.ObjectID generation uint64 inputSequence uint64 } func (handler *accessibilityEventHandler) defer_browser_button_name(event atspi.Event, activity input.Activity) bool { browser := handler.browser if event.Type != atspi.AccessibleNameEvent || event.Source.LiveRegion || event.Source.Editable || browser == nil || !browser.ready || !browser.document.Valid() || event.Source.BusName != browser.document.BusName || event.Source.ID() != browser.focus || event.Source.ID() != handler.state.focusedID { return false } switch event.Source.Role { case "button", "push button", "toggle button": default: return false } pending := &handler.state.pending.browserButtonName // Duplicate notifications must not postpone an already settled label. if pending.event.Source.ID() == event.Source.ID() && pending.event.Source.Name == event.Source.Name && pending.document == browser.document && pending.generation == browser.generation && pending.inputSequence == activity.TitleInterruptionSequence { return true } pending.schedule(event, input.NoIntent, browserButtonNameDelay) pending.document, pending.generation = browser.document, browser.generation pending.inputSequence = activity.TitleInterruptionSequence handler.logger.Debug("coalesced focused web button name", "bus", event.Source.BusName, "source_path", event.Source.Path) return true } func (handler *accessibilityEventHandler) flush_browser_button_name() { pending := &handler.state.pending.browserButtonName document, generation, inputSequence := pending.document, pending.generation, pending.inputSequence event, _ := pending.take() browser := handler.browser if event.Type == "" || handler.focusGate.Locked() || handler.focusGate.Suspended() || browser == nil || !browser.ready || browser.document != document || browser.generation != generation || browser.RecentInput().TitleInterruptionSequence != inputSequence || browser.focus != event.Source.ID() || handler.state.focusedID != event.Source.ID() || handler.state.current.ID() != event.Source.ID() || handler.state.owner.window != eventWindow(event) { return } // State events may have refreshed the same control during the quiet period. event.Source = handler.state.current handler.speak(event) }