mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
SessionMenuTab: add keybind setting per entry
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user