From 99b9de9d5c8ce23bbb3ab01e4a28de44e109d6ef Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:14:01 +0200 Subject: [PATCH] session menu --- .../widgets => bar}/SmallCircleButton.qml | 1 + .../quickshell/modules/common/Appearance.qml | 3 +- .../onScreenDisplay/OsdValueIndicator.qml | 6 +- .../quickshell/modules/session/Session.qml | 290 ++++++++++++++++++ .../modules/session/SessionActionButton.qml | 65 ++++ .../modules/sidebarRight/SidebarRight.qml | 18 +- .config/quickshell/shell.qml | 8 +- 7 files changed, 381 insertions(+), 10 deletions(-) rename .config/quickshell/modules/{common/widgets => bar}/SmallCircleButton.qml (96%) create mode 100644 .config/quickshell/modules/session/Session.qml create mode 100644 .config/quickshell/modules/session/SessionActionButton.qml diff --git a/.config/quickshell/modules/common/widgets/SmallCircleButton.qml b/.config/quickshell/modules/bar/SmallCircleButton.qml similarity index 96% rename from .config/quickshell/modules/common/widgets/SmallCircleButton.qml rename to .config/quickshell/modules/bar/SmallCircleButton.qml index 091e3720..97cb9307 100644 --- a/.config/quickshell/modules/common/widgets/SmallCircleButton.qml +++ b/.config/quickshell/modules/bar/SmallCircleButton.qml @@ -1,4 +1,5 @@ import "root:/modules/common" +import "root:/modules/common/widgets/" import QtQuick import QtQuick.Controls import QtQuick.Layouts diff --git a/.config/quickshell/modules/common/Appearance.qml b/.config/quickshell/modules/common/Appearance.qml index 0feb39f7..048fbbf2 100644 --- a/.config/quickshell/modules/common/Appearance.qml +++ b/.config/quickshell/modules/common/Appearance.qml @@ -164,6 +164,7 @@ Singleton { property int larger: 19 property int huge: 22 property int hugeass: 23 + property int title: 28 } } @@ -176,7 +177,7 @@ Singleton { property QtObject elementDecelFast: QtObject { property int duration: 140 property int type: Easing.OutCirc - property int velocity: 750 + property int velocity: 850 } property QtObject menuDecel: QtObject { property int duration: 350 diff --git a/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml b/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml index 5437c09e..d2407487 100644 --- a/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml +++ b/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml @@ -30,8 +30,8 @@ Item { RowLayout { // Icon on the left, stuff on the right id: valueRow - spacing: 5 Layout.margins: 10 + spacing: 10 MaterialSymbol { // Icon Layout.alignment: Qt.AlignVCenter @@ -47,8 +47,8 @@ Item { spacing: 5 RowLayout { // Name fill left, value on the right end - Layout.leftMargin: valueBarHeight / 2 // Align text with progressbar radius curve's left end - Layout.rightMargin: valueBarHeight / 2 // Align text with progressbar radius curve's left end + Layout.leftMargin: valueProgressBar.height / 2 // Align text with progressbar radius curve's left end + Layout.rightMargin: valueProgressBar.height / 2 // Align text with progressbar radius curve's left end StyledText { color: Appearance.colors.colOnLayer0 diff --git a/.config/quickshell/modules/session/Session.qml b/.config/quickshell/modules/session/Session.qml new file mode 100644 index 00000000..620ee835 --- /dev/null +++ b/.config/quickshell/modules/session/Session.qml @@ -0,0 +1,290 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets + +Scope { + id: root + readonly property Toplevel activeWindow: ToplevelManager.activeToplevel + + Variants { + id: sessionVariants + model: Quickshell.screens + + PanelWindow { // Session menu + id: sessionRoot + visible: false + + property var modelData + property string subtitle + + screen: modelData + exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell:session" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + color: Appearance.transparentize(Appearance.m3colors.m3background, 0.4) + + anchors { + top: true + left: true + right: true + } + + width: modelData.width + height: modelData.height + + HyprlandFocusGrab { + id: grab + windows: [ sessionRoot ] + active: false + onCleared: () => { + if (!active) sessionRoot.visible = false + } + } + + Connections { + target: sessionRoot + function onVisibleChanged() { + delayedGrabTimer.start() + } + } + + Timer { + id: delayedGrabTimer + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + onTriggered: { + grab.active = sessionRoot.visible + } + } + + MouseArea { + id: sessionMouseArea + anchors.fill: parent + onClicked: { + sessionRoot.visible = false + } + } + + ColumnLayout { // Content column + anchors.centerIn: parent + spacing: 15 + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + sessionRoot.visible = false; + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 0 + StyledText { // Title + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + font.family: Appearance.font.family.title + font.pixelSize: Appearance.font.pixelSize.title + font.weight: Font.DemiBold + text: "Session" + } + + StyledText { // Small instruction + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + font.family: Appearance.font.family.title + font.pixelSize: Appearance.font.pixelSize.normal + text: "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel" + } + } + + RowLayout { // First row of buttons + spacing: 15 + SessionActionButton { + id: sessionLock + focus: sessionRoot.visible + buttonIcon: "lock" + buttonText: "Lock" + onClicked: { lock.running = true; sessionRoot.visible = false } + onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } + KeyNavigation.right: sessionSleep + KeyNavigation.down: sessionHibernate + } + SessionActionButton { + id: sessionSleep + focus: sessionRoot.visible + buttonIcon: "dark_mode" + buttonText: "Sleep" + onClicked: { sleep.running = true; sessionRoot.visible = false } + onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } + KeyNavigation.left: sessionLock + KeyNavigation.right: sessionLogout + KeyNavigation.down: sessionShutdown + } + SessionActionButton { + id: sessionLogout + focus: sessionRoot.visible + buttonIcon: "logout" + buttonText: "Logout" + onClicked: { logout.running = true; sessionRoot.visible = false } + onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } + KeyNavigation.left: sessionSleep + KeyNavigation.right: sessionTaskManager + KeyNavigation.down: sessionReboot + } + SessionActionButton { + id: sessionTaskManager + focus: sessionRoot.visible + buttonIcon: "browse_activity" + buttonText: "Task Manager" + onClicked: { taskManager.running = true; sessionRoot.visible = false } + onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } + KeyNavigation.left: sessionLogout + KeyNavigation.down: sessionFirmwareReboot + } + } + + RowLayout { // Second row of buttons + spacing: 15 + SessionActionButton { + id: sessionHibernate + focus: sessionRoot.visible + buttonIcon: "downloading" + buttonText: "Hibernate" + onClicked: { hibernate.running = true; sessionRoot.visible = false } + onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } + KeyNavigation.up: sessionLock + KeyNavigation.right: sessionShutdown + } + SessionActionButton { + id: sessionShutdown + focus: sessionRoot.visible + buttonIcon: "power_settings_new" + buttonText: "Shutdown" + onClicked: { shutdown.running = true; sessionRoot.visible = false } + onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } + KeyNavigation.left: sessionHibernate + KeyNavigation.right: sessionReboot + KeyNavigation.up: sessionSleep + } + SessionActionButton { + id: sessionReboot + focus: sessionRoot.visible + buttonIcon: "restart_alt" + buttonText: "Reboot" + onClicked: { reboot.running = true; sessionRoot.visible = false } + onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } + KeyNavigation.left: sessionShutdown + KeyNavigation.right: sessionFirmwareReboot + KeyNavigation.up: sessionLogout + } + SessionActionButton { + id: sessionFirmwareReboot + focus: sessionRoot.visible + buttonIcon: "reset_wrench" + buttonText: "Reboot to firmware settings" + onClicked: { firmwareReboot.running = true; sessionRoot.visible = false } + onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } + KeyNavigation.up: sessionTaskManager + KeyNavigation.left: sessionReboot + } + } + + Rectangle { + Layout.alignment: Qt.AlignHCenter + radius: Appearance.rounding.normal + implicitHeight: sessionSubtitle.implicitHeight + 10 * 2 + implicitWidth: sessionSubtitle.implicitWidth + 10 * 2 + color: Appearance.colors.colTooltip + clip: true + + Behavior on implicitWidth { + SmoothedAnimation { + velocity: Appearance.animation.elementDecelFast.velocity + } + } + + StyledText { + id: sessionSubtitle + anchors.centerIn: parent + color: Appearance.colors.colOnTooltip + text: sessionRoot.subtitle + } + } + } + + } + + } + + Process { + id: lock + command: ["bash", "-c", "loginctl lock-session"] + } + Process { + id: sleep + command: ["bash", "-c", "systemctl suspend || loginctl suspend"] + } + Process { + id: logout + command: ["bash", "-c", "loginctl terminate-session $XDG_SESSION_ID"] + } + Process { + id: hibernate + command: ["bash", "-c", "systemctl hibernate || loginctl hibernate"] + } + Process { + id: shutdown + command: ["bash", "-c", "systemctl poweroff || loginctl poweroff"] + } + Process { + id: reboot + command: ["bash", "-c", "systemctl reboot || loginctl reboot"] + } + Process { + id: firmwareReboot + command: ["bash", "-c", "systemctl reboot --firmware-setup || loginctl reboot --firmware-setup"] + } + Process { + id: taskManager + command: ["bash", "-c", "gnome-system-monitor & disown"] + } + + IpcHandler { + target: "session" + + function toggle(): void { + for (let i = 0; i < sessionVariants.instances.length; i++) { + let panelWindow = sessionVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + panelWindow.visible = !panelWindow.visible; + } + } + } + + function close(): void { + for (let i = 0; i < sessionVariants.instances.length; i++) { + let panelWindow = sessionVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + panelWindow.visible = false; + } + } + } + + function open(): void { + for (let i = 0; i < sessionVariants.instances.length; i++) { + let panelWindow = sessionVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + panelWindow.visible = true; + } + } + } + } + +} diff --git a/.config/quickshell/modules/session/SessionActionButton.qml b/.config/quickshell/modules/session/SessionActionButton.qml new file mode 100644 index 00000000..c27b0b26 --- /dev/null +++ b/.config/quickshell/modules/session/SessionActionButton.qml @@ -0,0 +1,65 @@ +import "root:/modules/common" +import "root:/modules/common/widgets/" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io + +Button { + id: button + + property string buttonIcon + property string buttonText + property bool keyboardDown: false + + implicitHeight: 120 + implicitWidth: 120 + + PointingHandInteraction {} + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + keyboardDown = true + button.clicked() + event.accepted = true; + } + } + Keys.onReleased: (event) => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + keyboardDown = false + event.accepted = true; + } + } + + onClicked: { + console.log("Button clicked:", buttonText) + } + + background: Rectangle { + anchors.fill: parent + radius: Appearance.rounding.full + color: (button.down || button.keyboardDown) ? Appearance.colors.colLayer2Active : ((button.hovered || button.focus) ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2) + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.elementDecel.duration + easing.type: Appearance.animation.elementDecel.type + } + + } + + } + + contentItem: MaterialSymbol { + id: icon + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + text: buttonIcon + font.pixelSize: 40 + } + + StyledToolTip { + content: buttonText + } + +} diff --git a/.config/quickshell/modules/sidebarRight/SidebarRight.qml b/.config/quickshell/modules/sidebarRight/SidebarRight.qml index b2e9404b..2d903792 100644 --- a/.config/quickshell/modules/sidebarRight/SidebarRight.qml +++ b/.config/quickshell/modules/sidebarRight/SidebarRight.qml @@ -78,7 +78,6 @@ Scope { Keys.onPressed: (event) => { if (event.key === Qt.Key_Escape) { sidebarRoot.visible = false; - event.accepted = true; // Prevent further propagation of the event } } @@ -108,10 +107,23 @@ Scope { } Item { - Layout.fillHeight: true + Layout.fillWidth: true } - + QuickToggleButton { + toggled: false + buttonIcon: "power_settings_new" + onClicked: { + openSessionMenu.running = true + } + Process { + id: openSessionMenu + command: ["qs", "ipc", "call", "session", "open"] + } + StyledToolTip { + content: "Session" + } + } } Rectangle { diff --git a/.config/quickshell/shell.qml b/.config/quickshell/shell.qml index 9753bfbb..68f1f047 100644 --- a/.config/quickshell/shell.qml +++ b/.config/quickshell/shell.qml @@ -3,6 +3,7 @@ import "./modules/bar/" import "./modules/onScreenDisplay/" import "./modules/screenCorners/" +import "./modules/session/" import "./modules/sidebarRight/" import QtQuick import QtQuick.Controls @@ -12,10 +13,11 @@ import Quickshell ShellRoot { Bar {} - SidebarRight {} - ScreenCorners {} - ReloadPopup {} OnScreenDisplayBrightness {} OnScreenDisplayVolume {} + ReloadPopup {} + ScreenCorners {} + Session {} + SidebarRight {} }