SessionMenuTab: add keybind setting per entry

This commit is contained in:
Lysec
2026-02-06 12:50:45 +01:00
parent a6283d2962
commit 23e08a60d5
20 changed files with 317 additions and 54 deletions
+74 -17
View File
@@ -134,7 +134,8 @@ SmartPanel {
"title": metadata.title,
"isShutdown": metadata.isShutdown,
"countdownEnabled": settingOption.countdownEnabled !== undefined ? settingOption.countdownEnabled : true,
"command": settingOption.command || ""
"command": settingOption.command || "",
"keybind": settingOption.keybind || ""
});
}
}
@@ -436,6 +437,51 @@ SmartPanel {
selectPreviousWrapped();
}
function checkKeybind(event) {
if (powerOptions.length === 0)
return;
// Construct key string in the same format as the recorder
// 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) {
return;
}
let keyStr = "";
if (event.modifiers & Qt.ControlModifier)
keyStr += "Ctrl+";
if (event.modifiers & Qt.AltModifier)
keyStr += "Alt+";
if (event.modifiers & Qt.ShiftModifier)
keyStr += "Shift+";
if (event.modifiers & Qt.MetaModifier)
keyStr += "Meta+";
let keyName = "";
if (event.text && event.text.length > 0 && event.text.charCodeAt(0) > 31) {
keyName = event.text.toUpperCase();
} else {
// Only checking text based keys for now as per recorder
return;
}
if (!keyName)
return;
const pressedKeybind = keyStr + keyName;
for (var i = 0; i < powerOptions.length; i++) {
const option = powerOptions[i];
if (option.keybind === pressedKeybind) {
selectedIndex = i;
startTimer(option.action);
event.accepted = true;
return;
}
}
}
// Number selection handler (kept for backward compatibility if needed, though keybinds might override common keys)
function onNumberPressed(number) {
if (!Settings.data.sessionMenu.showNumberLabels) {
return;
@@ -481,6 +527,10 @@ SmartPanel {
}
}
Keys.onPressed: event => {
root.checkKeybind(event);
}
HoverHandler {
id: globalHoverHandler
@@ -566,6 +616,7 @@ SmartPanel {
startTimer(modelData.action);
}
pending: timerActive && pendingAction === modelData.action
keybind: modelData.keybind || ""
}
}
}
@@ -647,6 +698,7 @@ SmartPanel {
startTimer(modelData.action);
}
pending: timerActive && pendingAction === modelData.action
keybind: modelData.keybind || ""
}
}
}
@@ -697,25 +749,27 @@ SmartPanel {
font.weight: Style.fontWeightBold
}
// Number indicator (keybind)
// Keybind/Number indicator (keybind)
Rectangle {
id: numberIndicatorRect
anchors.left: countdownText.visible ? countdownText.right : parent.left
anchors.leftMargin: countdownText.visible ? Style.marginXS : 0
anchors.verticalCenter: parent.verticalCenter
width: Style.marginXL
height: width
width: Math.max(Style.marginXL, labelText.implicitWidth + Style.marginM)
height: Style.marginXL
radius: Math.min(Style.radiusM, height / 2)
color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mPrimary : Qt.alpha(Color.mSurfaceVariant, 0.5)
color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mOnPrimary : Qt.alpha(Color.mSurfaceVariant, 0.5)
border.width: Style.borderS
border.color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mPrimary : Color.mOutline
visible: Settings.data.sessionMenu.showNumberLabels && buttonRoot.number > 0
border.color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mOnPrimary : Color.mOutline
visible: (Settings.data.sessionMenu.showNumberLabels && buttonRoot.number > 0) || buttonRoot.keybind !== ""
NText {
id: labelText
anchors.centerIn: parent
text: buttonRoot.number
text: buttonRoot.keybind !== "" ? buttonRoot.keybind : buttonRoot.number
pointSize: Style.fontSizeS
color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mOnPrimary : Color.mOnSurface
font.weight: Style.fontWeightBold
color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mPrimary : Color.mOnSurface
Behavior on color {
ColorAnimation {
@@ -733,6 +787,7 @@ SmartPanel {
property bool isShutdown: false
property bool isSelected: false
property int number: 0
property string keybind: ""
property int buttonIndex: -1
// Effective hover state that respects ignoreMouseHover
@@ -886,6 +941,7 @@ SmartPanel {
property bool isShutdown: false
property bool isSelected: false
property int number: 0
property string keybind: ""
property int buttonIndex: -1
// Effective hover state that respects ignoreMouseHover
@@ -1026,28 +1082,29 @@ SmartPanel {
}
}
// Number indicator in top-right corner
// Keybind/Number indicator in top-right corner
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginM
width: Style.fontSizeM * 2
height: width
width: Math.max(Style.fontSizeM * 2, largeNumberText.implicitWidth + Style.marginM)
height: Style.fontSizeM * 2
radius: Math.min(Style.radiusM, height / 2)
color: Qt.alpha(Color.mSurfaceVariant, 0.7)
color: (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover) ? Color.mOnPrimary : Qt.alpha(Color.mSurfaceVariant, 0.7)
border.width: Style.borderS
border.color: Color.mOutline
visible: Settings.data.sessionMenu.showNumberLabels && largeButtonRoot.number > 0 && !largeButtonRoot.pending
border.color: (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover) ? Color.mOnPrimary : Color.mOutline
visible: (Settings.data.sessionMenu.showNumberLabels && largeButtonRoot.number > 0 || largeButtonRoot.keybind !== "") && !largeButtonRoot.pending
z: 10
NText {
id: largeNumberText
anchors.centerIn: parent
text: largeButtonRoot.number
text: largeButtonRoot.keybind !== "" ? largeButtonRoot.keybind : largeButtonRoot.number
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: {
if (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover)
return Color.mOnPrimary;
return Color.mPrimary;
return Color.mOnSurface;
}
@@ -13,7 +13,7 @@ Popup {
property string entryId: ""
property string entryText: ""
signal updateEntryCommand(int index, string command)
signal updateEntryProperties(int index, var properties)
// Default commands mapping
readonly property var defaultCommands: {
@@ -38,11 +38,19 @@ Popup {
// Load command when popup opens
if (entryData) {
commandInput.text = entryData.command || "";
keybindInput.text = entryData.keybind || "";
}
// Request focus to ensure keyboard input works
forceActiveFocus();
}
function save() {
root.updateEntryProperties(root.entryIndex, {
"command": commandInput.text,
"keybind": keybindInput.text
});
}
background: Rectangle {
id: bgRect
@@ -78,7 +86,10 @@ Popup {
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
onClicked: root.close()
onClicked: {
root.save();
root.close();
}
}
}
@@ -96,16 +107,7 @@ Popup {
label: I18n.tr("common.command")
description: I18n.tr("panels.session-menu.entry-settings-command-description")
placeholderText: I18n.tr("panels.session-menu.entry-settings-command-placeholder")
onEditingFinished: {
// Auto-focus on Enter
applyButton.forceActiveFocus();
}
Keys.onReturnPressed: {
applyButton.clicked();
}
Keys.onEnterPressed: {
applyButton.clicked();
}
onTextChanged: root.save()
}
// Default command info
@@ -152,31 +154,146 @@ Popup {
}
}
// Action buttons
// Keybind input
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
spacing: Style.marginM
spacing: Style.marginS
Item {
NTextInput {
id: keybindInput
Layout.fillWidth: true
label: I18n.tr("common.keybind")
description: I18n.tr("panels.session-menu.entry-settings-keybind-description")
placeholderText: listening ? I18n.tr("panels.session-menu.entry-settings-keybind-recording") : I18n.tr("panels.session-menu.entry-settings-keybind-placeholder")
inputIconName: listening ? "circle-dot" : ""
readOnly: true
property bool listening: false
// Clear text when starting to listen to show it's active
onListeningChanged: {
if (listening) {
text = "";
}
}
Keys.onPressed: event => {
if (!listening)
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) {
return;
}
let keyStr = "";
if (event.modifiers & Qt.ControlModifier)
keyStr += "Ctrl+";
if (event.modifiers & Qt.AltModifier)
keyStr += "Alt+";
if (event.modifiers & Qt.ShiftModifier)
keyStr += "Shift+";
if (event.modifiers & Qt.MetaModifier)
keyStr += "Meta+";
let keyName = "";
if (event.text && event.text.length > 0 && event.text.charCodeAt(0) > 31) {
keyName = event.text.toUpperCase();
} else {
keyName = event.text.toUpperCase();
}
if (keyName) {
keybindInput.text = keyStr + keyName;
listening = false;
focusScope.focus = true;
root.save();
}
}
}
NButton {
id: closeButton
text: I18n.tr("common.close")
outlined: true
onClicked: root.close()
}
NIconButton {
id: clearButton
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: Math.round(4 * Style.uiScaleRatio)
visible: !keybindInput.listening && keybindInput.text !== ""
icon: "circle-x"
NButton {
id: applyButton
text: I18n.tr("common.apply")
icon: "check"
colorBg: "transparent"
colorBgHover: Qt.alpha(Color.mError, 0.1)
colorFg: Color.mOnSurfaceVariant
colorFgHover: Color.mError
border.width: 0
tooltipText: I18n.tr("common.clear")
onClicked: {
root.updateEntryCommand(root.entryIndex, commandInput.text);
keybindInput.text = "";
root.save();
}
}
NIconButton {
id: recordButton
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: Math.round(4 * Style.uiScaleRatio)
Layout.rightMargin: Style.marginS
icon: keybindInput.listening ? "x" : "circle-dot"
// Standard colors when not listening, distinctive when listening
colorBg: keybindInput.listening ? Color.mError : Color.mSurfaceVariant
colorFg: keybindInput.listening ? Color.mOnError : Color.mPrimary
colorBgHover: keybindInput.listening ? Color.mError : Color.mHover
colorFgHover: keybindInput.listening ? Color.mOnError : Color.mOnHover
// Match NButton radius
customRadius: Style.iRadiusS
border.width: 0
Behavior on colorBg {
ColorAnimation {
duration: Style.animationFast
}
}
SequentialAnimation {
id: recordingPulse
running: keybindInput.listening
loops: Animation.Infinite
NumberAnimation {
target: recordButton
property: "opacity"
from: 1.0
to: 0.6
duration: 500
easing.type: Easing.InOutSine
}
NumberAnimation {
target: recordButton
property: "opacity"
from: 0.6
to: 1.0
duration: 500
easing.type: Easing.InOutSine
}
}
tooltipText: keybindInput.listening ? I18n.tr("common.cancel") : I18n.tr("common.record")
onClicked: {
if (keybindInput.listening) {
keybindInput.listening = false;
focusScope.focus = true;
} else {
keybindInput.listening = true;
keybindInput.forceActiveFocus();
}
}
}
}
// Bottom spacer to maintain padding
Item {
Layout.preferredHeight: Style.marginS
}
}
}
@@ -58,7 +58,8 @@ ColumnLayout {
"action": entriesModel[i].id,
"enabled": entriesModel[i].enabled,
"countdownEnabled": entriesModel[i].countdownEnabled !== undefined ? entriesModel[i].countdownEnabled : true,
"command": entriesModel[i].command || ""
"command": entriesModel[i].command || "",
"keybind": entriesModel[i].keybind || ""
});
}
Settings.data.sessionMenu.powerOptions = toSave;
@@ -112,11 +113,9 @@ ColumnLayout {
if (dialog) {
root._activeDialog = dialog;
dialog.updateEntryCommand.connect((idx, command) => {
root.updateEntry(idx, {
"command": command
});
});
dialog.updateEntryProperties.connect((idx, properties) => {
root.updateEntry(idx, properties);
});
dialog.closed.connect(() => {
if (root._activeDialog === dialog) {
root._activeDialog = null;
@@ -159,6 +158,7 @@ ColumnLayout {
entry.countdownEnabled = settingEntry.countdownEnabled !== undefined ? settingEntry.countdownEnabled : true;
// Load custom command if defined
entry.command = settingEntry.command || "";
entry.keybind = settingEntry.keybind || "";
entriesModel.push(entry);
}
}
@@ -180,6 +180,7 @@ ColumnLayout {
entry.countdownEnabled = true;
// Default command to empty string for new entries
entry.command = "";
entry.keybind = "";
entriesModel.push(entry);
}
}