1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 03:28:28 -04:00
Files
DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml
T

491 lines
19 KiB
QML

import QtQuick
import QtQuick.Shapes
import Quickshell.Services.UPower
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: battery
readonly property var log: Log.scoped("Battery")
property bool batteryPopupVisible: false
property var popoutTarget: null
property var widgetData: null
readonly property bool showPercentOnlyOnBattery: widgetData?.showBatteryPercentOnlyOnBattery !== undefined ? widgetData.showBatteryPercentOnlyOnBattery : SettingsData.showBatteryPercentOnlyOnBattery
readonly property bool showPercent: {
const base = widgetData?.showBatteryPercent !== undefined ? widgetData.showBatteryPercent : SettingsData.showBatteryPercent;
return base && !(showPercentOnlyOnBattery && BatteryService.isPluggedIn);
}
readonly property bool showTime: widgetData?.showBatteryTime !== undefined ? widgetData.showBatteryTime : SettingsData.showBatteryTime
readonly property bool showTimeOnlyOnBattery: widgetData?.showBatteryTimeOnlyOnBattery !== undefined ? widgetData.showBatteryTimeOnlyOnBattery : SettingsData.showBatteryTimeOnlyOnBattery
readonly property bool pillStyle: widgetData?.batteryPillStyle !== undefined ? widgetData.batteryPillStyle : SettingsData.batteryPillStyle
readonly property bool pillPercentSign: widgetData?.batteryPillPercentSign !== undefined ? widgetData.batteryPillPercentSign : SettingsData.batteryPillPercentSign
readonly property string batteryTimeText: {
if (showTimeOnlyOnBattery && BatteryService.isPluggedIn) {
return "";
}
const time = BatteryService.formatTimeRemaining();
return time !== "Unknown" ? time : "";
}
readonly property string verticalBatteryTimeText: {
if (!batteryTimeText)
return "";
// Parse batteryTimeText, e.g., "2h 41m" or "41m"
let hours = 0;
let minutes = 0;
const hourMatch = batteryTimeText.match(/(\d+)h/);
const minMatch = batteryTimeText.match(/(\d+)m/);
if (hourMatch) {
hours = parseInt(hourMatch[1], 10);
}
if (minMatch) {
minutes = parseInt(minMatch[1], 10);
}
const hoursStr = hours < 10 ? "0" + hours : hours.toString();
const minutesStr = minutes < 10 ? "0" + minutes : minutes.toString();
return `${hoursStr}\n${minutesStr}`;
}
readonly property string horizontalDisplayText: {
if (showPercent && showTime && batteryTimeText) {
return `${BatteryService.batteryLevel}% (${batteryTimeText})`;
}
if (showPercent) {
return `${BatteryService.batteryLevel}%`;
}
if (showTime && batteryTimeText) {
return batteryTimeText;
}
return "";
}
// Percent always stays inside the pill; only the time shows beside it.
readonly property string horizontalSideText: {
if (!pillStyle) {
return horizontalDisplayText;
}
return (showTime && batteryTimeText) ? batteryTimeText : "";
}
readonly property string verticalDisplayText: {
if (showPercent && showTime && batteryTimeText) {
return `${BatteryService.batteryLevel}\n${verticalBatteryTimeText}`;
}
if (showPercent) {
return BatteryService.batteryLevel.toString();
}
if (showTime && batteryTimeText) {
return verticalBatteryTimeText;
}
return "";
}
property real touchpadAccumulator: 0
readonly property int barPosition: {
switch (axis?.edge) {
case "top":
return 0;
case "bottom":
return 1;
case "left":
return 2;
case "right":
return 3;
default:
return 0;
}
}
signal toggleBatteryPopup
visible: true
// AOSP's battery bolt glyph (config_batterymeterBoltPath), solid-filled and
// cropped tight to its own ink so it reads bold at normal icon size instead
// of needing to be inflated past the bar's calibrated icon height.
component OfficialBolt: Shape {
id: officialBolt
property color fillColor: Theme.surfaceText
property real size: 16
implicitWidth: Math.round(officialBolt.size * (6 / 13))
implicitHeight: Math.round(officialBolt.size)
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: officialBolt.fillColor
strokeColor: "transparent"
startX: officialBolt.width * (1 / 3)
startY: officialBolt.height
PathLine {
x: officialBolt.width * (1 / 3)
y: officialBolt.height * (7.5 / 13)
}
PathLine {
x: 0
y: officialBolt.height * (7.5 / 13)
}
PathLine {
x: officialBolt.width * (2 / 3)
y: 0
}
PathLine {
x: officialBolt.width * (2 / 3)
y: officialBolt.height * (5.5 / 13)
}
PathLine {
x: officialBolt.width
y: officialBolt.height * (5.5 / 13)
}
PathLine {
x: officialBolt.width * (1 / 3)
y: officialBolt.height
}
}
}
// Material 3 style horizontal/vertical battery pill with the level inside
component BatteryPill: Item {
id: pill
property real thickness: 18
property bool vertical: false
property bool showNumber: true
property bool showPercentSign: false
readonly property int signSize: Math.max(1, Math.round(pill.glyphSize * 0.72))
readonly property real bodyLength: Math.round(pill.thickness * 1.95)
readonly property real level: Math.max(0, Math.min(100, BatteryService.batteryLevel))
readonly property bool charging: BatteryService.isCharging
readonly property bool lowState: BatteryService.isLowBattery && !BatteryService.isCharging
readonly property color fillColor: {
if (!BatteryService.batteryAvailable)
return Theme.surfaceVariant;
if (pill.lowState)
return Theme.error;
return Theme.primary;
}
readonly property color onFillColor: {
const c = pill.fillColor;
const lum = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
return lum > 0.5 ? Qt.rgba(0, 0, 0, 0.9) : Qt.rgba(1, 1, 1, 0.95);
}
readonly property string numberText: Math.round(pill.level).toString()
readonly property int glyphSize: Math.round(pill.thickness * 0.58)
readonly property int boltSize: Math.round(pill.thickness * 0.72)
readonly property real nubBreadth: Math.round(pill.thickness * 0.16)
readonly property real nubSpan: Math.round(pill.thickness * 0.46)
implicitWidth: pill.vertical ? pill.thickness : Math.max(pill.bodyLength, (!pill.vertical && pill.showNumber) ? numRowTrack.width + pill.thickness * 0.7 : 0) + pill.nubBreadth
implicitHeight: pill.vertical ? pill.bodyLength + pill.nubBreadth : pill.thickness
Rectangle {
id: body
x: 0
y: pill.vertical ? pill.nubBreadth : 0
width: pill.vertical ? parent.width : parent.width - pill.nubBreadth
height: pill.vertical ? parent.height - pill.nubBreadth : parent.height
radius: Math.round(Math.min(width, height) * 0.34)
color: Theme.withAlpha(Theme.surfaceVariant, 0.9)
Rectangle {
id: fill
radius: body.radius
color: pill.fillColor
anchors.left: parent.left
anchors.bottom: parent.bottom
width: pill.vertical ? parent.width : Math.round(parent.width * pill.level / 100)
height: pill.vertical ? Math.round(parent.height * pill.level / 100) : parent.height
Behavior on width {
enabled: !pill.vertical
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
enabled: pill.vertical
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
// Tinted for the empty track. Horizontal always shows the number;
// vertical swaps in a centered bolt while charging.
Item {
id: glyphTrack
anchors.fill: parent
visible: BatteryService.batteryAvailable && ((pill.charging && pill.vertical) || (!pill.vertical && pill.showNumber))
Row {
id: numRowTrack
visible: !pill.vertical && pill.showNumber
anchors.centerIn: parent
spacing: 1
StyledText {
id: numTrack
text: pill.numberText
color: Theme.surfaceText
font.pixelSize: pill.glyphSize
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "%"
visible: pill.showPercentSign
color: Theme.surfaceText
font.pixelSize: pill.signSize
font.weight: Font.Bold
anchors.baseline: numTrack.baseline
}
}
DankIcon {
name: "bolt"
size: pill.boltSize
color: Theme.surfaceText
visible: pill.charging && pill.vertical
anchors.centerIn: parent
}
}
// Same glyphs tinted for the fill, clipped so they stay legible over it.
Item {
visible: glyphTrack.visible
x: fill.x
y: fill.y
width: fill.width
height: fill.height
clip: true
Item {
x: -fill.x
y: -fill.y
width: body.width
height: body.height
Row {
visible: !pill.vertical && pill.showNumber
anchors.centerIn: parent
spacing: 1
StyledText {
id: numFill
text: pill.numberText
color: pill.onFillColor
font.pixelSize: pill.glyphSize
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "%"
visible: pill.showPercentSign
color: pill.onFillColor
font.pixelSize: pill.signSize
font.weight: Font.Bold
anchors.baseline: numFill.baseline
}
}
DankIcon {
name: "bolt"
size: pill.boltSize
color: pill.onFillColor
visible: pill.charging && pill.vertical
anchors.centerIn: parent
}
}
}
}
// Battery terminal nub, colored to match the outline/track so it
// reads as part of the same frame rather than a fill segment.
Rectangle {
visible: !pill.vertical
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: pill.nubBreadth
height: pill.nubSpan
radius: Math.round(pill.nubBreadth * 0.35)
color: Theme.withAlpha(Theme.surfaceVariant, 0.9)
}
Rectangle {
visible: pill.vertical
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: pill.nubSpan
height: pill.nubBreadth
radius: Math.round(pill.nubBreadth * 0.35)
color: Theme.withAlpha(Theme.surfaceVariant, 0.9)
}
}
content: Component {
Item {
implicitWidth: battery.isVerticalOrientation ? (battery.widgetThickness - battery.horizontalPadding * 2) : batteryContent.implicitWidth
implicitHeight: battery.isVerticalOrientation ? batteryColumn.implicitHeight : (battery.widgetThickness - battery.horizontalPadding * 2)
Column {
id: batteryColumn
visible: battery.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: BatteryService.getBatteryIcon()
visible: !battery.pillStyle
size: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: {
if (!BatteryService.batteryAvailable) {
return Theme.widgetIconColor;
}
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error;
}
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary;
}
return Theme.widgetIconColor;
}
anchors.horizontalCenter: parent.horizontalCenter
}
BatteryPill {
visible: battery.pillStyle
vertical: true
showNumber: false
thickness: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: battery.verticalDisplayText
font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale, battery.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
visible: BatteryService.batteryAvailable && battery.verticalDisplayText !== ""
}
}
Row {
id: batteryContent
visible: !battery.isVerticalOrientation
anchors.centerIn: parent
spacing: (barConfig?.noBackground ?? false) ? 1 : 2
DankIcon {
name: BatteryService.getBatteryIcon()
visible: !battery.pillStyle
size: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: {
if (!BatteryService.batteryAvailable) {
return Theme.widgetIconColor;
}
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error;
}
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary;
}
return Theme.widgetIconColor;
}
anchors.verticalCenter: parent.verticalCenter
}
BatteryPill {
visible: battery.pillStyle
vertical: false
showNumber: battery.showPercent
showPercentSign: battery.pillPercentSign
thickness: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
anchors.verticalCenter: parent.verticalCenter
}
OfficialBolt {
visible: battery.pillStyle && BatteryService.batteryAvailable && BatteryService.isCharging
fillColor: Theme.primary
size: Math.round(Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) * 0.85)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: battery.horizontalSideText
font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale, battery.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable && battery.horizontalSideText !== ""
}
}
}
}
MouseArea {
x: -battery.leftMargin
y: -battery.topMargin
width: battery.width + battery.leftMargin + battery.rightMargin
height: battery.height + battery.topMargin + battery.bottomMargin
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: mouse => {
battery.triggerRipple(this, mouse.x, mouse.y);
if (mouse.button === Qt.LeftButton) {
toggleBatteryPopup();
} else if (mouse.button === Qt.RightButton) {
if (PowerProfileWatcher.available) {
PowerProfileWatcher.cycleProfile();
} else {
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
}
}
}
onWheel: wheel => {
var delta = wheel.angleDelta.y;
if (delta === 0)
return;
// Check if this is a touchpad
if (delta !== 120 && delta !== -120) {
touchpadAccumulator += delta;
if (Math.abs(touchpadAccumulator) < 500)
return;
delta = touchpadAccumulator;
touchpadAccumulator = 0;
}
if (!DisplayService.brightnessAvailable) {
return;
}
const step = 5;
const change = delta > 0 ? step : -step;
const newBrightness = Math.max(0, Math.min(100, DisplayService.brightnessLevel + change));
DisplayService.setBrightness(newBrightness, "", false);
}
}
}