Squash commits

Organise file add MARKs
Increase width on NcomboBox.

So far this commit the most i struggled - >:|

moving more to service.

checkpoint 2/4

Add tooltip texts properly.

Health check avoided on bluetooth batteries
This commit is contained in:
Turann_
2026-01-28 01:56:11 +03:00
parent 491222594c
commit 1da94c27e5
3 changed files with 220 additions and 128 deletions
+41 -34
View File
@@ -55,7 +55,11 @@ Item {
readonly property var battery: BatteryService.findUPowerDevice(deviceNativePath)
readonly property var bluetoothDevice: deviceNativePath ? BatteryService.findBluetoothDevice(deviceNativePath) : null
readonly property var device: bluetoothDevice || battery
readonly property var device: {
if (deviceNativePath)
return bluetoothDevice || battery;
return BatteryService.primaryDevice;
}
readonly property bool hasBluetoothBattery: BatteryService.isBluetoothDevice(device)
readonly property bool isReady: testMode ? true : (initializationComplete && BatteryService.isDeviceReady(device))
@@ -89,7 +93,7 @@ Item {
hasNotifiedLowBattery = true;
ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", {
"percent": Math.round(currentPercent)
}));
}), "battery-exclamation", "warning", 4000, "", null);
} else if (hasNotifiedLowBattery && (charging || pluggedIn || currentPercent > warningThreshold + 5)) {
hasNotifiedLowBattery = false;
}
@@ -100,14 +104,15 @@ Item {
}
Connections {
target: battery
target: device
function onPercentageChanged() {
if (battery) {
if (device) {
maybeNotify(getCurrentPercent(), isCharging, isPluggedIn, isReady);
}
}
function onStateChanged() {
if (battery) {
if (device) {
if (isCharging || isPluggedIn) {
hasNotifiedLowBattery = false;
}
@@ -117,12 +122,7 @@ Item {
}
Connections {
target: bluetoothDevice
function onBatteryChanged() {
if (BatteryService.isDeviceReady(bluetoothDevice)) {
maybeNotify(BatteryService.getPercentage(bluetoothDevice), BatteryService.isCharging(bluetoothDevice), BatteryService.isPluggedIn(bluetoothDevice), true);
}
}
target: (device && BatteryService.isBluetoothDevice(device)) ? device : null
}
NPopupContextMenu {
@@ -148,7 +148,6 @@ Item {
BarPill {
id: pill
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, testPluggedIn, true) : BatteryService.getIcon(percent, isCharging, isPluggedIn, isReady)
@@ -163,40 +162,48 @@ Item {
tooltipText: {
let lines = [];
if (testMode) {
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`);
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345) + ".");
return lines.join("\n");
}
if (!isReady || !isDevicePresent) {
return I18n.tr("battery.no-battery-detected");
}
if (battery) {
if (!isPluggedIn && battery.timeToEmpty > 0) {
lines.push(I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToEmpty)
}));
const isInternal = device === BatteryService.primaryDevice && BatteryService.isLaptopBattery;
if (isInternal) {
let timeText = BatteryService.getTimeRemainingText(device);
if (timeText && timeText !== I18n.tr("common.idle") && timeText !== I18n.tr("battery.no-battery-detected") && timeText !== I18n.tr("battery.plugged-in")) {
lines.push(timeText);
}
if (!isPluggedIn && battery.timeToFull > 0) {
lines.push(I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToFull)
}));
let rateText = BatteryService.getRateText(device);
if (rateText) {
lines.push(rateText);
}
if (battery.changeRate !== undefined) {
const rate = Math.abs(battery.changeRate);
if (isPluggedIn) {
lines.push(I18n.tr("battery.plugged-in"));
} else if (isCharging) {
lines.push(I18n.tr("battery.charging-rate", {
"rate": rate.toFixed(2)
}));
} else {
lines.push(I18n.tr("battery.discharging-rate", {
"rate": rate.toFixed(2)
}));
} else if (device) {
// External / Peripheral Device (Phone, Keyboard, Mouse, Gamepad, Headphone etc.)
let name = BatteryService.getDeviceName(device);
let pct = Math.round(BatteryService.getPercentage(device));
lines.push(name + ": " + pct + suffix);
}
// If we are showing the main laptop battery, append external devices
if (isInternal) {
var external = BatteryService.externalBatteries;
if (external.length > 0) {
if (lines.length > 0)
lines.push(""); // Separator
for (var j = 0; j < external.length; j++) {
var dev = external[j];
var dName = BatteryService.getDeviceName(dev);
var dPct = Math.round(BatteryService.getPercentage(dev));
lines.push(dName + ": " + dPct + suffix);
}
}
}
return lines.join("\n");
}
onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this)
onRightClicked: {
PanelService.showContextMenu(contextMenu, pill, screen);
@@ -1,9 +1,9 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.UPower
import qs.Commons
import qs.Widgets
import qs.Services.Hardware
ColumnLayout {
id: root
@@ -22,60 +22,7 @@ ColumnLayout {
property bool valueHideIfNotDetected: widgetData.hideIfNotDetected !== undefined ? widgetData.hideIfNotDetected : widgetMetadata.hideIfNotDetected
property bool valueHideIfIdle: widgetData.hideIfIdle !== undefined ? widgetData.hideIfIdle : widgetMetadata.hideIfIdle
// Build model of available battery devices
function buildDeviceModel() {
var model = [
{
"key": "",
"name": I18n.tr("bar.battery.device-default")
}
];
if (!UPower.devices) {
return model;
}
var deviceArray = UPower.devices.values || [];
for (var i = 0; i < deviceArray.length; i++) {
var device = deviceArray[i];
if (!device || device.type === UPowerDeviceType.LinePower) {
continue;
}
var displayName = device.model || device.nativePath || "Unknown";
model.push({
"key": device.nativePath || "",
"name": displayName
});
}
return model;
}
readonly property int _deviceCount: (UPower.devices && UPower.devices.values) ? UPower.devices.values.length : 0
property var deviceModel: buildDeviceModel()
on_DeviceCountChanged: {
deviceModel = buildDeviceModel();
}
Connections {
target: UPower.devices
function onValuesChanged() {
deviceModel = buildDeviceModel();
}
}
Timer {
id: refreshTimer
interval: 2000
running: true
repeat: true
onTriggered: {
var currentCount = (UPower.devices && UPower.devices.values) ? UPower.devices.values.length : 0;
if (currentCount !== root._deviceCount) {
deviceModel = buildDeviceModel();
}
}
}
property var deviceModel: BatteryService.devicesModel
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
@@ -105,7 +52,7 @@ ColumnLayout {
Layout.fillWidth: true
label: I18n.tr("bar.battery.device-label")
description: I18n.tr("bar.battery.device-description")
minimumWidth: 134
minimumWidth: 200
model: root.deviceModel
currentKey: root.valueDeviceNativePath
onSelected: key => root.valueDeviceNativePath = key
@@ -123,7 +70,7 @@ ColumnLayout {
NIconButton {
icon: "refresh"
tooltipText: "Refresh device list"
onClicked: deviceModel = buildDeviceModel()
onClicked: BatteryService.devicesModel = BatteryService.buildDeviceModel()
}
}
+175 -37
View File
@@ -11,7 +11,20 @@ import qs.Services.UI
Singleton {
id: root
// Cached device lookups (computed once, used by all properties)
// MARK: BatteryService
// Primary battery device (prioritizes laptop over Bluetooth)
readonly property var primaryDevice: _laptopBattery || _bluetoothBattery || null
// Whether the primary device is a laptop battery
readonly property bool isLaptopBattery: _laptopBattery !== null && primaryDevice === _laptopBattery
readonly property real batteryPercentage: getPercentage(primaryDevice)
readonly property bool batteryCharging: isCharging(primaryDevice)
readonly property bool batteryPluggedIn: isPluggedIn(primaryDevice)
readonly property bool batteryReady: isDeviceReady(primaryDevice)
readonly property bool batteryPresent: isDevicePresent(primaryDevice)
property bool healthAvailable: false
property int healthPercent: -1
readonly property var _laptopBattery: {
if (!UPower.devices)
return UPower.displayDevice;
@@ -46,37 +59,36 @@ Singleton {
}
readonly property var _bluetoothBattery: {
var devices = BluetoothService.devices ? (BluetoothService.devices.values || []) : [];
for (var i = 0; i < devices.length; i++) {
var device = devices[i];
if (device && device.connected && device.batteryAvailable) {
return device;
}
}
if (externalBatteries.length > 0)
return externalBatteries[0];
return null;
}
// Primary battery device (prioritizes laptop over Bluetooth)
readonly property var primaryDevice: _laptopBattery || _bluetoothBattery || null
// Whether the primary device is a laptop battery
readonly property bool isLaptopBattery: _laptopBattery !== null && primaryDevice === _laptopBattery
readonly property real batteryPercentage: getPercentage(primaryDevice)
readonly property bool batteryCharging: isCharging(primaryDevice)
readonly property bool batteryPluggedIn: isPluggedIn(primaryDevice)
readonly property bool batteryReady: isDeviceReady(primaryDevice)
readonly property bool batteryPresent: isDevicePresent(primaryDevice)
property bool healthAvailable: false
property int healthPercent: -1
function findUPowerDevice(nativePath) {
// MARK: resolveDevice
function resolveDevice(nativePath) {
if (!nativePath || nativePath === "") {
return primaryDevice;
}
// Check for DisplayDevice explicitly (Literal key OR actual native path)
if ((nativePath === "DisplayDevice" || (UPower.displayDevice && nativePath === UPower.displayDevice.nativePath)) && UPower.displayDevice) {
return UPower.displayDevice;
}
var upowerDev = findUPowerDevice(nativePath);
if (upowerDev)
return upowerDev;
var btDev = findBluetoothDevice(nativePath);
if (btDev)
return btDev;
return null;
}
// MARK: findUPowerDevice
function findUPowerDevice(nativePath) {
if (!nativePath || nativePath === "" || nativePath === "DisplayDevice") {
return _laptopBattery;
}
@@ -97,6 +109,7 @@ Singleton {
return null;
}
// MARK: findBluetoothDevice
function findBluetoothDevice(nativePath) {
if (!nativePath || !BluetoothService.devices) {
return null;
@@ -119,6 +132,7 @@ Singleton {
return null;
}
// MARK: isDevicePresent
function isDevicePresent(device) {
if (!device)
return false;
@@ -141,6 +155,7 @@ Singleton {
return false;
}
// MARK: isDeviceReady
function isDeviceReady(device) {
if (!isDevicePresent(device))
return false;
@@ -152,6 +167,7 @@ Singleton {
return device.ready && device.percentage !== undefined;
}
// MARK: getPercentage
function getPercentage(device) {
if (!device)
return 0;
@@ -161,22 +177,34 @@ Singleton {
return (device.percentage || 0) * 100;
}
// MARK: isCharging
function isCharging(device) {
if (!device || device.batteryAvailable !== undefined)
return false;
return device.state === UPowerDeviceState.Charging;
if (!device || isBluetoothDevice(device))
// Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?!
return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely.
if (device.state !== undefined) {
return device.state === UPowerDeviceState.Charging;
}
return false;
}
// MARK: isPluggedIn
function isPluggedIn(device) {
if (!device || device.batteryAvailable !== undefined)
return false;
return device.state === UPowerDeviceState.FullyCharged || device.state === UPowerDeviceState.PendingCharge;
if (!device || isBluetoothDevice(device))
// Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?!
return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely.
if (device.state !== undefined) {
return device.state === UPowerDeviceState.FullyCharged || device.state === UPowerDeviceState.PendingCharge;
}
return false;
}
// MARK: isBluetoothDevice
function isBluetoothDevice(device) {
return device && device.batteryAvailable !== undefined;
}
// MARK: getDeviceName
function getDeviceName(device) {
if (!isDeviceReady(device))
return "";
@@ -197,8 +225,9 @@ Singleton {
return "";
}
// MARK: refreshHealth
function refreshHealth() {
if (!isLaptopBattery || !primaryDevice) {
if (!isLaptopBattery) {
healthAvailable = false;
healthPercent = -1;
return;
@@ -208,7 +237,7 @@ Singleton {
Process {
id: healthProcess
command: ["sh", "-c", "upower -i $(upower -e | grep battery | head -n 1) 2>/dev/null | grep -iE 'capacity'"]
command: ["sh", "-c", `upower -i ${primaryDevice && primaryDevice.nativePath ? "/org/freedesktop/UPower/devices/battery_" + primaryDevice.nativePath : "$(upower -e | grep battery | head -n 1)"} 2>/dev/null | grep -iE 'capacity'`]
environment: ({
"LC_ALL": "C"
})
@@ -234,7 +263,7 @@ Singleton {
Qt.callLater(refreshHealth);
}
}
// MARK: getIcon
function getIcon(percent, charging, pluggedIn, isReady) {
if (!isReady) {
return "battery-exclamation";
@@ -263,7 +292,116 @@ Singleton {
return "battery-off"; // New fallback icon clearly represent if nothing is true here.
}
// MARK: hasAnyBattery
function hasAnyBattery() {
return primaryDevice !== null;
}
// MARK: Battery
// MARK: getRateText
function getRateText(device) {
if (!device || device.changeRate === undefined)
return "";
const rate = Math.abs(device.changeRate);
if (isPluggedIn(device)) {
return I18n.tr("battery.plugged-in");
} else if (isCharging(device)) {
return I18n.tr("battery.charging-rate", {
"rate": rate.toFixed(2)
});
} else {
return I18n.tr("battery.discharging-rate", {
"rate": rate.toFixed(2)
});
}
}
// MARK: BatteryPanel
readonly property var externalBatteries: {
var list = [];
var devices = BluetoothService.devices ? (BluetoothService.devices.values || []) : [];
for (var i = 0; i < devices.length; i++) {
var device = devices[i];
if (device && device.connected && device.batteryAvailable) {
list.push(device);
}
}
return list;
}
// MARK: getTimeRemainingText
function getTimeRemainingText(device) {
if (!isDeviceReady(device)) {
return I18n.tr("battery.no-battery-detected");
}
if (isPluggedIn(device)) {
return I18n.tr("battery.plugged-in");
}
if (device) {
if (device.timeToFull > 0) {
return I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(device.timeToFull)
});
}
if (device.timeToEmpty > 0) {
return I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(device.timeToEmpty)
});
}
}
return I18n.tr("common.idle");
}
// MARK: BatterySettings
property var devicesModel: buildDeviceModel()
function buildDeviceModel() {
var model = [
{
"key": UPower.devices.DisplayDevice || "", // It was capital D and i spend an hour to figure out [why tf this do absolutely nothing] XD (I hate my left shift it sticks)
"name": I18n.tr("bar.battery.device-default")
}
];
// UPower Devices
if (UPower.devices && UPower.devices.values) {
var deviceArray = UPower.devices.values;
for (var i = 0; i < deviceArray.length; i++) {
var device = deviceArray[i];
if (!device || device.type === UPowerDeviceType.LinePower) {
continue;
}
var displayName = device.model || device.nativePath || "Unknown";
model.push({
"key": device.nativePath || "",
"name": displayName
});
}
}
return model;
}
// MARK: modelUpdateTimer
Timer {
id: modelUpdateTimer
interval: 2000
running: true
repeat: true
onTriggered: {
var newModel = buildDeviceModel();
// Simple change detection to avoid unnecessary bindings updates
if (JSON.stringify(newModel) !== JSON.stringify(devicesModel)) {
devicesModel = newModel;
}
}
}
Connections {
target: UPower.devices
function onValuesChanged() { modelUpdateTimer.restart(); devicesModel = buildDeviceModel(); }
}
}