mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
A compositor resolves a bind's keysym on the first level of the active layout. Layouts that put punctuation higher up therefore cannot reach it at all: de:neo has bracketleft on layer 4 and plain de on AltGr, so Mod+bracketleft is a bind that can never fire. The editor recorded and stored it anyway and gave no sign that anything was wrong (#3585). `dms keybinds keymap` now reports what the seat's xkb keymap puts on the first level of every physical key, and the editor says so instead of letting a dead bind through. The keymap a compositor hands out writes its keysyms as hex, so only the range the keymap package can name back is usable for the comparison. That is the ASCII range, which is where the problem lives, and the command reports the vocabulary alongside the keys so the caller can tell "the layout cannot reach this" from "we could not name it" and stay quiet in the second case.
1252 lines
45 KiB
QML
1252 lines
45 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell.Wayland
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
import qs.Modules.Settings.Widgets
|
|
import "../../Common/KeyUtils.js" as KeyUtils
|
|
import "../../Common/KeybindActions.js" as Actions
|
|
|
|
DankDialog {
|
|
id: root
|
|
|
|
readonly property var log: Log.scoped("KeybindEditorDialog")
|
|
|
|
property var bindData: ({})
|
|
property bool isNew: false
|
|
property bool readOnly: false
|
|
property bool locked: false
|
|
property bool saveBlocked: false
|
|
property bool busy: false
|
|
property var panelWindow: null
|
|
property alias topContent: topSlot.data
|
|
|
|
property bool recording: false
|
|
property int editingKeyIndex: -1
|
|
property bool addingNewKey: false
|
|
property string editKey: ""
|
|
property string editAction: ""
|
|
property string editDesc: ""
|
|
property int editCooldownMs: 0
|
|
property string editFlags: ""
|
|
property bool editAllowWhenLocked: false
|
|
property var editRepeat: undefined
|
|
property var editAllowInhibiting: undefined
|
|
property bool hasChanges: false
|
|
property bool useCustomCompositor: false
|
|
property bool _altShiftGhost: false
|
|
|
|
readonly property var keys: bindData.keys || []
|
|
readonly property var editingKey: keys[editingKeyIndex] ?? null
|
|
readonly property string originalKey: editingKey?.key ?? ""
|
|
readonly property string actionType: Actions.getActionType(editAction)
|
|
readonly property string actionLabel: KeybindsService.getActionLabel(editAction) || I18n.tr("Select", "verb, dropdown placeholder or option that opens a picker") + "…"
|
|
readonly property var configConflict: bindData.conflict || null
|
|
readonly property var conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds(), KeybindsService.modKey, KeybindsService.modSymbol) : []
|
|
readonly property string editKeysym: KeyUtils.keyFromToken(editKey)
|
|
readonly property bool keysymUnreachable: KeyUtils.keysymUnreachable(editKeysym, KeybindsService.firstLevelKeymap)
|
|
readonly property bool canReset: !isNew && !readOnly && editingKey?.isOverride === true && editingKey?.hasDefault === true
|
|
readonly property bool canSave: !readOnly && !saveBlocked && editKey !== "" && Actions.isValidAction(editAction)
|
|
readonly property bool canSubmit: canSave && (isNew || hasChanges) && !busy
|
|
readonly property bool formEnabled: !locked && !readOnly
|
|
readonly property bool wideForm: actionWidth + padding * 2 >= maximumWidth
|
|
|
|
readonly property var argConfig: Actions.getActionArgConfig(KeybindsService.currentProvider, editAction)
|
|
readonly property var dmsParsedArgs: argConfig?.type === "dms" ? Actions.parseDmsActionArgs(editAction) : null
|
|
readonly property var dmsArgDefs: dmsParsedArgs?.base ? (Actions.getDmsActionArgs()[dmsParsedArgs.base]?.args ?? []) : []
|
|
readonly property var dmsFlagArgs: dmsArgDefs.filter(arg => arg.type === "flag")
|
|
readonly property bool showDmsArgs: actionType === "dms" && argConfig?.type === "dms"
|
|
readonly property bool showCompositorPicker: actionType === "compositor" && !useCustomCompositor
|
|
readonly property var compositorParsedArgs: Actions.parseCompositorActionArgs(KeybindsService.currentProvider, editAction)
|
|
readonly property var compositorArgDefs: argConfig?.config?.args ?? []
|
|
readonly property var compositorFlags: compositorFlagOptions()
|
|
readonly property var bindOptionList: bindOptions()
|
|
|
|
readonly property string customCategory: I18n.tr("Custom")
|
|
readonly property string repeatTooltip: I18n.tr("Repeats while the key is held", "keybind option tooltip")
|
|
readonly property string lockedTooltip: I18n.tr("Also works on the lock screen", "keybind option tooltip")
|
|
readonly property string pointerTooltip: I18n.tr("Includes the mouse pointer", "keybind screenshot option tooltip")
|
|
readonly property string saveTooltip: I18n.tr("Also saves the screenshot to disk", "keybind screenshot option tooltip")
|
|
|
|
readonly property var typeTooltips: ({
|
|
"dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"),
|
|
"compositor": I18n.tr("Compositor actions (focus, move, etc.)", "keybind action type tooltip"),
|
|
"spawn": I18n.tr("Run a program (e.g., firefox, kitty)"),
|
|
"shell": I18n.tr("Run a shell command (e.g., notify-send)")
|
|
})
|
|
|
|
signal saveRequested(string originalKey, var data)
|
|
signal resetRequested(string key)
|
|
signal editChanged
|
|
|
|
embedded: false
|
|
opened: false
|
|
maximumWidth: SettingsMetrics.formDialogWidth
|
|
surfaceColor: Theme.hostSurface
|
|
title: isNew ? I18n.tr("Add shortcut") : I18n.tr("Edit shortcut", "keybind editor dialog title")
|
|
closeEnabled: !busy
|
|
acceptEnabled: canSubmit
|
|
onAccepted: save()
|
|
|
|
function present(bind, keyIndex, newBind, retained) {
|
|
// Needed to tell a key the layout cannot reach from one it can, and
|
|
// the layout may have changed since the editor was last open.
|
|
KeybindsService.loadFirstLevelKeymap();
|
|
recording = false;
|
|
bindData = bind;
|
|
isNew = newBind;
|
|
loadKey(keyIndex);
|
|
if (retained)
|
|
restoreRetainedEdit(retained);
|
|
opened = true;
|
|
forceActiveFocus();
|
|
}
|
|
|
|
function loadKey(index) {
|
|
const key = keys[index] ?? null;
|
|
addingNewKey = false;
|
|
editingKeyIndex = key ? index : -1;
|
|
editKey = key?.key ?? "";
|
|
editAction = bindData.action || "";
|
|
editDesc = key?.desc || bindData.desc || "";
|
|
editCooldownMs = key?.cooldownMs || 0;
|
|
editFlags = key?.flags || "";
|
|
editAllowWhenLocked = key?.allowWhenLocked || false;
|
|
editRepeat = key?.repeat;
|
|
editAllowInhibiting = key?.allowInhibiting;
|
|
useCustomCompositor = actionType === "compositor" && editAction !== "" && !KeybindsService.isKnownCompositorAction(editAction);
|
|
hasChanges = false;
|
|
editChanged();
|
|
}
|
|
|
|
function restoreRetainedEdit(retained) {
|
|
editingKeyIndex = keys.findIndex(key => key.key === retained.originalKey);
|
|
addingNewKey = !retained.originalKey;
|
|
updateEdit(retained.data);
|
|
}
|
|
|
|
function startAddingNewKey() {
|
|
if (readOnly) {
|
|
KeybindsService.showHyprlandReadOnlyWarning();
|
|
return;
|
|
}
|
|
addingNewKey = true;
|
|
editingKeyIndex = -1;
|
|
editKey = "";
|
|
hasChanges = true;
|
|
editChanged();
|
|
}
|
|
|
|
function changedFrom(key) {
|
|
if (editKey !== (key?.key ?? ""))
|
|
return true;
|
|
if (editAction !== (bindData.action || ""))
|
|
return true;
|
|
if (editDesc !== (key?.desc || bindData.desc || ""))
|
|
return true;
|
|
if (editCooldownMs !== (key?.cooldownMs || 0))
|
|
return true;
|
|
if (editFlags !== (key?.flags || ""))
|
|
return true;
|
|
if (editAllowWhenLocked !== (key?.allowWhenLocked || false))
|
|
return true;
|
|
return editRepeat !== key?.repeat || editAllowInhibiting !== key?.allowInhibiting;
|
|
}
|
|
|
|
function updateEdit(changes) {
|
|
if (readOnly)
|
|
return;
|
|
if (changes.key !== undefined)
|
|
editKey = changes.key;
|
|
if (changes.action !== undefined)
|
|
editAction = changes.action;
|
|
if (changes.desc !== undefined)
|
|
editDesc = changes.desc;
|
|
if (changes.cooldownMs !== undefined)
|
|
editCooldownMs = changes.cooldownMs;
|
|
if (changes.flags !== undefined)
|
|
editFlags = changes.flags;
|
|
if (changes.allowWhenLocked !== undefined)
|
|
editAllowWhenLocked = changes.allowWhenLocked;
|
|
if (changes.repeat !== undefined)
|
|
editRepeat = changes.repeat;
|
|
if (changes.allowInhibiting !== undefined)
|
|
editAllowInhibiting = changes.allowInhibiting;
|
|
hasChanges = addingNewKey || changedFrom(editingKey);
|
|
editChanged();
|
|
}
|
|
|
|
function selectType(type) {
|
|
if (readOnly || type === actionType)
|
|
return;
|
|
switch (type) {
|
|
case "dms":
|
|
updateEdit({
|
|
"action": KeybindsService.dmsActions[0].id,
|
|
"desc": KeybindsService.dmsActions[0].label
|
|
});
|
|
return;
|
|
case "compositor":
|
|
useCustomCompositor = false;
|
|
updateEdit({
|
|
"action": "close-window",
|
|
"desc": "Close Window"
|
|
});
|
|
return;
|
|
case "spawn":
|
|
updateEdit({
|
|
"action": "spawn ",
|
|
"desc": ""
|
|
});
|
|
return;
|
|
case "shell":
|
|
updateEdit({
|
|
"action": "spawn sh -c \"\"",
|
|
"desc": ""
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
function typeLabel(type) {
|
|
switch (type) {
|
|
case "dms":
|
|
return "DMS";
|
|
case "compositor":
|
|
return I18n.tr("Compositor");
|
|
case "spawn":
|
|
return I18n.tr("Command");
|
|
case "shell":
|
|
return I18n.tr("Shell", "noun, keybind editor field label for a shell command");
|
|
}
|
|
return type;
|
|
}
|
|
|
|
function selectCompositorCategory(category) {
|
|
if (category === customCategory) {
|
|
useCustomCompositor = true;
|
|
return;
|
|
}
|
|
if (!useCustomCompositor)
|
|
return;
|
|
useCustomCompositor = false;
|
|
const first = KeybindsService.getCompositorActions(category)[0];
|
|
if (!first)
|
|
return;
|
|
updateEdit({
|
|
"action": first.id,
|
|
"desc": first.label
|
|
});
|
|
}
|
|
|
|
function save() {
|
|
if (readOnly) {
|
|
KeybindsService.showHyprlandReadOnlyWarning();
|
|
return;
|
|
}
|
|
if (!canSubmit)
|
|
return;
|
|
recording = false;
|
|
saveRequested(addingNewKey ? "" : originalKey, {
|
|
"key": editKey,
|
|
"action": editAction,
|
|
"desc": editDesc,
|
|
"cooldownMs": editCooldownMs,
|
|
"flags": editFlags,
|
|
"allowWhenLocked": editAllowWhenLocked,
|
|
"repeat": editRepeat,
|
|
"allowInhibiting": editAllowInhibiting
|
|
});
|
|
}
|
|
|
|
function startRecording() {
|
|
if (readOnly) {
|
|
KeybindsService.showHyprlandReadOnlyWarning();
|
|
return;
|
|
}
|
|
recording = true;
|
|
}
|
|
|
|
function stopRecording() {
|
|
recording = false;
|
|
}
|
|
|
|
function captureKey(event) {
|
|
switch (event.key) {
|
|
case Qt.Key_Control:
|
|
case Qt.Key_Shift:
|
|
case Qt.Key_Alt:
|
|
case Qt.Key_Meta:
|
|
// lock keys are toggles; NumLock picks the numpad keysym (KP_7 vs KP_Home) and must not become the bind
|
|
case Qt.Key_NumLock:
|
|
case Qt.Key_CapsLock:
|
|
case Qt.Key_ScrollLock:
|
|
return;
|
|
}
|
|
|
|
if (event.key === 0 && (event.modifiers & Qt.AltModifier)) {
|
|
_altShiftGhost = true;
|
|
return;
|
|
}
|
|
|
|
let mods = KeyUtils.modsFromEvent(event.modifiers);
|
|
let qtKey = event.key;
|
|
|
|
if (_altShiftGhost && (event.modifiers & Qt.AltModifier) && !mods.includes("Shift"))
|
|
mods.push("Shift");
|
|
_altShiftGhost = false;
|
|
|
|
if (qtKey === Qt.Key_Backtab) {
|
|
qtKey = Qt.Key_Tab;
|
|
if (!mods.includes("Shift"))
|
|
mods.push("Shift");
|
|
}
|
|
const hasShift = mods.includes("Shift");
|
|
mods = KeyUtils.withSymbolicMod(mods, KeybindsService.modKey, KeybindsService.modSymbol);
|
|
|
|
const key = KeyUtils.xkbKeyFromQtKey(qtKey, !!(event.modifiers & Qt.KeypadModifier), hasShift, event.nativeScanCode);
|
|
if (!key) {
|
|
log.warn("Unknown key:", event.key, "mods:", event.modifiers);
|
|
return;
|
|
}
|
|
|
|
updateEdit({
|
|
"key": KeyUtils.formatToken(mods, key)
|
|
});
|
|
stopRecording();
|
|
}
|
|
|
|
function captureWheel(wheel) {
|
|
let mods = [];
|
|
if (wheel.modifiers & Qt.ControlModifier)
|
|
mods.push("Ctrl");
|
|
if (wheel.modifiers & Qt.ShiftModifier)
|
|
mods.push("Shift");
|
|
if (wheel.modifiers & Qt.AltModifier)
|
|
mods.push("Alt");
|
|
if (wheel.modifiers & Qt.MetaModifier)
|
|
mods.push("Super");
|
|
mods = KeyUtils.withSymbolicMod(mods, KeybindsService.modKey, KeybindsService.modSymbol);
|
|
|
|
const wheelKey = wheelKeyName(wheel.angleDelta);
|
|
if (!wheelKey)
|
|
return;
|
|
updateEdit({
|
|
"key": KeyUtils.formatToken(mods, wheelKey)
|
|
});
|
|
stopRecording();
|
|
}
|
|
|
|
function wheelKeyName(delta) {
|
|
if (delta.y > 0)
|
|
return "WheelScrollUp";
|
|
if (delta.y < 0)
|
|
return "WheelScrollDown";
|
|
if (delta.x > 0)
|
|
return "WheelScrollRight";
|
|
if (delta.x < 0)
|
|
return "WheelScrollLeft";
|
|
return "";
|
|
}
|
|
|
|
function setDmsArgs(changes) {
|
|
if (!dmsParsedArgs)
|
|
return;
|
|
const args = Object.assign({}, dmsParsedArgs.args, changes);
|
|
updateEdit({
|
|
"action": Actions.buildDmsAction(dmsParsedArgs.base, args)
|
|
});
|
|
}
|
|
|
|
function commitDmsAmount(text) {
|
|
if (!dmsParsedArgs)
|
|
return;
|
|
const oldAction = editAction;
|
|
const args = Object.assign({}, dmsParsedArgs.args, {
|
|
"amount": text || "5"
|
|
});
|
|
const newAction = Actions.buildDmsAction(dmsParsedArgs.base, args);
|
|
const changes = {
|
|
"action": newAction
|
|
};
|
|
if (editDesc === "" || editDesc === KeybindsService.getActionLabel(oldAction))
|
|
changes.desc = KeybindsService.getActionLabel(newAction);
|
|
updateEdit(changes);
|
|
}
|
|
|
|
function dashTabLabel(tab) {
|
|
switch (tab) {
|
|
case "media":
|
|
return I18n.tr("Media");
|
|
case "wallpaper":
|
|
return I18n.tr("Wallpaper");
|
|
case "weather":
|
|
return I18n.tr("Weather");
|
|
default:
|
|
return I18n.tr("Overview");
|
|
}
|
|
}
|
|
|
|
function dashTabValue(label) {
|
|
switch (label) {
|
|
case I18n.tr("Media"):
|
|
return "media";
|
|
case I18n.tr("Wallpaper"):
|
|
return "wallpaper";
|
|
case I18n.tr("Weather"):
|
|
return "weather";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function dmsFlagLabel(name) {
|
|
switch (name) {
|
|
case "no-file":
|
|
return I18n.tr("Save");
|
|
case "no-clipboard":
|
|
return I18n.tr("Clipboard");
|
|
case "cursor":
|
|
return I18n.tr("Pointer");
|
|
default:
|
|
return name;
|
|
}
|
|
}
|
|
|
|
function dmsFlagTooltip(name) {
|
|
switch (name) {
|
|
case "no-file":
|
|
return saveTooltip;
|
|
case "no-clipboard":
|
|
return I18n.tr("Copies the screenshot to the clipboard", "keybind screenshot option tooltip");
|
|
case "cursor":
|
|
return pointerTooltip;
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function dmsFlagChecked(flag) {
|
|
const set = dmsParsedArgs?.args[flag.name] === true;
|
|
return flag.inverted ? !set : set;
|
|
}
|
|
|
|
function setDmsFlag(flag, checked) {
|
|
const changes = {};
|
|
changes[flag.name] = flag.inverted ? !checked : checked;
|
|
setDmsArgs(changes);
|
|
}
|
|
|
|
function compositorCategory() {
|
|
const base = editAction.split(" ")[0];
|
|
const categories = KeybindsService.getCompositorCategories();
|
|
for (const category of categories) {
|
|
if (KeybindsService.getCompositorActions(category).some(action => action.id === base))
|
|
return category;
|
|
}
|
|
return categories[0] || "Window";
|
|
}
|
|
|
|
function showArgEditor(index, argDef) {
|
|
if (!argDef)
|
|
return false;
|
|
if (argDef.type !== "text" && argDef.type !== "number")
|
|
return false;
|
|
if (KeybindsService.currentProvider === "mangowc")
|
|
return true;
|
|
return index === 0;
|
|
}
|
|
|
|
function displayArgValue(argDef) {
|
|
if (!argDef || !compositorParsedArgs?.args)
|
|
return "";
|
|
const value = compositorParsedArgs.args[argDef.name];
|
|
if (value === undefined || value === null)
|
|
return "";
|
|
if (argDef.emptyValue !== undefined && value === argDef.emptyValue)
|
|
return "";
|
|
return String(value);
|
|
}
|
|
|
|
function commitArgValue(argDef, textValue) {
|
|
if (!argDef || !argConfig)
|
|
return;
|
|
const args = Object.assign({}, compositorParsedArgs?.args || {});
|
|
args[argDef.name] = textValue;
|
|
updateEdit({
|
|
"action": Actions.buildCompositorAction(KeybindsService.currentProvider, compositorParsedArgs?.base || argConfig.base, args)
|
|
});
|
|
}
|
|
|
|
function compositorFlagOptions() {
|
|
const base = argConfig?.base ?? "";
|
|
switch (base) {
|
|
case "move-column-to-workspace":
|
|
case "move-column-to-workspace-down":
|
|
case "move-column-to-workspace-up":
|
|
return [
|
|
{
|
|
"label": I18n.tr("Follow focus"),
|
|
"value": "focus",
|
|
"tooltip": I18n.tr("Moves focus along with the column", "keybind option tooltip, niri move column to workspace")
|
|
}
|
|
];
|
|
case "quit":
|
|
return [
|
|
{
|
|
"label": I18n.tr("Skip confirmation"),
|
|
"value": "skip-confirmation"
|
|
}
|
|
];
|
|
}
|
|
if (!base.startsWith("screenshot"))
|
|
return [];
|
|
const options = [];
|
|
if (base !== "screenshot-window")
|
|
options.push({
|
|
"label": I18n.tr("Pointer", "noun, mouse cursor, keybind screenshot option to include it"),
|
|
"value": "show-pointer",
|
|
"tooltip": pointerTooltip
|
|
});
|
|
if (base !== "screenshot")
|
|
options.push({
|
|
"label": I18n.tr("Save"),
|
|
"value": "write-to-disk",
|
|
"tooltip": saveTooltip
|
|
});
|
|
return options;
|
|
}
|
|
|
|
function compositorFlagSelected() {
|
|
const args = compositorParsedArgs?.args ?? {};
|
|
return compositorFlags.filter(flag => {
|
|
if (flag.value === "focus")
|
|
return args.focus !== false;
|
|
return args[flag.value] === true;
|
|
}).map(flag => flag.value);
|
|
}
|
|
|
|
function setCompositorFlag(name, checked) {
|
|
const base = argConfig?.base;
|
|
if (!base)
|
|
return;
|
|
updateEdit({
|
|
"action": compositorFlagAction(base, name, checked)
|
|
});
|
|
}
|
|
|
|
function compositorFlagAction(base, name, checked) {
|
|
const provider = KeybindsService.currentProvider;
|
|
if (name === "skip-confirmation")
|
|
return Actions.buildCompositorAction(provider, "quit", checked ? {
|
|
"skip-confirmation": true
|
|
} : {});
|
|
if (name === "focus") {
|
|
const focusArgs = {};
|
|
if (base === "move-column-to-workspace")
|
|
focusArgs.index = compositorParsedArgs?.args?.index || "";
|
|
if (!checked)
|
|
focusArgs.focus = false;
|
|
return Actions.buildCompositorAction(provider, base, focusArgs);
|
|
}
|
|
const args = Object.assign({}, compositorParsedArgs?.args || {});
|
|
args[name] = checked;
|
|
return Actions.buildCompositorAction(provider, compositorParsedArgs?.base || base, args);
|
|
}
|
|
|
|
function bindOptions() {
|
|
switch (KeybindsService.currentProvider) {
|
|
case "niri":
|
|
return [
|
|
{
|
|
"label": I18n.tr("Repeat"),
|
|
"value": "repeat",
|
|
"tooltip": repeatTooltip
|
|
},
|
|
{
|
|
"label": I18n.tr("When locked"),
|
|
"value": "locked",
|
|
"tooltip": lockedTooltip
|
|
},
|
|
{
|
|
"label": I18n.tr("Inhibitable", "adjective, niri keybind option, apps may inhibit this shortcut"),
|
|
"value": "inhibit",
|
|
"tooltip": I18n.tr("Apps that capture the keyboard can block it", "keybind option tooltip, niri allow inhibiting")
|
|
}
|
|
];
|
|
case "hyprland":
|
|
return [
|
|
{
|
|
"label": I18n.tr("Repeat", "noun, keybind option, action repeats while key is held"),
|
|
"value": "e",
|
|
"tooltip": repeatTooltip
|
|
},
|
|
{
|
|
"label": I18n.tr("Locked", "adjective, hyprland bind flag toggle, bind works on lock screen"),
|
|
"value": "l",
|
|
"tooltip": lockedTooltip
|
|
},
|
|
{
|
|
"label": I18n.tr("Release", "noun, hyprland bind flag toggle, trigger on key release"),
|
|
"value": "r",
|
|
"tooltip": I18n.tr("Triggers when the key is released", "keybind option tooltip, hyprland release flag")
|
|
},
|
|
{
|
|
"label": I18n.tr("Long press"),
|
|
"value": "o",
|
|
"tooltip": I18n.tr("Triggers after holding the key", "keybind option tooltip, hyprland long press flag")
|
|
}
|
|
];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function selectedBindOptions() {
|
|
if (KeybindsService.currentProvider === "hyprland")
|
|
return editFlags.split("");
|
|
const selected = [];
|
|
if (editRepeat !== false)
|
|
selected.push("repeat");
|
|
if (editAllowWhenLocked)
|
|
selected.push("locked");
|
|
if (editAllowInhibiting !== false)
|
|
selected.push("inhibit");
|
|
return selected;
|
|
}
|
|
|
|
function setBindOption(value, checked) {
|
|
switch (value) {
|
|
case "repeat":
|
|
updateEdit({
|
|
"repeat": checked ? undefined : false
|
|
});
|
|
return;
|
|
case "locked":
|
|
updateEdit({
|
|
"allowWhenLocked": checked
|
|
});
|
|
return;
|
|
case "inhibit":
|
|
updateEdit({
|
|
"allowInhibiting": checked ? undefined : false
|
|
});
|
|
return;
|
|
}
|
|
const flags = editFlags.split("").filter(flag => flag !== value);
|
|
if (checked)
|
|
flags.push(value);
|
|
updateEdit({
|
|
"flags": flags.join("")
|
|
});
|
|
}
|
|
|
|
ShortcutInhibitor {
|
|
window: root.panelWindow
|
|
enabled: root.recording
|
|
}
|
|
|
|
Binding {
|
|
target: keyChips
|
|
property: "currentIndex"
|
|
value: root.addingNewKey ? -1 : root.editingKeyIndex
|
|
}
|
|
|
|
Binding {
|
|
target: dmsActionDropdown
|
|
property: "currentValue"
|
|
value: root.actionLabel
|
|
}
|
|
|
|
Binding {
|
|
target: compositorCategoryDropdown
|
|
property: "currentValue"
|
|
value: root.useCustomCompositor ? root.customCategory : root.compositorCategory()
|
|
}
|
|
|
|
Binding {
|
|
target: compositorActionDropdown
|
|
property: "currentValue"
|
|
value: root.actionLabel
|
|
}
|
|
|
|
Binding {
|
|
target: dashTabDropdown
|
|
property: "currentValue"
|
|
value: root.dashTabLabel(root.dmsParsedArgs?.args?.tab || "")
|
|
}
|
|
|
|
actions: [
|
|
DankButton {
|
|
text: I18n.tr("Reset to default")
|
|
visible: root.canReset
|
|
enabled: !root.busy && !root.locked
|
|
backgroundColor: "transparent"
|
|
textColor: Theme.primary
|
|
onClicked: root.resetRequested(root.originalKey)
|
|
},
|
|
DankButton {
|
|
text: I18n.tr("Cancel")
|
|
enabled: root.closeEnabled
|
|
backgroundColor: "transparent"
|
|
textColor: Theme.primary
|
|
onClicked: root.rejected()
|
|
},
|
|
DankButton {
|
|
text: root.isNew ? I18n.tr("Add") : I18n.tr("Save")
|
|
visible: !root.readOnly
|
|
enabled: root.canSubmit
|
|
busy: root.busy
|
|
onClicked: root.save()
|
|
}
|
|
]
|
|
|
|
Column {
|
|
id: topSlot
|
|
width: parent.width
|
|
spacing: root.contentSpacing
|
|
}
|
|
|
|
SettingsGroup {
|
|
visible: root.configConflict !== null
|
|
|
|
SettingsRow {
|
|
iconName: "warning"
|
|
title: I18n.tr("This bind is overridden by config.kdl")
|
|
subtitle: I18n.tr("Config action: %1", "keybind conflict notice, %1 is the action from the compositor config").arg(root.configConflict?.action ?? "") + "\n" + I18n.tr("To use this DMS bind, remove or change the keybind in your config.kdl")
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: I18n.tr("Key", "noun, keybind editor row label for the key combination")
|
|
enabled: root.formEnabled
|
|
headerActions: DankButton {
|
|
visible: !root.isNew && !root.readOnly && !root.addingNewKey
|
|
text: I18n.tr("New key")
|
|
iconName: "add"
|
|
buttonHeight: Theme.buttonHeightXS
|
|
backgroundColor: "transparent"
|
|
textColor: Theme.primary
|
|
onClicked: root.startAddingNewKey()
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: !root.isNew && (root.keys.length > 1 || root.addingNewKey)
|
|
|
|
body: DankFilterChips {
|
|
id: keyChips
|
|
width: parent.width
|
|
showCounts: false
|
|
model: root.keys.map(key => ({
|
|
"label": key.key
|
|
}))
|
|
onSelectionChanged: index => root.loadKey(index)
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
body: Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
FocusScope {
|
|
id: captureScope
|
|
|
|
readonly property bool rootRecording: root.recording
|
|
|
|
width: parent.width
|
|
height: Theme.iconButtonSize
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: I18n.tr("Key", "noun, keybind editor row label for the key combination")
|
|
Accessible.description: captureHint.visible ? captureHint.text : root.editKey
|
|
Accessible.onPressAction: root.startRecording()
|
|
|
|
onRootRecordingChanged: {
|
|
if (rootRecording)
|
|
forceActiveFocus();
|
|
}
|
|
|
|
onActiveFocusChanged: {
|
|
if (!activeFocus)
|
|
root.stopRecording();
|
|
}
|
|
|
|
Keys.onPressed: event => {
|
|
if (!root.recording)
|
|
return;
|
|
event.accepted = true;
|
|
root.captureKey(event);
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: Theme.cornerRadiusXS
|
|
color: "transparent"
|
|
border.color: root.recording ? Theme.primary : Theme.outline
|
|
border.width: root.recording ? Theme.outlineWidthFocused : Theme.outlineWidth
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: root.recording ? Qt.CrossCursor : Qt.PointingHandCursor
|
|
onClicked: {
|
|
if (!root.recording)
|
|
root.startRecording();
|
|
}
|
|
onWheel: wheel => {
|
|
if (!root.recording) {
|
|
wheel.accepted = false;
|
|
return;
|
|
}
|
|
wheel.accepted = true;
|
|
root.captureWheel(wheel);
|
|
}
|
|
}
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.right: recordButton.left
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "keyboard"
|
|
size: Theme.iconSize
|
|
color: Theme.surfaceVariantText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankKeycap {
|
|
text: root.editKey
|
|
visible: root.editKey !== "" && !root.recording
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
id: captureHint
|
|
text: root.recording ? I18n.tr("Press key...") : I18n.tr("Click to capture")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
visible: root.editKey === "" || root.recording
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
id: recordButton
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
focusPolicy: Qt.TabFocus
|
|
iconName: root.recording ? "close" : "radio_button_checked"
|
|
iconColor: root.recording ? Theme.error : Theme.primary
|
|
tooltipText: root.recording ? I18n.tr("Stop") : I18n.tr("Record", "verb, tooltip on button that captures a key combination")
|
|
onClicked: root.recording ? root.stopRecording() : root.startRecording()
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
visible: root.conflicts.length > 0
|
|
|
|
DankIcon {
|
|
id: conflictIcon
|
|
name: "warning"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.primary
|
|
}
|
|
|
|
StyledText {
|
|
width: parent.width - conflictIcon.width - parent.spacing
|
|
text: I18n.tr("Conflicts with: %1", "keybind warning, %1 is a list of conflicting bind descriptions").arg(root.conflicts.map(bind => bind.desc).join(", "))
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.primary
|
|
wrapMode: Text.WordWrap
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
visible: root.keysymUnreachable
|
|
|
|
DankIcon {
|
|
id: unreachableIcon
|
|
name: "warning"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.error
|
|
}
|
|
|
|
StyledText {
|
|
width: parent.width - unreachableIcon.width - parent.spacing
|
|
text: I18n.tr("Your keyboard layout puts %1 above the first level, so this bind will never fire. Pick a key the layout types without an extra layer.", "keybind warning, %1 is a key name such as bracketleft").arg(root.editKeysym)
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.error
|
|
wrapMode: Text.WordWrap
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: I18n.tr("Action")
|
|
enabled: root.formEnabled
|
|
|
|
SettingsRow {
|
|
body: DankButtonGroup {
|
|
width: parent.width
|
|
fillWidth: true
|
|
maximumWidth: width
|
|
buttonPadding: Theme.spacingM
|
|
checkEnabled: false
|
|
iconOnly: !root.wideForm
|
|
model: KeybindsService.actionTypes.map(type => ({
|
|
"text": root.typeLabel(type.id),
|
|
"icon": root.wideForm ? "" : type.icon,
|
|
"tooltip": root.typeTooltips[type.id]
|
|
}))
|
|
currentIndex: KeybindsService.actionTypes.findIndex(type => type.id === root.actionType)
|
|
onSelectionChanged: (index, selected) => {
|
|
if (selected)
|
|
root.selectType(KeybindsService.actionTypes[index].id);
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.actionType === "dms"
|
|
|
|
body: DankDropdown {
|
|
id: dmsActionDropdown
|
|
width: parent.width
|
|
compactMode: true
|
|
enableFuzzySearch: true
|
|
options: KeybindsService.getDmsActions().map(action => action.label)
|
|
onValueChanged: value => {
|
|
const action = KeybindsService.getDmsActions().find(entry => entry.label === value);
|
|
if (!action)
|
|
return;
|
|
root.updateEdit({
|
|
"action": action.id,
|
|
"desc": KeybindsService.getActionLabel(action.id)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.actionType === "compositor"
|
|
|
|
body: Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
DankDropdown {
|
|
id: compositorCategoryDropdown
|
|
width: Math.round((parent.width - parent.spacing) / 3)
|
|
compactMode: true
|
|
options: KeybindsService.getCompositorCategories().concat([root.customCategory])
|
|
onValueChanged: value => root.selectCompositorCategory(value)
|
|
}
|
|
|
|
DankDropdown {
|
|
id: compositorActionDropdown
|
|
visible: !root.useCustomCompositor
|
|
width: parent.width - compositorCategoryDropdown.width - parent.spacing
|
|
compactMode: true
|
|
enableFuzzySearch: true
|
|
options: KeybindsService.getCompositorActions(compositorCategoryDropdown.currentValue).map(action => action.label)
|
|
onValueChanged: value => {
|
|
const action = KeybindsService.getCompositorActions(compositorCategoryDropdown.currentValue).find(entry => entry.label === value);
|
|
if (!action)
|
|
return;
|
|
root.updateEdit({
|
|
"action": action.id,
|
|
"desc": action.label
|
|
});
|
|
}
|
|
}
|
|
|
|
DankTextField {
|
|
visible: root.useCustomCompositor
|
|
width: parent.width - compositorCategoryDropdown.width - parent.spacing
|
|
outlined: true
|
|
placeholderText: KeybindsService.currentProvider === "hyprland" ? I18n.tr("e.g., hl.dsp.focus({ workspace = \"3\" })") : I18n.tr("e.g., focus-workspace 3, resize-column -10")
|
|
text: root.actionType === "compositor" ? root.editAction : ""
|
|
onTextChanged: {
|
|
if (root.actionType !== "compositor")
|
|
return;
|
|
root.updateEdit({
|
|
"action": text
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.actionType === "spawn"
|
|
|
|
body: DankTextField {
|
|
readonly property var parsedCommand: root.actionType === "spawn" ? Actions.parseSpawnCommand(root.editAction) : null
|
|
|
|
width: parent.width
|
|
outlined: true
|
|
leftIconName: "terminal"
|
|
labelText: I18n.tr("Command")
|
|
placeholderText: I18n.tr("e.g., firefox, kitty --title foo")
|
|
text: parsedCommand ? (parsedCommand.command + " " + parsedCommand.args.join(" ")).trim() : ""
|
|
onTextChanged: {
|
|
if (root.actionType !== "spawn")
|
|
return;
|
|
const parts = text.trim().split(" ").filter(part => part);
|
|
root.updateEdit({
|
|
"action": "spawn " + parts.join(" ")
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.actionType === "shell"
|
|
|
|
body: DankTextField {
|
|
width: parent.width
|
|
outlined: true
|
|
leftIconName: "terminal"
|
|
labelText: I18n.tr("Shell", "noun, keybind editor field label for a shell command")
|
|
placeholderText: I18n.tr("e.g., notify-send 'Hello' && sleep 1")
|
|
text: root.actionType === "shell" ? Actions.parseShellCommand(root.editAction) : ""
|
|
onTextChanged: {
|
|
if (root.actionType !== "shell")
|
|
return;
|
|
root.updateEdit({
|
|
"action": Actions.buildShellAction(KeybindsService.currentProvider, text, Actions.getShellFromAction(root.editAction))
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.showDmsArgs && root.dmsArgDefs.some(arg => arg.name === "amount")
|
|
|
|
body: DankTextField {
|
|
id: amountField
|
|
|
|
readonly property string amount: root.dmsParsedArgs?.args?.amount || ""
|
|
|
|
width: parent.width
|
|
outlined: true
|
|
leftIconName: "tune"
|
|
labelText: I18n.tr("Amount", "keybind editor field, numeric amount argument of a dms action")
|
|
placeholderText: "5"
|
|
rightAccessoryWidth: amountUnit.width + Theme.spacingS
|
|
onAmountChanged: {
|
|
if (text !== amount)
|
|
text = amount;
|
|
}
|
|
Component.onCompleted: text = amount
|
|
onEditingFinished: root.commitDmsAmount(text)
|
|
|
|
FieldUnit {
|
|
id: amountUnit
|
|
field: amountField
|
|
text: "%"
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.showDmsArgs && root.dmsArgDefs.some(arg => arg.name === "device")
|
|
|
|
body: DankTextField {
|
|
readonly property string device: root.dmsParsedArgs?.args?.device || ""
|
|
|
|
width: parent.width
|
|
outlined: true
|
|
leftIconName: "devices"
|
|
labelText: I18n.tr("Device")
|
|
placeholderText: I18n.tr("leave empty for default")
|
|
onDeviceChanged: {
|
|
if (text !== device)
|
|
text = device;
|
|
}
|
|
Component.onCompleted: text = device
|
|
onEditingFinished: root.setDmsArgs({
|
|
"device": text
|
|
})
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.showDmsArgs && root.dmsArgDefs.some(arg => arg.name === "tab")
|
|
title: I18n.tr("Tab", "noun, keybind argument label for a dashboard tab")
|
|
|
|
DankDropdown {
|
|
id: dashTabDropdown
|
|
compactMode: true
|
|
options: [I18n.tr("Overview"), I18n.tr("Media"), I18n.tr("Wallpaper"), I18n.tr("Weather")]
|
|
onValueChanged: value => root.setDmsArgs({
|
|
"tab": root.dashTabValue(value)
|
|
})
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.showDmsArgs && root.dmsFlagArgs.length > 0
|
|
|
|
body: DankFilterChips {
|
|
width: parent.width
|
|
multiSelect: true
|
|
showCounts: false
|
|
model: root.dmsFlagArgs.map(flag => ({
|
|
"label": root.dmsFlagLabel(flag.name),
|
|
"value": flag.name,
|
|
"tooltip": root.dmsFlagTooltip(flag.name)
|
|
}))
|
|
selectedValues: root.dmsFlagArgs.filter(flag => root.dmsFlagChecked(flag)).map(flag => flag.name)
|
|
onSelectionToggled: (index, selected) => root.setDmsFlag(root.dmsFlagArgs[index], selected)
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: root.showCompositorPicker ? root.compositorArgDefs.length : 0
|
|
|
|
delegate: SettingsRow {
|
|
id: argRow
|
|
|
|
required property int index
|
|
readonly property var argDef: root.compositorArgDefs[index] ?? null
|
|
|
|
visible: root.showArgEditor(index, argDef)
|
|
|
|
body: DankTextField {
|
|
id: argField
|
|
|
|
property bool syncing: false
|
|
readonly property string actionValue: root.displayArgValue(argRow.argDef)
|
|
|
|
function syncFromAction() {
|
|
if (text === actionValue)
|
|
return;
|
|
syncing = true;
|
|
text = actionValue;
|
|
syncing = false;
|
|
}
|
|
|
|
width: parent.width
|
|
outlined: true
|
|
leftIconName: "tune"
|
|
labelText: I18n.tr(argRow.argDef?.label || "", "keybind option label")
|
|
placeholderText: I18n.tr(argRow.argDef?.placeholder || "", "keybind option placeholder")
|
|
onActionValueChanged: syncFromAction()
|
|
Component.onCompleted: syncFromAction()
|
|
onTextChanged: {
|
|
if (!syncing)
|
|
root.commitArgValue(argRow.argDef, text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.showCompositorPicker && root.compositorFlags.length > 0
|
|
|
|
body: DankFilterChips {
|
|
width: parent.width
|
|
multiSelect: true
|
|
showCounts: false
|
|
model: root.compositorFlags
|
|
selectedValues: root.compositorFlagSelected()
|
|
onSelectionToggled: (index, selected) => root.setCompositorFlag(root.compositorFlags[index].value, selected)
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: KeybindsService.currentProvider !== "aqueous"
|
|
|
|
body: DankTextField {
|
|
width: parent.width
|
|
outlined: true
|
|
leftIconName: "title"
|
|
labelText: I18n.tr("Title")
|
|
placeholderText: I18n.tr("Hotkey overlay title (optional)")
|
|
text: root.editDesc
|
|
onTextChanged: root.updateEdit({
|
|
"desc": text
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: I18n.tr("Options")
|
|
enabled: root.formEnabled
|
|
visible: KeybindsService.currentProvider === "niri" || KeybindsService.currentProvider === "hyprland"
|
|
|
|
SettingsRow {
|
|
body: DankFilterChips {
|
|
width: parent.width
|
|
multiSelect: true
|
|
showCounts: false
|
|
model: root.bindOptionList
|
|
selectedValues: root.selectedBindOptions()
|
|
onSelectionToggled: (index, selected) => root.setBindOption(root.bindOptionList[index].value, selected)
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: KeybindsService.currentProvider === "niri"
|
|
|
|
body: DankTextField {
|
|
id: cooldownField
|
|
|
|
readonly property int cooldownMs: root.editCooldownMs
|
|
|
|
width: parent.width
|
|
outlined: true
|
|
leftIconName: "timer"
|
|
labelText: I18n.tr("Cooldown", "noun, niri keybind cooldown time field label")
|
|
placeholderText: "0"
|
|
rightAccessoryWidth: cooldownUnit.width + Theme.spacingS
|
|
onCooldownMsChanged: {
|
|
const newText = cooldownMs > 0 ? String(cooldownMs) : "";
|
|
if (text !== newText)
|
|
text = newText;
|
|
}
|
|
Component.onCompleted: text = cooldownMs > 0 ? String(cooldownMs) : ""
|
|
onTextChanged: {
|
|
const value = parseInt(text) || 0;
|
|
if (value !== root.editCooldownMs)
|
|
root.updateEdit({
|
|
"cooldownMs": value
|
|
});
|
|
}
|
|
|
|
FieldUnit {
|
|
id: cooldownUnit
|
|
field: cooldownField
|
|
text: I18n.tr("ms", "milliseconds unit suffix after a number field")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component FieldUnit: StyledText {
|
|
required property DankTextField field
|
|
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: field.contentPadding
|
|
y: field.containerTop + (field.containerHeight - height) / 2
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|