1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

feat: PowerMenu modal

This commit is contained in:
purian23
2025-08-25 22:08:17 -04:00
parent 673ec76fa7
commit f4f949ebbc
5 changed files with 518 additions and 31 deletions

View File

@@ -12,14 +12,27 @@ DankModal {
property string powerConfirmAction: "" property string powerConfirmAction: ""
property string powerConfirmTitle: "" property string powerConfirmTitle: ""
property string powerConfirmMessage: "" property string powerConfirmMessage: ""
property int selectedButton: -1 // -1 = none, 0 = Cancel, 1 = Confirm
property bool keyboardNavigation: false
function show(action, title, message) { function show(action, title, message) {
powerConfirmAction = action powerConfirmAction = action
powerConfirmTitle = title powerConfirmTitle = title
powerConfirmMessage = message powerConfirmMessage = message
selectedButton = -1 // No button selected initially
keyboardNavigation = false
open() open()
} }
function selectButton() {
if (selectedButton === 0) {
close()
} else {
close()
executePowerAction(powerConfirmAction)
}
}
function executePowerAction(action) { function executePowerAction(action) {
switch (action) { switch (action) {
case "logout": case "logout":
@@ -44,6 +57,35 @@ DankModal {
onBackgroundClicked: { onBackgroundClicked: {
close() close()
} }
onOpened: {
modalFocusScope.forceActiveFocus()
}
modalFocusScope.Keys.onPressed: function(event) {
switch (event.key) {
case Qt.Key_Left:
case Qt.Key_Up:
keyboardNavigation = true
selectedButton = 0
event.accepted = true
break
case Qt.Key_Right:
case Qt.Key_Down:
keyboardNavigation = true
selectedButton = 1
event.accepted = true
break
case Qt.Key_Tab:
keyboardNavigation = true
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
selectButton()
event.accepted = true
break
}
}
content: Component { content: Component {
Item { Item {
@@ -93,7 +135,16 @@ DankModal {
width: 120 width: 120
height: 40 height: 40
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: cancelButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha color: {
if (keyboardNavigation && selectedButton === 0)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
else if (cancelButton.containsMouse)
return Theme.surfacePressed
else
return Theme.surfaceVariantAlpha
}
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
StyledText { StyledText {
text: "Cancel" text: "Cancel"
@@ -110,9 +161,11 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
close() selectedButton = 0
selectButton()
} }
} }
} }
Rectangle { Rectangle {
@@ -132,12 +185,15 @@ DankModal {
baseColor = Theme.primary baseColor = Theme.primary
break break
} }
return confirmButton.containsMouse ? Qt.rgba( if (keyboardNavigation && selectedButton === 1)
baseColor.r, return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
baseColor.g, else if (confirmButton.containsMouse)
baseColor.b, return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
0.9) : baseColor else
return baseColor
} }
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
StyledText { StyledText {
text: "Confirm" text: "Confirm"
@@ -154,13 +210,19 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
close() selectedButton = 1
executePowerAction(powerConfirmAction) selectButton()
} }
} }
} }
} }
} }
} }
} }
} }

337
Modals/PowerMenuModal.qml Normal file
View File

@@ -0,0 +1,337 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property int selectedIndex: 0
property int optionCount: 4
signal powerActionRequested(string action, string title, string message)
function selectOption() {
close()
switch (selectedIndex) {
case 0:
root.powerActionRequested("logout", "Log Out", "Are you sure you want to log out?")
break
case 1:
root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend the system?")
break
case 2:
root.powerActionRequested("reboot", "Reboot", "Are you sure you want to reboot the system?")
break
case 3:
root.powerActionRequested("poweroff", "Power Off", "Are you sure you want to power off the system?")
break
}
}
shouldBeVisible: false
width: 320
height: 300
enableShadow: true
onBackgroundClicked: {
close()
}
onOpened: {
selectedIndex = 0
modalFocusScope.forceActiveFocus()
}
modalFocusScope.Keys.onPressed: function(event) {
switch (event.key) {
case Qt.Key_Up:
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
break
case Qt.Key_Down:
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
break
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption()
event.accepted = true
break
}
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: "Power Options"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
onClicked: {
close()
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 0)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else if (logoutArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
border.width: selectedIndex === 0 ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Log Out"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = 0
selectOption()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 1)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else if (suspendArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: selectedIndex === 1 ? Theme.primary : "transparent"
border.width: selectedIndex === 1 ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Suspend"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = 1
selectOption()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 2)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
else if (rebootArea.containsMouse)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: selectedIndex === 2 ? Theme.warning : "transparent"
border.width: selectedIndex === 2 ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Reboot"
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = 2
selectOption()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 3)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
else if (powerOffArea.containsMouse)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: selectedIndex === 3 ? Theme.error : "transparent"
border.width: selectedIndex === 3 ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Power Off"
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = 3
selectOption()
}
}
}
}
Item {
height: Theme.spacingS
}
StyledText {
text: "↑↓ Navigate • Tab Cycle • Enter Select • Esc Close"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
opacity: 0.7
}
}
}
}
}

View File

