mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-26 14:32:52 -05:00
- niri only for now - requires quickshell-git, hidden otherwise - Add, Edit, Delete keybinds - Large suite of pre-defined and custom actions - Works with niri 25.11+ include feature
1151 lines
47 KiB
QML
1151 lines
47 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell.Wayland
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var bindData: ({})
|
|
property bool isExpanded: false
|
|
property var panelWindow: null
|
|
property bool recording: false
|
|
property bool isNew: false
|
|
|
|
property int editingKeyIndex: -1
|
|
property string editKey: ""
|
|
property string editAction: ""
|
|
property string editDesc: ""
|
|
property bool hasChanges: false
|
|
property string _actionType: ""
|
|
property bool addingNewKey: false
|
|
property bool useCustomCompositor: false
|
|
|
|
readonly property var keys: bindData.keys || []
|
|
readonly property bool hasOverride: {
|
|
for (let i = 0; i < keys.length; i++) {
|
|
if (keys[i].isOverride)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
readonly property string _originalKey: editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : ""
|
|
|
|
signal toggleExpand
|
|
signal saveBind(string originalKey, var newData)
|
|
signal removeBind(string key)
|
|
signal cancelEdit
|
|
|
|
implicitHeight: contentColumn.implicitHeight
|
|
height: implicitHeight
|
|
|
|
onIsExpandedChanged: {
|
|
if (isExpanded)
|
|
resetEdits();
|
|
}
|
|
|
|
onEditActionChanged: {
|
|
_actionType = KeybindsService.getActionType(editAction);
|
|
}
|
|
|
|
function resetEdits() {
|
|
addingNewKey = false;
|
|
editingKeyIndex = keys.length > 0 ? 0 : -1;
|
|
editKey = editingKeyIndex >= 0 ? keys[editingKeyIndex].key : "";
|
|
editAction = bindData.action || "";
|
|
editDesc = bindData.desc || "";
|
|
hasChanges = false;
|
|
_actionType = KeybindsService.getActionType(editAction);
|
|
useCustomCompositor = _actionType === "compositor" && !isKnownCompositorAction(editAction);
|
|
}
|
|
|
|
function isKnownCompositorAction(action) {
|
|
if (!action)
|
|
return false;
|
|
const cats = KeybindsService.getCompositorCategories();
|
|
for (const cat of cats) {
|
|
const actions = KeybindsService.getCompositorActions(cat);
|
|
for (const act of actions) {
|
|
if (act.id === action)
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
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;
|
|
const origKey = editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : "";
|
|
hasChanges = editKey !== origKey || editAction !== (bindData.action || "") || editDesc !== (bindData.desc || "");
|
|
}
|
|
|
|
function canSave() {
|
|
if (!editKey)
|
|
return false;
|
|
if (!KeybindsService.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;
|
|
saveBind(origKey, {
|
|
key: editKey,
|
|
action: editAction,
|
|
desc: desc
|
|
});
|
|
hasChanges = false;
|
|
addingNewKey = false;
|
|
}
|
|
|
|
function startRecording() {
|
|
recording = true;
|
|
}
|
|
|
|
function stopRecording() {
|
|
recording = false;
|
|
}
|
|
|
|
function modsFromEvent(mods) {
|
|
const result = [];
|
|
if (mods & Qt.ControlModifier)
|
|
result.push("Ctrl");
|
|
if (mods & Qt.ShiftModifier)
|
|
result.push("Shift");
|
|
const hasAlt = mods & Qt.AltModifier;
|
|
const hasSuper = mods & Qt.MetaModifier;
|
|
if (hasAlt && hasSuper) {
|
|
result.push("Mod");
|
|
} else {
|
|
if (hasAlt)
|
|
result.push("Alt");
|
|
if (hasSuper)
|
|
result.push("Super");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function normalizeKeyCombo(keyCombo) {
|
|
return keyCombo.toLowerCase().replace(/\bmod\b/g, "super").replace(/\bsuper\b/g, "super");
|
|
}
|
|
|
|
function getConflictingBinds(keyCombo) {
|
|
if (!keyCombo)
|
|
return [];
|
|
const conflicts = [];
|
|
const allBinds = KeybindsService.getFlatBinds();
|
|
const normalizedKey = normalizeKeyCombo(keyCombo);
|
|
for (let i = 0; i < allBinds.length; i++) {
|
|
const bind = allBinds[i];
|
|
if (bind.action === bindData.action)
|
|
continue;
|
|
for (let k = 0; k < bind.keys.length; k++) {
|
|
if (normalizeKeyCombo(bind.keys[k].key) === normalizedKey) {
|
|
conflicts.push({
|
|
action: bind.action,
|
|
desc: bind.desc || bind.action
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return conflicts;
|
|
}
|
|
|
|
readonly property var _conflicts: editKey ? getConflictingBinds(editKey) : []
|
|
readonly property bool hasConflict: _conflicts.length > 0
|
|
|
|
readonly property var _keyMap: ({
|
|
[Qt.Key_Left]: "Left",
|
|
[Qt.Key_Right]: "Right",
|
|
[Qt.Key_Up]: "Up",
|
|
[Qt.Key_Down]: "Down",
|
|
[Qt.Key_Comma]: "Comma",
|
|
[Qt.Key_Period]: "Period",
|
|
[Qt.Key_Slash]: "Slash",
|
|
[Qt.Key_Semicolon]: "Semicolon",
|
|
[Qt.Key_Apostrophe]: "Apostrophe",
|
|
[Qt.Key_BracketLeft]: "BracketLeft",
|
|
[Qt.Key_BracketRight]: "BracketRight",
|
|
[Qt.Key_Backslash]: "Backslash",
|
|
[Qt.Key_Minus]: "Minus",
|
|
[Qt.Key_Equal]: "Equal",
|
|
[Qt.Key_QuoteLeft]: "grave",
|
|
[Qt.Key_Space]: "space",
|
|
[Qt.Key_Print]: "Print",
|
|
[Qt.Key_Return]: "Return",
|
|
[Qt.Key_Enter]: "Return",
|
|
[Qt.Key_Tab]: "Tab",
|
|
[Qt.Key_Backspace]: "BackSpace",
|
|
[Qt.Key_Delete]: "Delete",
|
|
[Qt.Key_Insert]: "Insert",
|
|
[Qt.Key_Home]: "Home",
|
|
[Qt.Key_End]: "End",
|
|
[Qt.Key_PageUp]: "Page_Up",
|
|
[Qt.Key_PageDown]: "Page_Down",
|
|
[Qt.Key_Escape]: "Escape",
|
|
[Qt.Key_CapsLock]: "Caps_Lock",
|
|
[Qt.Key_NumLock]: "Num_Lock",
|
|
[Qt.Key_ScrollLock]: "Scroll_Lock",
|
|
[Qt.Key_Pause]: "Pause",
|
|
[Qt.Key_VolumeUp]: "XF86AudioRaiseVolume",
|
|
[Qt.Key_VolumeDown]: "XF86AudioLowerVolume",
|
|
[Qt.Key_VolumeMute]: "XF86AudioMute",
|
|
[Qt.Key_MicMute]: "XF86AudioMicMute",
|
|
[Qt.Key_MediaPlay]: "XF86AudioPlay",
|
|
[Qt.Key_MediaPause]: "XF86AudioPause",
|
|
[Qt.Key_MediaStop]: "XF86AudioStop",
|
|
[Qt.Key_MediaNext]: "XF86AudioNext",
|
|
[Qt.Key_MediaPrevious]: "XF86AudioPrev",
|
|
[Qt.Key_MediaRecord]: "XF86AudioRecord",
|
|
[Qt.Key_MonBrightnessUp]: "XF86MonBrightnessUp",
|
|
[Qt.Key_MonBrightnessDown]: "XF86MonBrightnessDown",
|
|
[Qt.Key_KeyboardBrightnessUp]: "XF86KbdBrightnessUp",
|
|
[Qt.Key_KeyboardBrightnessDown]: "XF86KbdBrightnessDown",
|
|
[Qt.Key_PowerOff]: "XF86PowerOff",
|
|
[Qt.Key_Sleep]: "XF86Sleep",
|
|
[Qt.Key_WakeUp]: "XF86WakeUp",
|
|
[Qt.Key_Eject]: "XF86Eject",
|
|
[Qt.Key_Calculator]: "XF86Calculator",
|
|
[Qt.Key_Explorer]: "XF86Explorer",
|
|
[Qt.Key_HomePage]: "XF86HomePage",
|
|
[Qt.Key_Search]: "XF86Search",
|
|
[Qt.Key_LaunchMail]: "XF86Mail",
|
|
[Qt.Key_Launch0]: "XF86Launch0",
|
|
[Qt.Key_Launch1]: "XF86Launch1",
|
|
[Qt.Key_Exclam]: "1",
|
|
[Qt.Key_At]: "2",
|
|
[Qt.Key_NumberSign]: "3",
|
|
[Qt.Key_Dollar]: "4",
|
|
[Qt.Key_Percent]: "5",
|
|
[Qt.Key_AsciiCircum]: "6",
|
|
[Qt.Key_Ampersand]: "7",
|
|
[Qt.Key_Asterisk]: "8",
|
|
[Qt.Key_ParenLeft]: "9",
|
|
[Qt.Key_ParenRight]: "0",
|
|
[Qt.Key_Less]: "Comma",
|
|
[Qt.Key_Greater]: "Period",
|
|
[Qt.Key_Question]: "Slash",
|
|
[Qt.Key_Colon]: "Semicolon",
|
|
[Qt.Key_QuoteDbl]: "Apostrophe",
|
|
[Qt.Key_BraceLeft]: "BracketLeft",
|
|
[Qt.Key_BraceRight]: "BracketRight",
|
|
[Qt.Key_Bar]: "Backslash",
|
|
[Qt.Key_Underscore]: "Minus",
|
|
[Qt.Key_Plus]: "Equal",
|
|
[Qt.Key_AsciiTilde]: "grave"
|
|
})
|
|
|
|
function xkbKeyFromQtKey(qk) {
|
|
if (qk >= Qt.Key_A && qk <= Qt.Key_Z)
|
|
return String.fromCharCode(qk);
|
|
if (qk >= Qt.Key_0 && qk <= Qt.Key_9)
|
|
return String.fromCharCode(qk);
|
|
if (qk >= Qt.Key_F1 && qk <= Qt.Key_F35)
|
|
return "F" + (qk - Qt.Key_F1 + 1);
|
|
return _keyMap[qk] || "";
|
|
}
|
|
|
|
function formatToken(mods, key) {
|
|
return (mods.length ? mods.join("+") + "+" : "") + key;
|
|
}
|
|
|
|
Column {
|
|
id: contentColumn
|
|
width: parent.width
|
|
spacing: 0
|
|
|
|
Rectangle {
|
|
id: collapsedRect
|
|
width: parent.width
|
|
height: Math.max(52, 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: 140
|
|
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: 140
|
|
height: 28
|
|
radius: 6
|
|
color: isSelected ? Theme.primary : Theme.surfaceVariant
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: parent.radius
|
|
color: chipArea.pressed ? Theme.surfaceTextHover : (chipArea.containsMouse ? Theme.surfaceTextHover : "transparent")
|
|
}
|
|
|
|
StyledText {
|
|
id: keyChipText
|
|
text: modelData.key
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: isSelected ? Font.Medium : Font.Normal
|
|
isMonospace: true
|
|
color: 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
|
|
DankIcon {
|
|
name: root.isExpanded ? "expand_less" : "expand_more"
|
|
size: 20
|
|
color: Theme.surfaceVariantText
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 140 + 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
|
|
|
|
ShortcutInhibitor {
|
|
id: shortcutInhibitor
|
|
enabled: root.recording
|
|
window: root.panelWindow
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: expandedContent
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
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: 60
|
|
}
|
|
|
|
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: 28
|
|
radius: 6
|
|
color: isSelected ? Theme.primary : Theme.surfaceVariant
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: parent.radius
|
|
color: editKeyChipArea.pressed ? Theme.surfaceTextHover : (editKeyChipArea.containsMouse && !isSelected ? Theme.surfaceTextHover : "transparent")
|
|
}
|
|
|
|
StyledText {
|
|
id: editKeyChipText
|
|
text: modelData.key
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: isSelected ? Font.Medium : Font.Normal
|
|
isMonospace: true
|
|
color: 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: 28
|
|
height: 28
|
|
radius: 6
|
|
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: 16
|
|
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: 60
|
|
}
|
|
|
|
FocusScope {
|
|
id: captureScope
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40
|
|
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: 28
|
|
height: 28
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
circular: false
|
|
iconName: root.recording ? "close" : "radio_button_checked"
|
|
iconSize: 16
|
|
iconColor: root.recording ? Theme.error : Theme.primary
|
|
onClicked: root.recording ? root.stopRecording() : root.startRecording()
|
|
}
|
|
}
|
|
}
|
|
|
|
Keys.onPressed: event => {
|
|
if (!root.recording)
|
|
return;
|
|
if (event.key === Qt.Key_Escape) {
|
|
root.stopRecording();
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
|
|
switch (event.key) {
|
|
case Qt.Key_Control:
|
|
case Qt.Key_Shift:
|
|
case Qt.Key_Alt:
|
|
case Qt.Key_Meta:
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
|
|
const mods = [];
|
|
if (event.modifiers & Qt.ControlModifier)
|
|
mods.push("Ctrl");
|
|
if (event.modifiers & Qt.ShiftModifier)
|
|
mods.push("Shift");
|
|
if ((event.modifiers & Qt.AltModifier) && (event.modifiers & Qt.MetaModifier)) {
|
|
mods.push("Mod");
|
|
} else {
|
|
if (event.modifiers & Qt.AltModifier)
|
|
mods.push("Alt");
|
|
if (event.modifiers & Qt.MetaModifier)
|
|
mods.push("Super");
|
|
}
|
|
|
|
const key = root.xkbKeyFromQtKey(event.key);
|
|
if (key) {
|
|
root.updateEdit({
|
|
key: root.formatToken(mods, key)
|
|
});
|
|
root.stopRecording();
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: !root.recording
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.startRecording()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 40
|
|
Layout.preferredHeight: 40
|
|
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: 18
|
|
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: 60 + Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "warning"
|
|
size: 16
|
|
color: Theme.warning
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Conflicts with: %1").arg(root._conflicts.map(c => c.desc).join(", "))
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.warning
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
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: 60
|
|
}
|
|
|
|
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: 36
|
|
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: 16
|
|
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: {
|
|
if (typeDelegate.modelData.id === "dms") {
|
|
root.updateEdit({
|
|
action: KeybindsService.dmsActions[0].id,
|
|
desc: KeybindsService.dmsActions[0].label
|
|
});
|
|
} else if (typeDelegate.modelData.id === "compositor") {
|
|
root.updateEdit({
|
|
action: "close-window",
|
|
desc: "Close Window"
|
|
});
|
|
} else if (typeDelegate.modelData.id === "spawn") {
|
|
root.updateEdit({
|
|
action: "spawn ",
|
|
desc: ""
|
|
});
|
|
} else if (typeDelegate.modelData.id === "shell") {
|
|
root.updateEdit({
|
|
action: "spawn sh -c \"\"",
|
|
desc: ""
|
|
});
|
|
}
|
|
}
|
|
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: 60
|
|
}
|
|
|
|
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 {
|
|
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: 60
|
|
}
|
|
|
|
DankDropdown {
|
|
id: compositorCatDropdown
|
|
Layout.preferredWidth: 120
|
|
compactMode: true
|
|
currentValue: {
|
|
const action = root.editAction;
|
|
const cats = KeybindsService.getCompositorCategories();
|
|
for (const cat of cats) {
|
|
const actions = KeybindsService.getCompositorActions(cat);
|
|
for (const act of actions) {
|
|
if (act.id === action)
|
|
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: 40
|
|
Layout.preferredHeight: 40
|
|
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: 18
|
|
color: Theme.surfaceVariantText
|
|
anchors.centerIn: parent
|
|
}
|
|
|
|
MouseArea {
|
|
id: customToggleArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.useCustomCompositor = true
|
|
}
|
|
}
|
|
}
|
|
|
|
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: 60
|
|
}
|
|
|
|
DankTextField {
|
|
id: customCompositorField
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40
|
|
placeholderText: I18n.tr("e.g., focus-workspace 3, resize-column -10")
|
|
text: root._actionType === "compositor" ? root.editAction : ""
|
|
onEditingFinished: {
|
|
if (root._actionType !== "compositor")
|
|
return;
|
|
if (text.trim())
|
|
root.updateEdit({
|
|
action: text.trim()
|
|
});
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 40
|
|
Layout.preferredHeight: 40
|
|
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: 18
|
|
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: 60
|
|
}
|
|
|
|
DankTextField {
|
|
id: spawnTextField
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40
|
|
placeholderText: I18n.tr("e.g., firefox, kitty --title foo")
|
|
readonly property var _parsed: root._actionType === "spawn" ? KeybindsService.parseSpawnCommand(root.editAction) : null
|
|
text: _parsed ? (_parsed.command + " " + _parsed.args.join(" ")).trim() : ""
|
|
onEditingFinished: {
|
|
if (root._actionType !== "spawn")
|
|
return;
|
|
const parts = text.trim().split(" ").filter(p => p);
|
|
if (parts.length === 0)
|
|
return;
|
|
const changes = {
|
|
action: "spawn " + parts.join(" ")
|
|
};
|
|
if (!root.editDesc)
|
|
changes.desc = parts[0];
|
|
root.updateEdit(changes);
|
|
}
|
|
}
|
|
}
|
|
|
|
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: 60
|
|
}
|
|
|
|
DankTextField {
|
|
id: shellTextField
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40
|
|
placeholderText: I18n.tr("e.g., notify-send 'Hello' && sleep 1")
|
|
text: root._actionType === "shell" ? KeybindsService.parseShellCommand(root.editAction) : ""
|
|
onEditingFinished: {
|
|
if (root._actionType !== "shell")
|
|
return;
|
|
if (text.trim()) {
|
|
root.updateEdit({
|
|
action: KeybindsService.buildShellAction(text.trim())
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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: 60
|
|
}
|
|
|
|
DankTextField {
|
|
id: titleField
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40
|
|
placeholderText: I18n.tr("Hotkey overlay title (optional)")
|
|
text: root.editDesc
|
|
onEditingFinished: root.updateEdit({
|
|
desc: text
|
|
})
|
|
}
|
|
}
|
|
|
|
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: 32
|
|
Layout.preferredHeight: 32
|
|
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: 32
|
|
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: 32
|
|
enabled: root.canSave()
|
|
visible: root.hasChanges || root.isNew
|
|
onClicked: root.doSave()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|