Files
2026-03-25 19:12:30 -04:00

264 lines
9.9 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Hardware
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
Item {
id: root
property ShellScreen screen
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
// Explicit screenName property ensures reactive binding when screen changes
readonly property string screenName: screen ? screen.name : ""
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0 && screenName) {
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property bool useGraphicMode: displayMode === "graphic" || displayMode === "graphic-clean"
readonly property bool hideIfNotDetected: widgetSettings.hideIfNotDetected !== undefined ? widgetSettings.hideIfNotDetected : widgetMetadata.hideIfNotDetected
readonly property bool hideIfIdle: widgetSettings.hideIfIdle !== undefined ? widgetSettings.hideIfIdle : widgetMetadata.hideIfIdle
// Check if selected device is actually present/connected
readonly property bool isReady: BatteryService.isDeviceReady(selectedDevice)
readonly property bool isPresent: BatteryService.isDevicePresent(selectedDevice)
readonly property real percent: isReady ? BatteryService.getPercentage(selectedDevice) : -1
readonly property bool isCharging: isReady ? BatteryService.isCharging(selectedDevice) : false
readonly property bool isPluggedIn: isReady ? BatteryService.isPluggedIn(selectedDevice) : false
readonly property bool isLowBattery: isReady ? BatteryService.isLowBattery(selectedDevice) : false
readonly property bool isCriticalBattery: isReady ? BatteryService.isCriticalBattery(selectedDevice) : false
// Visibility: show if hideIfNotDetected is false, or if battery is ready
readonly property bool shouldShow: !hideIfNotDetected || (isReady && (hideIfIdle ? !isPluggedIn : true))
readonly property string deviceNativePath: widgetSettings.deviceNativePath !== undefined ? widgetSettings.deviceNativePath : widgetMetadata.deviceNativePath
readonly property var selectedDevice: BatteryService.isDevicePresent(BatteryService.findDevice(deviceNativePath)) ? BatteryService.findDevice(deviceNativePath) : null
readonly property var tooltipContent: {
if (!isReady || !isPresent) {
return I18n.tr("battery.no-battery-detected");
}
let rows = [];
const isInternal = selectedDevice.isLaptopBattery;
if (isInternal) {
// Show charge percentage
rows.push([I18n.tr("battery.battery-level"), `${percent}%`]);
let timeText = BatteryService.getTimeRemainingText(selectedDevice);
if (timeText) {
const colonIdx = timeText.indexOf(":");
if (colonIdx >= 0) {
rows.push([timeText.substring(0, colonIdx).trim(), timeText.substring(colonIdx + 1).trim()]);
} else {
rows.push([timeText, ""]);
}
}
let rateText = BatteryService.getRateText(selectedDevice);
if (!isPluggedIn && rateText) {
const colonIdx = rateText.indexOf(":");
if (colonIdx >= 0) {
rows.push([rateText.substring(0, colonIdx).trim(), rateText.substring(colonIdx + 1).trim()]);
} else {
rows.push([rateText, ""]);
}
}
// Show battery health if supported (check actual battery, not DisplayDevice)
let healthDevice = selectedDevice.healthSupported ? selectedDevice : (BatteryService.laptopBatteries.length > 0 ? BatteryService.laptopBatteries[0] : null);
if (healthDevice && healthDevice.healthSupported) {
rows.push([I18n.tr("battery.battery-health"), `${Math.round(healthDevice.healthPercentage)}%`]);
}
} else if (selectedDevice) {
// External / Peripheral Device (Phone, Keyboard, Mouse, Gamepad, Headphone etc.)
let name = BatteryService.getDeviceName(selectedDevice);
rows.push([name, `${percent}%`]);
}
// If we are showing the main laptop battery, append external devices
if (isInternal) {
var external = BatteryService.bluetoothBatteries;
if (external.length > 0) {
if (rows.length > 0) {
rows.push(["---", "---"]); // Separator
}
for (var j = 0; j < external.length; j++) {
var dev = external[j];
var dName = BatteryService.getDeviceName(dev);
var dPct = BatteryService.getPercentage(dev);
rows.push([dName, `${dPct}%`]);
}
}
}
return rows;
}
visible: shouldShow
opacity: shouldShow ? 1.0 : 0.0
implicitWidth: useGraphicMode ? capsule.width : pill.width
implicitHeight: useGraphicMode ? capsule.height : pill.height
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
},
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
}
// ==================== GRAPHIC MODE ====================
// Capsule background (graphic mode only)
Rectangle {
id: capsule
visible: root.useGraphicMode
anchors.centerIn: nBattery
width: root.isBarVertical ? root.capsuleHeight : nBattery.width + Style.margin2S
height: root.isBarVertical ? nBattery.height + Style.margin2S : root.capsuleHeight
radius: Math.min(Style.radiusL, width / 2)
color: graphicMouseArea.containsMouse ? Color.mHover : Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
}
NBattery {
id: nBattery
visible: root.useGraphicMode
anchors.centerIn: parent
baseSize: (Style.getBarHeightForScreen(root.screenName) / root.capsuleHeight) * Style.fontSizeXXS
showPercentageText: root.displayMode !== "graphic-clean"
vertical: root.isBarVertical
percentage: root.percent
ready: root.isReady
charging: root.isCharging
pluggedIn: root.isPluggedIn
low: root.isLowBattery
critical: root.isCriticalBattery
baseColor: graphicMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
textColor: graphicMouseArea.containsMouse ? Color.mHover : Color.mSurface
}
MouseArea {
id: graphicMouseArea
visible: root.useGraphicMode
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onEntered: {
if (!getBatteryPanel()?.isPanelOpen && root.tooltipContent) {
TooltipService.show(root, root.tooltipContent, BarService.getTooltipDirection(root.screen?.name));
tooltipRefreshTimer.start();
}
}
onExited: {
tooltipRefreshTimer.stop();
TooltipService.hide();
}
onClicked: mouse => {
TooltipService.hide();
if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, nBattery, screen);
} else {
toggleBatteryPanel();
}
}
}
Timer {
id: tooltipRefreshTimer
interval: 1000
repeat: true
onTriggered: {
if (graphicMouseArea.containsMouse) {
TooltipService.updateText(root.tooltipContent);
}
}
}
// ==================== ICON MODE ====================
BarPill {
id: pill
visible: !root.useGraphicMode
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
icon: BatteryService.getIcon(root.percent, root.isCharging, root.isPluggedIn, root.isReady)
text: root.isReady ? root.percent : "-"
suffix: "%"
autoHide: false
forceOpen: root.isReady && root.displayMode === "icon-always"
forceClose: root.displayMode === "icon-only" || !root.isReady
customBackgroundColor: root.isCharging ? Color.mPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mError : "transparent")
customTextIconColor: root.isCharging ? Color.mOnPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mOnError : "transparent")
tooltipText: !getBatteryPanel()?.isPanelOpen ? root.tooltipContent : ""
onClicked: toggleBatteryPanel()
onRightClicked: PanelService.showContextMenu(contextMenu, pill, screen)
}
// ==================== SHARED ====================
function getBatteryPanel() {
var panel = PanelService.getPanel("batteryPanel", screen);
if (panel) {
panel.panelID = {
showPowerProfiles: widgetSettings.showPowerProfiles !== undefined ? widgetSettings.showPowerProfiles : widgetMetadata.showPowerProfiles,
showNoctaliaPerformance: widgetSettings.showNoctaliaPerformance !== undefined ? widgetSettings.showNoctaliaPerformance : widgetMetadata.showNoctaliaPerformance
};
}
return panel;
}
function toggleBatteryPanel() {
getBatteryPanel()?.toggle(root);
}
}