add noctalia and fuzzel
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
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 string settingsPath: ""
|
||||
|
||||
property int maxKeybinds: 2
|
||||
property bool requireModifierForNormalKeys: true
|
||||
signal keybindsChanged(var newKeybinds)
|
||||
|
||||
implicitHeight: contentLayout.implicitHeight
|
||||
|
||||
// -1 = not recording, >= 0 = re-recording at index, -2 = adding new
|
||||
property int recordingIndex: -1
|
||||
property bool hasConflict: false
|
||||
|
||||
onRecordingIndexChanged: {
|
||||
PanelService.isKeybindRecording = recordingIndex !== -1;
|
||||
if (recordingIndex !== -1) {
|
||||
hasConflict = false;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real _pillHeight: Style.baseWidgetSize * 1.1 * Style.uiScaleRatio
|
||||
|
||||
function _applyKeybind(keyStr) {
|
||||
if (!keyStr)
|
||||
return;
|
||||
|
||||
// 1. Internal duplicate check (same action)
|
||||
for (let i = 0; i < root.currentKeybinds.length; i++) {
|
||||
if (i !== root.recordingIndex && String(root.currentKeybinds[i]).toLowerCase() === keyStr.toLowerCase()) {
|
||||
hasConflict = true;
|
||||
ToastService.showWarning(I18n.tr("panels.general.keybinds-conflict-title"), I18n.tr("panels.general.keybinds-conflict-description", {
|
||||
"action": root.label || "This action"
|
||||
}));
|
||||
conflictTimer.restart();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. External conflict check (other actions)
|
||||
const conflict = Keybinds.getKeybindConflict(keyStr, root.settingsPath, Settings.data);
|
||||
if (conflict) {
|
||||
hasConflict = true;
|
||||
ToastService.showWarning(I18n.tr("panels.general.keybinds-conflict-title"), I18n.tr("panels.general.keybinds-conflict-description", {
|
||||
"action": conflict
|
||||
}));
|
||||
conflictTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: conflictTimer
|
||||
interval: 2000
|
||||
onTriggered: {
|
||||
hasConflict = false;
|
||||
recordingIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
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("placeholders.keybind-recording") : (isOccupied ? root.currentKeybinds[index] : I18n.tr("placeholders.add-new-keybind"))
|
||||
|
||||
onClicked: {
|
||||
if (isRecordingThis) {
|
||||
root.recordingIndex = -1;
|
||||
} else {
|
||||
root.recordingIndex = index;
|
||||
keybindInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: slotBg
|
||||
anchors.fill: parent
|
||||
radius: Style.iRadiusS
|
||||
color: root.hasConflict && slotArea.isRecordingThis ? Color.mError : (slotArea.isRecordingThis ? Color.mSecondary : (slotArea.containsMouse ? Qt.alpha(Color.mSecondary, 0.15) : Color.mSurface))
|
||||
border.color: root.hasConflict && slotArea.isRecordingThis ? Color.mError : (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: root.hasConflict && slotArea.isRecordingThis ? "alert-circle" : (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 || root.hasConflict
|
||||
}
|
||||
|
||||
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.pointSize: slotArea.isOccupied ? Style.fontSizeM : Style.fontSizeS
|
||||
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 && root.recordingIndex === -1
|
||||
|
||||
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 || root.hasConflict)
|
||||
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 && rawText.charCodeAt(0) !== 127) {
|
||||
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) {
|
||||
// Enforce modifier requirement (Ctrl or Alt) for "normal" keys unless explicitly disabled
|
||||
// Allow Arrows, Nav, Function, and System keys without modifiers
|
||||
const isSpecialKey = (event.key >= Qt.Key_F1 && event.key <= Qt.Key_F35) || (event.key >= Qt.Key_Left && event.key <= Qt.Key_Down) || (event.key === Qt.Key_Home || event.key === Qt.Key_End || event.key === Qt.Key_PageUp || event.key === Qt.Key_PageDown) || (event.key === Qt.Key_Insert || event.key === Qt.Key_Delete || event.key
|
||||
=== Qt.Key_Backspace) || (event.key === Qt.Key_Tab || event.key
|
||||
=== Qt.Key_Return || event.key === Qt.Key_Enter
|
||||
|| event.key === Qt.Key_Escape || event.key
|
||||
=== Qt.Key_Space);
|
||||
|
||||
const hasModifier = (event.modifiers & Qt.ControlModifier) || (event.modifiers & Qt.AltModifier);
|
||||
|
||||
if (root.requireModifierForNormalKeys && !hasModifier && !isSpecialKey) {
|
||||
hasConflict = true;
|
||||
ToastService.showWarning(I18n.tr("panels.general.keybinds-modifier-title"), I18n.tr("panels.general.keybinds-modifier-description"));
|
||||
conflictTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
root._applyKeybind(keyStr + keyName);
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user