powermenu: add popout variant for bar widget

fixes #3199
This commit is contained in:
bbedward committed 2026-08-28 10:17:26 -04:00
1 parent 00f7db98b5
commit cef1eb962e
6 files changed
+1008 -902

No files matched your search

+63 -36
View File
@@ -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
+12 -854
View File
@@ -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()
}
}
}
+10 -12
View File
@@ -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"
});
}
}
}
@@ -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;
}
}
}
}
@@ -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
}
}
}
+7
View File
@@ -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;