Files
cthulhu/internal/app/progress_bars.go
2026-10-14 18:22:59 -04:00

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
}