@@ -301,6 +301,9 @@ binds {
Super+Alt+L hotkey-overlay-title="Lock Screen" { Super+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "qs" "-c" "dms" "ipc" "call" "lock" "lock"; spawn "qs" "-c" "dms" "ipc" "call" "lock" "lock";
} }
Mod+X hotkey-overlay-title="Power Menu" {
spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle";
}
XF86AudioRaiseVolume allow-when-locked=true { XF86AudioRaiseVolume allow-when-locked=true {
spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3";
} }
@@ -351,6 +354,7 @@ bind = SUPER, M, exec, qs -c dms ipc call processlist toggle
bind = SUPER, N, exec, qs -c dms ipc call notifications toggle bind = SUPER, N, exec, qs -c dms ipc call notifications toggle
bind = SUPER, comma, exec, qs -c dms ipc call settings toggle bind = SUPER, comma, exec, qs -c dms ipc call settings toggle
bind = SUPERALT, L, exec, qs -c dms ipc call lock lock bind = SUPERALT, L, exec, qs -c dms ipc call lock lock
bind = SUPER, X, exec, qs -c dms ipc call powermenu toggle
# Audio controls (function keys) # Audio controls (function keys)
bindl = , XF86AudioRaiseVolume, exec, qs -c dms ipc call audio increment 3 bindl = , XF86AudioRaiseVolume, exec, qs -c dms ipc call audio increment 3
@@ -378,6 +382,7 @@ qs -c dms ipc call audio mute
```bash ```bash
qs -c dms ipc call spotlight toggle qs -c dms ipc call spotlight toggle
qs -c dms ipc call processlist toggle qs -c dms ipc call processlist toggle
qs -c dms ipc call powermenu toggle
``` ```
# System control # System control
``` ```

View File

@@ -361,6 +361,14 @@ System process list and performance modal control.
- `close` - Hide process list modal - `close` - Hide process list modal
- `toggle` - Toggle process list modal visibility - `toggle` - Toggle process list modal visibility
### Target: `powermenu`
Power menu modal control for system power actions.
**Functions:**
- `open` - Show power menu modal
- `close` - Hide power menu modal
- `toggle` - Toggle power menu modal visibility
### Modal Examples ### Modal Examples
```bash ```bash
# Open application launcher # Open application launcher
@@ -377,6 +385,9 @@ qs -c dms ipc call settings open
# Show system monitor # Show system monitor
qs -c dms ipc call processlist toggle qs -c dms ipc call processlist toggle
# Show power menu
qs -c dms ipc call powermenu toggle
``` ```
## Common Usage Patterns ## Common Usage Patterns
@@ -389,6 +400,7 @@ These IPC commands are designed to be used with window manager keybindings. Exam
binds { binds {
Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; } Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; }
Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; } Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; }
Mod+X { spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle"; }
XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; } XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; }
XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; } XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; }
} }

115
shell.qml
View File

