diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index e37e1c4f..d74fe4e4 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -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 diff --git a/quickshell/DMSShellIPC.qml b/quickshell/DMSShellIPC.qml index f26544af..a9eed0f1 100644 --- a/quickshell/DMSShellIPC.qml +++ b/quickshell/DMSShellIPC.qml @@ -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" + } } diff --git a/quickshell/Modals/PowerProfileModal.qml b/quickshell/Modals/PowerProfileModal.qml new file mode 100644 index 00000000..1272e4a7 --- /dev/null +++ b/quickshell/Modals/PowerProfileModal.qml @@ -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 + } + } + } + } + } +} diff --git a/quickshell/Services/PopoutService.qml b/quickshell/Services/PopoutService.qml index c9505269..f3ee6e10 100644 --- a/quickshell/Services/PopoutService.qml +++ b/quickshell/Services/PopoutService.qml @@ -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();