1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-08 04:09:15 -04:00

feat(ipc): add native powerprofile target for power profiles management (#2515)

* feat: add native powerprofile IPC target for power profiles management

* feat: show centered PowerProfileModal with 3 square buttons for powerprofile IPC toggle

* style: enhance PowerProfileModal size, icons, description, and keyboard hints

* feat: add Space key binding to select highlighted power profile
This commit is contained in:
Huỳnh Thiện Lộc
2026-05-31 01:51:19 +07:00
committed by GitHub
parent 2b661e241d
commit 461da22b08
4 changed files with 409 additions and 0 deletions
+18
View File
@@ -1185,6 +1185,24 @@ Item {
}
}
LazyLoader {
id: powerProfileModalLoader
active: false
PowerProfileModal {
id: powerProfileModal
Component.onCompleted: {
PopoutService.powerProfileModal = powerProfileModal;
}
}
Component.onCompleted: {
PopoutService.powerProfileModalLoader = powerProfileModalLoader;
}
}
DMSShellIPC {
powerMenuModalLoader: powerMenuModalLoader
processListModalLoader: processListModalLoader
+83
View File
@@ -3,6 +3,7 @@ import Quickshell.Io
import Quickshell.Hyprland
import Quickshell.Wayland
import Quickshell.Services.SystemTray
import Quickshell.Services.UPower
import qs.Common
import qs.Services
import qs.Modules.Settings.DisplayConfig
@@ -1890,4 +1891,86 @@ Item {
target: "tray"
}
IpcHandler {
function open(): string {
if (typeof PowerProfiles === "undefined")
return "ERROR: power-profiles-daemon not available";
PopoutService.openPowerProfileModal();
return "POWERPROFILE_OPEN_SUCCESS";
}
function close(): string {
PopoutService.closePowerProfileModal();
return "POWERPROFILE_CLOSE_SUCCESS";
}
function toggle(): string {
if (typeof PowerProfiles === "undefined")
return "ERROR: power-profiles-daemon not available";
PopoutService.togglePowerProfileModal();
return "POWERPROFILE_TOGGLE_SUCCESS";
}
function list(): string {
if (typeof PowerProfiles === "undefined")
return "ERROR: power-profiles-daemon not available";
const profiles = ["power-saver", "balanced"];
if (PowerProfiles.hasPerformanceProfile)
profiles.push("performance");
return profiles.join("\n");
}
function set(profile: string): string {
if (typeof PowerProfiles === "undefined")
return "ERROR: power-profiles-daemon not available";
if (!profile)
return "ERROR: No profile specified";
const lower = profile.toLowerCase().trim();
if (lower === "power-saver" || lower === "powersaver" || lower === "saver" || lower === "0") {
PowerProfiles.profile = PowerProfile.PowerSaver;
return "POWERPROFILE_SET_SUCCESS";
} else if (lower === "balanced" || lower === "1") {
PowerProfiles.profile = PowerProfile.Balanced;
return "POWERPROFILE_SET_SUCCESS";
} else if (lower === "performance" || lower === "2") {
if (PowerProfiles.hasPerformanceProfile) {
PowerProfiles.profile = PowerProfile.Performance;
return "POWERPROFILE_SET_SUCCESS";
} else {
return "ERROR: Performance profile not supported by hardware";
}
} else {
return "ERROR: Unknown power profile. Supported options: power-saver, balanced, performance";
}
}
function cycle(): string {
if (typeof PowerProfiles === "undefined")
return "ERROR: power-profiles-daemon not available";
const current = PowerProfiles.profile;
const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced];
if (PowerProfiles.hasPerformanceProfile)
profiles.push(PowerProfile.Performance);
const index = profiles.indexOf(current);
if (index === -1) {
PowerProfiles.profile = PowerProfile.Balanced;
return "POWERPROFILE_CYCLE_SUCCESS";
}
const nextIndex = (index + 1) % profiles.length;
PowerProfiles.profile = profiles[nextIndex];
return "POWERPROFILE_CYCLE_SUCCESS";
}
target: "powerprofile"
}
}
+272
View File
@@ -0,0 +1,272 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
import Quickshell.Services.UPower
DankModal {
id: root
layerNamespace: "dms:power-profiles"
keepPopoutsOpen: true
property int selectedIndex: 0
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
function openCentered() {
open();
}
function hideDialog() {
close();
}
shouldBeVisible: false
modalWidth: 440
modalHeight: 290
enableShadow: true
onBackgroundClicked: hideDialog()
onShouldBeVisibleChanged: {
if (!shouldBeVisible)
return;
if (typeof PowerProfiles !== "undefined") {
const current = PowerProfiles.profile;
const idx = profileModel.indexOf(current);
if (idx !== -1) {
selectedIndex = idx;
}
}
}
onShouldHaveFocusChanged: {
if (!shouldHaveFocus)
return;
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
modalFocusScope.Keys.onPressed: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
switch (event.key) {
case Qt.Key_Left:
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + profileModel.length) % profileModel.length;
event.accepted = true;
break;
case Qt.Key_Right:
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % profileModel.length;
event.accepted = true;
break;
case Qt.Key_Space:
case Qt.Key_Return:
case Qt.Key_Enter:
if (selectedIndex >= 0 && selectedIndex < profileModel.length) {
setProfile(profileModel[selectedIndex]);
}
event.accepted = true;
break;
case Qt.Key_1:
if (profileModel.length > 0) {
setProfile(profileModel[0]);
}
event.accepted = true;
break;
case Qt.Key_2:
if (profileModel.length > 1) {
setProfile(profileModel[1]);
}
event.accepted = true;
break;
case Qt.Key_3:
if (profileModel.length > 2) {
setProfile(profileModel[2]);
}
event.accepted = true;
break;
case Qt.Key_Escape:
hideDialog();
event.accepted = true;
break;
}
}
function setProfile(profile) {
if (typeof PowerProfiles !== "undefined") {
PowerProfiles.profile = profile;
}
hideDialog();
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Power Mode")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Choose a power profile")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hideDialog()
}
}
Row {
id: buttonsRow
width: parent.width
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: root.profileModel
Rectangle {
id: profileButton
required property int index
required property int modelData
readonly property bool isSelected: root.selectedIndex === index
readonly property bool isActive: (typeof PowerProfiles !== "undefined") && PowerProfiles.profile === modelData
width: (parent.width - Theme.spacingM * (root.profileModel.length - 1)) / root.profileModel.length
height: 120
radius: Theme.cornerRadius
color: {
if (isActive)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
if (mouseArea.containsMouse)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06);
}
border.color: isActive ? Theme.primary : (isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) : "transparent")
border.width: (isActive || isSelected) ? 2 : 0
// Shortcut Key Badge on Top-Right Corner
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingS
width: 20
height: 20
radius: 4
color: isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
border.color: isActive ? Theme.primary : "transparent"
border.width: isActive ? 1 : 0
StyledText {
text: (index + 1).toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: isActive ? Theme.primary : Theme.surfaceTextMedium
anchors.centerIn: parent
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: Theme.getPowerProfileIcon(modelData)
size: Theme.iconSize + 16
color: isActive ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: Theme.getPowerProfileLabel(modelData)
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
root.selectedIndex = index;
}
onClicked: {
root.setProfile(modelData);
}
}
}
}
}
// Selected power profile description
StyledText {
text: (root.selectedIndex >= 0 && root.selectedIndex < root.profileModel.length) ? Theme.getPowerProfileDescription(root.profileModel[root.selectedIndex]) : ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
width: parent.width - Theme.spacingL * 2
}
// Keyboard Shortcut Guide Footer
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS
opacity: 0.5
DankIcon {
name: "keyboard"
size: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Use keys 1-3 or arrows, Enter/Space to select")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceText
}
}
}
}
}
}
+36
View File
@@ -50,6 +50,8 @@ Singleton {
property var bluetoothPairingModal: null
property var networkInfoModal: null
property var windowRuleModalLoader: null
property var powerProfileModal: null
property var powerProfileModalLoader: null
property var notepadSlideouts: []
@@ -675,6 +677,40 @@ Singleton {
}
}
function openPowerProfileModal() {
if (powerProfileModal) {
powerProfileModal.openCentered();
} else if (powerProfileModalLoader) {
powerProfileModalLoader.active = true;
Qt.callLater(() => powerProfileModal?.openCentered());
}
}
function closePowerProfileModal() {
powerProfileModal?.close();
}
function togglePowerProfileModal() {
if (powerProfileModal) {
if (powerProfileModal.shouldBeVisible) {
powerProfileModal.close();
} else {
powerProfileModal.openCentered();
}
} else if (powerProfileModalLoader) {
powerProfileModalLoader.active = true;
Qt.callLater(() => {
if (powerProfileModal) {
if (powerProfileModal.shouldBeVisible) {
powerProfileModal.close();
} else {
powerProfileModal.openCentered();
}
}
});
}
}
function showProcessListModal() {
if (processListModal) {
processListModal.show();