124 lines
4.3 KiB
Go
124 lines
4.3 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/presentation"
|
|
"git.stormux.org/storm/cthulhu/internal/sound"
|
|
"git.stormux.org/storm/cthulhu/internal/webscripts"
|
|
)
|
|
|
|
type ProgressBarEntry struct {
|
|
beep, speech float64
|
|
beepKnown, speechKnown bool
|
|
}
|
|
|
|
// ProgressBarHistory belongs to the event loop and survives focus changes.
|
|
// It is bounded and released with the loop. Native identities keep distinct
|
|
// bars independent even when they occupy the same screen position.
|
|
type ProgressBarHistory struct {
|
|
entries map[atspi.ObjectID]ProgressBarEntry
|
|
order []atspi.ObjectID
|
|
}
|
|
|
|
func (history *ProgressBarHistory) report(id atspi.ObjectID, percentage float64, settings config.ProgressBarSettings) (bool, bool) {
|
|
if history.entries == nil {
|
|
history.entries = make(map[atspi.ObjectID]ProgressBarEntry)
|
|
}
|
|
entry, exists := history.entries[id]
|
|
beep := (settings.OutputMode == "beep" || settings.OutputMode == "both") &&
|
|
(!entry.beepKnown || math.Abs(percentage-entry.beep) >= float64(settings.BeepPercentageInterval))
|
|
speak := (settings.OutputMode == "speak" || settings.OutputMode == "both") &&
|
|
(!entry.speechKnown || math.Abs(percentage-entry.speech) >= float64(settings.SpeechPercentageInterval))
|
|
if !beep && !speak {
|
|
return false, false
|
|
}
|
|
if beep {
|
|
entry.beep, entry.beepKnown = percentage, true
|
|
}
|
|
if speak {
|
|
entry.speech, entry.speechKnown = percentage, true
|
|
}
|
|
if exists {
|
|
for index, candidate := range history.order {
|
|
if candidate == id {
|
|
history.order = append(history.order[:index], history.order[index+1:]...)
|
|
break
|
|
}
|
|
}
|
|
} else if len(history.order) == 32 {
|
|
delete(history.entries, history.order[0])
|
|
history.order = history.order[1:]
|
|
}
|
|
history.entries[id] = entry
|
|
history.order = append(history.order, id)
|
|
return beep, speak
|
|
}
|
|
|
|
// Handle progress before ordinary focused-object filtering. This path never
|
|
// changes window ownership, native focus, review or browser mode.
|
|
func (handler *accessibilityEventHandler) handle_progress_bar(event atspi.Event) bool {
|
|
percentage, shaderProgress := webscripts.ShaderProgress(event.Source.ApplicationProcess, event.Type, event.Source.Role, event.Source.Name)
|
|
if !shaderProgress && (!isValueChangeEvent(event.Type) || event.Source.Role != "progress bar") {
|
|
return false
|
|
}
|
|
settings := config.DefaultSettings()
|
|
if handler.live != nil {
|
|
settings = handler.live.Load()
|
|
}
|
|
if settings.ProgressBar.OutputMode == "off" || event.Source.StatesKnown && (!event.Source.Visible || !event.Source.Showing) {
|
|
// NVDA falls back to ordinary focused-control value reporting here.
|
|
return false
|
|
}
|
|
if shaderProgress && (event.Source.Defunct || event.Source.Indeterminate) {
|
|
return false
|
|
}
|
|
if !shaderProgress {
|
|
var valid bool
|
|
percentage, valid = presentation.ProgressPercentage(event.Source)
|
|
if !valid {
|
|
return false
|
|
}
|
|
}
|
|
state := handler.state
|
|
if handler.foreground != nil && event.Foreground.Epoch != state.foregroundState.Epoch {
|
|
return true
|
|
}
|
|
if handler.focusGate.Locked() {
|
|
return true
|
|
}
|
|
// Like NVDA, refresh braille only if this is already the displayed object.
|
|
if event.Source.ID() == state.current.ID() {
|
|
state.current = event.Source
|
|
state.brailleSnapshot = event.Source
|
|
if handler.brailleOutput != nil {
|
|
handler.brailleOutput.EnqueueFrame(brailleFocusFrameWithOptions(event.Source, settings.Braille, settings.TextAttributes))
|
|
}
|
|
}
|
|
background := event.ForegroundMismatch || state.owner.window.Valid() &&
|
|
(eventWindow(event) != state.owner.window || event.Source.BusName != state.owner.applicationBus)
|
|
if background && !settings.ProgressBar.ReportBackground || handler.focusGate.Suspended() && !settings.ProgressBar.ReportBackground {
|
|
return true
|
|
}
|
|
beep, speak := state.progressBars.report(event.Source.ID(), percentage, settings.ProgressBar)
|
|
request := speechRequest{Role: "progress bar", Cause: event.Type, Append: true,
|
|
Window: eventWindow(event), BypassFocusGate: settings.ProgressBar.ReportBackground}
|
|
if beep {
|
|
request.Sound = []sound.Cue{{Name: sound.ProgressCueName(percentage)}}
|
|
}
|
|
if speak {
|
|
request.Text = fmt.Sprintf("%d percent", int(percentage))
|
|
}
|
|
if beep || speak {
|
|
handler.speech.Enqueue(request)
|
|
}
|
|
return true
|
|
}
|