diff --git a/config/Config.qml b/config/Config.qml index 8c010146f..71291adf0 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -152,7 +152,54 @@ Singleton { }, battery: { warnLevels: general.battery.warnLevels, - criticalLevel: general.battery.criticalLevel + criticalLevel: general.battery.criticalLevel, + powerManagement: { + enabled: general.battery.powerManagement.enabled, + thresholds: general.battery.powerManagement.thresholds, + onCharging: { + setPowerProfile: general.battery.powerManagement.onCharging.setPowerProfile, + setRefreshRate: general.battery.powerManagement.onCharging.setRefreshRate, + disableAnimations: general.battery.powerManagement.onCharging.disableAnimations, + disableBlur: general.battery.powerManagement.onCharging.disableBlur, + disableRounding: general.battery.powerManagement.onCharging.disableRounding, + disableShadows: general.battery.powerManagement.onCharging.disableShadows + }, + onUnplugged: { + setPowerProfile: general.battery.powerManagement.onUnplugged.setPowerProfile, + setRefreshRate: general.battery.powerManagement.onUnplugged.setRefreshRate, + disableAnimations: general.battery.powerManagement.onUnplugged.disableAnimations, + disableBlur: general.battery.powerManagement.onUnplugged.disableBlur, + disableRounding: general.battery.powerManagement.onUnplugged.disableRounding, + disableShadows: general.battery.powerManagement.onUnplugged.disableShadows, + evaluateThresholds: general.battery.powerManagement.onUnplugged.evaluateThresholds + }, + profileBehaviors: { + powerSaver: { + setPowerProfile: general.battery.powerManagement.profileBehaviors.powerSaver.setPowerProfile, + setRefreshRate: general.battery.powerManagement.profileBehaviors.powerSaver.setRefreshRate, + disableAnimations: general.battery.powerManagement.profileBehaviors.powerSaver.disableAnimations, + disableBlur: general.battery.powerManagement.profileBehaviors.powerSaver.disableBlur, + disableRounding: general.battery.powerManagement.profileBehaviors.powerSaver.disableRounding, + disableShadows: general.battery.powerManagement.profileBehaviors.powerSaver.disableShadows + }, + balanced: { + setPowerProfile: general.battery.powerManagement.profileBehaviors.balanced.setPowerProfile, + setRefreshRate: general.battery.powerManagement.profileBehaviors.balanced.setRefreshRate, + disableAnimations: general.battery.powerManagement.profileBehaviors.balanced.disableAnimations, + disableBlur: general.battery.powerManagement.profileBehaviors.balanced.disableBlur, + disableRounding: general.battery.powerManagement.profileBehaviors.balanced.disableRounding, + disableShadows: general.battery.powerManagement.profileBehaviors.balanced.disableShadows + }, + performance: { + setPowerProfile: general.battery.powerManagement.profileBehaviors.performance.setPowerProfile, + setRefreshRate: general.battery.powerManagement.profileBehaviors.performance.setRefreshRate, + disableAnimations: general.battery.powerManagement.profileBehaviors.performance.disableAnimations, + disableBlur: general.battery.powerManagement.profileBehaviors.performance.disableBlur, + disableRounding: general.battery.powerManagement.profileBehaviors.performance.disableRounding, + disableShadows: general.battery.powerManagement.profileBehaviors.performance.disableShadows + } + } + } } }; } diff --git a/config/GeneralConfig.qml b/config/GeneralConfig.qml index 52ef0de3e..ba2d639bb 100644 --- a/config/GeneralConfig.qml +++ b/config/GeneralConfig.qml @@ -5,6 +5,44 @@ JsonObject { property Apps apps: Apps {} property Idle idle: Idle {} property Battery battery: Battery {} + + + component PowerActionSchema: JsonObject { + property string setPowerProfile: "" + property string setRefreshRate: "" + property string disableAnimations: "" + property string disableBlur: "" + property string disableRounding: "" + property string disableShadows: "" + } + + component ChargingBehavior: PowerActionSchema { + setPowerProfile: "restore" + setRefreshRate: "restore" + } + + component UnpluggedBehavior: PowerActionSchema { + setPowerProfile: "restore" + setRefreshRate: "restore" + property bool evaluateThresholds: true + } + + component ProfileBehavior: PowerActionSchema { + } + + component ProfileBehaviors: JsonObject { + property ProfileBehavior powerSaver: ProfileBehavior {} + property ProfileBehavior balanced: ProfileBehavior {} + property ProfileBehavior performance: ProfileBehavior {} + } + + component PowerManagement: JsonObject { + property bool enabled: false + property list thresholds: [] + property ChargingBehavior onCharging: ChargingBehavior {} + property UnpluggedBehavior onUnplugged: UnpluggedBehavior {} + property ProfileBehaviors profileBehaviors: ProfileBehaviors {} + } component Apps: JsonObject { property list terminal: ["foot"] @@ -56,5 +94,7 @@ JsonObject { }, ] property int criticalLevel: 3 + + property PowerManagement powerManagement: PowerManagement {} } } diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index 1d69305ad..9cd637b95 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -9,19 +9,53 @@ Scope { id: root readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) + readonly property list powerThresholds: [...Config.general.battery.powerManagement.thresholds].sort((a, b) => b.level - a.level) + readonly property bool powerManagementEnabled: Config.general.battery.powerManagement.enabled + + property var originalSettings: ({ + refreshRates: {}, + animationsEnabled: null, + blurEnabled: null, + rounding: null, + shadowsEnabled: null + }) + + property int currentThresholdIndex: -1 + property bool settingsModified: false + property bool initialized: false + Component.onCompleted: { + // Mark as initialized after a short delay to suppress startup toasts + initTimer.start(); + } + + Timer { + id: initTimer + interval: 1000 + onTriggered: root.initialized = true + } + Connections { target: UPower function onOnBatteryChanged(): void { if (UPower.onBattery) { - if (Config.utilities.toasts.chargingChanged) + if (Config.utilities.toasts.chargingChanged && root.initialized) Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off"); + + if (root.powerManagementEnabled) { + root.handleUnpluggedState(); + } } else { - if (Config.utilities.toasts.chargingChanged) + if (Config.utilities.toasts.chargingChanged && root.initialized) Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power"); + for (const level of root.warnLevels) level.warned = false; + + if (root.powerManagementEnabled && root.settingsModified) { + root.handleChargingState(); + } } } } @@ -34,7 +68,7 @@ Scope { return; const p = UPower.displayDevice.percentage * 100; - for (const level of root.warnLevels) { + for (const level of root.warnLevels) { if (p <= level.level && !level.warned) { level.warned = true; Toaster.toast(level.title ?? qsTr("Battery warning"), level.message ?? qsTr("Battery level is low"), level.icon ?? "battery_android_alert", level.critical ? Toast.Error : Toast.Warning); @@ -45,6 +79,10 @@ Scope { Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error); hibernateTimer.start(); } + + if (root.powerManagementEnabled) { + root.evaluateThresholds(); + } } } @@ -52,32 +90,278 @@ Scope { target: PowerProfiles function onProfileChanged(): void { + if (!root.powerManagementEnabled) + return; + + const profileBehaviors = Config.general.battery.powerManagement.profileBehaviors; + let behavior = null; + let profileName = ""; + if (PowerProfiles.profile === PowerProfile.PowerSaver) { - // Apply Hyprland power saving options - Hypr.extras.applyOptions({ - "animations:enabled": 0, - "decoration:blur:enabled": 0, - "decoration:rounding": 0, - "decoration:shadow:enabled": 0 - }); - - // Set all monitors with refresh rate > 60 to 60Hz - const monitors = Hypr.monitors.values || Object.values(Hypr.monitors); - for (const monitor of monitors) { - const data = monitor.lastIpcObject; - if (data && data.refreshRate > 60) { - Hypr.extras.message(`keyword monitor ${data.name},${data.width}x${data.height}@60,${data.x}x${data.y},${data.scale}`); + behavior = profileBehaviors.powerSaver; + profileName = "Power Saver"; + } else if (PowerProfiles.profile === PowerProfile.Balanced) { + behavior = profileBehaviors.balanced; + profileName = "Balanced"; + } else if (PowerProfiles.profile === PowerProfile.Performance) { + behavior = profileBehaviors.performance; + profileName = "Performance"; + } + + if (behavior) { + if (behavior.setRefreshRate && behavior.setRefreshRate !== "" && behavior.setRefreshRate !== "unchanged") { + root.applyRefreshRate(behavior.setRefreshRate); + } + + root.applyVisualEffects(behavior); + + const hasSettings = behavior.disableAnimations !== "" || behavior.disableBlur !== "" || + behavior.disableRounding !== "" || behavior.disableShadows !== "" || + (behavior.setRefreshRate && behavior.setRefreshRate !== "" && behavior.setRefreshRate !== "restore"); + + if (Config.utilities.toasts.lowPowerModeChanged && hasSettings && root.initialized) { + const actions = []; + if (behavior.setRefreshRate && behavior.setRefreshRate !== "") { + actions.push(behavior.setRefreshRate === "auto" ? "lowest Hz" : behavior.setRefreshRate + "Hz"); + } + if (behavior.disableAnimations === "disable") actions.push("no animations"); + else if (behavior.disableAnimations === "enable") actions.push("animations on"); + if (behavior.disableBlur === "disable") actions.push("no blur"); + else if (behavior.disableBlur === "enable") actions.push("blur on"); + + if (actions.length > 0) { + Toaster.toast( + profileName + qsTr(" profile"), + qsTr("Applied: ") + actions.join(", "), + "battery_saver" + ); + } + } + } + } + } + + function applyVisualEffects(settings) { + const options = {}; + + if (settings.disableAnimations === "disable") options["animations:enabled"] = 0; + else if (settings.disableAnimations === "enable") options["animations:enabled"] = 1; + + if (settings.disableBlur === "disable") options["decoration:blur:enabled"] = 0; + else if (settings.disableBlur === "enable") options["decoration:blur:enabled"] = 1; + + if (settings.disableRounding === "disable") options["decoration:rounding"] = 0; + else if (settings.disableRounding === "enable") options["decoration:rounding"] = Appearance.rounding.normal; + + if (settings.disableShadows === "disable") options["decoration:shadow:enabled"] = 0; + else if (settings.disableShadows === "enable") options["decoration:shadow:enabled"] = 1; + + if (Object.keys(options).length > 0) { + Hypr.extras.applyOptions(options); + } + } + + function applyRefreshRate(rate) { + let targetRate; + + if (rate === "auto") { + targetRate = root.getLowestRefreshRate(); + } else if (rate === "restore") { + root.restoreRefreshRates(); + return; + } else { + targetRate = rate; + } + + const monitors = Hypr.monitors.values || Object.values(Hypr.monitors); + for (const monitor of monitors) { + const data = monitor.lastIpcObject; + if (data) { + Hypr.extras.message(`keyword monitor ${data.name},${data.width}x${data.height}@${targetRate},${data.x}x${data.y},${data.scale}`); + } + } + } + + function getLowestRefreshRate() { + const monitors = Hypr.monitors.values || Object.values(Hypr.monitors); + let lowestRate = 60; // Default fallback + + for (const monitor of monitors) { + const data = monitor.lastIpcObject; + if (data && data.availableModes && data.availableModes.length > 0) { + const supportedRates = []; + for (const mode of data.availableModes) { + const match = mode.match(/@(\d+(?:\.\d+)?)Hz/); + if (match) { + const rate = Math.round(parseFloat(match[1])); + if (!supportedRates.includes(rate)) { + supportedRates.push(rate); + } } } - - if (Config.utilities.toasts.lowPowerModeChanged) - Toaster.toast(qsTr("Low power mode enabled"), qsTr("Disabled animations, blur, rounding, shadows and set FPS to 60"), "battery_saver"); - } else { - // Restore default settings by reloading Hyprland config - Hypr.extras.message("reload"); - if (Config.utilities.toasts.lowPowerModeChanged) - Toaster.toast(qsTr("Low power mode disabled"), qsTr("Settings and performance restored"), "battery_saver"); + supportedRates.sort((a, b) => a - b); + if (supportedRates.length > 0) { + lowestRate = Math.min(lowestRate, supportedRates[0]); + } + } + } + + return lowestRate; + } + + function handleUnpluggedState() { + const unpluggedConfig = Config.general.battery.powerManagement.onUnplugged; + + const hasExplicitActions = (unpluggedConfig.setPowerProfile && unpluggedConfig.setPowerProfile !== "") || + (unpluggedConfig.setRefreshRate && unpluggedConfig.setRefreshRate !== "") || + unpluggedConfig.disableAnimations !== "" || + unpluggedConfig.disableBlur !== "" || + unpluggedConfig.disableRounding !== "" || + unpluggedConfig.disableShadows !== ""; + + if (hasExplicitActions) { + if (!root.settingsModified) { + root.saveOriginalSettings(); + } + + if (unpluggedConfig.setPowerProfile && unpluggedConfig.setPowerProfile !== "") { + const profileMap = { + "power-saver": PowerProfile.PowerSaver, + "balanced": PowerProfile.Balanced, + "performance": PowerProfile.Performance + }; + if (profileMap[unpluggedConfig.setPowerProfile] !== undefined) { + PowerProfiles.profile = profileMap[unpluggedConfig.setPowerProfile]; + } + } + + const settings = { + disableAnimations: unpluggedConfig.disableAnimations, + disableBlur: unpluggedConfig.disableBlur, + disableRounding: unpluggedConfig.disableRounding, + disableShadows: unpluggedConfig.disableShadows, + setRefreshRate: (unpluggedConfig.setRefreshRate && unpluggedConfig.setRefreshRate !== "") ? unpluggedConfig.setRefreshRate : null + }; + root.applyPowerSavingSettings(settings); + root.settingsModified = true; + } + + if (unpluggedConfig.evaluateThresholds) { + root.evaluateThresholds(); + } + } + + function evaluateThresholds() { + if (!UPower.onBattery || !root.powerManagementEnabled) + return; + + const p = UPower.displayDevice.percentage * 100; + + let targetThresholdIndex = -1; + for (let i = 0; i < root.powerThresholds.length; i++) { + if (p <= root.powerThresholds[i].level) { + targetThresholdIndex = i; + break; + } + } + + if (targetThresholdIndex !== root.currentThresholdIndex) { + root.currentThresholdIndex = targetThresholdIndex; + + if (targetThresholdIndex >= 0) { + root.applyThreshold(root.powerThresholds[targetThresholdIndex]); + } + } + } + + function applyThreshold(threshold) { + if (!root.settingsModified) { + root.saveOriginalSettings(); + } + + if (threshold.setPowerProfile && threshold.setPowerProfile !== "") { + root.setPowerProfile(threshold.setPowerProfile); + } + + root.applyPowerSavingSettings(threshold); + + root.settingsModified = true; + + const actions = []; + if (threshold.setPowerProfile && threshold.setPowerProfile !== "") actions.push("profile: " + threshold.setPowerProfile); + if (threshold.setRefreshRate && threshold.setRefreshRate !== "") actions.push(threshold.setRefreshRate + "Hz"); + if (threshold.disableAnimations === "disable") actions.push("no animations"); + if (threshold.disableBlur === "disable") actions.push("no blur"); + + if (Config.utilities.toasts.lowPowerModeChanged && root.initialized) { + Toaster.toast( + qsTr("Battery saving active"), + qsTr("Applied: ") + actions.join(", "), + "battery_saver" + ); + } + } + + function applyPowerSavingSettings(settings) { + root.applyVisualEffects(settings); + + if (settings.setRefreshRate !== null && settings.setRefreshRate !== undefined && settings.setRefreshRate !== "") { + root.applyRefreshRate(settings.setRefreshRate); + } + } + + function saveOriginalSettings() { + const monitors = Hypr.monitors.values || Object.values(Hypr.monitors); + for (const monitor of monitors) { + const data = monitor.lastIpcObject; + if (data) { + root.originalSettings.refreshRates[data.name] = data.refreshRate; + } + } + } + + function setPowerProfile(profileName) { + const profileMap = { + "power-saver": PowerProfile.PowerSaver, + "balanced": PowerProfile.Balanced, + "performance": PowerProfile.Performance + }; + if (profileMap[profileName] !== undefined) { + PowerProfiles.profile = profileMap[profileName]; + } + } + + function handleChargingState() { + const config = Config.general.battery.powerManagement.onCharging; + + if (config.setPowerProfile === "restore") { + PowerProfiles.profile = PowerProfile.Balanced; + } else if (config.setPowerProfile && config.setPowerProfile !== "") { + root.setPowerProfile(config.setPowerProfile); + } + + if (config.setRefreshRate && config.setRefreshRate !== "" && config.setRefreshRate !== "unchanged") { + root.applyRefreshRate(config.setRefreshRate); + } + + root.applyVisualEffects(config); + + root.settingsModified = false; + root.currentThresholdIndex = -1; + } + + function restoreSettings() { + Hypr.extras.message("reload"); + } + + function restoreRefreshRates() { + const monitors = Hypr.monitors.values || Object.values(Hypr.monitors); + for (const monitor of monitors) { + const data = monitor.lastIpcObject; + if (data && root.originalSettings.refreshRates[data.name]) { + const originalRate = root.originalSettings.refreshRates[data.name]; + Hypr.extras.message(`keyword monitor ${data.name},${data.width}x${data.height}@${originalRate},${data.x}x${data.y},${data.scale}`); } } } diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml index ca48551fc..3096a4ee2 100644 --- a/modules/controlcenter/PaneRegistry.qml +++ b/modules/controlcenter/PaneRegistry.qml @@ -1,55 +1,80 @@ pragma Singleton import QtQuick +import Quickshell.Services.UPower QtObject { id: root - readonly property list panes: [ + readonly property list allPanes: [ QtObject { readonly property string id: "network" readonly property string label: "network" readonly property string icon: "router" readonly property string component: "network/NetworkingPane.qml" + readonly property bool visible: true }, QtObject { readonly property string id: "bluetooth" readonly property string label: "bluetooth" readonly property string icon: "settings_bluetooth" readonly property string component: "bluetooth/BtPane.qml" + readonly property bool visible: true }, QtObject { readonly property string id: "audio" readonly property string label: "audio" readonly property string icon: "volume_up" readonly property string component: "audio/AudioPane.qml" + readonly property bool visible: true + }, + QtObject { + readonly property string id: "power" + readonly property string label: "power" + readonly property string icon: "battery_charging_full" + readonly property string component: "battery/BatteryPane.qml" + readonly property bool visible: UPower.displayDevice?.isLaptopBattery ?? false }, QtObject { readonly property string id: "appearance" readonly property string label: "appearance" readonly property string icon: "palette" readonly property string component: "appearance/AppearancePane.qml" + readonly property bool visible: true }, QtObject { readonly property string id: "taskbar" readonly property string label: "taskbar" readonly property string icon: "task_alt" readonly property string component: "taskbar/TaskbarPane.qml" + readonly property bool visible: true }, QtObject { readonly property string id: "launcher" readonly property string label: "launcher" readonly property string icon: "apps" readonly property string component: "launcher/LauncherPane.qml" + readonly property bool visible: true }, QtObject { readonly property string id: "dashboard" readonly property string label: "dashboard" readonly property string icon: "dashboard" readonly property string component: "dashboard/DashboardPane.qml" + readonly property bool visible: true } ] + readonly property var panes: { + const result = []; + for (let i = 0; i < allPanes.length; i++) { + if (allPanes[i].visible !== false) { + result.push(allPanes[i]); + } + } + return result; + } + readonly property int count: panes.length readonly property var labels: { diff --git a/modules/controlcenter/battery/BatteryPane.qml b/modules/controlcenter/battery/BatteryPane.qml new file mode 100644 index 000000000..b26a0fa7d --- /dev/null +++ b/modules/controlcenter/battery/BatteryPane.qml @@ -0,0 +1,133 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import "." as Power +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import qs.utils +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + + property bool enabled: Config.general.battery.powerManagement.enabled ?? false + + anchors.fill: parent + + function saveConfig() { + Config.general.battery.powerManagement.enabled = root.enabled; + Config.save(); + } + + ClippingRectangle { + id: powerClippingRect + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal + + radius: powerBorder.innerRadius + color: "transparent" + + Loader { + id: powerLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + + sourceComponent: powerContentComponent + } + } + + InnerBorder { + id: powerBorder + leftThickness: 0 + rightThickness: Appearance.padding.normal + } + + Component { + id: powerContentComponent + + StyledFlickable { + id: powerFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: powerLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: powerFlickable + } + + ColumnLayout { + id: powerLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + spacing: Appearance.spacing.normal + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Power Management") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SwitchRow { + label: qsTr("Enable power management") + checked: root.enabled + onToggled: checked => { + root.enabled = checked; + root.saveConfig(); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + Power.ChargingBehaviorSection { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 0 + Layout.alignment: Qt.AlignTop + rootItem: root + } + + Power.ThresholdsSection { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 0 + Layout.maximumWidth: parent.width * 0.4 + Layout.alignment: Qt.AlignTop + rootItem: root + } + } + + Power.ProfileBehaviorsSection { + Layout.fillWidth: true + rootItem: root + } + } + } + } +} diff --git a/modules/controlcenter/battery/ChargingBehaviorSection.qml b/modules/controlcenter/battery/ChargingBehaviorSection.qml new file mode 100644 index 000000000..4c11335ad --- /dev/null +++ b/modules/controlcenter/battery/ChargingBehaviorSection.qml @@ -0,0 +1,192 @@ +import ".." +import "../components" +import "." as Power +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +SectionContainer { + id: root + + required property var rootItem + + Layout.fillWidth: true + Layout.fillHeight: true + alignTop: true + + Behavior on implicitHeight { + Anim {} + } + + StyledText { + text: qsTr("Charging & Unplugged Behavior") + font.pointSize: Appearance.font.size.normal + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + // Charging behavior column + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("When Plugged In") + font.pointSize: Appearance.font.size.normal + } + + Power.PowerProfileSelector { + Layout.fillWidth: true + value: Config.general.battery.powerManagement.onCharging.setPowerProfile + showRestore: true + showUnchanged: true + onProfileChanged: newValue => { + Config.general.battery.powerManagement.onCharging.setPowerProfile = newValue; + Config.save(); + } + } + + Power.RefreshRateSelector { + Layout.fillWidth: true + value: Config.general.battery.powerManagement.onCharging.setRefreshRate + showRestore: true + showUnchanged: true + onRateChanged: newValue => { + Config.general.battery.powerManagement.onCharging.setRefreshRate = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Animations") + value: Config.general.battery.powerManagement.onCharging.disableAnimations + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.onCharging.disableAnimations = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Blur") + value: Config.general.battery.powerManagement.onCharging.disableBlur + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.onCharging.disableBlur = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Rounding") + value: Config.general.battery.powerManagement.onCharging.disableRounding + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.onCharging.disableRounding = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Shadows") + value: Config.general.battery.powerManagement.onCharging.disableShadows + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.onCharging.disableShadows = newValue; + Config.save(); + } + } + } + } + + // Unplugged behavior column + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("When Unplugged") + font.pointSize: Appearance.font.size.normal + } + + Power.PowerProfileSelector { + Layout.fillWidth: true + value: Config.general.battery.powerManagement.onUnplugged.setPowerProfile + showRestore: true + showUnchanged: true + onProfileChanged: newValue => { + Config.general.battery.powerManagement.onUnplugged.setPowerProfile = newValue; + Config.save(); + } + } + + Power.RefreshRateSelector { + Layout.fillWidth: true + value: Config.general.battery.powerManagement.onUnplugged.setRefreshRate + showRestore: true + showUnchanged: true + onRateChanged: newValue => { + Config.general.battery.powerManagement.onUnplugged.setRefreshRate = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Animations") + value: Config.general.battery.powerManagement.onUnplugged.disableAnimations + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.onUnplugged.disableAnimations = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Blur") + value: Config.general.battery.powerManagement.onUnplugged.disableBlur + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.onUnplugged.disableBlur = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Rounding") + value: Config.general.battery.powerManagement.onUnplugged.disableRounding + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.onUnplugged.disableRounding = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Shadows") + value: Config.general.battery.powerManagement.onUnplugged.disableShadows + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.onUnplugged.disableShadows = newValue; + Config.save(); + } + } + + SwitchRow { + label: qsTr("Evaluate battery thresholds") + checked: Config.general.battery.powerManagement.onUnplugged.evaluateThresholds + onToggled: checked => { + Config.general.battery.powerManagement.onUnplugged.evaluateThresholds = checked; + Config.save(); + } + } + } + } + } +} diff --git a/modules/controlcenter/battery/PowerProfileSelector.qml b/modules/controlcenter/battery/PowerProfileSelector.qml new file mode 100644 index 000000000..0ccc5142d --- /dev/null +++ b/modules/controlcenter/battery/PowerProfileSelector.qml @@ -0,0 +1,94 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +SplitButtonRow { + id: root + + required property string value + property bool showRestore: false + property bool showUnchanged: false + signal profileChanged(string newValue) + + label: qsTr("Power profile") + + menuItems: { + const items = []; + + if (showRestore) { + items.push(Qt.createQmlObject( + `import qs.components.controls; MenuItem { + text: "Restore" + icon: "refresh" + property string val: "restore" + }`, + root, + "restoreMenuItem" + )); + } + + if (showUnchanged) { + items.push(Qt.createQmlObject( + `import qs.components.controls; MenuItem { + text: "Unchanged" + icon: "block" + property string val: "" + }`, + root, + "unchangedMenuItem" + )); + } + + items.push(Qt.createQmlObject( + `import qs.components.controls; MenuItem { + text: "Power Saver" + icon: "battery_saver" + property string val: "power-saver" + }`, + root, + "powerSaverMenuItem" + )); + + items.push(Qt.createQmlObject( + `import qs.components.controls; MenuItem { + text: "Balanced" + icon: "balance" + property string val: "balanced" + }`, + root, + "balancedMenuItem" + )); + + items.push(Qt.createQmlObject( + `import qs.components.controls; MenuItem { + text: "Performance" + icon: "speed" + property string val: "performance" + }`, + root, + "performanceMenuItem" + )); + + return items; + } + + Component.onCompleted: { + for (let i = 0; i < menuItems.length; i++) { + if (menuItems[i].val === root.value) { + active = menuItems[i]; + break; + } + } + } + + onSelected: item => { + root.profileChanged(item.val); + } +} diff --git a/modules/controlcenter/battery/ProfileBehaviorsSection.qml b/modules/controlcenter/battery/ProfileBehaviorsSection.qml new file mode 100644 index 000000000..d81168117 --- /dev/null +++ b/modules/controlcenter/battery/ProfileBehaviorsSection.qml @@ -0,0 +1,255 @@ +import ".." +import "../components" +import "." as Power +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts + +SectionContainer { + id: root + + required property var rootItem + + Layout.fillWidth: true + alignTop: true + + property var availableRefreshRates: { + const rates = ["" /* unchanged */]; + const monitors = Hypr.monitors.values || Object.values(Hypr.monitors); + const uniqueRates = new Set(); + + for (const monitor of monitors) { + const data = monitor.lastIpcObject; + if (data && data.availableModes) { + for (const mode of data.availableModes) { + const match = mode.match(/@(\d+(?:\.\d+)?)Hz/); + if (match) { + const rate = Math.round(parseFloat(match[1])); + uniqueRates.add(rate); + } + } + } + } + + const sortedRates = Array.from(uniqueRates).sort((a, b) => a - b); + for (const rate of sortedRates) { + rates.push(rate.toString()); + } + rates.push("auto"); + return rates; + } + + StyledText { + text: qsTr("Power Profile Behaviors") + font.pointSize: Appearance.font.size.normal + } + + StyledText { + text: qsTr("Define what Hyprland settings to apply when each power profile is active") + font.pointSize: Appearance.font.size.smaller + opacity: 0.7 + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + // Power Saver Profile Column + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Power Saver") + font.pointSize: Appearance.font.size.normal + } + + Power.RefreshRateSelector { + Layout.fillWidth: true + value: Config.general.battery.powerManagement.profileBehaviors.powerSaver.setRefreshRate + showRestore: true + showUnchanged: true + onRateChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.powerSaver.setRefreshRate = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Animations") + value: Config.general.battery.powerManagement.profileBehaviors.powerSaver.disableAnimations + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.powerSaver.disableAnimations = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Blur") + value: Config.general.battery.powerManagement.profileBehaviors.powerSaver.disableBlur + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.powerSaver.disableBlur = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Rounding") + value: Config.general.battery.powerManagement.profileBehaviors.powerSaver.disableRounding + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.powerSaver.disableRounding = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Shadows") + value: Config.general.battery.powerManagement.profileBehaviors.powerSaver.disableShadows + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.powerSaver.disableShadows = newValue; + Config.save(); + } + } + } + } + + // Balanced Profile Column + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Balanced") + font.pointSize: Appearance.font.size.normal + } + + Power.RefreshRateSelector { + Layout.fillWidth: true + value: Config.general.battery.powerManagement.profileBehaviors.balanced.setRefreshRate + showRestore: true + showUnchanged: true + onRateChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.balanced.setRefreshRate = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Animations") + value: Config.general.battery.powerManagement.profileBehaviors.balanced.disableAnimations + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.balanced.disableAnimations = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Blur") + value: Config.general.battery.powerManagement.profileBehaviors.balanced.disableBlur + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.balanced.disableBlur = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Rounding") + value: Config.general.battery.powerManagement.profileBehaviors.balanced.disableRounding + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.balanced.disableRounding = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Shadows") + value: Config.general.battery.powerManagement.profileBehaviors.balanced.disableShadows + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.balanced.disableShadows = newValue; + Config.save(); + } + } + } + } + + // Performance Profile Column + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Performance") + font.pointSize: Appearance.font.size.normal + } + + Power.RefreshRateSelector { + Layout.fillWidth: true + value: Config.general.battery.powerManagement.profileBehaviors.performance.setRefreshRate + showRestore: true + showUnchanged: true + onRateChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.performance.setRefreshRate = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Animations") + value: Config.general.battery.powerManagement.profileBehaviors.performance.disableAnimations + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.performance.disableAnimations = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Blur") + value: Config.general.battery.powerManagement.profileBehaviors.performance.disableBlur + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.performance.disableBlur = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Rounding") + value: Config.general.battery.powerManagement.profileBehaviors.performance.disableRounding + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.performance.disableRounding = newValue; + Config.save(); + } + } + + TriStateRow { + label: qsTr("Shadows") + value: Config.general.battery.powerManagement.profileBehaviors.performance.disableShadows + onTriStateValueChanged: newValue => { + Config.general.battery.powerManagement.profileBehaviors.performance.disableShadows = newValue; + Config.save(); + } + } + } + } + } +} diff --git a/modules/controlcenter/battery/RefreshRateSelector.qml b/modules/controlcenter/battery/RefreshRateSelector.qml new file mode 100644 index 000000000..9cba48402 --- /dev/null +++ b/modules/controlcenter/battery/RefreshRateSelector.qml @@ -0,0 +1,85 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts + +SplitButtonRow { + id: root + + required property string value + property bool showRestore: false + property bool showUnchanged: false + signal rateChanged(string newValue) + + label: qsTr("Refresh rate") + + property var availableRates: { + const rates = []; + + if (showRestore) { + rates.push({ value: "restore", label: qsTr("Restore"), icon: "refresh" }); + } + if (showUnchanged) { + rates.push({ value: "", label: qsTr("Unchanged"), icon: "block" }); + } + + const monitors = Hypr.monitors.values || Object.values(Hypr.monitors); + const uniqueRates = new Set(); + + for (const monitor of monitors) { + const data = monitor.lastIpcObject; + if (data && data.availableModes) { + for (const mode of data.availableModes) { + const match = mode.match(/@(\d+(?:\.\d+)?)Hz/); + if (match) { + const rate = Math.round(parseFloat(match[1])); + uniqueRates.add(rate); + } + } + } + } + + const sortedRates = Array.from(uniqueRates).sort((a, b) => a - b); + for (const rate of sortedRates) { + rates.push({ value: rate.toString(), label: rate + " Hz", icon: "speed" }); + } + rates.push({ value: "auto", label: qsTr("Auto (lowest)"), icon: "battery_saver" }); + return rates; + } + + menuItems: { + const items = []; + for (const rate of availableRates) { + items.push(Qt.createQmlObject( + `import qs.components.controls; MenuItem { + text: "${rate.label}" + icon: "${rate.icon}" + property string val: "${rate.value}" + }`, + root, + "dynamicMenuItem" + )); + } + return items; + } + + Component.onCompleted: { + for (let i = 0; i < menuItems.length; i++) { + if (menuItems[i].val === root.value) { + active = menuItems[i]; + break; + } + } + } + + onSelected: item => { + root.rateChanged(item.val); + } +} diff --git a/modules/controlcenter/battery/ThresholdCard.qml b/modules/controlcenter/battery/ThresholdCard.qml new file mode 100644 index 000000000..a9951d825 --- /dev/null +++ b/modules/controlcenter/battery/ThresholdCard.qml @@ -0,0 +1,149 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +SectionContainer { + id: root + + required property int thresholdIndex + required property var thresholdData + signal removeRequested() + signal thresholdChanged(var newData) + + Layout.fillWidth: true + alignTop: true + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Battery Level: ") + thresholdData.level + "%" + font.pointSize: Appearance.font.size.normal + font.weight: 500 + Layout.fillWidth: true + } + + IconButton { + icon: "delete" + onClicked: root.removeRequested() + } + } + + // Battery level slider + SectionContainer { + contentSpacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Trigger at battery level") + font.pointSize: Appearance.font.size.smaller + } + + RowLayout { + spacing: Appearance.spacing.normal + + StyledSlider { + Layout.fillWidth: true + from: 5 + to: 95 + stepSize: 5 + value: thresholdData.level + onMoved: { + const newData = Object.assign({}, thresholdData); + newData.level = Math.round(value); + root.thresholdChanged(newData); + } + } + + StyledText { + text: Math.round(thresholdData.level) + "%" + font.pointSize: Appearance.font.size.normal + Layout.preferredWidth: 40 + } + } + } + + // Power profile selector + SectionContainer { + contentSpacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Power profile") + font.pointSize: Appearance.font.size.smaller + } + + Flow { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Repeater { + model: [ + { value: null, label: qsTr("No change") }, + { value: "power-saver", label: qsTr("Power Saver") }, + { value: "balanced", label: qsTr("Balanced") }, + { value: "performance", label: qsTr("Performance") } + ] + + delegate: ToggleButton { + required property var modelData + + text: modelData.label + checked: thresholdData.setPowerProfile === modelData.value + onToggled: { + const newData = Object.assign({}, thresholdData); + newData.setPowerProfile = modelData.value; + root.thresholdChanged(newData); + } + } + } + } + } + + // Effects toggles + TriStateRow { + label: qsTr("Animations") + value: thresholdData.disableAnimations + onTriStateValueChanged: newValue => { + const newData = Object.assign({}, thresholdData); + newData.disableAnimations = newValue; + root.thresholdChanged(newData); + } + } + + TriStateRow { + label: qsTr("Blur") + value: thresholdData.disableBlur + onTriStateValueChanged: newValue => { + const newData = Object.assign({}, thresholdData); + newData.disableBlur = newValue; + root.thresholdChanged(newData); + } + } + + TriStateRow { + label: qsTr("Rounding") + value: thresholdData.disableRounding + onTriStateValueChanged: newValue => { + const newData = Object.assign({}, thresholdData); + newData.disableRounding = newValue; + root.thresholdChanged(newData); + } + } + + TriStateRow { + label: qsTr("Shadows") + value: thresholdData.disableShadows + onTriStateValueChanged: newValue => { + const newData = Object.assign({}, thresholdData); + newData.disableShadows = newValue; + root.thresholdChanged(newData); + } + } +} diff --git a/modules/controlcenter/battery/ThresholdsSection.qml b/modules/controlcenter/battery/ThresholdsSection.qml new file mode 100644 index 000000000..7b63d294f --- /dev/null +++ b/modules/controlcenter/battery/ThresholdsSection.qml @@ -0,0 +1,374 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import "." as Power +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +SectionContainer { + id: root + + required property var rootItem + + Layout.fillWidth: true + Layout.fillHeight: true + alignTop: true + + Component.onCompleted: { + const configThresholds = Config.general.battery.powerManagement.thresholds || []; + thresholdsModel.clear(); + for (let i = 0; i < configThresholds.length; i++) { + const t = configThresholds[i]; + thresholdsModel.append({ + level: t.level || 40, + setPowerProfile: t.setPowerProfile || "", + setRefreshRate: t.setRefreshRate || "", + disableAnimations: t.disableAnimations || "", + disableBlur: t.disableBlur || "", + disableRounding: t.disableRounding || "", + disableShadows: t.disableShadows || "" + }); + } + } + + function saveThresholds() { + const thresholds = []; + for (let i = 0; i < thresholdsModel.count; i++) { + const item = thresholdsModel.get(i); + thresholds.push({ + level: item.level, + setPowerProfile: item.setPowerProfile, + setRefreshRate: item.setRefreshRate, + disableAnimations: item.disableAnimations, + disableBlur: item.disableBlur, + disableRounding: item.disableRounding, + disableShadows: item.disableShadows + }); + } + Config.general.battery.powerManagement.thresholds = thresholds; + Config.save(); + } + + ListModel { + id: thresholdsModel + } + + StyledText { + text: qsTr("Battery Level Thresholds") + font.pointSize: Appearance.font.size.normal + } + + StyledText { + text: qsTr("Define actions to take at specific battery levels") + font.pointSize: Appearance.font.size.smaller + opacity: 0.7 + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + Behavior on implicitHeight { + Anim {} + } + + // Add threshold button + StyledRect { + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 1) + border.width: 2 + border.color: Qt.alpha(Colours.palette.m3primary, 0.3) + + StateLayer { + function onClicked() { + thresholdsModel.append({ + level: 50, + setPowerProfile: "", + setRefreshRate: "auto", + disableAnimations: "", + disableBlur: "", + disableRounding: "", + disableShadows: "" + }); + root.saveThresholds(); + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "add_circle" + font.pointSize: Appearance.font.size.large + color: Colours.palette.m3primary + } + + StyledText { + text: qsTr("Add Threshold") + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3primary + } + + Item { Layout.fillWidth: true } + } + } + + // Threshold cards + Repeater { + model: thresholdsModel + + delegate: StyledRect { + id: card + + required property int index + required property int level + required property string setPowerProfile + required property string setRefreshRate + required property string disableAnimations + required property string disableBlur + required property string disableRounding + required property string disableShadows + + property bool editing: false + property bool removing: false + + Layout.fillWidth: true + Layout.preferredHeight: editing ? contentColumn.implicitHeight + Appearance.padding.normal * 2 : 60 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + scale: removing ? 0 : 1 + opacity: removing ? 0 : 1 + + Behavior on Layout.preferredHeight { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + } + + SequentialAnimation { + id: removeAnimation + + PropertyAction { + target: card + property: "removing" + value: true + } + PauseAnimation { + duration: Appearance.anim.durations.normal + } + ScriptAction { + script: { + thresholdsModel.remove(card.index); + root.saveThresholds(); + } + } + } + + ColumnLayout { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: card.level + "% Battery" + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + TextButton { + text: card.editing ? qsTr("Done") : qsTr("Edit") + onClicked: card.editing = !card.editing + } + + IconButton { + icon: "delete" + onClicked: removeAnimation.start() + } + } + + Loader { + Layout.fillWidth: true + active: true + asynchronous: false + visible: opacity > 0 + opacity: card.editing ? 1 : 0 + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + sourceComponent: ColumnLayout { + spacing: Appearance.spacing.normal + + StyledRect { + Layout.fillWidth: true + implicitHeight: batteryLevelRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: batteryLevelRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Battery Level") + } + + CustomSpinBox { + min: 5 + max: 95 + step: 1 + value: card.level + onValueModified: newValue => { + if (newValue !== card.level) { + thresholdsModel.setProperty(card.index, "level", Math.round(newValue)); + root.saveThresholds(); + } + } + } + } + } + + Power.PowerProfileSelector { + Layout.fillWidth: true + label: qsTr("Power Profile") + value: card.setPowerProfile + onProfileChanged: newValue => { + thresholdsModel.setProperty(card.index, "setPowerProfile", newValue); + root.saveThresholds(); + } + } + + Power.RefreshRateSelector { + Layout.fillWidth: true + label: qsTr("Refresh Rate") + showUnchanged: true + value: card.setRefreshRate + onRateChanged: newValue => { + thresholdsModel.setProperty(card.index, "setRefreshRate", newValue); + root.saveThresholds(); + } + } + + TriStateRow { + Layout.fillWidth: true + label: qsTr("Animations") + value: card.disableAnimations + onTriStateValueChanged: newValue => { + thresholdsModel.setProperty(card.index, "disableAnimations", newValue); + root.saveThresholds(); + } + } + + TriStateRow { + Layout.fillWidth: true + label: qsTr("Blur") + value: card.disableBlur + onTriStateValueChanged: newValue => { + thresholdsModel.setProperty(card.index, "disableBlur", newValue); + root.saveThresholds(); + } + } + + TriStateRow { + Layout.fillWidth: true + label: qsTr("Rounding") + value: card.disableRounding + onTriStateValueChanged: newValue => { + thresholdsModel.setProperty(card.index, "disableRounding", newValue); + root.saveThresholds(); + } + } + + TriStateRow { + Layout.fillWidth: true + label: qsTr("Shadows") + value: card.disableShadows + onTriStateValueChanged: newValue => { + thresholdsModel.setProperty(card.index, "disableShadows", newValue); + root.saveThresholds(); + } + } + } + } + } + + Component.onCompleted: { + scale = 0; + opacity = 0; + scaleInAnimation.start(); + } + + ParallelAnimation { + id: scaleInAnimation + + Anim { + target: card + property: "scale" + from: 0.7 + to: 1 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: card + property: "opacity" + from: 0 + to: 1 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + } + } + } +} diff --git a/modules/controlcenter/components/TriStateRow.qml b/modules/controlcenter/components/TriStateRow.qml new file mode 100644 index 000000000..75cfadba5 --- /dev/null +++ b/modules/controlcenter/components/TriStateRow.qml @@ -0,0 +1,254 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes + +StyledRect { + id: root + + required property string label + property string value: "" + signal triStateValueChanged(string newValue) + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + Item { + id: toggle + + property bool hovered: mouseArea.containsMouse + property bool pressed: mouseArea.pressed + property int state: root.value === "enable" ? 2 : root.value === "" ? 1 : 0 + + implicitWidth: implicitHeight * 2.2 + implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 + + Rectangle { + id: track + anchors.fill: parent + radius: height / 2 + color: toggle.state === 2 ? Colours.palette.m3primary : + toggle.state === 0 ? Colours.palette.m3error : + Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Rectangle { + id: thumb + readonly property real nonAnimWidth: toggle.pressed ? height * 1.3 : height + readonly property real thumbPadding: Appearance.padding.small / 2 + readonly property real availableWidth: parent.width - (thumbPadding * 2) - height + readonly property real leftPos: thumbPadding + readonly property real centerPos: thumbPadding + (availableWidth / 2) + readonly property real rightPos: thumbPadding + availableWidth + + radius: height / 2 + color: toggle.state === 2 ? Colours.palette.m3onPrimary : + toggle.state === 0 ? Colours.palette.m3onError : + Colours.layer(Colours.palette.m3outline, 2) + + x: toggle.state === 2 ? rightPos : toggle.state === 1 ? centerPos : leftPos + width: nonAnimWidth + height: parent.height - Appearance.padding.small + anchors.verticalCenter: parent.verticalCenter + + Behavior on x { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Behavior on width { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: toggle.state === 2 ? Colours.palette.m3primary : Colours.palette.m3onSurface + opacity: toggle.pressed ? 0.1 : toggle.hovered ? 0.08 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + Shape { + id: icon + + property point start1: { + if (toggle.pressed) + return Qt.point(width * 0.2, height / 2); + if (toggle.state === 2) + return Qt.point(width * 0.85, height / 2); + if (toggle.state === 1) + return Qt.point(width * 0.2, height / 2); + return Qt.point(width * 0.15, height * 0.15); + } + property point end1: { + if (toggle.pressed) { + if (toggle.state === 2) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.8, height / 2); + } + if (toggle.state === 2) + return Qt.point(width * 0.6, height * 0.3); + if (toggle.state === 1) + return Qt.point(width * 0.8, height / 2); + return Qt.point(width * 0.85, height * 0.85); + } + property point start2: { + if (toggle.pressed) { + if (toggle.state === 2) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.2, height / 2); + } + if (toggle.state === 2) + return Qt.point(width * 0.6, height * 0.3); + if (toggle.state === 1) + return Qt.point(width * 0.2, height / 2); + return Qt.point(width * 0.15, height * 0.85); + } + property point end2: { + if (toggle.pressed) + return Qt.point(width * 0.8, height / 2); + if (toggle.state === 2) + return Qt.point(width * 0.15, height * 0.8); + if (toggle.state === 1) + return Qt.point(width * 0.2, height / 2); + return Qt.point(width * 0.85, height * 0.15); + } + + anchors.centerIn: parent + width: height + height: parent.implicitHeight - Appearance.padding.small * 2 + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + strokeWidth: Appearance.font.size.larger * 0.15 + strokeColor: toggle.state === 2 ? Colours.palette.m3primary : + toggle.state === 0 ? Colours.palette.m3error : + Colours.palette.m3surfaceContainerHighest + fillColor: "transparent" + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + startX: icon.start1.x + startY: icon.start1.y + + PathLine { + x: icon.end1.x + y: icon.end1.y + } + PathMove { + x: icon.start2.x + y: icon.start2.y + } + PathLine { + x: icon.end2.x + y: icon.end2.y + } + + Behavior on strokeColor { + CAnim {} + } + } + + Behavior on start1 { + PropertyAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + Behavior on end1 { + PropertyAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + Behavior on start2 { + PropertyAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + Behavior on end2 { + PropertyAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.value === "") { + root.triStateValueChanged("enable"); + } else if (root.value === "enable") { + root.triStateValueChanged("disable"); + } else { + root.triStateValueChanged(""); + } + } + } + } + } +} diff --git a/services/GameMode.qml b/services/GameMode.qml index 83770b79f..99cdf21e6 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -40,7 +40,7 @@ Singleton { PersistentProperties { id: props - property bool enabled: Hypr.options["animations:enabled"] === 0 + property bool enabled: false reloadableId: "gameMode" }