Files

261 lines
10 KiB
QML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Accessibility.
//
// Organized by what a person came here unable to do -- see it, tolerate the
// motion, hear it, reach it from the keyboard, have it read aloud -- rather
// than by which subsystem happens to implement each control. The old page was
// grouped by mechanism (Pointer / Text / Motion / Magnifier / Contrast), which
// is the shape of the code and not the shape of the question.
//
// Everything on this page acts on this session. Two things deliberately do not
// ship as switches -- mono audio, and sticky/slow/bounce keys -- and both say
// why in place instead of being quietly absent or, worse, present and dead.
//
// Pointer size and text scale have to agree across three consumers that share
// no configuration system -- the compositor, GTK applications, and the shell.
// Panama's store is the source of truth and services/Accessibility.qml pushes
// the value to the other two.
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
title: "Accessibility"
lede: "Every switch on this page does something on this desktop — and the ones that cannot yet say why."
// The zoom chords come from the compositor's live keymap, matched on the
// descriptions hypr/keybinds.lua gives them, so rebinding a zoom key
// changes what this page says instead of quietly making it wrong. The
// literals are the shipped chords, standing in only until the keymap has
// loaded -- not a second source of truth.
function chordFor(needle: string, fallback: string): string {
for (const bind of Keybinds.binds) {
if (String(bind.description).toLowerCase().indexOf(needle) >= 0)
return String(bind.chord);
}
return fallback;
}
readonly property string zoomInChord: root.chordFor("zoom in", "Super + Alt + =")
readonly property string zoomOutChord: root.chordFor("zoom out", "Super + Alt + -")
readonly property string zoomResetChord: root.chordFor("reset zoom", "Super + Alt + 0")
// Probed when the page opens rather than polled all session: nothing else
// on this desktop needs to know whether Orca is running.
Component.onCompleted: Accessibility.refreshScreenReader()
readonly property string screenReaderDetail: {
if (Accessibility.orcaRunning)
return "Running — reading the focused application";
if (Accessibility.accessibilityBusRunning)
return "Not running · the accessibility bus is up, so applications are ready to be read";
return "Not running · the accessibility bus is not up, so Orca would start and read nothing";
}
SettingsCard {
title: "Vision"
subtitle: "Magnification is the compositor's own, so it follows the pointer across every window and every screen."
SliderRow { setting: "magnifierFactor" }
// One row per chord rather than three chords crammed into one row's
// trailing slot: three keycap chords side by side are wider than the
// control column, and squeezing them there costs the label its line.
SettingRow {
label: "Zoom in"
detail: "Works from anywhere — the OSD shows the magnification you land on"
controlWidth: 210
KeycapChord {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
chord: root.zoomInChord
}
}
SettingRow {
label: "Zoom out"
controlWidth: 210
KeycapChord {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
chord: root.zoomOutChord
}
}
SettingRow {
label: "Back to 1.00 ×"
detail: "Turns the magnifier off without coming back to Settings for it"
controlWidth: 210
KeycapChord {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
chord: root.zoomResetChord
}
}
ToggleRow { setting: "magnifierRigid" }
SliderRow { setting: "textScale" }
SliderRow { setting: "cursorSize" }
ToggleRow { setting: "highContrast" }
// The stored value is the enum; what the compositor wants is a shader
// path. SystemSettings owns that mapping and applies it live, and
// hypr/looks.lua does the same lookup at config time -- so the filter
// survives a reload without this page having to reload anything.
//
// Applied on change rather than on load: the compositor already read
// the preference at launch, and re-applying the value it is already
// running would be a hyprctl call for nothing every time this page
// opens.
//
// A picker rather than a segmented control: five options with a
// sentence under each get ninety pixels apiece in a strip, which is
// room for neither.
OptionPickerRow {
id: colorFilterRow
property string appliedFilter: ""
setting: "colorFilter"
divider: false
Component.onCompleted: colorFilterRow.appliedFilter = String(colorFilterRow.current)
onCurrentChanged: {
const next = String(colorFilterRow.current);
if (next === colorFilterRow.appliedFilter)
return;
colorFilterRow.appliedFilter = next;
SystemSettings.applyColorFilter(next);
}
}
}
SettingsCard {
title: "Motion"
subtitle: "Nothing animates while idle. This is the motion you asked for — windows opening, workspaces sliding, panels appearing."
// The detail says more than the schema's does because on this page the
// claim is the point: the shell's own durations now collapse to zero
// when this is off, so the bar, the dock and the panels genuinely stop
// moving. Until that landed, this switch reached the compositor and
// left the shell animating over the top of it.
ToggleRow {
setting: "animationsEnabled"
detail: "Window, workspace and panel motion — including the shell's own bar, dock and panels, which now stop with everything else"
}
ToggleRow { setting: "dimInactive" }
// The amount only means anything while dimming is on, so it goes quiet
// rather than disappearing: a row that vanishes takes the explanation
// of what the switch above it does with it.
SliderRow {
setting: "dimStrength"
enabled: DesktopPreferences.get("dimInactive") === true
opacity: enabled ? 1 : 0.4
}
SliderRow { setting: "inactiveOpacity"; divider: false }
}
SettingsCard {
title: "Hearing"
subtitle: "What the desktop does instead of making a sound."
ToggleRow { setting: "visualAlerts"; divider: false }
Item { width: 1; height: 10 }
// Said the same way the keyboard card says its absence, rather than as
// a row wearing a badge that stands for a control it never had.
SettingsNote {
headline: "Mono audio is not offered yet"
body: "Folding stereo into one channel is a real change to the PipeWire graph, and a switch that only looked like it did that would be worse than its absence. Balance lives on the Sound page meanwhile."
}
}
SettingsCard {
title: "Keyboard & pointer"
subtitle: "Key behavior lives with the Keyboard settings; what the pointer does when you stop moving it lives here."
ActionRow {
label: "Key repeat"
detail: "Delay and speed are on the Keyboard page, with the rest of the keymap"
action: "Open Keyboard"
onTriggered: ShellState.openSettings("shortcuts")
}
SliderRow { setting: "cursorInactiveTimeout"; zeroLabel: "Never"; divider: false }
// The card's rows sit flush against each other; the note is a
// separate thing and needs the gap to read as one.
Item { width: 1; height: 10 }
// The receipts, not a warning. See SettingsNote for why this is quiet.
SettingsNote {
headline: "Sticky, slow and bounce keys are not offered"
body: "Hyprland has no such options — asked of the running compositor rather than assumed — and GNOME's switches for them are applied by a daemon this session does not run, so a switch here would be wired to nothing. On Wayland each compositor implements these for itself; if Hyprland grows them, they land on this page."
}
}
SettingsCard {
title: "Screen reader"
subtitle: "Orca is a separate application and is reported as one: either its process is running or it is not."
SettingRow {
label: "Orca"
detail: root.screenReaderDetail
controlWidth: 200
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 9
StatusBadge {
anchors.verticalCenter: parent.verticalCenter
text: Accessibility.orcaRunning ? "Running" : "Stopped"
tone: Accessibility.orcaRunning ? Theme.ok : Theme.fgMuted
}
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
text: Accessibility.orcaRunning ? "Stop" : "Start"
onClicked: {
if (Accessibility.orcaRunning)
Accessibility.stopOrca();
else
Accessibility.startOrca();
}
}
}
}
SettingRow {
label: "This application, read aloud"
detail: "Every settings row carries a spoken name and takes keyboard focus — Tab walks the page, Space flips a switch, the arrows move a slider or step a choice"
controlWidth: 90
divider: false
StatusBadge {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "Built in"
tone: Theme.ok
}
}
}
SettingsCard {
visible: Accessibility.lastError !== ""
title: "Could not apply"
subtitle: Accessibility.lastError
}
}