// 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 } }