86 lines
3.0 KiB
Go
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)
|
|
}
|