@@ -3,22 +3,22 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets import Quickshell.Widgets
import qs.Common
import qs.Modals import qs.Modals
import qs.Modules import qs.Modules
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Modules.OSD
import qs.Modules.CentcomCenter import qs.Modules.CentcomCenter
import qs.Modules.ControlCenter import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Network import qs.Modules.ControlCenter.Network
import qs.Modules.Dock
import qs.Modules.Lock import qs.Modules.Lock
import qs.Modules.Notifications.Center import qs.Modules.Notifications.Center
import qs.Modules.Notifications.Popup import qs.Modules.Notifications.Popup
import qs.Modules.OSD
import qs.Modules.ProcessList import qs.Modules.ProcessList
import qs.Modules.Settings import qs.Modules.Settings
import qs.Modules.TopBar import qs.Modules.TopBar
import qs.Modules.Dock
import qs.Services import qs.Services
import qs.Common
ShellRoot { ShellRoot {
id: root id: root
@@ -27,7 +27,8 @@ ShellRoot {
PortalService.init() PortalService.init()
} }
WallpaperBackground {} WallpaperBackground {
}
Lock { Lock {
id: lock id: lock
@@ -41,6 +42,7 @@ ShellRoot {
delegate: TopBar { delegate: TopBar {
modelData: item modelData: item
} }
} }
Variants { Variants {
@@ -49,40 +51,47 @@ ShellRoot {
delegate: Dock { delegate: Dock {
modelData: item modelData: item
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
Component.onCompleted: { Component.onCompleted: {
dockContextMenuLoader.active = true dockContextMenuLoader.active = true
} }
} }
} }
Loader { Loader {
id: centcomPopoutLoader id: centcomPopoutLoader
active: false active: false
sourceComponent: Component { sourceComponent: Component {
CentcomPopout { CentcomPopout {
id: centcomPopout id: centcomPopout
} }
} }
} }
LazyLoader { LazyLoader {
id: dockContextMenuLoader id: dockContextMenuLoader
active: false active: false
DockContextMenu { DockContextMenu {
id: dockContextMenu id: dockContextMenu
} }
}
}
LazyLoader { LazyLoader {
id: notificationCenterLoader id: notificationCenterLoader
active: false active: false
NotificationCenterPopout { NotificationCenterPopout {
id: notificationCenter id: notificationCenter
} }
} }
Variants { Variants {
@@ -91,87 +100,99 @@ ShellRoot {
delegate: NotificationPopupManager { delegate: NotificationPopupManager {
modelData: item modelData: item
} }
} }
LazyLoader { LazyLoader {
id: controlCenterLoader id: controlCenterLoader
active: false active: false
ControlCenterPopout { ControlCenterPopout {
id: controlCenterPopout id: controlCenterPopout
onPowerActionRequested: (action, title, message) => { onPowerActionRequested: (action, title, message) => {
powerConfirmModalLoader.active = true powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) { if (powerConfirmModalLoader.item)
powerConfirmModalLoader.item.show( powerConfirmModalLoader.item.show(action, title, message)
action, title, message) }
}
}
onLockRequested: { onLockRequested: {
lock.activate() lock.activate()
} }
} }
} }
LazyLoader { LazyLoader {
id: wifiPasswordModalLoader id: wifiPasswordModalLoader
active: false active: false
WifiPasswordModal { WifiPasswordModal {
id: wifiPasswordModal id: wifiPasswordModal
} }
} }
LazyLoader { LazyLoader {
id: networkInfoModalLoader id: networkInfoModalLoader
active: false active: false
NetworkInfoModal { NetworkInfoModal {
id: networkInfoModal id: networkInfoModal
} }
} }
LazyLoader { LazyLoader {
id: batteryPopoutLoader id: batteryPopoutLoader
active: false active: false
BatteryPopout { BatteryPopout {
id: batteryPopout id: batteryPopout
} }
} }
LazyLoader { LazyLoader {
id: powerMenuLoader id: powerMenuLoader
active: false active: false
PowerMenu { PowerMenu {
id: powerMenu id: powerMenu
onPowerActionRequested: (action, title, message) => { onPowerActionRequested: (action, title, message) => {
powerConfirmModalLoader.active = true powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) { if (powerConfirmModalLoader.item)
powerConfirmModalLoader.item.show( powerConfirmModalLoader.item.show(action, title, message)
action, title, message) }
}
}
} }
} }
LazyLoader { LazyLoader {
id: powerConfirmModalLoader id: powerConfirmModalLoader
active: false active: false
PowerConfirmModal { PowerConfirmModal {
id: powerConfirmModal id: powerConfirmModal
} }
} }
LazyLoader { LazyLoader {
id: processListPopoutLoader id: processListPopoutLoader
active: false active: false
ProcessListPopout { ProcessListPopout {
id: processListPopout id: processListPopout
} }
} }
SettingsModal { SettingsModal {
@@ -180,11 +201,13 @@ ShellRoot {
LazyLoader { LazyLoader {
id: appDrawerLoader id: appDrawerLoader
active: false active: false
AppDrawerPopout { AppDrawerPopout {
id: appDrawerPopout id: appDrawerPopout
} }
} }
SpotlightModal { SpotlightModal {
@@ -207,6 +230,51 @@ ShellRoot {
ProcessListModal { ProcessListModal {
id: processListModal id: processListModal
} }
}
LazyLoader {
id: powerMenuModalLoader
active: false
PowerMenuModal {
id: powerMenuModal
onPowerActionRequested: (action, title, message) => {
powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item)
powerConfirmModalLoader.item.show(action, title, message)
}
}
}
IpcHandler {
function open() {
powerMenuModalLoader.active = true
if (powerMenuModalLoader.item)
powerMenuModalLoader.item.open()
return "POWERMENU_OPEN_SUCCESS"
}
function close() {
if (powerMenuModalLoader.item)
powerMenuModalLoader.item.close()
return "POWERMENU_CLOSE_SUCCESS"
}
function toggle() {
powerMenuModalLoader.active = true
if (powerMenuModalLoader.item)
powerMenuModalLoader.item.toggle()
return "POWERMENU_TOGGLE_SUCCESS"
}
target: "powermenu"
} }
IpcHandler { IpcHandler {
@@ -243,6 +311,7 @@ ShellRoot {
modelData: item modelData: item
visible: ToastService.toastVisible visible: ToastService.toastVisible
} }
} }
Variants { Variants {
@@ -251,17 +320,16 @@ ShellRoot {
delegate: VolumeOSD { delegate: VolumeOSD {
modelData: item modelData: item
} }
} }
Variants { Variants {
model: SettingsData.getFilteredScreens("osd") model: SettingsData.getFilteredScreens("osd")
delegate: MicMuteOSD { delegate: MicMuteOSD {
modelData: item modelData: item
} }
} }
Variants { Variants {
@@ -270,6 +338,7 @@ ShellRoot {
delegate: BrightnessOSD { delegate: BrightnessOSD {
modelData: item modelData: item
} }
} }
Variants { Variants {
@@ -278,5 +347,7 @@ ShellRoot {
delegate: IdleInhibitorOSD { delegate: IdleInhibitorOSD {
modelData: item modelData: item
} }
} }
}
}