Files
noctalia-shell/Modules/Panels/Battery/BatteryPanel.qml
T
2026-02-20 21:11:06 -05:00

402 lines
15 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Modules.MainScreen
import qs.Services.Hardware
import qs.Services.Networking
import qs.Services.Power
import qs.Services.UI
import qs.Widgets
SmartPanel {
id: root
preferredWidth: Math.round(440 * Style.uiScaleRatio)
preferredHeight: Math.round(460 * Style.uiScaleRatio)
panelContent: Item {
id: panelContent
property real contentPreferredHeight: mainLayout.implicitHeight + Style.margin2L
property var batteryWidgetInstance: BarService.lookupWidget("Battery", screen ? screen.name : null)
readonly property var batteryWidgetSettings: batteryWidgetInstance ? batteryWidgetInstance.widgetSettings : null
readonly property var batteryWidgetMetadata: BarWidgetRegistry.widgetMetadata["Battery"]
readonly property bool powerProfileAvailable: PowerProfileService.available
readonly property var powerProfiles: [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
readonly property bool profilesAvailable: PowerProfileService.available
property int profileIndex: profileToIndex(PowerProfileService.profile)
readonly property bool showPowerProfiles: panelID ? panelID.showPowerProfiles : resolveWidgetSetting("showPowerProfiles", false)
readonly property bool showNoctaliaPerformance: panelID ? panelID.showNoctaliaPerformance : resolveWidgetSetting("showNoctaliaPerformance", false)
readonly property bool isLowBattery: BatteryService.isLowBattery
readonly property bool isCriticalBattery: BatteryService.isCriticalBattery
readonly property var primaryDevice: BatteryService.primaryDevice
function profileToIndex(p) {
return powerProfiles.indexOf(p) ?? 1;
}
function indexToProfile(idx) {
return powerProfiles[idx] ?? PowerProfile.Balanced;
}
function setProfileByIndex(idx) {
var prof = indexToProfile(idx);
profileIndex = idx;
PowerProfileService.setProfile(prof);
}
function resolveWidgetSetting(key, defaultValue) {
if (batteryWidgetSettings && batteryWidgetSettings[key] !== undefined)
return batteryWidgetSettings[key];
if (batteryWidgetMetadata && batteryWidgetMetadata[key] !== undefined)
return batteryWidgetMetadata[key];
return defaultValue;
}
Connections {
target: PowerProfileService
function onProfileChanged() {
panelContent.profileIndex = panelContent.profileToIndex(PowerProfileService.profile);
}
}
Connections {
target: BarService
function onActiveWidgetsChanged() {
panelContent.batteryWidgetInstance = BarService.lookupWidget("Battery", screen ? screen.name : null);
}
}
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// HEADER
NBox {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + Style.margin2M
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
pointSize: Style.fontSizeXXL
color: (BatteryService.isCharging(primaryDevice) || BatteryService.isPluggedIn(primaryDevice)) ? Color.mPrimary : (BatteryService.isCriticalBattery(primaryDevice) || BatteryService.isLowBattery(primaryDevice)) ? Color.mError : Color.mOnSurface
icon: BatteryService.getIcon(BatteryService.getPercentage(primaryDevice), BatteryService.isCharging(primaryDevice), BatteryService.isPluggedIn(primaryDevice), BatteryService.isDeviceReady(primaryDevice))
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NText {
text: I18n.tr("common.battery")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
elide: Text.ElideRight
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
}
}
}
// Charge level + health/time
NBox {
Layout.fillWidth: true
implicitHeight: chargeLayout.implicitHeight + Style.margin2L
visible: BatteryService.laptopBatteries.length > 0 || BatteryService.bluetoothBatteries.length > 0
ColumnLayout {
id: chargeLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginL
// Laptop batteries section
Repeater {
model: BatteryService.laptopBatteries
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
RowLayout {
Item {
id: batteryInfoItem
implicitWidth: batteryInfoRow.implicitWidth
implicitHeight: batteryInfoRow.implicitHeight
RowLayout {
id: batteryInfoRow
anchors.fill: parent
NIcon {
icon: BatteryService.getIcon(BatteryService.getPercentage(modelData), BatteryService.isCharging(modelData), BatteryService.isPluggedIn(modelData), BatteryService.isDeviceReady(modelData))
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
}
NText {
readonly property string dName: BatteryService.getDeviceName(modelData)
text: dName ? dName : I18n.tr("common.battery")
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
pointSize: Style.fontSizeS
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
if (modelData.healthSupported) {
TooltipService.show(batteryInfoItem, `${I18n.tr("battery.battery-health")}: ${Math.round(modelData.healthPercentage)}%`);
}
}
onExited: TooltipService.hide(batteryInfoItem)
}
}
Item {
Layout.fillWidth: true
}
NText {
text: BatteryService.getTimeRemainingText(modelData)
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
Rectangle {
Layout.fillWidth: true
height: Math.round(8 * Style.uiScaleRatio)
radius: Math.min(Style.radiusL, height / 2)
color: Color.mSurface
Rectangle {
anchors.verticalCenter: parent.verticalCenter
height: parent.height
radius: parent.radius
width: {
var p = BatteryService.getPercentage(modelData);
var ratio = Math.max(0, Math.min(1, p / 100));
return parent.width * ratio;
}
color: Color.mPrimary
}
}
NText {
Layout.preferredWidth: 40 * Style.uiScaleRatio
horizontalAlignment: Text.AlignRight
text: `${BatteryService.getPercentage(modelData)}%`
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
}
}
}
}
}
}
NDivider {
Layout.fillWidth: true
visible: BatteryService.laptopBatteries.length > 0 && BatteryService.bluetoothBatteries.length > 0
}
// Other devices (Bluetooth) section
Repeater {
model: BatteryService.bluetoothBatteries
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: BluetoothService.getDeviceIcon(modelData)
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
}
NText {
readonly property string dName: BatteryService.getDeviceName(modelData)
text: dName ? dName : I18n.tr("common.bluetooth")
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
pointSize: Style.fontSizeS
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
Rectangle {
Layout.fillWidth: true
height: Math.round(8 * Style.uiScaleRatio)
radius: Math.min(Style.radiusL, height / 2)
color: Color.mSurface
Rectangle {
anchors.verticalCenter: parent.verticalCenter
height: parent.height
radius: parent.radius
width: {
var p = BatteryService.getPercentage(modelData);
var ratio = Math.max(0, Math.min(1, p / 100));
return parent.width * ratio;
}
color: Color.mPrimary
}
}
NText {
Layout.preferredWidth: 40 * Style.uiScaleRatio
horizontalAlignment: Text.AlignRight
text: `${BatteryService.getPercentage(modelData)}%`
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
}
}
}
}
}
}
NBox {
Layout.fillWidth: true
height: controlsLayout.implicitHeight + Style.margin2L
visible: showPowerProfiles || showNoctaliaPerformance
ColumnLayout {
id: controlsLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
ColumnLayout {
visible: powerProfileAvailable && showPowerProfiles
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("battery.power-profile")
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NText {
text: PowerProfileService.getName(profileIndex)
color: Color.mOnSurfaceVariant
}
}
NValueSlider {
Layout.fillWidth: true
from: 0
to: 2
stepSize: 1
snapAlways: true
heightRatio: 0.5
value: profileIndex
enabled: profilesAvailable
onPressedChanged: (pressed, v) => {
if (!pressed) {
setProfileByIndex(v);
}
}
onMoved: v => {
profileIndex = v;
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: "powersaver"
pointSize: Style.fontSizeS
color: PowerProfileService.getIcon() === "powersaver" ? Color.mPrimary : Color.mOnSurfaceVariant
}
NIcon {
icon: "balanced"
pointSize: Style.fontSizeS
color: PowerProfileService.getIcon() === "balanced" ? Color.mPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
}
NIcon {
icon: "performance"
pointSize: Style.fontSizeS
color: PowerProfileService.getIcon() === "performance" ? Color.mPrimary : Color.mOnSurfaceVariant
}
}
}
NDivider {
Layout.fillWidth: true
visible: showPowerProfiles && PowerProfileService.available && showNoctaliaPerformance
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
visible: showNoctaliaPerformance
NText {
text: I18n.tr("toast.noctalia-performance.label")
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIcon {
icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off"
pointSize: Style.fontSizeL
color: PowerProfileService.noctaliaPerformanceMode ? Color.mPrimary : Color.mOnSurfaceVariant
}
NToggle {
checked: PowerProfileService.noctaliaPerformanceMode
onToggled: checked => PowerProfileService.noctaliaPerformanceMode = checked
}
}
}
}
}
}
}