From cef1eb962ee1ee95fea008b5a430e471c2b9325a Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 28 Aug 2026 10:04:51 -0400 Subject: [PATCH] powermenu: add popout variant for bar widget fixes #3199 --- quickshell/DMSShell.qml | 99 +- quickshell/Modals/PowerMenuModal.qml | 866 +---------------- quickshell/Modules/DankBar/DankBarContent.qml | 22 +- .../DankBar/Popouts/PowerMenuPopout.qml | 48 + .../Modules/PowerMenu/PowerMenuContent.qml | 868 ++++++++++++++++++ quickshell/Services/PopoutService.qml | 7 + 6 files changed, 1008 insertions(+), 902 deletions(-) create mode 100644 quickshell/Modules/DankBar/Popouts/PowerMenuPopout.qml create mode 100644 quickshell/Modules/PowerMenu/PowerMenuContent.qml diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 348827b22..43017b3f5 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -1065,42 +1065,9 @@ Item { PowerMenuModal { id: powerMenuModal - onPowerActionRequested: (action, title, message) => { - PopoutService.closeControlCenter(); - switch (action) { - case "logout": - SessionService.logout(); - break; - case "suspend": - SessionService.suspend(); - break; - case "hibernate": - SessionService.hibernate(); - break; - case "reboot": - SessionService.reboot(); - break; - case "softreboot": - SessionService.softReboot(); - break; - case "poweroff": - SessionService.poweroff(); - break; - } - } - - onLockRequested: { - PopoutService.closeControlCenter(); - lock.activate(); - } - - onSwitchUserRequested: { - switchUserModalLoader.active = true; - Qt.callLater(() => { - if (switchUserModalLoader.loadedModal) - switchUserModalLoader.loadedModal.showFromPowerMenu(); - }); - } + onPowerActionRequested: (action, title, message) => root._executePowerAction(action) + onLockRequested: root._lockFromPowerMenu() + onSwitchUserRequested: root._switchUserFromPowerMenu() Component.onCompleted: { PopoutService.powerMenuModal = powerMenuModal; @@ -1108,6 +1075,66 @@ Item { } } + function _executePowerAction(action) { + PopoutService.closeControlCenter(); + switch (action) { + case "logout": + SessionService.logout(); + break; + case "suspend": + SessionService.suspend(); + break; + case "hibernate": + SessionService.hibernate(); + break; + case "reboot": + SessionService.reboot(); + break; + case "softreboot": + SessionService.softReboot(); + break; + case "poweroff": + SessionService.poweroff(); + break; + } + } + + function _lockFromPowerMenu() { + PopoutService.closeControlCenter(); + lock.activate(); + } + + function _switchUserFromPowerMenu() { + switchUserModalLoader.active = true; + Qt.callLater(() => { + if (switchUserModalLoader.loadedModal) + switchUserModalLoader.loadedModal.showFromPowerMenu(); + }); + } + + LazyLoader { + id: powerMenuPopoutLoader + + active: false + + Component.onCompleted: { + PopoutService.powerMenuPopoutLoader = powerMenuPopoutLoader; + } + + PowerMenuPopout { + id: powerMenuPopout + + onPowerActionRequested: action => root._executePowerAction(action) + onLockRequested: root._lockFromPowerMenu() + onSwitchUserRequested: root._switchUserFromPowerMenu() + onPopoutClosed: PopoutService.unloadPowerMenuPopout() + + Component.onCompleted: { + PopoutService.powerMenuPopout = powerMenuPopout; + } + } + } + LazyLoader { id: switchUserModalLoader diff --git a/quickshell/Modals/PowerMenuModal.qml b/quickshell/Modals/PowerMenuModal.qml index 279b7da6d..45fe255ae 100644 --- a/quickshell/Modals/PowerMenuModal.qml +++ b/quickshell/Modals/PowerMenuModal.qml @@ -1,10 +1,7 @@ import QtQuick -import Quickshell -import Quickshell.Widgets import qs.Common import qs.Modals.Common -import qs.Services -import qs.Widgets +import qs.Modules.PowerMenu DankModal { id: root @@ -13,158 +10,13 @@ DankModal { keepPopoutsOpen: true useOverlayLayer: true - property int selectedIndex: 0 - property int selectedRow: 0 - property int selectedCol: 0 property rect parentBounds: Qt.rect(0, 0, 0, 0) property var parentScreen: null - property var visibleActions: [] - property int gridColumns: 3 - property int gridRows: 2 - - property string holdAction: "" - property int holdActionIndex: -1 - property real holdProgress: 0 - property bool showHoldHint: false - property bool holdFromKeyboard: false - property string committedAction: "" - - readonly property bool needsConfirmation: SettingsData.powerActionConfirm - readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000 signal powerActionRequested(string action, string title, string message) signal lockRequested - - function actionNeedsConfirm(action) { - return action !== "lock" && action !== "restart"; - } - - function actionWakesOnKeyRelease(action) { - return action === "suspend" || action === "hibernate"; - } - - function startHold(action, actionIndex) { - if (committedAction !== "") - return; - if (!needsConfirmation || !actionNeedsConfirm(action)) { - if (holdFromKeyboard && actionWakesOnKeyRelease(action)) { - commitAction(action, actionIndex); - return; - } - executeAction(action); - return; - } - holdAction = action; - holdActionIndex = actionIndex; - holdProgress = 0; - showHoldHint = false; - holdTimer.start(); - } - - function cancelHold() { - if (holdAction === "") - return; - const wasHolding = holdProgress > 0; - holdTimer.stop(); - if (wasHolding && holdProgress < 1) { - showHoldHint = true; - hintTimer.restart(); - } - holdAction = ""; - holdActionIndex = -1; - holdProgress = 0; - } - - function completeHold() { - if (holdProgress < 1) { - cancelHold(); - return; - } - holdTimer.stop(); - if (holdFromKeyboard && actionWakesOnKeyRelease(holdAction)) { - commitAction(holdAction, holdActionIndex); - return; - } - const action = holdAction; - holdAction = ""; - holdActionIndex = -1; - holdProgress = 0; - executeAction(action); - } - - function commitAction(action, actionIndex) { - holdTimer.stop(); - holdAction = ""; - holdActionIndex = actionIndex; - committedAction = action; - commitFallbackTimer.restart(); - } - - function executeCommittedAction() { - if (committedAction === "") - return; - commitFallbackTimer.stop(); - const action = committedAction; - committedAction = ""; - holdActionIndex = -1; - holdProgress = 0; - executeAction(action); - } - signal switchUserRequested - function executeAction(action) { - if (action === "lock") { - close(); - lockRequested(); - return; - } - if (action === "restart") { - close(); - Quickshell.execDetached(["dms", "restart"]); - return; - } - if (action === "switchuser") { - close(); - switchUserRequested(); - return; - } - if (action.startsWith("custom:")) { - const button = (SettingsData.customPowerButtons || [])[parseInt(action.slice(7), 10)]; - close(); - if (button?.command) - Quickshell.execDetached(["sh", "-c", button.command]); - return; - } - close(); - root.powerActionRequested(action, "", ""); - } - - Timer { - id: holdTimer - interval: 16 - repeat: true - onTriggered: { - root.holdProgress = Math.min(1, root.holdProgress + (interval / root.holdDurationMs)); - if (root.holdProgress >= 1) { - stop(); - root.completeHold(); - } - } - } - - Timer { - id: hintTimer - interval: 2000 - onTriggered: root.showHoldHint = false - } - - Timer { - id: commitFallbackTimer - interval: 5000 - onTriggered: root.executeCommittedAction() - } - function openCentered() { parentBounds = Qt.rect(0, 0, 0, 0); parentScreen = null; @@ -177,133 +29,8 @@ DankModal { open(); } - function updateVisibleActions() { - const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]; - const customButtons = SettingsData.customPowerButtons || []; - visibleActions = allActions.filter(action => { - if (action === "hibernate" && !SessionService.hibernateSupported) - return false; - if (action === "softreboot" && !SessionService.softRebootSupported) - return false; - return true; - }).concat(customButtons.map((button, i) => "custom:" + i)); - - if (!SettingsData.powerMenuGridLayout) - return; - const count = visibleActions.length; - if (count === 0) { - gridColumns = 1; - gridRows = 1; - return; - } - - if (count <= 3) { - gridColumns = 1; - gridRows = count; - return; - } - - if (count === 4) { - gridColumns = 2; - gridRows = 2; - return; - } - - gridColumns = 3; - gridRows = Math.ceil(count / 3); - } - - function getDefaultActionIndex() { - const defaultAction = SettingsData.powerMenuDefaultAction || "logout"; - const index = visibleActions.indexOf(defaultAction); - return index >= 0 ? index : 0; - } - - function getActionAtIndex(index) { - if (index < 0 || index >= visibleActions.length) - return ""; - return visibleActions[index]; - } - - function getActionData(action) { - if (action.startsWith("custom:")) { - const button = (SettingsData.customPowerButtons || [])[parseInt(action.slice(7), 10)]; - return { - "icon": button?.icon || "terminal", - "label": button?.label || button?.command || "", - "key": "" - }; - } - switch (action) { - case "reboot": - return { - "icon": "restart_alt", - "label": I18n.tr("Reboot"), - "key": "R" - }; - case "softreboot": - return { - "icon": "autorenew", - "label": I18n.tr("Soft Reboot"), - "key": "B" - }; - case "logout": - return { - "icon": "logout", - "label": I18n.tr("Log Out"), - "key": "X" - }; - case "poweroff": - return { - "icon": "power_settings_new", - "label": I18n.tr("Power Off"), - "key": "P" - }; - case "lock": - return { - "icon": "lock", - "label": I18n.tr("Lock"), - "key": "L" - }; - case "suspend": - return { - "icon": "bedtime", - "label": I18n.tr("Suspend"), - "key": "S" - }; - case "hibernate": - return { - "icon": "ac_unit", - "label": I18n.tr("Hibernate"), - "key": "H" - }; - case "restart": - return { - "icon": "refresh", - "label": I18n.tr("Restart DMS"), - "key": "D" - }; - case "switchuser": - return { - "icon": "switch_account", - "label": I18n.tr("Switch User"), - "key": "U" - }; - default: - return { - "icon": "help", - "label": action, - "key": "?" - }; - } - } - - function selectOption(action, actionIndex) { - startHold(action, actionIndex !== undefined ? actionIndex : -1); - } - shouldBeVisible: false - modalWidth: SettingsData.powerMenuGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 400 + modalWidth: contentLoader.item?.desiredWidth ?? 400 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight : 300 enableShadow: true targetScreen: parentScreen @@ -334,600 +61,31 @@ DankModal { return Qt.point(0, 0); } onBackgroundClicked: () => { - cancelHold(); + contentLoader.item?.cancelHold(); close(); } onShouldBeVisibleChanged: { if (!shouldBeVisible) return; - holdAction = ""; - holdActionIndex = -1; - holdProgress = 0; - showHoldHint = false; - holdFromKeyboard = false; - committedAction = ""; - commitFallbackTimer.stop(); - updateVisibleActions(); - const defaultIndex = getDefaultActionIndex(); - selectedIndex = defaultIndex; - if (SettingsData.powerMenuGridLayout) { - selectedRow = Math.floor(defaultIndex / gridColumns); - selectedCol = defaultIndex % gridColumns; - } + contentLoader.item?.resetState(); } onShouldHaveFocusChanged: { if (!shouldHaveFocus) return; - Qt.callLater(() => modalFocusScope.forceActiveFocus()); + Qt.callLater(() => contentLoader.item?.forceActiveFocus()); } onDialogClosed: () => { - cancelHold(); - } - Component.onCompleted: updateVisibleActions() - modalFocusScope.Keys.onPressed: event => { - if (event.isAutoRepeat) { - event.accepted = true; - return; - } - if (committedAction !== "") { - event.accepted = true; - return; - } - holdFromKeyboard = true; - if (SettingsData.powerMenuGridLayout) { - handleGridNavigation(event, true); - } else { - handleListNavigation(event, true); - } - } - modalFocusScope.Keys.onReleased: event => { - if (event.isAutoRepeat) { - event.accepted = true; - return; - } - if (committedAction !== "") { - event.accepted = true; - executeCommittedAction(); - return; - } - if (SettingsData.powerMenuGridLayout) { - handleGridNavigation(event, false); - } else { - handleListNavigation(event, false); - } - } - - function handleListNavigation(event, isPressed) { - if (!isPressed) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { - cancelHold(); - event.accepted = true; - } - return; - } - - switch (event.key) { - case Qt.Key_Up: - case Qt.Key_Backtab: - selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length; - event.accepted = true; - break; - case Qt.Key_Down: - case Qt.Key_Tab: - selectedIndex = (selectedIndex + 1) % visibleActions.length; - event.accepted = true; - break; - case Qt.Key_Return: - case Qt.Key_Enter: - startHold(getActionAtIndex(selectedIndex), selectedIndex); - event.accepted = true; - break; - case Qt.Key_N: - if (event.modifiers & Qt.ControlModifier) { - selectedIndex = (selectedIndex + 1) % visibleActions.length; - event.accepted = true; - } - break; - case Qt.Key_P: - if (!(event.modifiers & Qt.ControlModifier)) { - if (visibleActions.includes("poweroff")) { - const idx = visibleActions.indexOf("poweroff"); - startHold("poweroff", idx); - event.accepted = true; - } - } else { - selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length; - event.accepted = true; - } - break; - case Qt.Key_J: - if (event.modifiers & Qt.ControlModifier) { - selectedIndex = (selectedIndex + 1) % visibleActions.length; - event.accepted = true; - } - break; - case Qt.Key_K: - if (event.modifiers & Qt.ControlModifier) { - selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length; - event.accepted = true; - } - break; - case Qt.Key_R: - if (visibleActions.includes("reboot")) { - startHold("reboot", visibleActions.indexOf("reboot")); - event.accepted = true; - } - break; - case Qt.Key_B: - if (visibleActions.includes("softreboot")) { - startHold("softreboot", visibleActions.indexOf("softreboot")); - event.accepted = true; - } - break; - case Qt.Key_X: - if (visibleActions.includes("logout")) { - startHold("logout", visibleActions.indexOf("logout")); - event.accepted = true; - } - break; - case Qt.Key_L: - if (visibleActions.includes("lock")) { - startHold("lock", visibleActions.indexOf("lock")); - event.accepted = true; - } - break; - case Qt.Key_S: - if (visibleActions.includes("suspend")) { - startHold("suspend", visibleActions.indexOf("suspend")); - event.accepted = true; - } - break; - case Qt.Key_H: - if (visibleActions.includes("hibernate")) { - startHold("hibernate", visibleActions.indexOf("hibernate")); - event.accepted = true; - } - break; - case Qt.Key_D: - if (visibleActions.includes("restart")) { - startHold("restart", visibleActions.indexOf("restart")); - event.accepted = true; - } - break; - } - } - - function handleGridNavigation(event, isPressed) { - if (!isPressed) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { - cancelHold(); - event.accepted = true; - } - return; - } - - switch (event.key) { - case Qt.Key_Left: - selectedCol = (selectedCol - 1 + gridColumns) % gridColumns; - selectedIndex = selectedRow * gridColumns + selectedCol; - event.accepted = true; - break; - case Qt.Key_Right: - selectedCol = (selectedCol + 1) % gridColumns; - selectedIndex = selectedRow * gridColumns + selectedCol; - event.accepted = true; - break; - case Qt.Key_Up: - case Qt.Key_Backtab: - selectedRow = (selectedRow - 1 + gridRows) % gridRows; - selectedIndex = selectedRow * gridColumns + selectedCol; - event.accepted = true; - break; - case Qt.Key_Down: - case Qt.Key_Tab: - selectedRow = (selectedRow + 1) % gridRows; - selectedIndex = selectedRow * gridColumns + selectedCol; - event.accepted = true; - break; - case Qt.Key_Return: - case Qt.Key_Enter: - startHold(getActionAtIndex(selectedIndex), selectedIndex); - event.accepted = true; - break; - case Qt.Key_N: - if (event.modifiers & Qt.ControlModifier) { - selectedCol = (selectedCol + 1) % gridColumns; - selectedIndex = selectedRow * gridColumns + selectedCol; - event.accepted = true; - } - break; - case Qt.Key_P: - if (!(event.modifiers & Qt.ControlModifier)) { - if (visibleActions.includes("poweroff")) { - const idx = visibleActions.indexOf("poweroff"); - startHold("poweroff", idx); - event.accepted = true; - } - } else { - selectedCol = (selectedCol - 1 + gridColumns) % gridColumns; - selectedIndex = selectedRow * gridColumns + selectedCol; - event.accepted = true; - } - break; - case Qt.Key_J: - if (event.modifiers & Qt.ControlModifier) { - selectedRow = (selectedRow + 1) % gridRows; - selectedIndex = selectedRow * gridColumns + selectedCol; - event.accepted = true; - } - break; - case Qt.Key_K: - if (event.modifiers & Qt.ControlModifier) { - selectedRow = (selectedRow - 1 + gridRows) % gridRows; - selectedIndex = selectedRow * gridColumns + selectedCol; - event.accepted = true; - } - break; - case Qt.Key_R: - if (visibleActions.includes("reboot")) { - startHold("reboot", visibleActions.indexOf("reboot")); - event.accepted = true; - } - break; - case Qt.Key_B: - if (visibleActions.includes("softreboot")) { - startHold("softreboot", visibleActions.indexOf("softreboot")); - event.accepted = true; - } - break; - case Qt.Key_X: - if (visibleActions.includes("logout")) { - startHold("logout", visibleActions.indexOf("logout")); - event.accepted = true; - } - break; - case Qt.Key_L: - if (visibleActions.includes("lock")) { - startHold("lock", visibleActions.indexOf("lock")); - event.accepted = true; - } - break; - case Qt.Key_S: - if (visibleActions.includes("suspend")) { - startHold("suspend", visibleActions.indexOf("suspend")); - event.accepted = true; - } - break; - case Qt.Key_H: - if (visibleActions.includes("hibernate")) { - startHold("hibernate", visibleActions.indexOf("hibernate")); - event.accepted = true; - } - break; - case Qt.Key_D: - if (visibleActions.includes("restart")) { - startHold("restart", visibleActions.indexOf("restart")); - event.accepted = true; - } - break; - } + contentLoader.item?.cancelHold(); } content: Component { - Item { + PowerMenuContent { anchors.fill: parent - implicitHeight: (SettingsData.powerMenuGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + Theme.spacingL * 2 + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0) - - Grid { - id: buttonGrid - visible: SettingsData.powerMenuGridLayout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Theme.spacingL - columns: root.gridColumns - columnSpacing: Theme.spacingS - rowSpacing: Theme.spacingS - - Repeater { - model: root.visibleActions - - Rectangle { - id: gridButtonRect - required property int index - required property string modelData - - readonly property var actionData: root.getActionData(modelData) - readonly property bool isSelected: root.selectedIndex === index - readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff" - readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 - - width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns - height: 100 - radius: Theme.cornerRadius - color: { - if (isSelected) - return Theme.primaryHover; - if (mouseArea.containsMouse) - return Theme.primaryHoverLight; - return Theme.surfaceHover; - } - border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) - border.width: isSelected ? 2 : 0 - - ClippingRectangle { - anchors.fill: parent - radius: parent.radius - color: "transparent" - visible: gridButtonRect.isHolding - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: parent.width * root.holdProgress - color: { - if (gridButtonRect.modelData === "poweroff") - return Theme.errorSelected; - if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot") - return Theme.withAlpha(Theme.warning, 0.3); - return Theme.primarySelected; - } - } - } - - Column { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - name: gridButtonRect.actionData.icon - size: Theme.iconSize + 8 - color: { - if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) { - return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning; - } - return Theme.surfaceText; - } - anchors.horizontalCenter: parent.horizontalCenter - } - - StyledText { - text: gridButtonRect.actionData.label - font.pixelSize: Theme.fontSizeMedium - color: { - if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) { - return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning; - } - return Theme.surfaceText; - } - font.weight: Font.Medium - anchors.horizontalCenter: parent.horizontalCenter - } - - Rectangle { - width: 20 - height: 16 - radius: 4 - color: Theme.onSurface_12 - visible: gridButtonRect.actionData.key !== "" - anchors.horizontalCenter: parent.horizontalCenter - - StyledText { - text: gridButtonRect.actionData.key - font.pixelSize: Theme.fontSizeSmall - 1 - color: Theme.surfaceTextSecondary - font.weight: Font.Medium - anchors.centerIn: parent - } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - root.holdFromKeyboard = false; - root.selectedRow = Math.floor(index / root.gridColumns); - root.selectedCol = index % root.gridColumns; - root.selectedIndex = index; - root.startHold(modelData, index); - } - onReleased: root.cancelHold() - onCanceled: root.cancelHold() - } - } - } - } - - Column { - id: buttonColumn - visible: !SettingsData.powerMenuGridLayout - anchors { - left: parent.left - right: parent.right - top: parent.top - leftMargin: Theme.spacingL - rightMargin: Theme.spacingL - topMargin: Theme.spacingL - } - spacing: Theme.spacingS - - Repeater { - model: root.visibleActions - - Rectangle { - id: listButtonRect - required property int index - required property string modelData - - readonly property var actionData: root.getActionData(modelData) - readonly property bool isSelected: root.selectedIndex === index - readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff" - readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 - - width: parent.width - height: 56 - radius: Theme.cornerRadius - color: { - if (isSelected) - return Theme.primaryHover; - if (listMouseArea.containsMouse) - return Theme.primaryHoverLight; - return Theme.surfaceHover; - } - border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) - border.width: isSelected ? 2 : 0 - - ClippingRectangle { - anchors.fill: parent - radius: parent.radius - color: "transparent" - visible: listButtonRect.isHolding - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: parent.width * root.holdProgress - color: { - if (listButtonRect.modelData === "poweroff") - return Theme.errorSelected; - if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot") - return Theme.withAlpha(Theme.warning, 0.3); - return Theme.primarySelected; - } - } - } - - Row { - anchors { - left: parent.left - right: parent.right - leftMargin: Theme.spacingM - rightMargin: Theme.spacingM - verticalCenter: parent.verticalCenter - } - spacing: Theme.spacingM - - DankIcon { - name: listButtonRect.actionData.icon - size: Theme.iconSize + 4 - color: { - if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) { - return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning; - } - return Theme.surfaceText; - } - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: listButtonRect.actionData.label - font.pixelSize: Theme.fontSizeMedium - color: { - if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) { - return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning; - } - return Theme.surfaceText; - } - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - Rectangle { - width: 28 - height: 20 - radius: 4 - color: Theme.onSurface_12 - visible: listButtonRect.actionData.key !== "" - anchors { - right: parent.right - rightMargin: Theme.spacingM - verticalCenter: parent.verticalCenter - } - - StyledText { - text: listButtonRect.actionData.key - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextSecondary - font.weight: Font.Medium - anchors.centerIn: parent - } - } - - MouseArea { - id: listMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - root.holdFromKeyboard = false; - root.selectedIndex = index; - root.startHold(modelData, index); - } - onReleased: root.cancelHold() - onCanceled: root.cancelHold() - } - } - } - } - - Row { - id: hintRow - readonly property bool selectedNeedsHold: root.actionNeedsConfirm(root.getActionAtIndex(root.selectedIndex)) - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Theme.spacingS - spacing: Theme.spacingXS - visible: root.needsConfirmation - opacity: root.showHoldHint ? 1 : 0.5 - - Behavior on opacity { - NumberAnimation { - duration: 150 - } - } - - DankIcon { - name: { - if (root.showHoldHint) - return "warning"; - if (!hintRow.selectedNeedsHold) - return "bolt"; - return "touch_app"; - } - size: Theme.fontSizeSmall - color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000 - readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress)) - text: { - if (root.committedAction !== "") - return I18n.tr("Release to confirm"); - if (root.showHoldHint) - return I18n.tr("Hold longer to confirm"); - if (!hintRow.selectedNeedsHold) - return I18n.tr("Activates immediately"); - if (root.holdProgress > 0) { - if (totalMs < 1000) - return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs); - return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000)); - } - if (totalMs < 1000) - return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs); - return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration); - } - font.pixelSize: Theme.fontSizeSmall - color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary - anchors.verticalCenter: parent.verticalCenter - } - } + focus: true + onPowerActionRequested: action => root.powerActionRequested(action, "", "") + onLockRequested: root.lockRequested() + onSwitchUserRequested: root.switchUserRequested() + onCloseRequested: root.close() } } } diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index 4c607709a..dc8585ea0 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -493,7 +493,7 @@ Item { if (loader.item) return loader.item; - const pairs = [[PopoutService.appDrawerLoader, PopoutService.appDrawerPopout], [PopoutService.batteryPopoutLoader, PopoutService.batteryPopout], [PopoutService.clipboardHistoryPopoutLoader, PopoutService.clipboardHistoryPopout], [PopoutService.controlCenterLoader, PopoutService.controlCenterPopout], [PopoutService.dankDashPopoutLoader, PopoutService.dankDashPopout], [PopoutService.layoutPopoutLoader, PopoutService.layoutPopout], [PopoutService.notificationCenterLoader, PopoutService.notificationCenterPopout], [PopoutService.processListPopoutLoader, PopoutService.processListPopout], [PopoutService.systemUpdateLoader, PopoutService.systemUpdatePopout], [PopoutService.vpnPopoutLoader, PopoutService.vpnPopout], [PopoutService.colorPickerPopoutLoader, PopoutService.colorPickerPopout]]; + const pairs = [[PopoutService.appDrawerLoader, PopoutService.appDrawerPopout], [PopoutService.batteryPopoutLoader, PopoutService.batteryPopout], [PopoutService.clipboardHistoryPopoutLoader, PopoutService.clipboardHistoryPopout], [PopoutService.controlCenterLoader, PopoutService.controlCenterPopout], [PopoutService.dankDashPopoutLoader, PopoutService.dankDashPopout], [PopoutService.layoutPopoutLoader, PopoutService.layoutPopout], [PopoutService.notificationCenterLoader, PopoutService.notificationCenterPopout], [PopoutService.processListPopoutLoader, PopoutService.processListPopout], [PopoutService.systemUpdateLoader, PopoutService.systemUpdatePopout], [PopoutService.vpnPopoutLoader, PopoutService.vpnPopout], [PopoutService.colorPickerPopoutLoader, PopoutService.colorPickerPopout], [PopoutService.powerMenuPopoutLoader, PopoutService.powerMenuPopout]]; for (let i = 0; i < pairs.length; i++) { if (loader === pairs[i][0] && pairs[i][1]) return pairs[i][1]; @@ -932,23 +932,21 @@ Item { id: powerMenuButtonComponent PowerMenuButton { + id: powerMenuWidget widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) parentScreen: barWindow.screen + isActive: PopoutService.powerMenuPopoutLoader?.item ? PopoutService.powerMenuPopoutLoader?.item.shouldBeVisible : false onClicked: { - const loader = PopoutService.powerMenuModalLoader; - if (!loader) - return; - loader.active = true; - if (!loader.item) - return; - if (loader.item.shouldBeVisible) { - loader.item.close(); - return; - } - loader.item.openCentered(); + topBarContent.openWidgetPopout({ + loader: PopoutService.powerMenuPopoutLoader, + widgetItem: powerMenuWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "powerMenu", + mode: "click" + }); } } } diff --git a/quickshell/Modules/DankBar/Popouts/PowerMenuPopout.qml b/quickshell/Modules/DankBar/Popouts/PowerMenuPopout.qml new file mode 100644 index 000000000..aff4af244 --- /dev/null +++ b/quickshell/Modules/DankBar/Popouts/PowerMenuPopout.qml @@ -0,0 +1,48 @@ +import QtQuick +import qs.Common +import qs.Modules.PowerMenu +import qs.Widgets + +DankPopout { + id: root + + signal powerActionRequested(string action) + signal lockRequested + signal switchUserRequested + + layerNamespace: "dms:power-menu" + popupWidth: contentLoader.item?.desiredWidth ?? 400 + popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 300 + triggerWidth: 40 + positioning: "" + shouldBeVisible: false + + onBackgroundClicked: { + contentLoader.item?.cancelHold(); + close(); + } + + onOpened: Qt.callLater(() => { + contentLoader.item?.resetState(); + contentLoader.item?.forceActiveFocus(); + }) + + onPopoutClosed: contentLoader.item?.cancelHold() + + content: Component { + PowerMenuContent { + anchors.fill: parent + focus: true + onPowerActionRequested: action => root.powerActionRequested(action) + onLockRequested: root.lockRequested() + onSwitchUserRequested: root.switchUserRequested() + onCloseRequested: root.close() + + Keys.onEscapePressed: event => { + cancelHold(); + root.close(); + event.accepted = true; + } + } + } +} diff --git a/quickshell/Modules/PowerMenu/PowerMenuContent.qml b/quickshell/Modules/PowerMenu/PowerMenuContent.qml new file mode 100644 index 000000000..c5f6b5b04 --- /dev/null +++ b/quickshell/Modules/PowerMenu/PowerMenuContent.qml @@ -0,0 +1,868 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +FocusScope { + id: root + + property int selectedIndex: 0 + property int selectedRow: 0 + property int selectedCol: 0 + property var visibleActions: [] + property int gridColumns: 3 + property int gridRows: 2 + + property string holdAction: "" + property int holdActionIndex: -1 + property real holdProgress: 0 + property bool showHoldHint: false + property bool holdFromKeyboard: false + property string committedAction: "" + + readonly property bool needsConfirmation: SettingsData.powerActionConfirm + readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000 + readonly property real desiredWidth: SettingsData.powerMenuGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 400 + + signal powerActionRequested(string action) + signal lockRequested + signal switchUserRequested + signal closeRequested + + implicitHeight: (SettingsData.powerMenuGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + Theme.spacingL * 2 + (needsConfirmation ? hintRow.height + Theme.spacingM : 0) + + function resetState() { + holdAction = ""; + holdActionIndex = -1; + holdProgress = 0; + showHoldHint = false; + holdFromKeyboard = false; + committedAction = ""; + commitFallbackTimer.stop(); + updateVisibleActions(); + const defaultIndex = getDefaultActionIndex(); + selectedIndex = defaultIndex; + if (SettingsData.powerMenuGridLayout) { + selectedRow = Math.floor(defaultIndex / gridColumns); + selectedCol = defaultIndex % gridColumns; + } + } + + function actionNeedsConfirm(action) { + return action !== "lock" && action !== "restart"; + } + + function actionWakesOnKeyRelease(action) { + return action === "suspend" || action === "hibernate"; + } + + function startHold(action, actionIndex) { + if (committedAction !== "") + return; + if (!needsConfirmation || !actionNeedsConfirm(action)) { + if (holdFromKeyboard && actionWakesOnKeyRelease(action)) { + commitAction(action, actionIndex); + return; + } + executeAction(action); + return; + } + holdAction = action; + holdActionIndex = actionIndex; + holdProgress = 0; + showHoldHint = false; + holdTimer.start(); + } + + function cancelHold() { + if (holdAction === "") + return; + const wasHolding = holdProgress > 0; + holdTimer.stop(); + if (wasHolding && holdProgress < 1) { + showHoldHint = true; + hintTimer.restart(); + } + holdAction = ""; + holdActionIndex = -1; + holdProgress = 0; + } + + function completeHold() { + if (holdProgress < 1) { + cancelHold(); + return; + } + holdTimer.stop(); + if (holdFromKeyboard && actionWakesOnKeyRelease(holdAction)) { + commitAction(holdAction, holdActionIndex); + return; + } + const action = holdAction; + holdAction = ""; + holdActionIndex = -1; + holdProgress = 0; + executeAction(action); + } + + function commitAction(action, actionIndex) { + holdTimer.stop(); + holdAction = ""; + holdActionIndex = actionIndex; + committedAction = action; + commitFallbackTimer.restart(); + } + + function executeCommittedAction() { + if (committedAction === "") + return; + commitFallbackTimer.stop(); + const action = committedAction; + committedAction = ""; + holdActionIndex = -1; + holdProgress = 0; + executeAction(action); + } + + function executeAction(action) { + if (action === "lock") { + closeRequested(); + lockRequested(); + return; + } + if (action === "restart") { + closeRequested(); + Quickshell.execDetached(["dms", "restart"]); + return; + } + if (action === "switchuser") { + closeRequested(); + switchUserRequested(); + return; + } + if (action.startsWith("custom:")) { + const button = (SettingsData.customPowerButtons || [])[parseInt(action.slice(7), 10)]; + closeRequested(); + if (button?.command) + Quickshell.execDetached(["sh", "-c", button.command]); + return; + } + closeRequested(); + root.powerActionRequested(action); + } + + Timer { + id: holdTimer + interval: 16 + repeat: true + onTriggered: { + root.holdProgress = Math.min(1, root.holdProgress + (interval / root.holdDurationMs)); + if (root.holdProgress >= 1) { + stop(); + root.completeHold(); + } + } + } + + Timer { + id: hintTimer + interval: 2000 + onTriggered: root.showHoldHint = false + } + + Timer { + id: commitFallbackTimer + interval: 5000 + onTriggered: root.executeCommittedAction() + } + + function updateVisibleActions() { + const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]; + const customButtons = SettingsData.customPowerButtons || []; + visibleActions = allActions.filter(action => { + if (action === "hibernate" && !SessionService.hibernateSupported) + return false; + if (action === "softreboot" && !SessionService.softRebootSupported) + return false; + return true; + }).concat(customButtons.map((button, i) => "custom:" + i)); + + if (!SettingsData.powerMenuGridLayout) + return; + const count = visibleActions.length; + if (count === 0) { + gridColumns = 1; + gridRows = 1; + return; + } + + if (count <= 3) { + gridColumns = 1; + gridRows = count; + return; + } + + if (count === 4) { + gridColumns = 2; + gridRows = 2; + return; + } + + gridColumns = 3; + gridRows = Math.ceil(count / 3); + } + + function getDefaultActionIndex() { + const defaultAction = SettingsData.powerMenuDefaultAction || "logout"; + const index = visibleActions.indexOf(defaultAction); + return index >= 0 ? index : 0; + } + + function getActionAtIndex(index) { + if (index < 0 || index >= visibleActions.length) + return ""; + return visibleActions[index]; + } + + function getActionData(action) { + if (action.startsWith("custom:")) { + const button = (SettingsData.customPowerButtons || [])[parseInt(action.slice(7), 10)]; + return { + "icon": button?.icon || "terminal", + "label": button?.label || button?.command || "", + "key": "" + }; + } + switch (action) { + case "reboot": + return { + "icon": "restart_alt", + "label": I18n.tr("Reboot"), + "key": "R" + }; + case "softreboot": + return { + "icon": "autorenew", + "label": I18n.tr("Soft Reboot"), + "key": "B" + }; + case "logout": + return { + "icon": "logout", + "label": I18n.tr("Log Out"), + "key": "X" + }; + case "poweroff": + return { + "icon": "power_settings_new", + "label": I18n.tr("Power Off"), + "key": "P" + }; + case "lock": + return { + "icon": "lock", + "label": I18n.tr("Lock"), + "key": "L" + }; + case "suspend": + return { + "icon": "bedtime", + "label": I18n.tr("Suspend"), + "key": "S" + }; + case "hibernate": + return { + "icon": "ac_unit", + "label": I18n.tr("Hibernate"), + "key": "H" + }; + case "restart": + return { + "icon": "refresh", + "label": I18n.tr("Restart DMS"), + "key": "D" + }; + case "switchuser": + return { + "icon": "switch_account", + "label": I18n.tr("Switch User"), + "key": "U" + }; + default: + return { + "icon": "help", + "label": action, + "key": "?" + }; + } + } + + function selectOption(action, actionIndex) { + startHold(action, actionIndex !== undefined ? actionIndex : -1); + } + + Keys.onPressed: event => { + if (event.isAutoRepeat) { + event.accepted = true; + return; + } + if (committedAction !== "") { + event.accepted = true; + return; + } + holdFromKeyboard = true; + if (SettingsData.powerMenuGridLayout) { + handleGridNavigation(event, true); + } else { + handleListNavigation(event, true); + } + } + + Keys.onReleased: event => { + if (event.isAutoRepeat) { + event.accepted = true; + return; + } + if (committedAction !== "") { + event.accepted = true; + executeCommittedAction(); + return; + } + if (SettingsData.powerMenuGridLayout) { + handleGridNavigation(event, false); + } else { + handleListNavigation(event, false); + } + } + + function handleListNavigation(event, isPressed) { + if (!isPressed) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { + cancelHold(); + event.accepted = true; + } + return; + } + + switch (event.key) { + case Qt.Key_Up: + case Qt.Key_Backtab: + selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length; + event.accepted = true; + break; + case Qt.Key_Down: + case Qt.Key_Tab: + selectedIndex = (selectedIndex + 1) % visibleActions.length; + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + startHold(getActionAtIndex(selectedIndex), selectedIndex); + event.accepted = true; + break; + case Qt.Key_N: + if (event.modifiers & Qt.ControlModifier) { + selectedIndex = (selectedIndex + 1) % visibleActions.length; + event.accepted = true; + } + break; + case Qt.Key_P: + if (!(event.modifiers & Qt.ControlModifier)) { + if (visibleActions.includes("poweroff")) { + const idx = visibleActions.indexOf("poweroff"); + startHold("poweroff", idx); + event.accepted = true; + } + } else { + selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length; + event.accepted = true; + } + break; + case Qt.Key_J: + if (event.modifiers & Qt.ControlModifier) { + selectedIndex = (selectedIndex + 1) % visibleActions.length; + event.accepted = true; + } + break; + case Qt.Key_K: + if (event.modifiers & Qt.ControlModifier) { + selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length; + event.accepted = true; + } + break; + case Qt.Key_R: + if (visibleActions.includes("reboot")) { + startHold("reboot", visibleActions.indexOf("reboot")); + event.accepted = true; + } + break; + case Qt.Key_B: + if (visibleActions.includes("softreboot")) { + startHold("softreboot", visibleActions.indexOf("softreboot")); + event.accepted = true; + } + break; + case Qt.Key_X: + if (visibleActions.includes("logout")) { + startHold("logout", visibleActions.indexOf("logout")); + event.accepted = true; + } + break; + case Qt.Key_L: + if (visibleActions.includes("lock")) { + startHold("lock", visibleActions.indexOf("lock")); + event.accepted = true; + } + break; + case Qt.Key_S: + if (visibleActions.includes("suspend")) { + startHold("suspend", visibleActions.indexOf("suspend")); + event.accepted = true; + } + break; + case Qt.Key_H: + if (visibleActions.includes("hibernate")) { + startHold("hibernate", visibleActions.indexOf("hibernate")); + event.accepted = true; + } + break; + case Qt.Key_D: + if (visibleActions.includes("restart")) { + startHold("restart", visibleActions.indexOf("restart")); + event.accepted = true; + } + break; + } + } + + function handleGridNavigation(event, isPressed) { + if (!isPressed) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { + cancelHold(); + event.accepted = true; + } + return; + } + + switch (event.key) { + case Qt.Key_Left: + selectedCol = (selectedCol - 1 + gridColumns) % gridColumns; + selectedIndex = selectedRow * gridColumns + selectedCol; + event.accepted = true; + break; + case Qt.Key_Right: + selectedCol = (selectedCol + 1) % gridColumns; + selectedIndex = selectedRow * gridColumns + selectedCol; + event.accepted = true; + break; + case Qt.Key_Up: + case Qt.Key_Backtab: + selectedRow = (selectedRow - 1 + gridRows) % gridRows; + selectedIndex = selectedRow * gridColumns + selectedCol; + event.accepted = true; + break; + case Qt.Key_Down: + case Qt.Key_Tab: + selectedRow = (selectedRow + 1) % gridRows; + selectedIndex = selectedRow * gridColumns + selectedCol; + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + startHold(getActionAtIndex(selectedIndex), selectedIndex); + event.accepted = true; + break; + case Qt.Key_N: + if (event.modifiers & Qt.ControlModifier) { + selectedCol = (selectedCol + 1) % gridColumns; + selectedIndex = selectedRow * gridColumns + selectedCol; + event.accepted = true; + } + break; + case Qt.Key_P: + if (!(event.modifiers & Qt.ControlModifier)) { + if (visibleActions.includes("poweroff")) { + const idx = visibleActions.indexOf("poweroff"); + startHold("poweroff", idx); + event.accepted = true; + } + } else { + selectedCol = (selectedCol - 1 + gridColumns) % gridColumns; + selectedIndex = selectedRow * gridColumns + selectedCol; + event.accepted = true; + } + break; + case Qt.Key_J: + if (event.modifiers & Qt.ControlModifier) { + selectedRow = (selectedRow + 1) % gridRows; + selectedIndex = selectedRow * gridColumns + selectedCol; + event.accepted = true; + } + break; + case Qt.Key_K: + if (event.modifiers & Qt.ControlModifier) { + selectedRow = (selectedRow - 1 + gridRows) % gridRows; + selectedIndex = selectedRow * gridColumns + selectedCol; + event.accepted = true; + } + break; + case Qt.Key_R: + if (visibleActions.includes("reboot")) { + startHold("reboot", visibleActions.indexOf("reboot")); + event.accepted = true; + } + break; + case Qt.Key_B: + if (visibleActions.includes("softreboot")) { + startHold("softreboot", visibleActions.indexOf("softreboot")); + event.accepted = true; + } + break; + case Qt.Key_X: + if (visibleActions.includes("logout")) { + startHold("logout", visibleActions.indexOf("logout")); + event.accepted = true; + } + break; + case Qt.Key_L: + if (visibleActions.includes("lock")) { + startHold("lock", visibleActions.indexOf("lock")); + event.accepted = true; + } + break; + case Qt.Key_S: + if (visibleActions.includes("suspend")) { + startHold("suspend", visibleActions.indexOf("suspend")); + event.accepted = true; + } + break; + case Qt.Key_H: + if (visibleActions.includes("hibernate")) { + startHold("hibernate", visibleActions.indexOf("hibernate")); + event.accepted = true; + } + break; + case Qt.Key_D: + if (visibleActions.includes("restart")) { + startHold("restart", visibleActions.indexOf("restart")); + event.accepted = true; + } + break; + } + } + + Component.onCompleted: resetState() + + Grid { + id: buttonGrid + visible: SettingsData.powerMenuGridLayout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Theme.spacingL + columns: root.gridColumns + columnSpacing: Theme.spacingS + rowSpacing: Theme.spacingS + + Repeater { + model: root.visibleActions + + Rectangle { + id: gridButtonRect + required property int index + required property string modelData + + readonly property var actionData: root.getActionData(modelData) + readonly property bool isSelected: root.selectedIndex === index + readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff" + readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 + + width: (root.width - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns + height: 100 + radius: Theme.cornerRadius + color: { + if (isSelected) + return Theme.primaryHover; + if (mouseArea.containsMouse) + return Theme.primaryHoverLight; + return Theme.surfaceHover; + } + border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) + border.width: isSelected ? 2 : 0 + + ClippingRectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + visible: gridButtonRect.isHolding + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * root.holdProgress + color: { + if (gridButtonRect.modelData === "poweroff") + return Theme.errorSelected; + if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot") + return Theme.withAlpha(Theme.warning, 0.3); + return Theme.primarySelected; + } + } + } + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: gridButtonRect.actionData.icon + size: Theme.iconSize + 8 + color: { + if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) { + return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning; + } + return Theme.surfaceText; + } + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: gridButtonRect.actionData.label + font.pixelSize: Theme.fontSizeMedium + color: { + if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) { + return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning; + } + return Theme.surfaceText; + } + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + Rectangle { + width: 20 + height: 16 + radius: 4 + color: Theme.onSurface_12 + visible: gridButtonRect.actionData.key !== "" + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: gridButtonRect.actionData.key + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceTextSecondary + font.weight: Font.Medium + anchors.centerIn: parent + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.holdFromKeyboard = false; + root.selectedRow = Math.floor(index / root.gridColumns); + root.selectedCol = index % root.gridColumns; + root.selectedIndex = index; + root.startHold(modelData, index); + } + onReleased: root.cancelHold() + onCanceled: root.cancelHold() + } + } + } + } + + Column { + id: buttonColumn + visible: !SettingsData.powerMenuGridLayout + anchors { + left: parent.left + right: parent.right + top: parent.top + leftMargin: Theme.spacingL + rightMargin: Theme.spacingL + topMargin: Theme.spacingL + } + spacing: Theme.spacingS + + Repeater { + model: root.visibleActions + + Rectangle { + id: listButtonRect + required property int index + required property string modelData + + readonly property var actionData: root.getActionData(modelData) + readonly property bool isSelected: root.selectedIndex === index + readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff" + readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 + + width: parent.width + height: 56 + radius: Theme.cornerRadius + color: { + if (isSelected) + return Theme.primaryHover; + if (listMouseArea.containsMouse) + return Theme.primaryHoverLight; + return Theme.surfaceHover; + } + border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) + border.width: isSelected ? 2 : 0 + + ClippingRectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + visible: listButtonRect.isHolding + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * root.holdProgress + color: { + if (listButtonRect.modelData === "poweroff") + return Theme.errorSelected; + if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot") + return Theme.withAlpha(Theme.warning, 0.3); + return Theme.primarySelected; + } + } + } + + Row { + anchors { + left: parent.left + right: parent.right + leftMargin: Theme.spacingM + rightMargin: Theme.spacingM + verticalCenter: parent.verticalCenter + } + spacing: Theme.spacingM + + DankIcon { + name: listButtonRect.actionData.icon + size: Theme.iconSize + 4 + color: { + if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) { + return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning; + } + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: listButtonRect.actionData.label + font.pixelSize: Theme.fontSizeMedium + color: { + if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) { + return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning; + } + return Theme.surfaceText; + } + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: 28 + height: 20 + radius: 4 + color: Theme.onSurface_12 + visible: listButtonRect.actionData.key !== "" + anchors { + right: parent.right + rightMargin: Theme.spacingM + verticalCenter: parent.verticalCenter + } + + StyledText { + text: listButtonRect.actionData.key + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextSecondary + font.weight: Font.Medium + anchors.centerIn: parent + } + } + + MouseArea { + id: listMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.holdFromKeyboard = false; + root.selectedIndex = index; + root.startHold(modelData, index); + } + onReleased: root.cancelHold() + onCanceled: root.cancelHold() + } + } + } + } + + Row { + id: hintRow + readonly property bool selectedNeedsHold: root.actionNeedsConfirm(root.getActionAtIndex(root.selectedIndex)) + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.spacingS + spacing: Theme.spacingXS + visible: root.needsConfirmation + opacity: root.showHoldHint ? 1 : 0.5 + + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + + DankIcon { + name: { + if (root.showHoldHint) + return "warning"; + if (!hintRow.selectedNeedsHold) + return "bolt"; + return "touch_app"; + } + size: Theme.fontSizeSmall + color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000 + readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress)) + text: { + if (root.committedAction !== "") + return I18n.tr("Release to confirm"); + if (root.showHoldHint) + return I18n.tr("Hold longer to confirm"); + if (!hintRow.selectedNeedsHold) + return I18n.tr("Activates immediately"); + if (root.holdProgress > 0) { + if (totalMs < 1000) + return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs); + return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000)); + } + if (totalMs < 1000) + return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs); + return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration); + } + font.pixelSize: Theme.fontSizeSmall + color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary + anchors.verticalCenter: parent.verticalCenter + } + } +} diff --git a/quickshell/Services/PopoutService.qml b/quickshell/Services/PopoutService.qml index e5917d1b6..b0d3910bf 100644 --- a/quickshell/Services/PopoutService.qml +++ b/quickshell/Services/PopoutService.qml @@ -42,6 +42,8 @@ Singleton { property var spotlightBarModalLoader: null property var powerMenuModal: null property var powerMenuModalLoader: null + property var powerMenuPopout: null + property var powerMenuPopoutLoader: null property var processListModal: null property var processListModalLoader: null property var colorPickerModal: null @@ -121,6 +123,7 @@ Singleton { "battery": () => _unloadPopoutNow("batteryPopout", "batteryPopoutLoader"), "vpn": () => _unloadPopoutNow("vpnPopout", "vpnPopoutLoader"), "colorPicker": () => _unloadPopoutNow("colorPickerPopout", "colorPickerPopoutLoader"), + "powerMenuPopout": () => _unloadPopoutNow("powerMenuPopout", "powerMenuPopoutLoader"), "systemUpdate": () => _unloadPopoutNow("systemUpdatePopout", "systemUpdateLoader"), "layout": () => _unloadPopoutNow("layoutPopout", "layoutPopoutLoader"), "clipboardHistory": () => _unloadPopoutNow("clipboardHistoryPopout", "clipboardHistoryPopoutLoader"), @@ -970,6 +973,10 @@ Singleton { _scheduleUnload("colorPicker"); } + function unloadPowerMenuPopout() { + _scheduleUnload("powerMenuPopout"); + } + function ensureBluetoothPairingModal() { if (bluetoothPairingModal) return bluetoothPairingModal;