Make every settings row reachable, and every accessibility switch honest

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 21:03:36 -04:00
parent e1ff25fc66
commit 9ffaf45a4d
33 changed files with 2384 additions and 76 deletions
+47 -11
View File
@@ -412,35 +412,71 @@ Singleton {
// dozen overlapping bells, which is a noise rather than a notification.
property real lastBellAt: 0
function playBell(notification: var): void {
if (!SoundFeedback.eventSounds)
return;
// Emitted for every notification that is bell-eligible, whether or not a
// bell is actually audible. modules/notifications/VisualBell.qml listens.
signal bellEligible(var notification)
// Would this notification ring the bell, setting aside whether sound is
// switched on at all?
//
// THE PINNED RULE, because it is the whole point of visual alerts: the
// flash follows this predicate, and the bell follows this predicate AND
// SoundFeedback.eventSounds. Every gate below is shared -- an application
// you silenced stays silent both ways, a low-urgency notification stays
// quiet both ways, and an application that says it played its own sound is
// taken at its word both ways -- but the event-sounds switch is NOT.
//
// Gating the flash on event sounds would make Visual Alerts do nothing for
// exactly the person it exists for: somebody who cannot hear the bell has
// no reason to have event sounds on, and would turn on a switch that stays
// dark. The flash is not a picture of the bell; it is the same alert in the
// sense the person can receive.
//
// Sound RESOLUTION is deliberately not part of this. playBell gives up when
// it cannot find a file to play, which is a fact about the sound theme on
// disk; losing the flash because a theme is missing an ogg would be absurd.
function bellWouldRing(notification: var): bool {
// The per-application sound switch. Narrower than turning the
// application off: its notifications still arrive and still show, they
// just stop making noise.
if (!root.appRule(root.notificationAppId(notification)).sound)
return;
return false;
// Low urgency is the "you did not need to know this" tier -- battery
// reaching full, a sync completing. It stays silent by design, and it
// is the effective urgency, so "treat as low" is a way to keep an
// application audible in principle but quiet in practice.
if (root.effectiveUrgency(notification) === NotificationUrgency.Low)
return;
return false;
// The freedesktop sound hints. This is the fix for the double chime:
// an application that plays its own sound sets suppress-sound so the
// The freedesktop sound hint. This is the fix for the double chime: an
// application that plays its own sound sets suppress-sound so the
// notification server stays quiet, and Panama ignoring it meant one
// notification made two noises a beat apart.
//
// The other two say what to play instead of the theme bell --
// sound-file is an absolute path the application supplies, sound-name
// is a theme sound resolved through the same chain the bell uses.
const hints = notification.hints ?? {};
if (hints["suppress-sound"] === true)
return false;
return true;
}
function playBell(notification: var): void {
if (!root.bellWouldRing(notification))
return;
// Announced BEFORE the event-sounds gate, on purpose. See the rule
// pinned above bellWouldRing.
root.bellEligible(notification);
if (!SoundFeedback.eventSounds)
return;
// The remaining two sound hints say what to play instead of the theme
// bell -- sound-file is an absolute path the application supplies,
// sound-name is a theme sound resolved through the same chain the bell
// uses. Neither can make a notification ineligible, so neither is part
// of the predicate above.
const hints = notification.hints ?? {};
const soundFile = String(hints["sound-file"] ?? "");
const soundName = String(hints["sound-name"] ?? "");
const candidates = soundFile.startsWith("/")