Files
noctalia-shell/Widgets/NKeybindRecorder.qml
T
2026-02-11 16:11:35 +01:00

274 lines
10 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
Item {
id: root
property string label: ""
property string description: ""
property var currentKeybinds: []
property string defaultKeybind: ""
property bool allowEmpty: false
property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant
property int maxKeybinds: 2
signal keybindsChanged(var newKeybinds)
implicitHeight: contentLayout.implicitHeight
// -1 = not recording, >= 0 = re-recording at index, -2 = adding new
property int recordingIndex: -1
onRecordingIndexChanged: PanelService.isKeybindRecording = recordingIndex !== -1
readonly property real _pillHeight: Style.baseWidgetSize * 1.1 * Style.uiScaleRatio
function _applyKeybind(keyStr) {
var newKeybinds = Array.from(root.currentKeybinds);
if (recordingIndex >= 0) {
newKeybinds[recordingIndex] = keyStr;
}
// Ensure array is dense and limited to maxKeybinds
newKeybinds = newKeybinds.filter(k => k !== undefined && k !== "").slice(0, root.maxKeybinds);
recordingIndex = -1;
root.keybindsChanged(newKeybinds);
}
RowLayout {
id: contentLayout
width: parent.width
spacing: Style.marginL
// Label and Description (optional)
NLabel {
id: labelContainer
label: root.label
description: root.description
labelColor: root.labelColor
descriptionColor: root.descriptionColor
visible: label !== "" || description !== ""
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
id: slotsRow
spacing: Style.marginS
Layout.alignment: Qt.AlignVCenter | (labelContainer.visible ? Qt.AlignRight : Qt.AlignLeft)
Repeater {
model: root.maxKeybinds
delegate: MouseArea {
id: slotArea
width: Math.round(180 * Style.uiScaleRatio)
height: root._pillHeight
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
readonly property bool isOccupied: index < root.currentKeybinds.length
readonly property bool isRecordingThis: root.recordingIndex === index
readonly property string keybindText: isRecordingThis ? I18n.tr("panels.session-menu.entry-settings-keybind-recording") : (isOccupied ? root.currentKeybinds[index] : I18n.tr("placeholders.type-new-binding"))
onClicked: {
if (isRecordingThis) {
root.recordingIndex = -1;
} else {
root.recordingIndex = index;
keybindInput.forceActiveFocus();
}
}
Rectangle {
id: slotBg
anchors.fill: parent
radius: Style.iRadiusS
color: slotArea.isRecordingThis ? Color.mSecondary : (slotArea.containsMouse ? Qt.alpha(Color.mSecondary, 0.15) : Color.mSurface)
border.color: slotArea.isRecordingThis ? Color.mPrimary : (slotArea.containsMouse ? Color.mSecondary : Color.mOutline)
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginM
anchors.rightMargin: Style.marginS
spacing: Style.marginXS
NIcon {
icon: slotArea.isRecordingThis ? "circle-dot" : "keyboard"
color: slotArea.isRecordingThis ? Color.mOnSecondary : (slotArea.isOccupied ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.4))
opacity: 0.8
visible: !slotArea.isRecordingThis
}
NText {
Layout.fillWidth: true
text: slotArea.keybindText
color: slotArea.isRecordingThis ? Color.mOnSecondary : (slotArea.isOccupied ? Color.mOnSurface : Color.mOnSurfaceVariant)
font.family: slotArea.isOccupied && !slotArea.isRecordingThis ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
font.weight: slotArea.isOccupied ? Style.fontWeightBold : Style.fontWeightRegular
elide: Text.ElideRight
opacity: slotArea.isOccupied || slotArea.isRecordingThis ? 1.0 : 0.6
}
Item {
Layout.preferredWidth: Math.round(root._pillHeight * 0.7)
Layout.fillHeight: true
visible: slotArea.isOccupied
NIconButton {
anchors.centerIn: parent
visible: root.recordingIndex === -1 && (root.currentKeybinds.length > 1 || root.allowEmpty)
icon: "x"
colorBg: "transparent"
colorBgHover: Qt.alpha(Color.mError, 0.1)
colorFg: Color.mOnSurfaceVariant
colorFgHover: Color.mError
border.width: 0
baseSize: Style.baseWidgetSize * 0.7
onClicked: {
var newKeybinds = Array.from(root.currentKeybinds);
newKeybinds.splice(index, 1);
root.keybindsChanged(newKeybinds);
}
}
}
}
}
}
}
}
// Hidden Item to capture keys
Item {
id: keybindInput
width: 0
height: 0
focus: true
Keys.onPressed: event => {
if (root.recordingIndex === -1)
return;
// Handle Escape specifically to ensure it doesn't close the panel
if (event.key === Qt.Key_Escape) {
event.accepted = true;
root._applyKeybind("Esc");
return;
}
// Ignore modifier keys by themselves
if (event.key === Qt.Key_Control || event.key === Qt.Key_Shift || event.key === Qt.Key_Alt || event.key === Qt.Key_Meta) {
event.accepted = true; // Consume modifiers too while listening
return;
}
let keyStr = "";
if (event.modifiers & Qt.ControlModifier)
keyStr += "Ctrl+";
if (event.modifiers & Qt.AltModifier)
keyStr += "Alt+";
if (event.modifiers & Qt.ShiftModifier)
keyStr += "Shift+";
let keyName = "";
let rawText = event.text;
if (event.key >= Qt.Key_A && event.key <= Qt.Key_Z || event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
keyName = String.fromCharCode(event.key);
} else if (event.key >= Qt.Key_F1 && event.key <= Qt.Key_F12) {
keyName = "F" + (event.key - Qt.Key_F1 + 1);
} else if (rawText && rawText.length > 0 && rawText.charCodeAt(0) > 31) {
keyName = rawText.toUpperCase();
// Handle shifted digits
if (event.modifiers & Qt.ShiftModifier) {
const shiftMap = {
"!": "1",
"\"": "2",
"§": "3",
"$": "4",
"%": "5",
"&": "6",
"/": "7",
"(": "8",
")": "9",
"=": "0",
"@": "2",
"#": "3",
"^": "6",
"*": "8"
};
if (shiftMap[keyName])
keyName = shiftMap[keyName];
}
} else {
switch (event.key) {
case Qt.Key_Return:
keyName = "Return";
break;
case Qt.Key_Enter:
keyName = "Enter";
break;
case Qt.Key_Tab:
keyName = "Tab";
break;
case Qt.Key_Backspace:
keyName = "Backspace";
break;
case Qt.Key_Delete:
keyName = "Del";
break;
case Qt.Key_Insert:
keyName = "Ins";
break;
case Qt.Key_Home:
keyName = "Home";
break;
case Qt.Key_End:
keyName = "End";
break;
case Qt.Key_PageUp:
keyName = "PgUp";
break;
case Qt.Key_PageDown:
keyName = "PgDn";
break;
case Qt.Key_Left:
keyName = "Left";
break;
case Qt.Key_Right:
keyName = "Right";
break;
case Qt.Key_Up:
keyName = "Up";
break;
case Qt.Key_Down:
keyName = "Down";
break;
}
}
if (keyName) {
root._applyKeybind(keyStr + keyName);
}
event.accepted = true;
}
}
}
}