From 7176e890afb39251039bb704ee50bf585d39e1c7 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 15 Nov 2025 13:48:39 +0100 Subject: [PATCH] SessionMenuTab: add per entry countdown toggle (implements #746 ) --- Modules/Panels/SessionMenu/SessionMenu.qml | 49 +++- .../Panels/Settings/Tabs/SessionMenuTab.qml | 272 ++++++++++++++++-- 2 files changed, 298 insertions(+), 23 deletions(-) diff --git a/Modules/Panels/SessionMenu/SessionMenu.qml b/Modules/Panels/SessionMenu/SessionMenu.qml index b72d0200b..e4aaece1d 100644 --- a/Modules/Panels/SessionMenu/SessionMenu.qml +++ b/Modules/Panels/SessionMenu/SessionMenu.qml @@ -88,7 +88,7 @@ SmartPanel { } // Build powerOptions from settings, filtering enabled ones and adding metadata - readonly property var powerOptions: (function () { + property var powerOptions: { var options = [] var settingsOptions = Settings.data.sessionMenu.powerOptions || [] @@ -100,13 +100,39 @@ SmartPanel { "action": settingOption.action, "icon": metadata.icon, "title": metadata.title, - "isShutdown": metadata.isShutdown + "isShutdown": metadata.isShutdown, + "countdownEnabled": settingOption.countdownEnabled !== undefined ? settingOption.countdownEnabled : true }) } } return options - })() + } + + // Update powerOptions when settings change + Connections { + target: Settings.data.sessionMenu + function onPowerOptionsChanged() { + var options = [] + var settingsOptions = Settings.data.sessionMenu.powerOptions || [] + + for (var i = 0; i < settingsOptions.length; i++) { + var settingOption = settingsOptions[i] + if (settingOption.enabled && actionMetadata[settingOption.action]) { + var metadata = actionMetadata[settingOption.action] + options.push({ + "action": settingOption.action, + "icon": metadata.icon, + "title": metadata.title, + "isShutdown": metadata.isShutdown, + "countdownEnabled": settingOption.countdownEnabled !== undefined ? settingOption.countdownEnabled : true + }) + } + } + + root.powerOptions = options + } + } // Lifecycle handlers onOpened: { @@ -120,12 +146,27 @@ SmartPanel { // Timer management function startTimer(action) { - // If countdown is disabled, execute immediately + // Check if global countdown is disabled if (!Settings.data.sessionMenu.enableCountdown) { executeAction(action) return } + // Check per-item countdown setting + var option = null + for (var i = 0; i < powerOptions.length; i++) { + if (powerOptions[i].action === action) { + option = powerOptions[i] + break + } + } + + // If this specific action has countdown disabled, execute immediately + if (option && option.countdownEnabled === false) { + executeAction(action) + return + } + if (timerActive && pendingAction === action) { // Second click - execute immediately executeAction(action) diff --git a/Modules/Panels/Settings/Tabs/SessionMenuTab.qml b/Modules/Panels/Settings/Tabs/SessionMenuTab.qml index 267aa5b49..130f53376 100644 --- a/Modules/Panels/Settings/Tabs/SessionMenuTab.qml +++ b/Modules/Panels/Settings/Tabs/SessionMenuTab.qml @@ -49,7 +49,8 @@ ColumnLayout { for (var i = 0; i < entriesModel.length; i++) { toSave.push({ "action": entriesModel[i].id, - "enabled": entriesModel[i].enabled + "enabled": entriesModel[i].enabled, + "countdownEnabled": entriesModel[i].countdownEnabled !== undefined ? entriesModel[i].countdownEnabled : true }) } Settings.data.sessionMenu.powerOptions = toSave @@ -66,6 +67,8 @@ ColumnLayout { if (settingEntry.action === entriesDefault[j].id) { var entry = entriesDefault[j] entry.enabled = settingEntry.enabled + // Default countdownEnabled to true for backward compatibility + entry.countdownEnabled = settingEntry.countdownEnabled !== undefined ? settingEntry.countdownEnabled : true entriesModel.push(entry) } } @@ -83,6 +86,8 @@ ColumnLayout { if (!found) { var entry = entriesDefault[i] + // Default countdownEnabled to true for new entries + entry.countdownEnabled = true entriesModel.push(entry) } } @@ -172,7 +177,7 @@ ColumnLayout { // Entries Management Section ColumnLayout { - spacing: Style.marginXXS + spacing: Style.marginM Layout.fillWidth: true NHeader { @@ -180,24 +185,253 @@ ColumnLayout { description: I18n.tr("settings.session-menu.entries.section.description") } - NReorderCheckboxes { + // List of items + Item { Layout.fillWidth: true - model: entriesModel - disabledIds: [] - onItemToggled: function (index, enabled) { - var newModel = entriesModel.slice() - newModel[index] = Object.assign({}, newModel[index], { - "enabled": enabled - }) - entriesModel = newModel - saveEntries() - } - onItemsReordered: function (fromIndex, toIndex) { - var newModel = entriesModel.slice() - var item = newModel.splice(fromIndex, 1)[0] - newModel.splice(toIndex, 0, item) - entriesModel = newModel - saveEntries() + implicitHeight: listView.contentHeight + + ListView { + id: listView + anchors.fill: parent + spacing: Style.marginS + interactive: false + clip: true + model: entriesModel + + // Store reference to root's saveEntries function + property var saveEntriesFunc: root.saveEntries + + delegate: Item { + id: delegateItem + width: listView.width + height: contentRow.height + + required property int index + required property var modelData + + property bool dragging: false + property int dragStartY: 0 + property int dragStartIndex: -1 + property int dragTargetIndex: -1 + property var saveEntriesFunc: listView.saveEntriesFunc + + Rectangle { + anchors.fill: parent + radius: Style.radiusM + color: delegateItem.dragging ? Color.mSurfaceVariant : Color.transparent + border.color: delegateItem.dragging ? Color.mOutline : Color.transparent + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + RowLayout { + id: contentRow + width: parent.width + spacing: Style.marginS + + // Drag handle + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 + Layout.preferredHeight: Style.baseWidgetSize * 0.7 + Layout.alignment: Qt.AlignVCenter + radius: Style.radiusXS + color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : Color.transparent + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 2 + + Repeater { + model: 3 + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 0.28 + Layout.preferredHeight: 2 + radius: 1 + color: Color.mOutline + } + } + } + + MouseArea { + id: dragHandleMouseArea + anchors.fill: parent + cursorShape: Qt.SizeVerCursor + hoverEnabled: true + preventStealing: false + z: 1000 + + onPressed: mouse => { + delegateItem.dragStartIndex = delegateItem.index + delegateItem.dragTargetIndex = delegateItem.index + delegateItem.dragStartY = delegateItem.y + delegateItem.dragging = true + delegateItem.z = 999 + preventStealing = true + } + + onPositionChanged: mouse => { + if (delegateItem.dragging) { + var dy = mouse.y - height / 2 + var newY = delegateItem.y + dy + newY = Math.max(0, Math.min(newY, listView.contentHeight - delegateItem.height)) + delegateItem.y = newY + var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + Style.marginS)) + targetIndex = Math.max(0, Math.min(targetIndex, listView.count - 1)) + delegateItem.dragTargetIndex = targetIndex + } + } + + onReleased: { + preventStealing = false + if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) { + var newModel = entriesModel.slice() + var item = newModel.splice(delegateItem.dragStartIndex, 1)[0] + newModel.splice(delegateItem.dragTargetIndex, 0, item) + entriesModel = newModel + root.saveEntries() + } + delegateItem.dragging = false + delegateItem.dragStartIndex = -1 + delegateItem.dragTargetIndex = -1 + delegateItem.z = 0 + } + + onCanceled: { + preventStealing = false + delegateItem.dragging = false + delegateItem.dragStartIndex = -1 + delegateItem.dragTargetIndex = -1 + delegateItem.z = 0 + } + } + } + + // Enable checkbox + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 + Layout.preferredHeight: Style.baseWidgetSize * 0.7 + Layout.alignment: Qt.AlignVCenter + radius: Style.radiusXS + color: modelData.enabled ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: modelData.enabled + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: Color.mOnPrimary + pointSize: Math.max(Style.fontSizeXS, Style.baseWidgetSize * 0.35) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + var newModel = entriesModel.slice() + newModel[index] = Object.assign({}, newModel[index], { + "enabled": !modelData.enabled + }) + entriesModel = newModel + root.saveEntries() + } + } + } + + // Label + NText { + Layout.fillWidth: true + text: modelData.text + color: Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + // Countdown toggle with icon (only shown when global countdown is enabled) + RowLayout { + visible: Settings.data.sessionMenu.enableCountdown + spacing: Style.marginXS + Layout.alignment: Qt.AlignVCenter + + NIcon { + icon: "clock" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + + NToggle { + checked: modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true + onToggled: function (checked) { + var newModel = entriesModel.slice() + newModel[delegateItem.index] = Object.assign({}, newModel[delegateItem.index], { + "countdownEnabled": checked + }) + entriesModel = newModel + delegateItem.saveEntriesFunc() + } + } + } + } + + // Position binding for non-dragging state + y: { + if (delegateItem.dragging) { + return delegateItem.y + } + + var draggedIndex = -1 + var targetIndex = -1 + for (var i = 0; i < listView.count; i++) { + var item = listView.itemAtIndex(i) + if (item && item.dragging) { + draggedIndex = item.dragStartIndex + targetIndex = item.dragTargetIndex + break + } + } + + if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) { + var currentIndex = delegateItem.index + if (draggedIndex < targetIndex) { + if (currentIndex > draggedIndex && currentIndex <= targetIndex) { + return (currentIndex - 1) * (delegateItem.height + Style.marginS) + } + } else { + if (currentIndex >= targetIndex && currentIndex < draggedIndex) { + return (currentIndex + 1) * (delegateItem.height + Style.marginS) + } + } + } + + return delegateItem.index * (delegateItem.height + Style.marginS) + } + + Behavior on y { + enabled: !delegateItem.dragging + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + } } } }