mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
settings: stop repeating the page title as the first card label translations: index untitled cards by their page, not a nested row fixes #3604 fixes #3609
1225 lines
44 KiB
QML
1225 lines
44 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 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) {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|