245 lines
9.7 KiB
QML
245 lines
9.7 KiB
QML
// Input & Shortcuts.
|
|
//
|
|
// The shortcut list is generated from `hyprctl binds -j` rather than typed out
|
|
// here. The previous version was a hand-maintained array of nineteen entries
|
|
// against a real keymap of a hundred and thirteen: it could not show the rest,
|
|
// and it went stale the moment a bind changed. Every bind now carries its own
|
|
// description in hypr/keybinds.lua, and this page just groups and renders them.
|
|
//
|
|
// The hardware settings above the list are real controls. Keyboard layout,
|
|
// repeat behaviour, and pointer response are Hyprland's, so Panama owns them;
|
|
// device-specific configuration stays with GNOME.
|
|
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
// The chord of the bind currently being re-recorded, in the Lua form; empty
|
|
// when nothing is being captured. Held here rather than per row so that
|
|
// starting a new capture cancels any other.
|
|
property string capturingChord: ""
|
|
readonly property string storedXkbOptions: String(DesktopPreferences.get("keyboardOptions") ?? "")
|
|
|
|
function xkbOptions(): var {
|
|
return root.storedXkbOptions
|
|
.split(",")
|
|
.map(option => option.trim())
|
|
.filter(option => option !== "");
|
|
}
|
|
|
|
function currentXkbOption(prefix: string): string {
|
|
return root.xkbOptions().find(option => option.indexOf(prefix) === 0) ?? "";
|
|
}
|
|
|
|
function setXkbOption(prefix: string, option: string): void {
|
|
const options = root.xkbOptions().filter(option => option.indexOf(prefix) !== 0);
|
|
if (option !== "")
|
|
options.push(option);
|
|
SystemSettings.commitPreference("keyboardOptions", options.join(","));
|
|
}
|
|
|
|
title: "Input & Shortcuts"
|
|
lede: "The Forge mental model, carried forward into native tiling."
|
|
|
|
SettingsCard {
|
|
title: "Keyboard"
|
|
|
|
// These were read-only text, on the grounds that a layout change needed
|
|
// a compositor reload. It does not: setting input:kb_variant through
|
|
// hl.config re-keymaps attached keyboards immediately -- verified by
|
|
// watching active_keymap on a real keyboard change and change back. So
|
|
// they are real controls.
|
|
TextEntryRow { setting: "keyboardLayout"; placeholder: "us" }
|
|
TextEntryRow { setting: "keyboardVariant"; placeholder: "none" }
|
|
|
|
ChoiceGrid {
|
|
width: parent.width
|
|
label: "Caps Lock"
|
|
detail: "Keep it conventional, or turn a prime keyboard position into Escape or Control"
|
|
current: root.currentXkbOption("caps:")
|
|
options: [
|
|
{ value: "", label: "Standard" },
|
|
{ value: "caps:escape_shifted_capslock", label: "Esc · Shift for Caps" },
|
|
{ value: "caps:escape", label: "Escape" },
|
|
{ value: "caps:ctrl_modifier", label: "Control" }
|
|
]
|
|
onPicked: value => root.setXkbOption("caps:", value)
|
|
}
|
|
|
|
ChoiceGrid {
|
|
width: parent.width
|
|
label: "Compose key"
|
|
detail: "Type accented characters and symbols with memorable key sequences"
|
|
current: root.currentXkbOption("compose:")
|
|
options: [
|
|
{ value: "", label: "Off" },
|
|
{ value: "compose:ralt", label: "Right Alt" },
|
|
{ value: "compose:rwin", label: "Right Super" },
|
|
{ value: "compose:menu", label: "Menu" }
|
|
]
|
|
onPicked: value => root.setXkbOption("compose:", value)
|
|
}
|
|
|
|
ChoiceGrid {
|
|
width: parent.width
|
|
label: "Layout switching"
|
|
detail: "Used when Keyboard layout contains more than one comma-separated layout"
|
|
current: root.currentXkbOption("grp:")
|
|
options: [
|
|
{ value: "", label: "Off" },
|
|
{ value: "grp:win_space_toggle", label: "Super + Space" },
|
|
{ value: "grp:alt_shift_toggle", label: "Alt + Shift" },
|
|
{ value: "grp:ctrl_shift_toggle", label: "Ctrl + Shift" },
|
|
{ value: "grp:caps_toggle", label: "Caps Lock" }
|
|
]
|
|
onPicked: value => root.setXkbOption("grp:", value)
|
|
}
|
|
|
|
// Presets preserve every option outside their own category. The raw
|
|
// value remains visible for less common xkeyboard-config features.
|
|
TextEntryRow { setting: "keyboardOptions"; placeholder: "compose:ralt" }
|
|
SliderRow { setting: "keyRepeatDelay" }
|
|
SliderRow { setting: "keyRepeatRate" }
|
|
ToggleRow { setting: "numlockByDefault"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Hardware input"
|
|
|
|
ActionRow {
|
|
label: "Mouse, touchpad, and keyboard devices"
|
|
detail: "Device-specific settings stay with Fedora's hardware-backed panels"
|
|
action: "Open keyboard"
|
|
divider: false
|
|
onTriggered: SystemSettings.openGnomePanel("keyboard")
|
|
}
|
|
}
|
|
|
|
// One card per group, built from what the compositor actually has bound.
|
|
//
|
|
// Both Repeaters address their model through an explicit id. Inside a
|
|
// SettingsCard the surrounding `parent` is the card's internal Column, not
|
|
// the card, so `parent.modelData` is undefined there and the rows silently
|
|
// never appear -- the cards render with their heading and nothing under it.
|
|
Repeater {
|
|
model: Keybinds.grouped()
|
|
|
|
SettingsCard {
|
|
id: groupCard
|
|
|
|
required property var modelData
|
|
|
|
title: groupCard.modelData.name
|
|
subtitle: groupCard.modelData.binds.length === 1
|
|
? "1 shortcut"
|
|
: `${groupCard.modelData.binds.length} shortcuts`
|
|
|
|
Repeater {
|
|
id: bindRows
|
|
|
|
model: groupCard.modelData.binds
|
|
|
|
SettingRow {
|
|
id: bindRow
|
|
|
|
required property var modelData
|
|
required property int index
|
|
|
|
readonly property bool capturing: root.capturingChord === bindRow.modelData.luaChord
|
|
readonly property bool overridden: Keybinds.isOverridden(bindRow.modelData.luaChord)
|
|
|
|
label: bindRow.modelData.description
|
|
detail: bindRow.overridden
|
|
? "Moved from " + Keybinds.shippedChordFor(bindRow.modelData.luaChord)
|
|
: ""
|
|
controlWidth: 300
|
|
divider: bindRow.index < bindRows.count - 1
|
|
|
|
Item {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 300
|
|
height: 30
|
|
|
|
ShortcutCapture {
|
|
anchors.right: parent.right
|
|
width: 230
|
|
height: 30
|
|
visible: bindRow.capturing
|
|
focus: bindRow.capturing
|
|
onCaptured: chord => {
|
|
Keybinds.rebind(bindRow.modelData.luaChord, chord);
|
|
root.capturingChord = "";
|
|
}
|
|
onCancelled: root.capturingChord = ""
|
|
}
|
|
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: !bindRow.capturing
|
|
spacing: 8
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: bindRow.modelData.chord
|
|
color: bindRow.overridden ? Theme.accent : Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
}
|
|
|
|
SettingsButton {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "Change"
|
|
enabled: !Keybinds.reloading && !bindRow.modelData.mouse
|
|
onClicked: root.capturingChord = bindRow.modelData.luaChord
|
|
}
|
|
|
|
SettingsButton {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: bindRow.overridden
|
|
text: "Reset"
|
|
enabled: !Keybinds.reloading
|
|
onClicked: Keybinds.resetBind(bindRow.modelData.luaChord)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: Object.keys(Keybinds.overrides).length > 0
|
|
title: "Changed shortcuts"
|
|
subtitle: "Rebinding stores only the new chord; what a shortcut does always comes from the desktop's configuration."
|
|
|
|
ActionRow {
|
|
label: "Restore every shipped shortcut"
|
|
detail: Object.keys(Keybinds.overrides).length
|
|
+ (Object.keys(Keybinds.overrides).length === 1 ? " shortcut moved" : " shortcuts moved")
|
|
action: "Restore all"
|
|
divider: false
|
|
enabled: !Keybinds.reloading
|
|
onTriggered: Keybinds.resetAll()
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: Keybinds.lastError !== ""
|
|
title: "Shortcuts unavailable"
|
|
subtitle: Keybinds.lastError
|
|
|
|
ActionRow {
|
|
label: "Read the keymap again"
|
|
detail: "Shortcuts are read from the running compositor"
|
|
action: "Retry"
|
|
divider: false
|
|
onTriggered: Keybinds.refresh()
|
|
}
|
|
}
|
|
}
|