Make every settings row reachable, and every accessibility switch honest
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
// The visual bell: one flash at the edges of the screen when a notification
|
||||
// arrives that would have rung.
|
||||
//
|
||||
// For people who cannot hear the bell. It fires on exactly the notifications
|
||||
// services/Notifs.qml calls bell-eligible -- same per-application switch, same
|
||||
// low-urgency rule, same suppress-sound hint -- but NOT on the event-sounds
|
||||
// switch, which would make this do nothing for the person it is for. That rule
|
||||
// is pinned in Notifs.qml above `bellWouldRing`; this file only listens.
|
||||
//
|
||||
// Edges rather than the whole screen. A full-screen white flash is what X11's
|
||||
// visual bell did, and it is genuinely unpleasant: it destroys dark adaptation,
|
||||
// hides the thing you were reading at the moment it demands attention, and is
|
||||
// the shape of flash that photosensitivity guidance warns about. A soft glow
|
||||
// inward from the four edges is unmissable in peripheral vision and leaves the
|
||||
// middle of the screen -- the part being read -- alone.
|
||||
//
|
||||
// ONE animation per notification. There is no `loops`, no Timer that restarts
|
||||
// it, and a burst of notifications cannot stack flashes: while the animation is
|
||||
// running, further triggers are ignored outright. A strobing screen is a
|
||||
// seizure risk, not a notification.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property var modelData: null
|
||||
screen: root.modelData
|
||||
|
||||
// Mapped only while flashing. The rest of the session this costs nothing,
|
||||
// and no surface sits over the desktop waiting for something to happen.
|
||||
property bool mapped: false
|
||||
visible: root.mapped
|
||||
|
||||
anchors.top: true
|
||||
anchors.bottom: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
|
||||
// Reserve nothing and respect nothing: the glow is drawn over the whole
|
||||
// output including under the bar and the dock, which is what makes it
|
||||
// visible from wherever the eyes happen to be.
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: "qs-visual-bell"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
// Entirely click-through: an empty mask means no part of this surface
|
||||
// takes a pointer event, so a flash cannot swallow the click you were in
|
||||
// the middle of making.
|
||||
mask: Region {}
|
||||
|
||||
// How far the glow reaches in from each edge. Fixed pixels rather than a
|
||||
// share of the screen: this is about peripheral vision, which does not
|
||||
// scale with the size of the monitor.
|
||||
readonly property int reach: 72
|
||||
|
||||
// Deliberately NOT Theme.durFast / Theme.durNormal. Those collapse to zero
|
||||
// when Reduce motion is on, which would make the flash instantaneous and
|
||||
// therefore invisible -- switching on Reduce motion would silently switch
|
||||
// off Visual alerts. A flash is information, not decoration, so it keeps
|
||||
// its own timings. They are slow enough not to strobe and quick enough to
|
||||
// be over before it becomes irritating.
|
||||
readonly property int riseMs: 110
|
||||
readonly property int fallMs: 340
|
||||
|
||||
Connections {
|
||||
target: Notifs
|
||||
function onBellEligible(notification: var): void { root.flash(); }
|
||||
}
|
||||
|
||||
// The one-shot. A trigger arriving mid-flash is dropped rather than
|
||||
// queued or restarted, so ten notifications landing together are one
|
||||
// flash -- the same coalescing the audible bell gets from its throttle.
|
||||
function flash(): void {
|
||||
if (!Settings.visualAlerts || pulse.running)
|
||||
return;
|
||||
root.mapped = true;
|
||||
pulse.restart();
|
||||
}
|
||||
|
||||
Item {
|
||||
id: glow
|
||||
|
||||
anchors.fill: parent
|
||||
opacity: 0
|
||||
|
||||
readonly property color tint: Theme.alpha(Theme.accent, 0.62)
|
||||
readonly property color fade: Theme.alpha(Theme.accent, 0)
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: root.reach
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: glow.tint }
|
||||
GradientStop { position: 1.0; color: glow.fade }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: root.reach
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: glow.fade }
|
||||
GradientStop { position: 1.0; color: glow.tint }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.reach
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: glow.tint }
|
||||
GradientStop { position: 1.0; color: glow.fade }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.reach
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: glow.fade }
|
||||
GradientStop { position: 1.0; color: glow.tint }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Up, down, gone. One sequence, run once per notification: no `loops`, no
|
||||
// repeat, and the window unmaps itself at the end so nothing is left over
|
||||
// the desktop between notifications.
|
||||
SequentialAnimation {
|
||||
id: pulse
|
||||
|
||||
NumberAnimation {
|
||||
target: glow
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: root.riseMs
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: glow
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: root.fallMs
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
ScriptAction { script: root.mapped = false }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user