Files

86 lines
3.0 KiB
Go

// Copyright (c) 2026 Storm Dragon
// 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"
"git.stormux.org/storm/cthulhu/internal/presentation"
"git.stormux.org/storm/cthulhu/internal/sound"
)
const radioFocusSoundDelay = 250 * time.Millisecond
// GTK can focus an unchecked radio before selecting it. Retain one state cue
// in the event loop while speech and Braille update immediately. A checked
// event replaces it; focus/input changes, lock and shutdown retire it.
type pendingRadioSound struct {
pendingPresentation
inputSequence uint64
}
func (presenter *eventPresenter) coalesce_radio_sound(event atspi.Event, cues []sound.Cue) []sound.Cue {
pending := &presenter.state.pending.radioSound
if isFocusGainEvent(event) && event.Source.ID() != pending.event.Source.ID() {
pending.clear()
}
if event.Source.Role != "radio button" {
return cues
}
if event.Type == "object:state-changed:checked" || event.Source.Checked {
pending.clear()
return cues
}
if !isFocusGainEvent(event) {
return cues
}
for index, cue := range cues {
if cue.Name != "radiobutton_unchecked" {
continue
}
sequence := recentInput(presenter.inputSource).InputSequence
if pending.event.Source.ID() != event.Source.ID() || pending.inputSequence != sequence || pending.ready == nil {
pending.schedule(event, input.NoIntent, radioFocusSoundDelay)
pending.inputSequence = sequence
presenter.logger.Debug("deferred unchecked radio focus sound")
}
retained := append([]sound.Cue(nil), cues[:index]...)
return append(retained, cues[index+1:]...)
}
return cues
}
func (presenter *eventPresenter) flush_radio_sound(gate *focusGate) {
pending := &presenter.state.pending.radioSound
sequence := pending.inputSequence
event, _ := pending.take()
state := presenter.state
if event.Type == "" || gate.Locked() || gate.Suspended() ||
state.focusedID != event.Source.ID() || state.current.ID() != event.Source.ID() ||
state.current.Defunct || state.owner.window != eventWindow(event) ||
recentInput(presenter.inputSource).InputSequence != sequence {
return
}
options := state.presentationOptions
if presenter.live != nil {
options = presentationSettingsFromConfig(presenter.live.Load(), true)
options.SoundCueAvailable = state.presentationOptions.SoundCueAvailable
}
event.Type = "object:state-changed:checked"
event.Source = state.current
// Admission already verified the cue. Let the output queue provide its
// normal speech fallback if the asset disappears before delayed playback.
options.SoundCueAvailable = nil
cues := presentation.EventSoundCues(event, options)
if len(cues) == 0 {
return
}
fallback := presentationFallbackSpeech(event, options)
presenter.speech.Enqueue(speechRequest{Sound: cues, FallbackText: fallback.Text, FallbackChunks: fallback.Chunks,
Role: event.Source.Role, Cause: "radio-focus-state"})
presenter.logger.Debug("queued settled radio focus sound", "cue", cues[0].Name)
}