summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/KeybindItem.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/KeybindItem.qml')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/KeybindItem.qml1840
1 files changed, 1840 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/KeybindItem.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/KeybindItem.qml
new file mode 100644
index 0000000..a68cc4a
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Widgets/KeybindItem.qml
@@ -0,0 +1,1840 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+import qs.Widgets
+import "../Common/KeyUtils.js" as KeyUtils
+import "../Common/KeybindActions.js" as Actions
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var bindData: ({})
+ property bool isExpanded: false
+ property var panelWindow: null
+ property bool recording: false
+ property bool isNew: false
+ property string restoreKey: ""
+
+ property int editingKeyIndex: -1
+ 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 int _savedCooldownMs: -1
+ property string _savedFlags: ""
+ property var _savedAllowWhenLocked: undefined
+ property var _savedRepeat: undefined
+ property var _savedAllowInhibiting: undefined
+ property bool hasChanges: false
+ property string _actionType: ""
+ property bool addingNewKey: false
+ property bool useCustomCompositor: false
+ property var _shortcutInhibitor: null
+ property bool _altShiftGhost: false
+
+ readonly property bool _shortcutInhibitorAvailable: {
+ try {
+ return typeof ShortcutInhibitor !== "undefined";
+ } catch (e) {
+ return false;
+ }
+ }
+
+ readonly property var keys: bindData.keys || []
+ readonly property bool hasOverride: {
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i].isOverride)
+ return true;
+ }
+ return false;
+ }
+ readonly property var configConflict: bindData.conflict || null
+ readonly property bool hasConfigConflict: configConflict !== null
+ readonly property string _originalKey: editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : ""
+ readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : []
+ readonly property bool hasConflict: _conflicts.length > 0
+
+ readonly property real _inputHeight: Math.round(Theme.fontSizeMedium * 3)
+ readonly property real _chipHeight: Math.round(Theme.fontSizeSmall * 2.3)
+ readonly property real _buttonHeight: Math.round(Theme.fontSizeMedium * 2.3)
+ readonly property real _keysColumnWidth: Math.round(Theme.fontSizeSmall * 12)
+ readonly property real _labelWidth: Math.round(Theme.fontSizeSmall * 5)
+
+ signal toggleExpand
+ signal saveBind(string originalKey, var newData)
+ signal removeBind(string key)
+ signal cancelEdit
+
+ implicitHeight: contentColumn.implicitHeight
+ height: implicitHeight
+
+ Component.onDestruction: _destroyShortcutInhibitor()
+
+ Component.onCompleted: {
+ if (isNew && isExpanded)
+ resetEdits();
+ }
+
+ onIsExpandedChanged: {
+ if (!isExpanded)
+ return;
+ if (restoreKey) {
+ restoreToKey(restoreKey);
+ } else {
+ resetEdits();
+ }
+ }
+
+ onRestoreKeyChanged: {
+ if (!isExpanded || !restoreKey)
+ return;
+ restoreToKey(restoreKey);
+ }
+
+ function restoreToKey(keyToFind) {
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i].key === keyToFind) {
+ editingKeyIndex = i;
+ editKey = keyToFind;
+ editAction = bindData.action || "";
+ editDesc = bindData.desc || "";
+ if (_savedCooldownMs >= 0) {
+ editCooldownMs = _savedCooldownMs;
+ _savedCooldownMs = -1;
+ } else {
+ editCooldownMs = keys[i].cooldownMs || 0;
+ }
+ if (_savedFlags) {
+ editFlags = _savedFlags;
+ _savedFlags = "";
+ } else {
+ editFlags = keys[i].flags || "";
+ }
+ if (_savedAllowWhenLocked !== undefined) {
+ editAllowWhenLocked = _savedAllowWhenLocked;
+ _savedAllowWhenLocked = undefined;
+ } else {
+ editAllowWhenLocked = keys[i].allowWhenLocked || false;
+ }
+ if (_savedRepeat !== undefined) {
+ editRepeat = _savedRepeat;
+ _savedRepeat = undefined;
+ } else {
+ editRepeat = keys[i].repeat;
+ }
+ if (_savedAllowInhibiting !== undefined) {
+ editAllowInhibiting = _savedAllowInhibiting;
+ _savedAllowInhibiting = undefined;
+ } else {
+ editAllowInhibiting = keys[i].allowInhibiting;
+ }
+ hasChanges = false;
+ _actionType = Actions.getActionType(editAction);
+ useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
+ return;
+ }
+ }
+ resetEdits();
+ }
+
+ onEditActionChanged: {
+ _actionType = Actions.getActionType(editAction);
+ }
+
+ function resetEdits() {
+ addingNewKey = false;
+ editingKeyIndex = keys.length > 0 ? 0 : -1;
+ editKey = editingKeyIndex >= 0 ? keys[editingKeyIndex].key : "";
+ editAction = bindData.action || "";
+ editDesc = bindData.desc || "";
+ editCooldownMs = editingKeyIndex >= 0 ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
+ editFlags = editingKeyIndex >= 0 ? (keys[editingKeyIndex].flags || "") : "";
+ editAllowWhenLocked = editingKeyIndex >= 0 ? (keys[editingKeyIndex].allowWhenLocked || false) : false;
+ editRepeat = editingKeyIndex >= 0 ? keys[editingKeyIndex].repeat : undefined;
+ editAllowInhibiting = editingKeyIndex >= 0 ? keys[editingKeyIndex].allowInhibiting : undefined;
+ hasChanges = false;
+ _actionType = Actions.getActionType(editAction);
+ useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
+ }
+
+ function startAddingNewKey() {
+ addingNewKey = true;
+ editingKeyIndex = -1;
+ editKey = "";
+ hasChanges = true;
+ }
+
+ function selectKeyForEdit(index) {
+ if (index < 0 || index >= keys.length)
+ return;
+ addingNewKey = false;
+ editingKeyIndex = index;
+ editKey = keys[index].key;
+ editCooldownMs = keys[index].cooldownMs || 0;
+ editFlags = keys[index].flags || "";
+ editAllowWhenLocked = keys[index].allowWhenLocked || false;
+ editRepeat = keys[index].repeat;
+ editAllowInhibiting = keys[index].allowInhibiting;
+ hasChanges = false;
+ }
+
+ function updateEdit(changes) {
+ 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;
+ const hasKey = editingKeyIndex >= 0 && editingKeyIndex < keys.length;
+ const origKey = hasKey ? keys[editingKeyIndex].key : "";
+ const origCooldown = hasKey ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
+ const origFlags = hasKey ? (keys[editingKeyIndex].flags || "") : "";
+ const origAllowWhenLocked = hasKey ? (keys[editingKeyIndex].allowWhenLocked || false) : false;
+ const origRepeat = hasKey ? keys[editingKeyIndex].repeat : undefined;
+ const origAllowInhibiting = hasKey ? keys[editingKeyIndex].allowInhibiting : undefined;
+ hasChanges = editKey !== origKey || editAction !== (bindData.action || "") || editDesc !== (bindData.desc || "") || editCooldownMs !== origCooldown || editFlags !== origFlags || editAllowWhenLocked !== origAllowWhenLocked || editRepeat !== origRepeat || editAllowInhibiting !== origAllowInhibiting;
+ }
+
+ function canSave() {
+ if (!editKey)
+ return false;
+ if (!Actions.isValidAction(editAction))
+ return false;
+ return true;
+ }
+
+ function doSave() {
+ if (!canSave())
+ return;
+ const origKey = addingNewKey ? "" : _originalKey;
+ let desc = editDesc;
+ if (expandedLoader.item?.currentTitle !== undefined)
+ desc = expandedLoader.item.currentTitle;
+ _savedCooldownMs = editCooldownMs;
+ _savedFlags = editFlags;
+ _savedAllowWhenLocked = editAllowWhenLocked;
+ _savedRepeat = editRepeat;
+ _savedAllowInhibiting = editAllowInhibiting;
+ saveBind(origKey, {
+ "key": editKey,
+ "action": editAction,
+ "desc": desc,
+ "cooldownMs": editCooldownMs,
+ "flags": editFlags,
+ "allowWhenLocked": editAllowWhenLocked,
+ "repeat": editRepeat,
+ "allowInhibiting": editAllowInhibiting
+ });
+ hasChanges = false;
+ addingNewKey = false;
+ }
+
+ function _createShortcutInhibitor() {
+ if (!_shortcutInhibitorAvailable || _shortcutInhibitor)
+ return;
+ const qmlString = `
+ import QtQuick
+ import Quickshell.Wayland
+
+ ShortcutInhibitor {
+ enabled: false
+ window: null
+ }
+ `;
+
+ _shortcutInhibitor = Qt.createQmlObject(qmlString, root, "KeybindItem.ShortcutInhibitor");
+ _shortcutInhibitor.enabled = Qt.binding(() => root.recording);
+ _shortcutInhibitor.window = Qt.binding(() => root.panelWindow);
+ }
+
+ function _destroyShortcutInhibitor() {
+ if (_shortcutInhibitor) {
+ _shortcutInhibitor.enabled = false;
+ _shortcutInhibitor.destroy();
+ _shortcutInhibitor = null;
+ }
+ }
+
+ function startRecording() {
+ _destroyShortcutInhibitor();
+ _createShortcutInhibitor();
+ recording = true;
+ }
+
+ function stopRecording() {
+ recording = false;
+ _destroyShortcutInhibitor();
+ }
+
+ Column {
+ id: contentColumn
+ width: parent.width
+ spacing: 0
+
+ Rectangle {
+ id: collapsedRect
+ width: parent.width
+ height: Math.max(root._inputHeight + Theme.spacingM, keysColumn.implicitHeight + Theme.spacingM * 2)
+ radius: root.isExpanded ? 0 : Theme.cornerRadius
+ topLeftRadius: Theme.cornerRadius
+ topRightRadius: Theme.cornerRadius
+ color: root.hasOverride ? Theme.surfaceContainer : Theme.surfaceContainerHighest
+ border.color: root.hasOverride ? Theme.outlineVariant : "transparent"
+ border.width: root.hasOverride ? 1 : 0
+
+ RowLayout {
+ id: collapsedContent
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Column {
+ id: keysColumn
+ Layout.preferredWidth: root._keysColumnWidth
+ Layout.alignment: Qt.AlignVCenter
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: root.keys
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ property bool isSelected: root.isExpanded && root.editingKeyIndex === index && !root.addingNewKey
+
+ width: root._keysColumnWidth
+ height: root._chipHeight
+ radius: root._chipHeight / 4
+ color: isSelected ? Theme.primary : Theme.surfaceVariant
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: chipArea.pressed ? Theme.surfaceTextHover : (chipArea.containsMouse ? Theme.surfaceTextHover : "transparent")
+ }
+
+ StyledText {
+ text: modelData.key
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: parent.isSelected ? Font.Medium : Font.Normal
+ isMonospace: true
+ color: parent.isSelected ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.centerIn: parent
+ width: parent.width - Theme.spacingS
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ }
+
+ MouseArea {
+ id: chipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.selectKeyForEdit(index);
+ if (!root.isExpanded)
+ root.toggleExpand();
+ }
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
+ spacing: 2
+
+ StyledText {
+ text: root.bindData.desc || root.bindData.action || I18n.tr("No action")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ RowLayout {
+ spacing: Theme.spacingS
+ Layout.fillWidth: true
+
+ StyledText {
+ text: root.bindData.category || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: text.length > 0
+ }
+
+ Rectangle {
+ width: 4
+ height: 4
+ radius: 2
+ color: Theme.surfaceVariantText
+ visible: root.hasOverride && (root.bindData.category ?? "")
+ }
+
+ StyledText {
+ text: I18n.tr("Override")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ visible: root.hasOverride && !root.hasConfigConflict
+ }
+
+ DankIcon {
+ name: "warning"
+ size: Theme.iconSizeSmall
+ color: Theme.primary
+ visible: root.hasConfigConflict
+ }
+
+ StyledText {
+ text: I18n.tr("Overridden by config")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ visible: root.hasConfigConflict
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+ }
+
+ DankIcon {
+ name: root.isExpanded ? "expand_less" : "expand_more"
+ size: Theme.iconSize - 4
+ color: Theme.surfaceVariantText
+ Layout.alignment: Qt.AlignVCenter
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ anchors.leftMargin: root._keysColumnWidth + Theme.spacingM * 2
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.toggleExpand()
+ }
+ }
+
+ Loader {
+ id: expandedLoader
+ width: parent.width
+ active: root.isExpanded
+ visible: status === Loader.Ready
+ asynchronous: true
+ sourceComponent: expandedComponent
+ }
+ }
+
+ Component {
+ id: expandedComponent
+
+ Rectangle {
+ id: expandedRect
+ width: parent ? parent.width : 0
+ height: expandedContent.implicitHeight + Theme.spacingL * 2
+ color: Theme.surfaceContainerHigh
+ border.color: root.hasOverride ? Theme.outlineVariant : "transparent"
+ border.width: root.hasOverride ? 1 : 0
+ bottomLeftRadius: Theme.cornerRadius
+ bottomRightRadius: Theme.cornerRadius
+
+ property alias currentTitle: titleField.text
+
+ ColumnLayout {
+ id: expandedContent
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingM
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: conflictColumn.implicitHeight + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.primary, 0.15)
+ border.color: Theme.withAlpha(Theme.primary, 0.3)
+ border.width: 1
+ visible: root.hasConfigConflict
+
+ Column {
+ id: conflictColumn
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ RowLayout {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "warning"
+ size: Theme.iconSizeSmall
+ color: Theme.primary
+ }
+
+ StyledText {
+ text: I18n.tr("This bind is overridden by config.kdl")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.primary
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Config action: %1").arg(root.configConflict?.action ?? "")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ StyledText {
+ text: I18n.tr("To use this DMS bind, remove or change the keybind in your config.kdl")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root.keys.length > 1 || root.addingNewKey
+
+ StyledText {
+ text: I18n.tr("Keys")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: root.keys
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ property bool isSelected: root.editingKeyIndex === index && !root.addingNewKey
+
+ width: editKeyChipText.implicitWidth + Theme.spacingM
+ height: root._chipHeight
+ radius: root._chipHeight / 4
+ color: isSelected ? Theme.primary : Theme.surfaceVariant
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: editKeyChipArea.pressed ? Theme.surfaceTextHover : (editKeyChipArea.containsMouse && !parent.isSelected ? Theme.surfaceTextHover : "transparent")
+ }
+
+ StyledText {
+ id: editKeyChipText
+ text: modelData.key
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: parent.isSelected ? Font.Medium : Font.Normal
+ isMonospace: true
+ color: parent.isSelected ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: editKeyChipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.selectKeyForEdit(index)
+ }
+ }
+ }
+
+ Rectangle {
+ width: root._chipHeight
+ height: root._chipHeight
+ radius: root._chipHeight / 4
+ color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
+ visible: !root.isNew
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: addKeyArea.pressed ? Theme.surfaceTextHover : (addKeyArea.containsMouse && !root.addingNewKey ? Theme.surfaceTextHover : "transparent")
+ }
+
+ DankIcon {
+ name: "add"
+ size: Theme.iconSizeSmall
+ color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: addKeyArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.startAddingNewKey()
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: root.addingNewKey ? I18n.tr("New Key") : I18n.tr("Key")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ FocusScope {
+ id: captureScope
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ focus: root.recording
+
+ Component.onCompleted: {
+ if (root.recording)
+ forceActiveFocus();
+ }
+
+ Connections {
+ target: root
+ function onRecordingChanged() {
+ if (root.recording)
+ captureScope.forceActiveFocus();
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: root.recording ? Theme.primaryContainer : Theme.surfaceContainer
+ border.color: root.recording ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
+ border.width: root.recording ? 2 : 1
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: root.editKey || (root.recording ? I18n.tr("Press key...") : I18n.tr("Click to capture"))
+ font.pixelSize: Theme.fontSizeMedium
+ isMonospace: root.editKey ? true : false
+ color: root.editKey ? Theme.surfaceText : Theme.surfaceVariantText
+ width: parent.width - recordBtn.width - parent.spacing
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ }
+
+ DankActionButton {
+ id: recordBtn
+ width: root._chipHeight
+ height: root._chipHeight
+ anchors.verticalCenter: parent.verticalCenter
+ circular: false
+ iconName: root.recording ? "close" : "radio_button_checked"
+ iconSize: Theme.iconSizeSmall
+ iconColor: root.recording ? Theme.error : Theme.primary
+ onClicked: root.recording ? root.stopRecording() : root.startRecording()
+ }
+ }
+ }
+
+ Keys.onPressed: event => {
+ if (!root.recording)
+ return;
+ event.accepted = true;
+
+ switch (event.key) {
+ case Qt.Key_Control:
+ case Qt.Key_Shift:
+ case Qt.Key_Alt:
+ case Qt.Key_Meta:
+ return;
+ }
+
+ if (event.key === 0 && (event.modifiers & Qt.AltModifier)) {
+ root._altShiftGhost = true;
+ return;
+ }
+
+ let mods = KeyUtils.modsFromEvent(event.modifiers);
+ let qtKey = event.key;
+
+ if (root._altShiftGhost && (event.modifiers & Qt.AltModifier) && !mods.includes("Shift")) {
+ mods.push("Shift");
+ }
+ root._altShiftGhost = false;
+
+ if (qtKey === Qt.Key_Backtab) {
+ qtKey = Qt.Key_Tab;
+ if (!mods.includes("Shift"))
+ mods.push("Shift");
+ }
+
+ const key = KeyUtils.xkbKeyFromQtKey(qtKey);
+ if (!key) {
+ console.warn("[KeybindItem] Unknown key:", event.key, "mods:", event.modifiers);
+ return;
+ }
+
+ root.updateEdit({
+ "key": KeyUtils.formatToken(mods, key)
+ });
+ root.stopRecording();
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: root.recording ? Qt.CrossCursor : Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+
+ onClicked: {
+ if (!root.recording)
+ root.startRecording();
+ }
+
+ onWheel: wheel => {
+ if (!root.recording) {
+ wheel.accepted = false;
+ return;
+ }
+ wheel.accepted = true;
+
+ const 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");
+
+ let wheelKey = "";
+ if (wheel.angleDelta.y > 0)
+ wheelKey = "WheelScrollUp";
+ else if (wheel.angleDelta.y < 0)
+ wheelKey = "WheelScrollDown";
+ else if (wheel.angleDelta.x > 0)
+ wheelKey = "WheelScrollRight";
+ else if (wheel.angleDelta.x < 0)
+ wheelKey = "WheelScrollLeft";
+
+ if (!wheelKey)
+ return;
+ root.updateEdit({
+ "key": KeyUtils.formatToken(mods, wheelKey)
+ });
+ root.stopRecording();
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: root._inputHeight
+ Layout.preferredHeight: root._inputHeight
+ radius: Theme.cornerRadius
+ color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
+ visible: root.keys.length === 1 && !root.isNew
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: singleAddKeyArea.pressed ? Theme.surfaceTextHover : (singleAddKeyArea.containsMouse && !root.addingNewKey ? Theme.surfaceTextHover : "transparent")
+ }
+
+ DankIcon {
+ name: "add"
+ size: Theme.iconSizeSmall + 2
+ color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: singleAddKeyArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.startAddingNewKey()
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingS
+ visible: root.hasConflict
+ Layout.leftMargin: root._labelWidth + Theme.spacingM
+
+ DankIcon {
+ name: "warning"
+ size: Theme.iconSizeSmall
+ color: Theme.primary
+ }
+
+ StyledText {
+ text: I18n.tr("Conflicts with: %1").arg(root._conflicts.map(c => c.desc).join(", "))
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Type")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: KeybindsService.actionTypes
+
+ delegate: Rectangle {
+ id: typeDelegate
+ required property var modelData
+ required property int index
+
+ readonly property var tooltipTexts: ({
+ "dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"),
+ "compositor": I18n.tr("Niri compositor actions (focus, move, etc.)"),
+ "spawn": I18n.tr("Run a program (e.g., firefox, kitty)"),
+ "shell": I18n.tr("Run a shell command (e.g., notify-send)")
+ })
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._buttonHeight
+ radius: Theme.cornerRadius
+ color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
+ border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
+ border.width: 1
+
+ RowLayout {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: typeDelegate.modelData.icon
+ size: Theme.iconSizeSmall
+ color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: typeDelegate.modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
+ visible: typeDelegate.width > 100
+ }
+ }
+
+ MouseArea {
+ id: typeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ switch (typeDelegate.modelData.id) {
+ case "dms":
+ root.updateEdit({
+ "action": KeybindsService.dmsActions[0].id,
+ "desc": KeybindsService.dmsActions[0].label
+ });
+ break;
+ case "compositor":
+ root.updateEdit({
+ "action": "close-window",
+ "desc": "Close Window"
+ });
+ break;
+ case "spawn":
+ root.updateEdit({
+ "action": "spawn ",
+ "desc": ""
+ });
+ break;
+ case "shell":
+ root.updateEdit({
+ "action": "spawn sh -c \"\"",
+ "desc": ""
+ });
+ break;
+ }
+ }
+ onContainsMouseChanged: {
+ if (containsMouse) {
+ typeTooltip.show(typeDelegate.tooltipTexts[typeDelegate.modelData.id], typeDelegate, 0, 0, "bottom");
+ } else {
+ typeTooltip.hide();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ DankTooltipV2 {
+ id: typeTooltip
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "dms"
+
+ StyledText {
+ text: I18n.tr("Action")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankDropdown {
+ Layout.fillWidth: true
+ compactMode: true
+ currentValue: KeybindsService.getActionLabel(root.editAction) || I18n.tr("Select...")
+ options: KeybindsService.getDmsActions().map(a => a.label)
+ enableFuzzySearch: true
+ maxPopupHeight: 300
+ onValueChanged: value => {
+ const actions = KeybindsService.getDmsActions();
+ for (const act of actions) {
+ if (act.label === value) {
+ root.updateEdit({
+ "action": act.id,
+ "desc": act.label
+ });
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ id: dmsArgsRow
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ readonly property var argConfig: Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
+ readonly property var parsedArgs: argConfig?.type === "dms" ? Actions.parseDmsActionArgs(root.editAction) : null
+ readonly property var dmsActionArgs: Actions.getDmsActionArgs()
+ readonly property bool hasAmountArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "amount") ?? false) : false
+ readonly property bool hasDeviceArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "device") ?? false) : false
+ readonly property bool hasTabArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "tab") ?? false) : false
+
+ visible: root._actionType === "dms" && argConfig?.type === "dms"
+
+ StyledText {
+ text: I18n.tr("Amount")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ visible: dmsArgsRow.hasAmountArg
+ }
+
+ DankTextField {
+ id: dmsAmountField
+ Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 5.5)
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: "5"
+ visible: dmsArgsRow.hasAmountArg
+
+ Connections {
+ target: dmsArgsRow
+ function onParsedArgsChanged() {
+ const newText = dmsArgsRow.parsedArgs?.args?.amount || "";
+ if (dmsAmountField.text !== newText)
+ dmsAmountField.text = newText;
+ }
+ }
+
+ Component.onCompleted: {
+ text = dmsArgsRow.parsedArgs?.args?.amount || "";
+ }
+
+ onEditingFinished: {
+ if (!dmsArgsRow.parsedArgs)
+ return;
+ const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
+ newArgs.amount = text || "5";
+ root.updateEdit({
+ "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
+ });
+ }
+ }
+
+ StyledText {
+ text: "%"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: dmsArgsRow.hasAmountArg
+ }
+
+ StyledText {
+ text: I18n.tr("Device")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.leftMargin: dmsArgsRow.hasAmountArg ? Theme.spacingM : 0
+ Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : root._labelWidth
+ visible: dmsArgsRow.hasDeviceArg
+ }
+
+ DankTextField {
+ id: dmsDeviceField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("leave empty for default")
+ visible: dmsArgsRow.hasDeviceArg
+
+ Connections {
+ target: dmsArgsRow
+ function onParsedArgsChanged() {
+ const newText = dmsArgsRow.parsedArgs?.args?.device || "";
+ if (dmsDeviceField.text !== newText)
+ dmsDeviceField.text = newText;
+ }
+ }
+
+ Component.onCompleted: {
+ text = dmsArgsRow.parsedArgs?.args?.device || "";
+ }
+
+ onEditingFinished: {
+ if (!dmsArgsRow.parsedArgs)
+ return;
+ const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
+ newArgs.device = text;
+ root.updateEdit({
+ "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
+ });
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ visible: !dmsArgsRow.hasDeviceArg && !dmsArgsRow.hasTabArg
+ }
+
+ StyledText {
+ text: I18n.tr("Tab")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ visible: dmsArgsRow.hasTabArg
+ }
+
+ DankDropdown {
+ id: dmsTabDropdown
+ Layout.fillWidth: true
+ compactMode: true
+ visible: dmsArgsRow.hasTabArg
+ currentValue: {
+ const tab = dmsArgsRow.parsedArgs?.args?.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");
+ }
+ }
+ options: [I18n.tr("Overview"), I18n.tr("Media"), I18n.tr("Wallpaper"), I18n.tr("Weather")]
+ onValueChanged: value => {
+ if (!dmsArgsRow.parsedArgs)
+ return;
+ const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
+ switch (value) {
+ case I18n.tr("Media"):
+ newArgs.tab = "media";
+ break;
+ case I18n.tr("Wallpaper"):
+ newArgs.tab = "wallpaper";
+ break;
+ case I18n.tr("Weather"):
+ newArgs.tab = "weather";
+ break;
+ default:
+ newArgs.tab = "";
+ break;
+ }
+ root.updateEdit({
+ "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
+ });
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "compositor" && !root.useCustomCompositor
+
+ StyledText {
+ text: I18n.tr("Action")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankDropdown {
+ id: compositorCatDropdown
+ Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 8.5)
+ compactMode: true
+ currentValue: {
+ const base = root.editAction.split(" ")[0];
+ const cats = KeybindsService.getCompositorCategories();
+ for (const cat of cats) {
+ const actions = KeybindsService.getCompositorActions(cat);
+ for (const act of actions) {
+ if (act.id === base)
+ return cat;
+ }
+ }
+ return cats[0] || "Window";
+ }
+ options: KeybindsService.getCompositorCategories()
+ }
+
+ DankDropdown {
+ Layout.fillWidth: true
+ compactMode: true
+ currentValue: KeybindsService.getActionLabel(root.editAction) || I18n.tr("Select...")
+ options: KeybindsService.getCompositorActions(compositorCatDropdown.currentValue).map(a => a.label)
+ enableFuzzySearch: true
+ maxPopupHeight: 300
+ onValueChanged: value => {
+ const actions = KeybindsService.getCompositorActions(compositorCatDropdown.currentValue);
+ for (const act of actions) {
+ if (act.label === value) {
+ root.updateEdit({
+ "action": act.id,
+ "desc": act.label
+ });
+ return;
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: root._inputHeight
+ Layout.preferredHeight: root._inputHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceVariant
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: customToggleArea.pressed ? Theme.surfaceTextHover : (customToggleArea.containsMouse ? Theme.surfaceTextHover : "transparent")
+ }
+
+ DankIcon {
+ name: "edit"
+ size: Theme.iconSizeSmall + 2
+ color: Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: customToggleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.useCustomCompositor = true
+ }
+ }
+ }
+
+ RowLayout {
+ id: optionsRow
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "compositor" && !root.useCustomCompositor && Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
+
+ readonly property var argConfig: Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
+ readonly property var parsedArgs: Actions.parseCompositorActionArgs(KeybindsService.currentProvider, root.editAction)
+
+ StyledText {
+ text: I18n.tr("Options")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingS
+
+ DankTextField {
+ id: argValueField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+
+ readonly property string _argName: optionsRow.argConfig?.config?.args[0]?.name || "value"
+
+ visible: {
+ const cfg = optionsRow.argConfig;
+ if (!cfg?.config?.args)
+ return false;
+ const firstArg = cfg.config.args[0];
+ return firstArg && (firstArg.type === "text" || firstArg.type === "number");
+ }
+ placeholderText: optionsRow.argConfig?.config?.args[0]?.placeholder || ""
+
+ Connections {
+ target: optionsRow
+ function onParsedArgsChanged() {
+ const argName = argValueField._argName;
+ const newText = optionsRow.parsedArgs?.args[argName] || "";
+ if (argValueField.text !== newText)
+ argValueField.text = newText;
+ }
+ }
+
+ Component.onCompleted: {
+ text = optionsRow.parsedArgs?.args[_argName] || "";
+ }
+
+ onEditingFinished: {
+ const cfg = optionsRow.argConfig;
+ if (!cfg)
+ return;
+ const parsed = optionsRow.parsedArgs;
+ const args = Object.assign({}, parsed?.args || {});
+ args[_argName] = text;
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, parsed?.base || cfg.base, args)
+ });
+ }
+ }
+
+ RowLayout {
+ visible: {
+ const cfg = optionsRow.argConfig;
+ if (!cfg)
+ return false;
+ switch (cfg.base) {
+ case "move-column-to-workspace":
+ case "move-column-to-workspace-down":
+ case "move-column-to-workspace-up":
+ return true;
+ }
+ return false;
+ }
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ id: focusToggle
+ checked: optionsRow.parsedArgs?.args?.focus !== false
+ onToggled: newChecked => {
+ const cfg = optionsRow.argConfig;
+ if (!cfg)
+ return;
+ const parsed = optionsRow.parsedArgs;
+ const args = {};
+ if (cfg.base === "move-column-to-workspace")
+ args.index = parsed?.args?.index || "";
+ if (!newChecked)
+ args.focus = false;
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, cfg.base, args)
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Follow focus")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ visible: optionsRow.argConfig?.base?.startsWith("screenshot") ?? false
+ spacing: Theme.spacingM
+
+ RowLayout {
+ visible: optionsRow.argConfig?.base !== "screenshot-window"
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ id: showPointerToggle
+ checked: optionsRow.parsedArgs?.args["show-pointer"] === true
+ onToggled: newChecked => {
+ const parsed = optionsRow.parsedArgs;
+ const base = parsed?.base || "screenshot";
+ const args = Object.assign({}, parsed?.args || {});
+ args["show-pointer"] = newChecked;
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, base, args)
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Pointer")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ visible: optionsRow.argConfig?.base !== "screenshot"
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ id: writeToDiskToggle
+ checked: optionsRow.parsedArgs?.args["write-to-disk"] === true
+ onToggled: newChecked => {
+ const parsed = optionsRow.parsedArgs;
+ const base = parsed?.base || "screenshot-screen";
+ const args = Object.assign({}, parsed?.args || {});
+ args["write-to-disk"] = newChecked;
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, base, args)
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Save")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ RowLayout {
+ visible: optionsRow.argConfig?.base === "quit"
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: optionsRow.parsedArgs?.args["skip-confirmation"] === true
+ onToggled: newChecked => {
+ const args = newChecked ? {
+ "skip-confirmation": true
+ } : {};
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, "quit", args)
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Skip confirmation")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "compositor" && root.useCustomCompositor
+
+ StyledText {
+ text: I18n.tr("Custom")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: customCompositorField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: 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
+ });
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: root._inputHeight
+ Layout.preferredHeight: root._inputHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceVariant
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: presetToggleArea.pressed ? Theme.surfaceTextHover : (presetToggleArea.containsMouse ? Theme.surfaceTextHover : "transparent")
+ }
+
+ DankIcon {
+ name: "list"
+ size: Theme.iconSizeSmall + 2
+ color: Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: presetToggleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.useCustomCompositor = false;
+ root.updateEdit({
+ "action": "close-window",
+ "desc": "Close Window"
+ });
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "spawn"
+
+ StyledText {
+ text: I18n.tr("Command")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: spawnTextField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("e.g., firefox, kitty --title foo")
+ readonly property var _parsed: root._actionType === "spawn" ? Actions.parseSpawnCommand(root.editAction) : null
+ text: _parsed ? (_parsed.command + " " + _parsed.args.join(" ")).trim() : ""
+ onTextChanged: {
+ if (root._actionType !== "spawn")
+ return;
+ const parts = text.trim().split(" ").filter(p => p);
+ const action = parts.length > 0 ? "spawn " + parts.join(" ") : "spawn ";
+ root.updateEdit({
+ "action": action
+ });
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "shell"
+
+ StyledText {
+ text: I18n.tr("Shell")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: shellTextField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("e.g., notify-send 'Hello' && sleep 1")
+ text: root._actionType === "shell" ? Actions.parseShellCommand(root.editAction) : ""
+ onTextChanged: {
+ if (root._actionType !== "shell")
+ return;
+ var shell = Actions.getShellFromAction(root.editAction);
+ root.updateEdit({
+ "action": Actions.buildShellAction(KeybindsService.currentProvider, text, shell)
+ });
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Title")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: titleField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("Hotkey overlay title (optional)")
+ text: root.editDesc
+ onTextChanged: root.updateEdit({
+ "desc": text
+ })
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: KeybindsService.currentProvider === "hyprland"
+
+ StyledText {
+ text: I18n.tr("Flags")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editFlags.indexOf("e") !== -1
+ onToggled: newChecked => {
+ let flags = root.editFlags.split("").filter(f => f !== "e");
+ if (newChecked)
+ flags.push("e");
+ root.updateEdit({
+ "flags": flags.join("")
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Repeat")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editFlags.indexOf("l") !== -1
+ onToggled: newChecked => {
+ let flags = root.editFlags.split("").filter(f => f !== "l");
+ if (newChecked)
+ flags.push("l");
+ root.updateEdit({
+ "flags": flags.join("")
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Locked")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editFlags.indexOf("r") !== -1
+ onToggled: newChecked => {
+ let flags = root.editFlags.split("").filter(f => f !== "r");
+ if (newChecked)
+ flags.push("r");
+ root.updateEdit({
+ "flags": flags.join("")
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Release")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editFlags.indexOf("o") !== -1
+ onToggled: newChecked => {
+ let flags = root.editFlags.split("").filter(f => f !== "o");
+ if (newChecked)
+ flags.push("o");
+ root.updateEdit({
+ "flags": flags.join("")
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Long press")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: KeybindsService.currentProvider === "niri"
+
+ StyledText {
+ text: I18n.tr("Cooldown")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: cooldownField
+ Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 7)
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: "0"
+
+ Connections {
+ target: root
+ function onEditCooldownMsChanged() {
+ const newText = root.editCooldownMs > 0 ? String(root.editCooldownMs) : "";
+ if (cooldownField.text !== newText)
+ cooldownField.text = newText;
+ }
+ }
+
+ Component.onCompleted: {
+ text = root.editCooldownMs > 0 ? String(root.editCooldownMs) : "";
+ }
+
+ onTextChanged: {
+ const val = parseInt(text) || 0;
+ if (val !== root.editCooldownMs)
+ root.updateEdit({
+ "cooldownMs": val
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("ms")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: KeybindsService.currentProvider === "niri"
+
+ StyledText {
+ text: I18n.tr("Options")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editRepeat !== false
+ onToggled: newChecked => {
+ root.updateEdit({
+ "repeat": newChecked ? undefined : false
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Repeat")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editAllowWhenLocked
+ onToggled: newChecked => {
+ root.updateEdit({
+ "allowWhenLocked": newChecked
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("When locked")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editAllowInhibiting !== false
+ onToggled: newChecked => {
+ root.updateEdit({
+ "allowInhibiting": newChecked ? undefined : false
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Inhibitable")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ DankActionButton {
+ Layout.preferredWidth: root._buttonHeight
+ Layout.preferredHeight: root._buttonHeight
+ circular: false
+ iconName: "delete"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.error
+ visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && root.keys[root.editingKeyIndex].isOverride && !root.isNew
+ onClicked: root.removeBind(root._originalKey)
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ StyledText {
+ text: !root.canSave() ? I18n.tr("Set key and action to save") : (root.hasChanges ? I18n.tr("Unsaved changes") : I18n.tr("No changes"))
+ font.pixelSize: Theme.fontSizeSmall
+ color: root.hasChanges ? Theme.surfaceText : Theme.surfaceVariantText
+ visible: !root.isNew
+ }
+
+ DankButton {
+ text: I18n.tr("Cancel")
+ buttonHeight: root._buttonHeight
+ backgroundColor: Theme.surfaceContainer
+ textColor: Theme.surfaceText
+ visible: root.hasChanges || root.isNew
+ onClicked: {
+ if (root.isNew) {
+ root.cancelEdit();
+ } else {
+ root.resetEdits();
+ root.toggleExpand();
+ }
+ }
+ }
+
+ DankButton {
+ text: root.isNew ? I18n.tr("Add") : I18n.tr("Save")
+ buttonHeight: root._buttonHeight
+ enabled: root.canSave()
+ visible: root.hasChanges || root.isNew
+ onClicked: root.doSave()
+ }
+ }
+ }
+ }
+ }
+}