Files
noctalia-shell/Modules/Panels/SessionMenu/SessionMenu.qml
T
2025-12-05 14:58:32 -05:00

571 lines
15 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Modules.MainScreen
import qs.Services.Compositor
import qs.Services.UI
import qs.Widgets
SmartPanel {
id: root
preferredWidth: Math.round(420 * Style.uiScaleRatio)
preferredHeight: {
var headerHeight = Settings.data.sessionMenu.showHeader ? Style.baseWidgetSize * 0.6 : 0;
var dividerHeight = Settings.data.sessionMenu.showHeader ? Style.marginS : 0;
var buttonHeight = Style.baseWidgetSize * 1.3 * Style.uiScaleRatio;
var buttonSpacing = Style.marginS;
var enabledCount = powerOptions.length;
var headerSpacing = Settings.data.sessionMenu.showHeader ? (Style.marginL * 2) : 0;
var baseHeight = (Style.marginL * 4) + headerHeight + dividerHeight + headerSpacing;
var buttonsHeight = enabledCount > 0 ? (buttonHeight * enabledCount) + (buttonSpacing * (enabledCount - 1)) : 0;
return Math.round(baseHeight + buttonsHeight);
}
// Positioning
readonly property string panelPosition: Settings.data.sessionMenu.position
panelAnchorHorizontalCenter: panelPosition === "center" || panelPosition.endsWith("_center")
panelAnchorVerticalCenter: panelPosition === "center"
panelAnchorLeft: panelPosition !== "center" && panelPosition.endsWith("_left")
panelAnchorRight: panelPosition !== "center" && panelPosition.endsWith("_right")
panelAnchorBottom: panelPosition.startsWith("bottom_")
panelAnchorTop: panelPosition.startsWith("top_")
// SessionMenu handle it's own closing logic
property bool closeWithEscape: false
// Timer properties
readonly property int timerDuration: Settings.data.sessionMenu.countdownDuration
property string pendingAction: ""
property bool timerActive: false
property int timeRemaining: 0
// Navigation properties
property int selectedIndex: 0
// Action metadata mapping
readonly property var actionMetadata: {
"lock": {
"icon": "lock",
"title": I18n.tr("session-menu.lock"),
"isShutdown": false
},
"suspend": {
"icon": "suspend",
"title": I18n.tr("session-menu.suspend"),
"isShutdown": false
},
"hibernate": {
"icon": "hibernate",
"title": I18n.tr("session-menu.hibernate"),
"isShutdown": false
},
"reboot": {
"icon": "reboot",
"title": I18n.tr("session-menu.reboot"),
"isShutdown": false
},
"logout": {
"icon": "logout",
"title": I18n.tr("session-menu.logout"),
"isShutdown": false
},
"shutdown": {
"icon": "shutdown",
"title": I18n.tr("session-menu.shutdown"),
"isShutdown": true
}
}
// Build powerOptions from settings, filtering enabled ones and adding metadata
property var powerOptions: {
var options = [];
var settingsOptions = Settings.data.sessionMenu.powerOptions || [];
for (var i = 0; i < settingsOptions.length; i++) {
var settingOption = settingsOptions[i];
if (settingOption.enabled && actionMetadata[settingOption.action]) {
var metadata = actionMetadata[settingOption.action];
options.push({
"action": settingOption.action,
"icon": metadata.icon,
"title": metadata.title,
"isShutdown": metadata.isShutdown,
"countdownEnabled": settingOption.countdownEnabled !== undefined ? settingOption.countdownEnabled : true,
"command": settingOption.command || ""
});
}
}
return options;
}
// Update powerOptions when settings change
Connections {
target: Settings.data.sessionMenu
function onPowerOptionsChanged() {
var options = [];
var settingsOptions = Settings.data.sessionMenu.powerOptions || [];
for (var i = 0; i < settingsOptions.length; i++) {
var settingOption = settingsOptions[i];
if (settingOption.enabled && actionMetadata[settingOption.action]) {
var metadata = actionMetadata[settingOption.action];
options.push({
"action": settingOption.action,
"icon": metadata.icon,
"title": metadata.title,
"isShutdown": metadata.isShutdown,
"countdownEnabled": settingOption.countdownEnabled !== undefined ? settingOption.countdownEnabled : true,
"command": settingOption.command || ""
});
}
}
root.powerOptions = options;
}
}
// Lifecycle handlers
onOpened: {
selectedIndex = 0;
}
onClosed: {
cancelTimer();
selectedIndex = 0;
}
// Timer management
function startTimer(action) {
// Check if global countdown is disabled
if (!Settings.data.sessionMenu.enableCountdown) {
executeAction(action);
return;
}
// Check per-item countdown setting
var option = null;
for (var i = 0; i < powerOptions.length; i++) {
if (powerOptions[i].action === action) {
option = powerOptions[i];
break;
}
}
// If this specific action has countdown disabled, execute immediately
if (option && option.countdownEnabled === false) {
executeAction(action);
return;
}
if (timerActive && pendingAction === action) {
// Second click - execute immediately
executeAction(action);
return;
}
pendingAction = action;
timeRemaining = timerDuration;
timerActive = true;
countdownTimer.start();
}
function cancelTimer() {
timerActive = false;
pendingAction = "";
timeRemaining = 0;
countdownTimer.stop();
}
function executeAction(action) {
// Stop timer but don't reset other properties yet
countdownTimer.stop();
// Find the option to check for custom command
var option = null;
for (var i = 0; i < powerOptions.length; i++) {
if (powerOptions[i].action === action) {
option = powerOptions[i];
break;
}
}
// If custom command is defined, execute it
if (option && option.command && option.command.trim() !== "") {
Logger.i("SessionMenu", "Executing custom command for action:", action, "Command:", option.command);
Quickshell.execDetached(["sh", "-c", option.command]);
cancelTimer();
root.close();
return;
}
// Otherwise, use default behavior
switch (action) {
case "lock":
// Access lockScreen via PanelService
if (PanelService.lockScreen && !PanelService.lockScreen.active) {
PanelService.lockScreen.active = true;
}
break;
case "suspend":
// Check if we should lock before suspending
if (Settings.data.general.lockOnSuspend) {
CompositorService.lockAndSuspend();
} else {
CompositorService.suspend();
}
break;
case "hibernate":
CompositorService.hibernate();
break;
case "reboot":
CompositorService.reboot();
break;
case "logout":
CompositorService.logout();
break;
case "shutdown":
CompositorService.shutdown();
break;
}
// Reset timer state and close panel
cancelTimer();
root.close();
}
// Navigation functions
function selectNextWrapped() {
if (powerOptions.length > 0) {
selectedIndex = (selectedIndex + 1) % powerOptions.length;
}
}
function selectPreviousWrapped() {
if (powerOptions.length > 0) {
selectedIndex = (((selectedIndex - 1) % powerOptions.length) + powerOptions.length) % powerOptions.length;
}
}
function selectFirst() {
selectedIndex = 0;
}
function selectLast() {
if (powerOptions.length > 0) {
selectedIndex = powerOptions.length - 1;
} else {
selectedIndex = 0;
}
}
function activate() {
if (powerOptions.length > 0 && powerOptions[selectedIndex]) {
const option = powerOptions[selectedIndex];
startTimer(option.action);
}
}
// Override keyboard handlers from SmartPanel
function onEscapePressed() {
if (timerActive) {
cancelTimer();
} else {
root.close();
}
}
function onTabPressed() {
selectNextWrapped();
}
function onBackTabPressed() {
selectPreviousWrapped();
}
function onUpPressed() {
selectPreviousWrapped();
}
function onDownPressed() {
selectNextWrapped();
}
function onReturnPressed() {
activate();
}
function onHomePressed() {
selectFirst();
}
function onEndPressed() {
selectLast();
}
function onCtrlJPressed() {
selectNextWrapped();
}
function onCtrlKPressed() {
selectPreviousWrapped();
}
// Countdown timer
Timer {
id: countdownTimer
interval: 100
repeat: true
onTriggered: {
timeRemaining -= interval;
if (timeRemaining <= 0) {
executeAction(pendingAction);
}
}
}
panelContent: Rectangle {
id: ui
color: Color.transparent
// Navigation functions
function selectFirst() {
root.selectFirst();
}
function selectLast() {
root.selectLast();
}
function selectNextWrapped() {
root.selectNextWrapped();
}
function selectPreviousWrapped() {
root.selectPreviousWrapped();
}
function activate() {
root.activate();
}
NBox {
anchors.fill: parent
anchors.margins: Style.marginL
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginL
// Header with title and close button
RowLayout {
visible: Settings.data.sessionMenu.showHeader
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 0.6
NText {
text: timerActive ? I18n.tr("session-menu.action-in-seconds", {
"action": I18n.tr("session-menu." + pendingAction),
"seconds": Math.ceil(timeRemaining / 1000)
}) : I18n.tr("session-menu.title")
font.weight: Style.fontWeightBold
pointSize: Style.fontSizeL
color: timerActive ? Color.mPrimary : Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NIconButton {
icon: timerActive ? "stop" : "close"
tooltipText: timerActive ? I18n.tr("tooltips.cancel-timer") : I18n.tr("tooltips.close")
Layout.alignment: Qt.AlignVCenter
baseSize: Style.baseWidgetSize * 0.7
colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
colorFg: timerActive ? Color.mError : Color.mOnSurface
onClicked: {
if (timerActive) {
cancelTimer();
} else {
cancelTimer();
root.close();
}
}
}
}
NDivider {
visible: Settings.data.sessionMenu.showHeader
Layout.fillWidth: true
}
// Power options
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
Repeater {
model: powerOptions
delegate: PowerButton {
Layout.fillWidth: true
icon: modelData.icon
title: modelData.title
isShutdown: modelData.isShutdown || false
isSelected: index === selectedIndex
onClicked: {
selectedIndex = index;
startTimer(modelData.action);
}
pending: timerActive && pendingAction === modelData.action
}
}
}
}
}
}
// Custom power button component
component PowerButton: Rectangle {
id: buttonRoot
property string icon: ""
property string title: ""
property bool pending: false
property bool isShutdown: false
property bool isSelected: false
signal clicked
height: Style.baseWidgetSize * 1.3 * Style.uiScaleRatio
radius: Style.radiusS
color: {
if (pending) {
return Qt.alpha(Color.mPrimary, 0.08);
}
if (isSelected || mouseArea.containsMouse) {
return Color.mHover;
}
return Color.transparent;
}
border.width: pending ? Math.max(Style.borderM) : 0
border.color: pending ? Color.mPrimary : Color.mOutline
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
Item {
anchors.fill: parent
anchors.margins: Style.marginM
// Icon on the left
NIcon {
id: iconElement
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
icon: buttonRoot.icon
color: {
if (buttonRoot.pending)
return Color.mPrimary;
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError;
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnHover;
return Color.mOnSurface;
}
pointSize: Style.fontSizeXXL
width: Style.baseWidgetSize * 0.5
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
}
// Text content in the middle
ColumnLayout {
anchors.left: iconElement.right
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginL
anchors.rightMargin: pendingIndicator.visible ? Style.marginM : 0
spacing: 0
NText {
text: buttonRoot.title
font.weight: Style.fontWeightMedium
pointSize: Style.fontSizeM
color: {
if (buttonRoot.pending)
return Color.mPrimary;
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError;
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnHover;
return Color.mOnSurface;
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
}
}
// Pending indicator on the right
Rectangle {
id: pendingIndicator
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 20
height: 20
radius: Math.min(Style.radiusL, width / 2)
color: Color.mPrimary
visible: buttonRoot.pending
NText {
anchors.centerIn: parent
text: Math.ceil(timeRemaining / 1000)
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: buttonRoot.clicked()
}
}
}