262 lines
10 KiB
QML
262 lines
10 KiB
QML
// Accessibility.
|
||
//
|
||
// Organised 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.
|
||
ChoiceRow {
|
||
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" }
|
||
|
||
SettingRow {
|
||
label: "Mono audio"
|
||
detail: "Not offered yet — 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."
|
||
controlWidth: 90
|
||
divider: false
|
||
|
||
SoundBadge {
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: "Not yet"
|
||
tone: Theme.fgMuted
|
||
}
|
||
}
|
||
}
|
||
|
||
SettingsCard {
|
||
title: "Keyboard & pointer"
|
||
subtitle: "Key behaviour 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
|
||
|
||
SoundBadge {
|
||
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 app, 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
|
||
|
||
SoundBadge {
|
||
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
|
||
}
|
||
}
|