diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index e3d687e8..2512e951 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Standard (Anzeigegerät)", + "description": "Wählen Sie das anzuzeigende Batteriegerät aus.", + "label": "Batteriebetriebenes Gerät" + }, "display-mode": { "description": "Wählen Sie, wie dieser Wert angezeigt werden soll.", "label": "Anzeigemodus" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "Speicherverbrauch" }, + "network-section": { + "label": "Netzwerk" + }, "polling-interval": { "label": "Abfrageintervall" }, @@ -1848,9 +1856,6 @@ "description": "Passe die Warn-/Kritisch-Schwellen und die Abfrageintervalle für jede Systemmetrik an.", "label": "Schwellenwerte" }, - "network-section": { - "label": "Netzwerk" - }, "title": "Systemmonitor", "use-custom-highlight-colors": { "description": "Wenn deaktiviert, werden die Standard-Hervorhebungsfarben des Themes verwendet.", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 0c42d1e2..23192b8a 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Default (Display Device)", + "description": "Select which battery device to display.", + "label": "Battery device" + }, "display-mode": { "description": "Choose how you'd like this value to appear.", "label": "Display mode" diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index fd7d1e40..82c5a603 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Predeterminado (Dispositivo de visualización)", + "description": "Seleccione qué dispositivo de batería mostrar.", + "label": "Dispositivo de batería" + }, "display-mode": { "description": "Elige cómo te gustaría que apareciera este valor.", "label": "Modo de visualización" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "Uso de memoria" }, + "network-section": { + "label": "Red" + }, "polling-interval": { "label": "Intervalo de sondeo" }, @@ -1848,9 +1856,6 @@ "description": "Ajusta los umbrales de advertencia/crítico y los intervalos de sondeo para cada métrica del sistema.", "label": "Umbrales" }, - "network-section": { - "label": "Red" - }, "title": "Monitor del sistema", "use-custom-highlight-colors": { "description": "Cuando está desactivado, se usan los colores de resaltado predeterminados del tema.", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 61e1ce48..8bd15051 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Par défaut (périphérique d'affichage)", + "description": "Sélectionnez le périphérique de batterie à afficher.", + "label": "Dispositif à pile" + }, "display-mode": { "description": "Choisissez comment vous souhaitez que cette valeur apparaisse.", "label": "Mode d'affichage" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "Utilisation mémoire" }, + "network-section": { + "label": "Réseau" + }, "polling-interval": { "label": "Intervalle d'interrogation" }, @@ -1848,9 +1856,6 @@ "description": "Ajustez les seuils d’avertissement/critiques et les intervalles d’interrogation pour chaque métrique système.", "label": "Seuils" }, - "network-section": { - "label": "Réseau" - }, "title": "Moniteur système", "use-custom-highlight-colors": { "description": "Lorsque cette option est désactivée, les couleurs de surbrillance par défaut du thème sont utilisées.", diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index dea9eec1..ba791fd4 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "標準 (表示デバイス)", + "description": "表示するバッテリーデバイスを選択してください。", + "label": "電池デバイス" + }, "display-mode": { "description": "値の表示方法を選択します。", "label": "表示モード" diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index a5057133..a5ed88ea 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Standaard (weergaveapparaat)", + "description": "Selecteer welk batterijapparaat u wilt weergeven.", + "label": "Batterijapparaat" + }, "display-mode": { "description": "Kies hoe je wilt dat deze waarde wordt weergegeven.", "label": "Weergavemodus" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "Geheugengebruik" }, + "network-section": { + "label": "Netwerk" + }, "polling-interval": { "label": "Peilingsinterval" }, @@ -1848,9 +1856,6 @@ "description": "Pas de waarschuwing/kritieke drempels en de pollingsintervallen voor elke systeemmetriek aan.", "label": "Drempels" }, - "network-section": { - "label": "Netwerk" - }, "title": "Systeemmonitor", "use-custom-highlight-colors": { "description": "Indien uitgeschakeld, worden de standaard markeerkleuren van het thema gebruikt.", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index b74cdcbe..3e1aa123 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Padrão (Dispositivo de Exibição)", + "description": "Selecione qual dispositivo de bateria exibir.", + "label": "Dispositivo de bateria" + }, "display-mode": { "description": "Escolha como você gostaria que este valor aparecesse.", "label": "Modo de exibição" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "Uso de memória" }, + "network-section": { + "label": "Rede" + }, "polling-interval": { "label": "Intervalo de pesquisa" }, @@ -1848,9 +1856,6 @@ "description": "Ajuste os limites de aviso/crítico e os intervalos de consulta para cada métrica do sistema.", "label": "Limiares" }, - "network-section": { - "label": "Rede" - }, "title": "Monitor do Sistema", "use-custom-highlight-colors": { "description": "Quando desativado, são usadas as cores de destaque padrão do tema.", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 966a6646..678771b0 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Устройство отображения по умолчанию", + "description": "Выберите, какое устройство с батареей отображать.", + "label": "Батарейное устройство" + }, "display-mode": { "description": "Выберите, как это значение должно отображаться.", "label": "Режим отображения" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "Использование памяти" }, + "network-section": { + "label": "Сеть" + }, "polling-interval": { "label": "Интервал опроса" }, @@ -1848,9 +1856,6 @@ "description": "Настройте пороги предупреждения/критические пороги и интервалы опроса для каждой системной метрики.", "label": "Пороги" }, - "network-section": { - "label": "Сеть" - }, "title": "Системный монитор", "use-custom-highlight-colors": { "description": "Если отключено, используются цвета выделения по умолчанию темы.", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 1975ea79..8d193b70 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Varsayılan (Görüntü Aygıtı)", + "description": "Görüntülenecek pil cihazını seçin.", + "label": "Pil cihazı" + }, "display-mode": { "description": "Bu değerin nasıl görünmesini istediğinizi seçin.", "label": "Görüntüleme modu" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "Bellek Kullanımı" }, + "network-section": { + "label": "Ağ" + }, "polling-interval": { "label": "Yoklama aralığı" }, @@ -1848,9 +1856,6 @@ "description": "Her sistem metriği için uyarı/kritik eşiklerini ve yoklama aralıklarını ayarlayın.", "label": "Eşikler" }, - "network-section": { - "label": "Ağ" - }, "title": "Sistem İzleme", "use-custom-highlight-colors": { "description": "Devre dışı bırakıldığında, tema varsayılan vurgulama renkleri kullanılır.", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 9c035c9a..c3d67d49 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "Пристрій відображення за замовчуванням", + "description": "Виберіть пристрій з акумулятором для відображення.", + "label": "Пристрій живлення від батареї" + }, "display-mode": { "description": "Виберіть, як ви хочете, щоб це значення відображалося.", "label": "Режим відображення" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "Використання пам'яті" }, + "network-section": { + "label": "Мережа" + }, "polling-interval": { "label": "Інтервал опитування" }, @@ -1848,9 +1856,6 @@ "description": "Налаштуйте пороги попередження/критичні пороги та інтервали опитування для кожного системного показника.", "label": "Пороги" }, - "network-section": { - "label": "Мережа" - }, "title": "Системний монітор", "use-custom-highlight-colors": { "description": "Якщо вимкнено, використовуються кольори підсвічування за замовчуванням теми.", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 44f18867..f9600262 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -46,6 +46,11 @@ } }, "battery": { + "device": { + "default": "默认(显示设备)", + "description": "选择要显示的电池设备。", + "label": "电池设备" + }, "display-mode": { "description": "选择您希望此值显示的方式。", "label": "显示模式" @@ -1832,6 +1837,9 @@ "memory-section": { "label": "内存使用率" }, + "network-section": { + "label": "网络" + }, "polling-interval": { "label": "轮询间隔" }, @@ -1848,9 +1856,6 @@ "description": "为每个系统指标调整警告/严重阈值和轮询间隔。", "label": "阈值" }, - "network-section": { - "label": "网络" - }, "title": "系统监视器", "use-custom-highlight-colors": { "description": "关闭时将使用主题默认高亮颜色。", diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index e88432a4..e0689c3a 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -6,6 +6,7 @@ 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 @@ -34,55 +35,128 @@ Item { readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold - readonly property bool isLowBattery: !charging && percent <= warningThreshold + // Only show low battery warning if device is ready (prevents false positive during initialization) + readonly property bool isLowBattery: isReady && !charging && percent <= warningThreshold // Test mode readonly property bool testMode: false readonly property int testPercent: 35 readonly property bool testCharging: false - // Main properties - readonly property var battery: UPower.displayDevice - readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) - readonly property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) + readonly property string deviceNativePath: widgetSettings.deviceNativePath || "" + + function findBatteryDevice(nativePath) { + if (!nativePath || !UPower.devices) { + return UPower.displayDevice; + } + var devices = UPower.devices.values || []; + for (var i = 0; i < devices.length; i++) { + var device = devices[i]; + if (device && device.nativePath === nativePath && device.type !== UPowerDeviceType.LinePower && device.percentage !== undefined) { + return device; + } + } + return UPower.displayDevice; + } + + function findBluetoothDevice(nativePath) { + if (!nativePath || !BluetoothService.devices) { + return null; + } + var macMatch = nativePath.match(/([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})/); + if (!macMatch) { + return null; + } + var macAddress = macMatch[1].toUpperCase(); + var devices = BluetoothService.devices.values || []; + for (var i = 0; i < devices.length; i++) { + var device = devices[i]; + if (device && device.address && device.address.toUpperCase() === macAddress) { + return device; + } + } + return null; + } + + readonly property var battery: findBatteryDevice(deviceNativePath) + readonly property var bluetoothDevice: deviceNativePath ? findBluetoothDevice(deviceNativePath) : null + readonly property bool hasBluetoothBattery: bluetoothDevice && bluetoothDevice.batteryAvailable && bluetoothDevice.battery !== undefined + readonly property bool isBluetoothConnected: bluetoothDevice && bluetoothDevice.connected === true + + property bool initializationComplete: false + Timer { + interval: 500 + running: true + onTriggered: root.initializationComplete = true + } + + readonly property bool isDevicePresent: { + if (testMode) + return true; + if (deviceNativePath) { + if (bluetoothDevice) { + return isBluetoothConnected; + } + if (battery && battery.nativePath === deviceNativePath) { + if (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) { + return battery.isPresent; + } + return battery.ready && battery.percentage !== undefined && (battery.percentage > 0 || battery.state === UPowerDeviceState.Charging); + } + return false; + } + if (battery) { + return (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) ? battery.isPresent : (battery.ready && battery.percentage !== undefined); + } + return false; + } + + readonly property bool isReady: testMode ? true : (initializationComplete && battery && battery.ready && isDevicePresent && (battery.percentage !== undefined || hasBluetoothBattery)) + readonly property real percent: testMode ? testPercent : (isReady ? (hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery.percentage * 100)) : 0) readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) property bool hasNotifiedLowBattery: false implicitWidth: pill.width implicitHeight: pill.height - // Helper to evaluate and possibly notify - function maybeNotify(percent, charging) { - // Only notify once we are a below threshold - if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) { - root.hasNotifiedLowBattery = true; + function maybeNotify(currentPercent, isCharging) { + if (!isCharging && !hasNotifiedLowBattery && currentPercent <= warningThreshold) { + hasNotifiedLowBattery = true; ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", { - "percent": Math.round(percent) + "percent": Math.round(currentPercent) })); - } else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) { - // Reset when charging starts or when battery recovers 5% above threshold - root.hasNotifiedLowBattery = false; + } else if (hasNotifiedLowBattery && (isCharging || currentPercent > warningThreshold + 5)) { + hasNotifiedLowBattery = false; } } - // Watch for battery changes - Connections { - target: UPower.displayDevice - function onPercentageChanged() { - var currentPercent = UPower.displayDevice.percentage * 100; - var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging; - root.maybeNotify(currentPercent, isCharging); - } + function getCurrentPercent() { + return hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery ? battery.percentage * 100 : 0); + } - function onStateChanged() { - var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging; - // Reset notification flag when charging starts - if (isCharging) { - root.hasNotifiedLowBattery = false; + Connections { + target: battery + function onPercentageChanged() { + if (battery) { + maybeNotify(getCurrentPercent(), battery.state === UPowerDeviceState.Charging); + } + } + function onStateChanged() { + if (battery) { + if (battery.state === UPowerDeviceState.Charging) { + hasNotifiedLowBattery = false; + } + maybeNotify(getCurrentPercent(), battery.state === UPowerDeviceState.Charging); + } + } + } + + Connections { + target: bluetoothDevice + function onBatteryChanged() { + if (bluetoothDevice && hasBluetoothBattery) { + maybeNotify(bluetoothDevice.battery * 100, battery ? battery.state === UPowerDeviceState.Charging : false); } - // Also re-evaluate maybeNotify, as state might have changed - var currentPercent = UPower.displayDevice.percentage * 100; - root.maybeNotify(currentPercent, isCharging); } } @@ -119,12 +193,10 @@ Item { text: (isReady || testMode) ? Math.round(percent) : "-" suffix: "%" autoHide: false - forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow" - forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery) - - // Charging is the most important, then low battery - customBackgroundColor: charging ? Color.mPrimary : (isLowBattery ? Color.mError : Color.transparent) - customTextIconColor: charging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : Color.transparent) + forceOpen: isReady && displayMode === "alwaysShow" + forceClose: displayMode === "alwaysHide" || (initializationComplete && !isReady) + customBackgroundColor: !initializationComplete ? Color.transparent : (charging ? Color.mPrimary : (isLowBattery ? Color.mError : Color.transparent)) + customTextIconColor: !initializationComplete ? Color.transparent : (charging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : Color.transparent)) tooltipText: { let lines = []; @@ -132,7 +204,7 @@ Item { lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`); return lines.join("\n"); } - if (!isReady || !battery.isLaptopBattery) { + if (!isReady || !isDevicePresent) { return I18n.tr("battery.no-battery-detected"); } if (battery.timeToEmpty > 0) { diff --git a/Modules/Panels/Battery/BatteryPanel.qml b/Modules/Panels/Battery/BatteryPanel.qml index c554120b..af774c1f 100644 --- a/Modules/Panels/Battery/BatteryPanel.qml +++ b/Modules/Panels/Battery/BatteryPanel.qml @@ -6,7 +6,7 @@ import Quickshell.Services.UPower import qs.Commons import qs.Modules.MainScreen import qs.Services.Hardware -import qs.Services.Power +import qs.Services.Networking import qs.Widgets SmartPanel { @@ -15,16 +15,133 @@ SmartPanel { preferredWidth: Math.round(360 * Style.uiScaleRatio) preferredHeight: Math.round(460 * Style.uiScaleRatio) - readonly property var battery: UPower.displayDevice - readonly property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent - readonly property int percent: isReady ? Math.round(battery.percentage * 100) : -1 + // Get device selection from Battery widget settings (check right section first, then any Battery widget) + function getBatteryDevicePath() { + // Check right section first (most common location for Battery widget) + var rightWidgets = Settings.data.bar.widgets.right || []; + for (var i = 0; i < rightWidgets.length; i++) { + if (rightWidgets[i].id === "Battery" && rightWidgets[i].deviceNativePath) { + return rightWidgets[i].deviceNativePath; + } + } + // Check other sections + var sections = ["left", "center"]; + for (var s = 0; s < sections.length; s++) { + var widgets = Settings.data.bar.widgets[sections[s]] || []; + for (var j = 0; j < widgets.length; j++) { + if (widgets[j].id === "Battery" && widgets[j].deviceNativePath) { + return widgets[j].deviceNativePath; + } + } + } + return ""; + } + + // Helper function to find battery device by nativePath + function findBatteryDevice(nativePath) { + if (!nativePath || nativePath === "") { + return UPower.displayDevice; + } + + if (!UPower.devices) { + return UPower.displayDevice; + } + + var deviceArray = UPower.devices.values || []; + for (var i = 0; i < deviceArray.length; i++) { + var device = deviceArray[i]; + if (device && device.nativePath === nativePath) { + if (device.type === UPowerDeviceType.LinePower) { + continue; + } + if (device.percentage !== undefined) { + return device; + } + } + } + return UPower.displayDevice; + } + + // Helper function to find Bluetooth device by MAC address from nativePath + function findBluetoothDevice(nativePath) { + if (!nativePath || !BluetoothService.devices) { + return null; + } + + var macMatch = nativePath.match(/([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})/); + if (!macMatch) { + return null; + } + + var macAddress = macMatch[1].toUpperCase(); + var deviceArray = BluetoothService.devices.values || []; + + for (var i = 0; i < deviceArray.length; i++) { + var device = deviceArray[i]; + if (device && device.address && device.address.toUpperCase() === macAddress) { + return device; + } + } + return null; + } + + readonly property string deviceNativePath: getBatteryDevicePath() + readonly property var battery: findBatteryDevice(deviceNativePath) + readonly property var bluetoothDevice: deviceNativePath ? findBluetoothDevice(deviceNativePath) : null + readonly property bool hasBluetoothBattery: bluetoothDevice && bluetoothDevice.batteryAvailable && bluetoothDevice.battery !== undefined + readonly property bool isBluetoothConnected: bluetoothDevice && bluetoothDevice.connected !== undefined ? bluetoothDevice.connected : false + + // Check if device is actually present/connected + readonly property bool isDevicePresent: { + if (deviceNativePath && deviceNativePath !== "") { + if (bluetoothDevice) { + return isBluetoothConnected; + } + if (battery && battery.nativePath === deviceNativePath) { + if (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) { + return battery.isPresent; + } + return battery.ready && battery.percentage !== undefined && (battery.percentage > 0 || battery.state === UPowerDeviceState.Charging); + } + return false; + } + if (battery) { + if (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) { + return battery.isPresent; + } + return battery.ready && battery.percentage !== undefined; + } + return false; + } + + readonly property bool isReady: battery && battery.ready && isDevicePresent && (battery.percentage !== undefined || hasBluetoothBattery) + readonly property int percent: isReady ? Math.round(hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery.percentage * 100)) : -1 readonly property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false readonly property bool healthAvailable: isReady && battery.healthSupported readonly property int healthPercent: healthAvailable ? Math.round(battery.healthPercentage) : -1 - readonly property bool powerProfileAvailable: PowerProfileService.available - readonly property var powerProfiles: [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] + + function getDeviceName() { + if (!isReady) { + return ""; + } + // Don't show name for laptop batteries + if (battery && battery.isLaptopBattery) { + return ""; + } + if (bluetoothDevice && bluetoothDevice.name) { + return bluetoothDevice.name; + } + if (battery && battery.model) { + return battery.model; + } + return ""; + } + + readonly property string deviceName: getDeviceName() + readonly property string panelTitle: deviceName ? `${I18n.tr("battery.panel-title")} - ${deviceName}` : I18n.tr("battery.panel-title") + readonly property string timeText: { - if (!isReady) + if (!isReady || !isDevicePresent) return I18n.tr("battery.no-battery-detected"); if (charging && battery.timeToFull > 0) { return I18n.tr("battery.time-until-full", { @@ -39,9 +156,6 @@ SmartPanel { return I18n.tr("battery.idle"); } readonly property string iconName: BatteryService.getIcon(percent, charging, isReady) - readonly property bool profilesAvailable: PowerProfileService.available - property int profileIndex: profileToIndex(PowerProfileService.profile) - property bool manualInhibitActive: manualInhibitorEnabled() panelContent: Item { property real contentPreferredHeight: mainLayout.implicitHeight + Style.marginL * 2 @@ -74,11 +188,12 @@ SmartPanel { Layout.fillWidth: true NText { - text: I18n.tr("battery.panel-title") + text: root.panelTitle pointSize: Style.fontSizeL font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true + elide: Text.ElideRight } NText { @@ -103,6 +218,7 @@ SmartPanel { NBox { Layout.fillWidth: true height: chargeLayout.implicitHeight + Style.marginL * 2 + visible: isReady ColumnLayout { id: chargeLayout @@ -165,131 +281,6 @@ SmartPanel { } } } - - // Power profile and idle inhibit controls - NBox { - Layout.fillWidth: true - height: controlsLayout.implicitHeight + Style.marginM * 2 - - ColumnLayout { - id: controlsLayout - anchors.fill: parent - anchors.margins: Style.marginM - spacing: Style.marginM - - ColumnLayout { - id: ppd - visible: root.powerProfileAvailable - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS - NIcon { - icon: PowerProfileService.getIcon() - pointSize: Style.fontSizeM - color: Color.mPrimary - } - 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 - value: profileIndex - enabled: profilesAvailable - onPressedChanged: (pressed, v) => { - if (!pressed) { - setProfileByIndex(v); - } - } - onMoved: v => { - profileIndex = v; - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS - - NIcon { - icon: manualInhibitActive ? "keep-awake-on" : "keep-awake-off" - pointSize: Style.fontSizeL - color: manualInhibitActive ? Color.mPrimary : Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignVCenter - } - - NToggle { - Layout.fillWidth: true - checked: manualInhibitActive - label: I18n.tr("battery.inhibit-idle-label") - description: I18n.tr("battery.inhibit-idle-description") - onToggled: function (checked) { - if (checked) { - IdleInhibitorService.addManualInhibitor(null); - } else { - IdleInhibitorService.removeManualInhibitor(); - } - manualInhibitActive = checked; - } - } - } - } - } - } - } - - 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 manualInhibitorEnabled() { - return IdleInhibitorService.activeInhibitors && IdleInhibitorService.activeInhibitors.indexOf("manual") >= 0; - } - - Connections { - target: IdleInhibitorService - - function onIsInhibitedChanged() { - manualInhibitActive = manualInhibitorEnabled(); - } - } - - Timer { - id: inhibitorPoll - interval: 1000 - repeat: true - running: true - onTriggered: manualInhibitActive = manualInhibitorEnabled() - } - - Connections { - target: PowerProfileService - - function onProfileChanged() { - profileIndex = profileToIndex(PowerProfileService.profile); } } } diff --git a/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml index e23700d8..8e7a4a35 100644 --- a/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml @@ -153,9 +153,15 @@ Popup { function loadWidgetSettings() { const source = BarWidgetRegistry.widgetSettingsMap[widgetId]; if (source) { - // Use setSource to pass properties at creation time + var currentWidgetData = widgetData; + if (sectionId && widgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[sectionId]; + if (widgets && widgetIndex < widgets.length) { + currentWidgetData = widgets[widgetIndex]; + } + } settingsLoader.setSource(source, { - "widgetData": widgetData, + "widgetData": currentWidgetData, "widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId] }); } diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml index 8b23d553..ad2e331f 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell.Services.UPower import qs.Commons import qs.Widgets @@ -15,14 +16,109 @@ ColumnLayout { // Local state property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode property int valueWarningThreshold: widgetData.warningThreshold !== undefined ? widgetData.warningThreshold : widgetMetadata.warningThreshold + property string valueDeviceNativePath: widgetData.deviceNativePath !== undefined ? widgetData.deviceNativePath : "" + + // Build model of available battery devices + function buildDeviceModel() { + var model = [ + { + "key": "", + "name": I18n.tr("bar.widget-settings.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(); + } + } + } function saveSettings() { var settings = Object.assign({}, widgetData || {}); + if (widgetData && widgetData.id) { + settings.id = widgetData.id; + } settings.displayMode = valueDisplayMode; settings.warningThreshold = valueWarningThreshold; + if (valueDeviceNativePath && valueDeviceNativePath !== "") { + settings.deviceNativePath = valueDeviceNativePath; + } else { + delete settings.deviceNativePath; + } return settings; } + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NComboBox { + id: deviceComboBox + Layout.fillWidth: true + label: I18n.tr("bar.widget-settings.battery.device.label") + description: I18n.tr("bar.widget-settings.battery.device.description") + minimumWidth: 134 + model: root.deviceModel + currentKey: root.valueDeviceNativePath + onSelected: key => root.valueDeviceNativePath = key + } + + // Update currentKey when model changes to ensure selection is preserved + Connections { + target: root + function onDeviceModelChanged() { + // Force update of currentKey to trigger selection update + deviceComboBox.currentKey = root.valueDeviceNativePath; + } + } + + NIconButton { + icon: "refresh" + tooltipText: "Refresh device list" + onClicked: deviceModel = buildDeviceModel() + } + } + NComboBox { label: I18n.tr("bar.widget-settings.battery.display-mode.label") description: I18n.tr("bar.widget-settings.battery.display-mode.description") diff --git a/Services/UI/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml index 931b583b..9cde54c7 100644 --- a/Services/UI/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -87,7 +87,8 @@ Singleton { "Battery": { "allowUserSettings": true, "displayMode": "onhover", - "warningThreshold": 30 + "warningThreshold": 30, + "deviceNativePath": "" }, "Bluetooth": { "allowUserSettings": true,