1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -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 powerConfirmTitle: ""
property string powerConfirmMessage: ""
property int selectedButton: -1 // -1 = none, 0 = Cancel, 1 = Confirm
property bool keyboardNavigation: false
function show(action, title, message) {
powerConfirmAction = action
powerConfirmTitle = title
powerConfirmMessage = message
selectedButton = -1 // No button selected initially
keyboardNavigation = false
open()
}
function selectButton() {
if (selectedButton === 0) {
close()
} else {
close()
executePowerAction(powerConfirmAction)
}
}
function executePowerAction(action) {
switch (action) {
case "logout":
@@ -44,6 +57,35 @@ DankModal {
onBackgroundClicked: {
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 {
Item {
@@ -93,7 +135,16 @@ DankModal {
width: 120
height: 40
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 {
text: "Cancel"
@@ -110,9 +161,11 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
close()
selectedButton = 0
selectButton()
}
}
}
Rectangle {
@@ -132,12 +185,15 @@ DankModal {
baseColor = Theme.primary
break
}
return confirmButton.containsMouse ? Qt.rgba(
baseColor.r,
baseColor.g,
baseColor.b,
0.9) : baseColor
if (keyboardNavigation && selectedButton === 1)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
else if (confirmButton.containsMouse)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
else
return baseColor
}
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
StyledText {
text: "Confirm"
@@ -154,13 +210,19 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
close()
executePowerAction(powerConfirmAction)
selectedButton = 1
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" {
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 {
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, comma, exec, qs -c dms ipc call settings toggle
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)
bindl = , XF86AudioRaiseVolume, exec, qs -c dms ipc call audio increment 3
@@ -378,6 +382,7 @@ qs -c dms ipc call audio mute
```bash
qs -c dms ipc call spotlight toggle
qs -c dms ipc call processlist toggle
qs -c dms ipc call powermenu toggle
```
# System control
```

View File

@@ -361,6 +361,14 @@ System process list and performance modal control.
- `close` - Hide process list modal
- `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
```bash
# Open application launcher
@@ -377,6 +385,9 @@ qs -c dms ipc call settings open
# Show system monitor
qs -c dms ipc call processlist toggle
# Show power menu
qs -c dms ipc call powermenu toggle
```
## Common Usage Patterns
@@ -389,6 +400,7 @@ These IPC commands are designed to be used with window manager keybindings. Exam
binds {
Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "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"; }
XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; }
}

113
shell.qml
View File

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