mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell
This commit is contained in:
@@ -21,4 +21,6 @@ background {{colors.surface.default.hex}}
|
||||
foreground {{colors.on_surface.default.hex}}
|
||||
selection_foreground {{colors.on_surface_variant.default.hex}}
|
||||
selection_background {{colors.surface_variant.default.hex}}
|
||||
active_border_color {{colors.primary.default.hex}}
|
||||
inactive_border_color {{colors.surface_variant.default.hex}}
|
||||
url_color {{colors.primary.default.hex}}
|
||||
|
||||
@@ -1038,7 +1038,7 @@
|
||||
"system-default": "По умолчанию"
|
||||
},
|
||||
"launcher": {
|
||||
"clipboard-desc": "Получайте доступ к истории буфера обмена и управляйте ею из панели запуска.",
|
||||
"clipboard-desc": "Получайте доступ к истории буфера обмена и управляйте ею из лаунчера.",
|
||||
"execute-desc": "Настройте способ запуска приложений.",
|
||||
"execute-title": "Исполнение",
|
||||
"settings-annotation-tool-description": "Команда для запуска при нажатии кнопки аннотирования в истории буфера обмена. Изображение будет передано в эту команду.",
|
||||
@@ -1068,7 +1068,7 @@
|
||||
"settings-icon-mode-label": "Использовать нативные иконки",
|
||||
"settings-ignore-mouse-input-description": "Отключить взаимодействие с мышью и колесом прокрутки в лаунчере.",
|
||||
"settings-ignore-mouse-input-label": "Игнорировать ввод мыши",
|
||||
"settings-position-description": "Выберите, где появляется панель запуска.",
|
||||
"settings-position-description": "Выберите, где появляется панель лаунчера.",
|
||||
"settings-show-categories-description": "Показывать вкладки категорий для фильтрации приложений.",
|
||||
"settings-show-categories-label": "Показывать категории",
|
||||
"settings-show-icon-background-description": "Показывать закруглённый прямоугольник в качестве фона для иконок.",
|
||||
@@ -1079,7 +1079,7 @@
|
||||
"settings-terminal-command-label": "Команда терминала",
|
||||
"settings-use-app2unit-description": "Использует альтернативный метод запуска для лучшего управления процессами приложений и предотвращения проблем.",
|
||||
"settings-use-app2unit-label": "Использовать App2Unit для запуска приложений",
|
||||
"title": "Запуск"
|
||||
"title": "Лаунчер"
|
||||
},
|
||||
"location": {
|
||||
"calendar-cards-desc": "Организуйте и включайте/выключайте карточки в панели календаря.",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"NotificationHistory": {
|
||||
"showUnreadBadge": true,
|
||||
"hideWhenZero": false,
|
||||
"hideWhenZeroUnread": false
|
||||
"hideWhenZeroUnread": false,
|
||||
"unreadBadgeColor": "primary"
|
||||
},
|
||||
"SessionMenu": {
|
||||
"colorName": "error"
|
||||
@@ -177,7 +178,12 @@
|
||||
"unfocusedIconsOpacity": 1,
|
||||
"groupedBorderOpacity": 1,
|
||||
"enableScrollWheel": true,
|
||||
"iconScale": 0.8
|
||||
"iconScale": 0.8,
|
||||
"focusedColor": "primary",
|
||||
"occupiedColor": "secondary",
|
||||
"emptyColor": "secondary",
|
||||
"showBadge": true,
|
||||
"reverseScroll": false
|
||||
},
|
||||
"Volume": {
|
||||
"displayMode": "onhover",
|
||||
|
||||
@@ -195,7 +195,7 @@ Item {
|
||||
x: isVerticalBar ? Style.pixelAlignCenter(parent.width, width) : 0
|
||||
y: isVerticalBar ? 0 : Style.pixelAlignCenter(parent.height, height)
|
||||
width: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : ((!hasFocusedWindow) && (hideMode === "hidden") ? 0 : dynamicWidth)
|
||||
height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : barHeight
|
||||
height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : capsuleHeight
|
||||
radius: Style.radiusM
|
||||
color: Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
|
||||
@@ -53,21 +53,32 @@ Item {
|
||||
readonly property bool testPluggedIn: false
|
||||
readonly property string deviceNativePath: widgetSettings.deviceNativePath || ""
|
||||
|
||||
readonly property var device: BatteryService.resolveDevice(deviceNativePath)
|
||||
readonly property var battery: device && !BatteryService.isBluetoothDevice(device) ? device : null
|
||||
readonly property var bluetoothDevice: device && BatteryService.isBluetoothDevice(device) ? device : null
|
||||
readonly property var battery: BatteryService.findUPowerDevice(deviceNativePath)
|
||||
readonly property var bluetoothDevice: deviceNativePath ? BatteryService.findBluetoothDevice(deviceNativePath) : null
|
||||
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 : (BatteryService.ready && BatteryService.isDeviceReady(device))
|
||||
readonly property bool isReady: testMode ? true : (initializationComplete && BatteryService.isDeviceReady(device))
|
||||
readonly property real percent: testMode ? testPercent : (isReady ? BatteryService.getPercentage(device) : 0)
|
||||
readonly property bool isCharging: testMode ? testCharging : (isReady ? BatteryService.isCharging(device) : false)
|
||||
readonly property bool isPluggedIn: testMode ? testPluggedIn : (isReady ? BatteryService.isPluggedIn(device) : false)
|
||||
|
||||
property bool initializationComplete: false
|
||||
property bool hasNotifiedLowBattery: false
|
||||
|
||||
visible: shouldShow
|
||||
opacity: shouldShow ? 1.0 : 0.0
|
||||
|
||||
Timer {
|
||||
interval: 500
|
||||
running: true
|
||||
onTriggered: root.initializationComplete = true
|
||||
}
|
||||
|
||||
readonly property bool isDevicePresent: {
|
||||
if (testMode)
|
||||
return true;
|
||||
@@ -82,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;
|
||||
}
|
||||
@@ -93,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;
|
||||
}
|
||||
@@ -110,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 {
|
||||
@@ -141,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)
|
||||
@@ -149,47 +155,55 @@ Item {
|
||||
suffix: "%"
|
||||
autoHide: false
|
||||
forceOpen: isReady && displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide" || (BatteryService.ready && !isReady)
|
||||
customBackgroundColor: !BatteryService.ready ? "transparent" : (isCharging ? Color.mPrimary : (isLowBattery ? Color.mError : "transparent"))
|
||||
customTextIconColor: !BatteryService.ready ? "transparent" : (isCharging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : "transparent"))
|
||||
forceClose: displayMode === "alwaysHide" || (initializationComplete && !isReady)
|
||||
customBackgroundColor: !initializationComplete ? "transparent" : (isCharging ? Color.mPrimary : (isLowBattery ? Color.mError : "transparent"))
|
||||
customTextIconColor: !initializationComplete ? "transparent" : (isCharging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : "transparent"))
|
||||
|
||||
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);
|
||||
|
||||
@@ -37,11 +37,19 @@ NIconButton {
|
||||
readonly property bool hideWhenZeroUnread: (widgetSettings.hideWhenZeroUnread !== undefined) ? widgetSettings.hideWhenZeroUnread : widgetMetadata.hideWhenZeroUnread
|
||||
readonly property string unreadBadgeColor: (widgetSettings.unreadBadgeColor !== undefined) ? widgetSettings.unreadBadgeColor : (widgetMetadata.unreadBadgeColor || "primary")
|
||||
|
||||
readonly property var colorMap: {
|
||||
"primary": Color.mPrimary,
|
||||
"secondary": Color.mSecondary,
|
||||
"tertiary": Color.mTertiary,
|
||||
"onSurface": Color.mOnSurface
|
||||
function getColor(colorKey) {
|
||||
switch (colorKey) {
|
||||
case "primary":
|
||||
return Color.mPrimary;
|
||||
case "secondary":
|
||||
return Color.mSecondary;
|
||||
case "tertiary":
|
||||
return Color.mTertiary;
|
||||
case "onSurface":
|
||||
return Color.mOnSurface;
|
||||
default:
|
||||
return Color.mPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
function computeUnreadCount() {
|
||||
@@ -130,7 +138,7 @@ NIconButton {
|
||||
height: 7
|
||||
width: height
|
||||
radius: Style.radiusXS
|
||||
color: root.colorMap[root.unreadBadgeColor] || Color.mError
|
||||
color: root.hovering ? Color.mOnHover : (root.getColor(root.unreadBadgeColor) || Color.mError)
|
||||
border.color: Color.mSurface
|
||||
border.width: Style.borderS
|
||||
visible: count > 0
|
||||
|
||||
@@ -607,7 +607,7 @@ Item {
|
||||
|
||||
return Math.round(calculatedWidth);
|
||||
}
|
||||
readonly property real contentHeight: visible ? (isVerticalBar ? Math.round(taskbarLayout.implicitHeight + Style.marginS * 2) : barHeight) : 0
|
||||
readonly property real contentHeight: visible ? (isVerticalBar ? Math.round(taskbarLayout.implicitHeight + Style.marginS * 2) : capsuleHeight) : 0
|
||||
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: contentHeight
|
||||
@@ -786,7 +786,7 @@ Item {
|
||||
visible: shouldShowTitle
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: root.barHeight
|
||||
height: root.capsuleHeight
|
||||
color: titleBgColor
|
||||
radius: Style.radiusM
|
||||
|
||||
|
||||
@@ -430,10 +430,23 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
root.hoveredItemIndex = trayDelegate.index;
|
||||
TooltipService.show(tooltipAnchor, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name));
|
||||
} else if (root.hoveredItemIndex === trayDelegate.index) {
|
||||
root.hoveredItemIndex = -1;
|
||||
TooltipService.hide(tooltipAnchor);
|
||||
}
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (!modelData) {
|
||||
return;
|
||||
@@ -499,17 +512,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
root.hoveredItemIndex = trayDelegate.index;
|
||||
TooltipService.show(tooltipAnchor, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name));
|
||||
}
|
||||
onExited: {
|
||||
root.hoveredItemIndex = -1;
|
||||
TooltipService.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,15 @@ SmartPanel {
|
||||
}
|
||||
|
||||
readonly property string deviceNativePath: getBatteryDevicePath()
|
||||
readonly property var selectedBattery: BatteryService.findUPowerDevice(deviceNativePath)
|
||||
readonly property var selectedBluetoothDevice: deviceNativePath ? BatteryService.findBluetoothDevice(deviceNativePath) : null
|
||||
readonly property var selectedDevice: {
|
||||
var dev = selectedBluetoothDevice || selectedBattery;
|
||||
if (BatteryService.isDevicePresent(dev))
|
||||
return dev;
|
||||
|
||||
// Use the centralized helper to find the specific device or fallback to primary
|
||||
readonly property var selectedDevice: BatteryService.resolveDevice(deviceNativePath)
|
||||
return allDevices.length > 0 ? allDevices[0] : null;
|
||||
}
|
||||
|
||||
// Check if selected device is actually present/connected
|
||||
readonly property bool isDevicePresent: BatteryService.isDevicePresent(selectedDevice)
|
||||
@@ -46,22 +52,76 @@ SmartPanel {
|
||||
readonly property int percent: isReady ? Math.round(BatteryService.getPercentage(selectedDevice)) : -1
|
||||
readonly property bool isCharging: BatteryService.isCharging(selectedDevice)
|
||||
readonly property bool isPluggedIn: BatteryService.isPluggedIn(selectedDevice)
|
||||
|
||||
readonly property bool isLaptopBattery: selectedDevice && !BatteryService.isBluetoothDevice(selectedDevice)
|
||||
|
||||
readonly property bool healthAvailable: (isReady && isLaptopBattery && selectedDevice.healthSupported) || (isLaptopBattery && BatteryService.healthAvailable)
|
||||
readonly property int healthPercent: (isReady && isLaptopBattery && selectedDevice.healthSupported) ? Math.round(selectedDevice.healthPercentage) : BatteryService.healthPercent
|
||||
readonly property bool healthAvailable: (isReady && selectedBattery && selectedBattery.healthSupported) || (selectedBattery && BatteryService.healthAvailable)
|
||||
readonly property int healthPercent: (isReady && selectedBattery && selectedBattery.healthSupported) ? Math.round(selectedBattery.healthPercentage) : BatteryService.healthPercent
|
||||
|
||||
readonly property string deviceName: BatteryService.getDeviceName(selectedDevice)
|
||||
readonly property string panelTitle: deviceName ? `${I18n.tr("common.battery")} - ${deviceName}` : I18n.tr("common.battery")
|
||||
|
||||
// Use the centralized list of all devices
|
||||
readonly property var allDevices: BatteryService.devices
|
||||
readonly property var allDevices: {
|
||||
var list = [];
|
||||
var seenPaths = new Set();
|
||||
|
||||
readonly property var laptopBatteries: BatteryService.laptopBatteries
|
||||
readonly property var otherDevices: BatteryService.externalBatteries
|
||||
// Add UPower batteries
|
||||
if (UPower.devices) {
|
||||
var upowerArray = UPower.devices.values || [];
|
||||
for (var i = 0; i < upowerArray.length; i++) {
|
||||
var d = upowerArray[i];
|
||||
if (BatteryService.isDevicePresent(d) && d.type === UPowerDeviceType.Battery) {
|
||||
if (d.nativePath && !seenPaths.has(d.nativePath)) {
|
||||
list.push(d);
|
||||
seenPaths.add(d.nativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add Bluetooth batteries
|
||||
if (BluetoothService.devices) {
|
||||
var btArray = BluetoothService.devices.values || [];
|
||||
for (var j = 0; j < btArray.length; j++) {
|
||||
var btd = btArray[j];
|
||||
if (BatteryService.isDevicePresent(btd) && btd.batteryAvailable) {
|
||||
// Bluetooth devices use address as unique ID
|
||||
if (btd.address && !seenPaths.has(btd.address)) {
|
||||
list.push(btd);
|
||||
seenPaths.add(btd.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string timeText: BatteryService.getTimeRemainingText(selectedDevice)
|
||||
// Fallback: if no specific batteries found but display device is a battery, use it
|
||||
if (list.length === 0 && UPower.displayDevice && UPower.displayDevice.type === UPowerDeviceType.Battery && BatteryService.isDevicePresent(UPower.displayDevice)) {
|
||||
list.push(UPower.displayDevice);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
readonly property var laptopBatteries: allDevices.filter(d => !BatteryService.isBluetoothDevice(d))
|
||||
readonly property var otherDevices: allDevices.filter(d => BatteryService.isBluetoothDevice(d))
|
||||
|
||||
readonly property string timeText: {
|
||||
if (!isReady || !isDevicePresent) {
|
||||
return I18n.tr("battery.no-battery-detected");
|
||||
}
|
||||
if (isPluggedIn) {
|
||||
return I18n.tr("battery.plugged-in");
|
||||
}
|
||||
if (selectedDevice) {
|
||||
if (selectedDevice.timeToFull > 0) {
|
||||
return I18n.tr("battery.time-until-full", {
|
||||
"time": Time.formatVagueHumanReadableDuration(selectedDevice.timeToFull)
|
||||
});
|
||||
}
|
||||
if (selectedDevice.timeToEmpty > 0) {
|
||||
return I18n.tr("battery.time-left", {
|
||||
"time": Time.formatVagueHumanReadableDuration(selectedDevice.timeToEmpty)
|
||||
});
|
||||
}
|
||||
}
|
||||
return I18n.tr("common.idle");
|
||||
}
|
||||
readonly property string iconName: BatteryService.getIcon(percent, isCharging, isPluggedIn, isReady)
|
||||
|
||||
property var batteryWidgetInstance: BarService.lookupWidget("Battery", screen ? screen.name : null)
|
||||
@@ -242,7 +302,7 @@ SmartPanel {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
visible: modelData.healthSupported || (modelData === BatteryService.primaryDevice && BatteryService.healthAvailable)
|
||||
visible: modelData.healthSupported || (modelData === selectedBattery && BatteryService.healthAvailable)
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
@@ -273,14 +333,14 @@ SmartPanel {
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
width: {
|
||||
var h = modelData.healthSupported ? modelData.healthPercentage : (modelData === BatteryService.primaryDevice ? BatteryService.healthPercent : 0);
|
||||
var h = modelData.healthSupported ? modelData.healthPercentage : (modelData === selectedBattery ? BatteryService.healthPercent : 0);
|
||||
if (h <= 0)
|
||||
return 0;
|
||||
var ratio = Math.max(0, Math.min(1, h / 100));
|
||||
return parent.width * ratio;
|
||||
}
|
||||
color: {
|
||||
var h = modelData.healthSupported ? modelData.healthPercentage : (modelData === BatteryService.primaryDevice ? BatteryService.healthPercent : 0);
|
||||
var h = modelData.healthSupported ? modelData.healthPercentage : (modelData === selectedBattery ? BatteryService.healthPercent : 0);
|
||||
return h >= 80 ? Color.mPrimary : (h >= 50 ? Color.mTertiary : Color.mError);
|
||||
}
|
||||
}
|
||||
@@ -289,7 +349,7 @@ SmartPanel {
|
||||
Layout.preferredWidth: 40 * Style.uiScaleRatio
|
||||
horizontalAlignment: Text.AlignRight
|
||||
|
||||
readonly property int h: modelData.healthSupported ? Math.round(modelData.healthPercentage) : (modelData === BatteryService.primaryDevice ? BatteryService.healthPercent : -1)
|
||||
readonly property int h: modelData.healthSupported ? Math.round(modelData.healthPercentage) : (modelData === selectedBattery ? BatteryService.healthPercent : -1)
|
||||
text: h >= 0 ? `${h}%` : "--"
|
||||
color: Color.mOnSurface
|
||||
pointSize: Style.fontSizeS
|
||||
|
||||
@@ -179,9 +179,8 @@ SmartPanel {
|
||||
readonly property int gridContentWidth: listPanelWidth - (2 * Style.marginXS)
|
||||
readonly property int gridCellSize: Math.floor((gridContentWidth - ((targetGridColumns - 1) * Style.marginS)) / targetGridColumns)
|
||||
|
||||
// Actual columns that fit in the GridView
|
||||
// This gets updated dynamically by the GridView when its actual width is known
|
||||
property int gridColumns: 5
|
||||
// Actual columns in the GridView - tracks targetGridColumns
|
||||
readonly property int gridColumns: targetGridColumns
|
||||
|
||||
// Listen for plugin provider registry changes
|
||||
Connections {
|
||||
@@ -1375,20 +1374,6 @@ SmartPanel {
|
||||
focus: false
|
||||
interactive: !Settings.data.appLauncher.ignoreMouseInput
|
||||
|
||||
Component.onCompleted: {
|
||||
// Initialize gridColumns when grid view is created
|
||||
updateGridColumns();
|
||||
}
|
||||
|
||||
function updateGridColumns() {
|
||||
// Since cellWidth = width / targetGridColumns, the number of columns is always targetGridColumns
|
||||
root.gridColumns = root.targetGridColumns;
|
||||
}
|
||||
|
||||
onWidthChanged: {
|
||||
updateGridColumns();
|
||||
}
|
||||
|
||||
// Completely disable GridView key handling
|
||||
Keys.enabled: false
|
||||
|
||||
|
||||
@@ -547,8 +547,9 @@ Item {
|
||||
"_score": (score !== undefined ? score : 0),
|
||||
"provider": root,
|
||||
"onActivate": function () {
|
||||
// Close the launcher/SmartPanel with animation.
|
||||
launcher.close();
|
||||
// Close the launcher/SmartPanel immediately without any animations.
|
||||
// Ensures we are not preventing the future focusing of the app
|
||||
launcher.closeImmediately();
|
||||
|
||||
// Defer execution to next event loop iteration to ensure panel is fully closed
|
||||
Qt.callLater(() => {
|
||||
|
||||
@@ -39,7 +39,7 @@ Item {
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.close();
|
||||
launcher.closeImmediately();
|
||||
Qt.callLater(() => {
|
||||
Quickshell.execDetached(["sh", "-lc", expression]);
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ Item {
|
||||
readonly property string emptyBrowsingMessage: selectedCategory === "recent" ? I18n.tr("launcher.providers.emoji-no-recent") : ""
|
||||
|
||||
property var categoryIcons: ({
|
||||
"all": "apps",
|
||||
"recent": "clock",
|
||||
"people": "user",
|
||||
"animals": "paw",
|
||||
@@ -35,10 +36,11 @@ Item {
|
||||
"flags": "flag"
|
||||
})
|
||||
|
||||
property var categories: ["recent", "people", "animals", "nature", "food", "activity", "travel", "objects", "symbols", "flags"]
|
||||
property var categories: ["all", "recent", "people", "animals", "nature", "food", "activity", "travel", "objects", "symbols", "flags"]
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
"all": I18n.tr("launcher.categories.all"),
|
||||
"recent": I18n.tr("launcher.categories.emoji-recent"),
|
||||
"people": I18n.tr("launcher.categories.emoji-people"),
|
||||
"animals": I18n.tr("launcher.categories.emoji-animals"),
|
||||
@@ -121,16 +123,14 @@ Item {
|
||||
}
|
||||
|
||||
var query = searchText.slice(6).trim();
|
||||
var emojis = [];
|
||||
|
||||
if (query === "") {
|
||||
showsCategories = true;
|
||||
var emojis = EmojiService.getEmojisByCategory(selectedCategory);
|
||||
return emojis.map(formatEmojiEntry);
|
||||
if (query !== "" || selectedCategory === "all") {
|
||||
emojis = EmojiService.search(query);
|
||||
} else {
|
||||
showsCategories = false;
|
||||
var emojis = EmojiService.search(query);
|
||||
return emojis.map(formatEmojiEntry);
|
||||
emojis = EmojiService.getEmojisByCategory(selectedCategory);
|
||||
}
|
||||
return emojis.map(formatEmojiEntry);
|
||||
}
|
||||
|
||||
// Format an emoji entry for the results list
|
||||
|
||||
@@ -24,14 +24,7 @@ ColumnLayout {
|
||||
property bool valueHideIfNotDetected: widgetData.hideIfNotDetected !== undefined ? widgetData.hideIfNotDetected : widgetMetadata.hideIfNotDetected
|
||||
property bool valueHideIfIdle: widgetData.hideIfIdle !== undefined ? widgetData.hideIfIdle : widgetMetadata.hideIfIdle
|
||||
|
||||
property var deviceModel: BatteryService.getDeviceOptionsModel()
|
||||
|
||||
Connections {
|
||||
target: BatteryService
|
||||
function onDevicesChanged() {
|
||||
deviceModel = BatteryService.getDeviceOptionsModel();
|
||||
}
|
||||
}
|
||||
property var deviceModel: BatteryService.devicesModel
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
@@ -81,9 +74,8 @@ ColumnLayout {
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
// TODO i18n
|
||||
tooltipText: "Refresh device list"
|
||||
onClicked: deviceModel = BatteryService.getDeviceOptionsModel()
|
||||
tooltipText: I18n.tr("common.refresh")
|
||||
onClicked: BatteryService.devicesModel = BatteryService.buildDeviceModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -554,7 +554,9 @@ Item {
|
||||
ProgramCheckerService.checkAllPrograms();
|
||||
updateTabsModel();
|
||||
selectTabById(requestedTab);
|
||||
if (sidebarExpanded) {
|
||||
// Skip auto-focus on Nvidia GPUs - cursor blink causes UI choppiness
|
||||
const isNvidia = SystemStatService.gpuType === "nvidia";
|
||||
if (sidebarExpanded && !isNvidia) {
|
||||
Qt.callLater(() => {
|
||||
if (searchInput.inputItem)
|
||||
searchInput.inputItem.forceActiveFocus();
|
||||
@@ -737,7 +739,7 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("common.search")
|
||||
inputIconName: "search"
|
||||
visible: root.sidebarExpanded
|
||||
visible: opacity > 0
|
||||
opacity: root.sidebarExpanded ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
@@ -759,7 +761,7 @@ Item {
|
||||
id: searchCollapsedContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(searchCollapsedRow.implicitHeight + Style.marginS * 2)
|
||||
visible: !root.sidebarExpanded
|
||||
visible: opacity > 0
|
||||
opacity: !root.sidebarExpanded ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
|
||||
@@ -194,12 +194,15 @@ ColumnLayout {
|
||||
}
|
||||
var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen);
|
||||
if (setupPanel) {
|
||||
setupPanel.telemetryOnlyMode = false;
|
||||
setupPanel.open();
|
||||
} else {
|
||||
Qt.callLater(() => {
|
||||
var sp = PanelService.getPanel("setupWizardPanel", targetScreen);
|
||||
if (sp)
|
||||
sp.open();
|
||||
if (sp) {
|
||||
sp.telemetryOnlyMode = false;
|
||||
sp.open();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,14 @@ ghostty)
|
||||
CONFIG_FILE="$HOME/.config/ghostty/config"
|
||||
# Check if the config file exists before trying to modify it.
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# Check if theme is already set to noctalia
|
||||
if ! grep -q "^theme = noctalia" "$CONFIG_FILE"; then
|
||||
# Remove any existing theme include line to prevent duplicates.
|
||||
sed -i '/theme/d' "$CONFIG_FILE"
|
||||
# Add the new theme include line to the end of the file.
|
||||
# Check if theme is already set to noctalia (flexible spacing)
|
||||
if grep -qE "^theme\s*=\s*noctalia$" "$CONFIG_FILE"; then
|
||||
: # Already correct
|
||||
elif grep -qE "^theme\s*=" "$CONFIG_FILE"; then
|
||||
# Replace existing theme line in-place
|
||||
sed -i -E 's/^theme\s*=.*/theme = noctalia/' "$CONFIG_FILE"
|
||||
else
|
||||
# Add the new theme line to the end of the file
|
||||
echo "theme = noctalia" >>"$CONFIG_FILE"
|
||||
fi
|
||||
# Only signal if ghostty is running
|
||||
@@ -158,10 +161,13 @@ include=~/.config/fuzzel/themes/noctalia
|
||||
EOF
|
||||
else
|
||||
# Check if theme is already set to noctalia
|
||||
if ! grep -q "include=~/.config/fuzzel/themes/noctalia" "$CONFIG_FILE"; then
|
||||
# Remove any existing theme include line.
|
||||
sed -i '/themes/d' "$CONFIG_FILE"
|
||||
# Add the new theme include line.
|
||||
if grep -q "^include=~/.config/fuzzel/themes/noctalia$" "$CONFIG_FILE"; then
|
||||
: # Already correct
|
||||
elif grep -q "^include=.*themes" "$CONFIG_FILE"; then
|
||||
# Replace existing theme include line in-place
|
||||
sed -i 's|^include=.*themes.*|include=~/.config/fuzzel/themes/noctalia|' "$CONFIG_FILE"
|
||||
else
|
||||
# Add the new theme include line
|
||||
echo "include=~/.config/fuzzel/themes/noctalia" >>"$CONFIG_FILE"
|
||||
fi
|
||||
fi
|
||||
@@ -172,14 +178,14 @@ walker)
|
||||
|
||||
# Check if the config file exists.
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# Check if theme is already set to noctalia
|
||||
if ! grep -q '^theme = "noctalia"' "$CONFIG_FILE"; then
|
||||
# Check if a theme line exists and replace it, otherwise append
|
||||
if grep -q '^theme = ' "$CONFIG_FILE"; then
|
||||
sed -i 's/^theme = .*/theme = "noctalia"/' "$CONFIG_FILE"
|
||||
else
|
||||
echo 'theme = "noctalia"' >>"$CONFIG_FILE"
|
||||
fi
|
||||
# Check if theme is already set to noctalia (flexible spacing)
|
||||
if grep -qE '^theme\s*=\s*"noctalia"' "$CONFIG_FILE"; then
|
||||
: # Already correct
|
||||
elif grep -qE '^theme\s*=' "$CONFIG_FILE"; then
|
||||
# Replace existing theme line in-place
|
||||
sed -i -E 's/^theme\s*=.*/theme = "noctalia"/' "$CONFIG_FILE"
|
||||
else
|
||||
echo 'theme = "noctalia"' >>"$CONFIG_FILE"
|
||||
fi
|
||||
else
|
||||
echo "Error: walker config file not found at $CONFIG_FILE" >&2
|
||||
@@ -213,18 +219,17 @@ cava)
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# Check if [color] section exists
|
||||
if grep -q '^\[color\]' "$CONFIG_FILE"; then
|
||||
# Check if theme is already set to noctalia under [color]
|
||||
if ! sed -n '/^\[color\]/,/^\[/p' "$CONFIG_FILE" | grep -q '^theme = "noctalia"'; then
|
||||
# Check if theme line exists under [color] section
|
||||
if sed -n '/^\[color\]/,/^\[/p' "$CONFIG_FILE" | grep -q '^theme = '; then
|
||||
# Replace existing theme line under [color]
|
||||
sed -i '/^\[color\]/,/^\[/{s/^theme = .*/theme = "noctalia"/}' "$CONFIG_FILE"
|
||||
THEME_MODIFIED=true
|
||||
else
|
||||
# Add theme line after [color]
|
||||
sed -i '/^\[color\]/a theme = "noctalia"' "$CONFIG_FILE"
|
||||
THEME_MODIFIED=true
|
||||
fi
|
||||
# Check if theme is already set to noctalia under [color] (flexible spacing)
|
||||
if sed -n '/^\[color\]/,/^\[/p' "$CONFIG_FILE" | grep -qE '^theme\s*=\s*"noctalia"'; then
|
||||
: # Already correct
|
||||
elif sed -n '/^\[color\]/,/^\[/p' "$CONFIG_FILE" | grep -qE '^theme\s*='; then
|
||||
# Replace existing theme line under [color]
|
||||
sed -i -E '/^\[color\]/,/^\[/{s/^theme\s*=.*/theme = "noctalia"/}' "$CONFIG_FILE"
|
||||
THEME_MODIFIED=true
|
||||
else
|
||||
# Add theme line after [color]
|
||||
sed -i '/^\[color\]/a theme = "noctalia"' "$CONFIG_FILE"
|
||||
THEME_MODIFIED=true
|
||||
fi
|
||||
else
|
||||
# Add [color] section with theme at the end of file
|
||||
@@ -292,8 +297,10 @@ niri)
|
||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||
echo -e "\n$INCLUDE_LINE\n" >"$CONFIG_FILE"
|
||||
else
|
||||
# Check if include line already exists
|
||||
if ! grep -qF "$INCLUDE_LINE" "$CONFIG_FILE"; then
|
||||
# Check if noctalia include already exists (flexible: quotes, ./ prefix)
|
||||
if grep -qE 'include\s+["'"'"'](\./)?noctalia\.kdl["'"'"']' "$CONFIG_FILE"; then
|
||||
: # Already included
|
||||
else
|
||||
# Add the include line to the end of the file
|
||||
echo -e "\n$INCLUDE_LINE\n" >>"$CONFIG_FILE"
|
||||
fi
|
||||
@@ -321,8 +328,8 @@ hyprland)
|
||||
chmod +w "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Check if include line already exists
|
||||
if grep -qF "$INCLUDE_LINE" "$CONFIG_FILE"; then
|
||||
# Check if noctalia theme source already exists (flexible matching)
|
||||
if grep -qE 'source\s*=\s*.*noctalia.*\.conf' "$CONFIG_FILE"; then
|
||||
echo "Theme already included, skipping modification."
|
||||
else
|
||||
# Add the include line to the end of the file
|
||||
@@ -394,12 +401,14 @@ btop)
|
||||
CONFIG_FILE="$HOME/.config/btop/btop.conf"
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
if ! grep -q '^color_theme = "noctalia"' "$CONFIG_FILE"; then
|
||||
if grep -q '^color_theme = ' "$CONFIG_FILE"; then
|
||||
sed -i 's/^color_theme = .*/color_theme = "noctalia"/' "$CONFIG_FILE"
|
||||
else
|
||||
echo 'color_theme = "noctalia"' >>"$CONFIG_FILE"
|
||||
fi
|
||||
# Check if theme is already set to noctalia (flexible spacing)
|
||||
if grep -qE '^color_theme\s*=\s*"noctalia"' "$CONFIG_FILE"; then
|
||||
: # Already correct
|
||||
elif grep -qE '^color_theme\s*=' "$CONFIG_FILE"; then
|
||||
# Replace existing color_theme line in-place
|
||||
sed -i -E 's/^color_theme\s*=.*/color_theme = "noctalia"/' "$CONFIG_FILE"
|
||||
else
|
||||
echo 'color_theme = "noctalia"' >>"$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
if pgrep -x btop >/dev/null; then
|
||||
|
||||
@@ -46,6 +46,10 @@ class TerminalColors:
|
||||
visual_bell: str
|
||||
indexed: dict[int, str]
|
||||
tab_bar: dict
|
||||
|
||||
# Kitty border colors
|
||||
active_border: str
|
||||
inactive_border: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict, scheme: dict) -> "TerminalColors":
|
||||
@@ -69,6 +73,7 @@ class TerminalColors:
|
||||
m_primary = scheme.get("mPrimary", cursor)
|
||||
m_on_primary = scheme.get("mOnPrimary", cursor_text)
|
||||
m_secondary = scheme.get("mSecondary", normal["yellow"])
|
||||
m_surface_variant = scheme.get("mSurfaceVariant", selection_bg)
|
||||
|
||||
return cls(
|
||||
foreground=foreground,
|
||||
@@ -94,6 +99,10 @@ class TerminalColors:
|
||||
"newTab": {"bg": selection_bg, "fg": foreground},
|
||||
"newTabHover": {"bg": bright["black"], "fg": foreground},
|
||||
},
|
||||
|
||||
# Kitty border colors
|
||||
active_border=m_primary,
|
||||
inactive_border=m_secondary
|
||||
)
|
||||
|
||||
|
||||
@@ -186,6 +195,8 @@ class TerminalGenerator:
|
||||
lines.append(f"cursor_text_color {self._ensure_hash(c.cursor_text)}")
|
||||
lines.append(f"foreground {self._ensure_hash(c.foreground)}")
|
||||
lines.append(f"selection_background {self._ensure_hash(c.foreground)}")
|
||||
lines.append(f"active_border_color {self._ensure_hash(c.active_border)}")
|
||||
lines.append(f"inactive_border_color {self._ensure_hash(c.inactive_border)}")
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
@@ -422,15 +422,6 @@ Item {
|
||||
function disable() {
|
||||
NetworkService.setWifiEnabled(false);
|
||||
}
|
||||
|
||||
// TODO REMOVE IN FEB. 2026
|
||||
function togglePanel() {
|
||||
ToastService.showWarning("This IPC call will be deprecated soon, use 'network togglePanel' instead.");
|
||||
root.screenDetector.withCurrentScreen(screen => {
|
||||
var networkPanel = PanelService.getPanel("networkPanel", screen);
|
||||
networkPanel?.toggle(null, "WiFi");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
@@ -509,19 +500,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO REMOVE IN FEB. 2026
|
||||
IpcHandler {
|
||||
target: "osd"
|
||||
|
||||
function showText(text: string) {
|
||||
ToastService.showNotice(text, "This IPC call will be deprecated soon, use 'toast send' instead.");
|
||||
}
|
||||
|
||||
function showTextWithIcon(text: string, icon: string) {
|
||||
ToastService.showNotice(text, "This IPC call will be deprecated soon, use 'toast send' instead.", icon);
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "toast"
|
||||
|
||||
|
||||
@@ -11,60 +11,27 @@ import qs.Services.UI
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// 1. Centralized list of all batteries
|
||||
readonly property var devices: {
|
||||
var list = [];
|
||||
var seenPaths = new Set();
|
||||
// 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)
|
||||
|
||||
// Add UPower batteries
|
||||
var upowerBatteryCount = 0;
|
||||
if (UPower.devices) {
|
||||
var upowerArray = UPower.devices.values || [];
|
||||
for (var i = 0; i < upowerArray.length; i++) {
|
||||
var d = upowerArray[i];
|
||||
if (isDevicePresent(d) && d.type === UPowerDeviceType.Battery) {
|
||||
if (d.nativePath && !seenPaths.has(d.nativePath)) {
|
||||
list.push(d);
|
||||
seenPaths.add(d.nativePath);
|
||||
upowerBatteryCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add DisplayDevice (Aggregate)
|
||||
if (UPower.displayDevice && UPower.displayDevice.type === UPowerDeviceType.Battery && isDevicePresent(UPower.displayDevice)) {
|
||||
if (upowerBatteryCount !== 1) {
|
||||
list.push(UPower.displayDevice);
|
||||
}
|
||||
}
|
||||
// Add Bluetooth batteries
|
||||
if (BluetoothService.devices) {
|
||||
var btArray = BluetoothService.devices.values || [];
|
||||
for (var j = 0; j < btArray.length; j++) {
|
||||
var btd = btArray[j];
|
||||
if (isDevicePresent(btd) && btd.batteryAvailable) {
|
||||
// Bluetooth devices use address as unique ID
|
||||
if (btd.address && !seenPaths.has(btd.address)) {
|
||||
list.push(btd);
|
||||
seenPaths.add(btd.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
property bool healthAvailable: false
|
||||
property int healthPercent: -1
|
||||
|
||||
// 2. Determine the primary device (System Battery)
|
||||
readonly property var primaryDevice: {
|
||||
if (devices.length === 0)
|
||||
return null;
|
||||
readonly property var _laptopBattery: {
|
||||
if (!UPower.devices)
|
||||
return UPower.displayDevice;
|
||||
|
||||
// Prioritize DisplayDevice (Aggregate)
|
||||
if (UPower.displayDevice && UPower.displayDevice.type === UPowerDeviceType.Battery && isDevicePresent(UPower.displayDevice)) {
|
||||
return UPower.displayDevice;
|
||||
}
|
||||
var devices = UPower.devices.values || [];
|
||||
|
||||
// Prioritize BAT0
|
||||
// 1. Explicitly look for BAT0 first
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
var d = devices[i];
|
||||
if (d && (d.nativePath === "BAT0" || d.objectPath === "/org/freedesktop/UPower/devices/battery_BAT0")) {
|
||||
@@ -72,79 +39,105 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Prioritize (any) Laptop Battery
|
||||
// 2. Fallback to displayDevice if it's a laptop battery
|
||||
if (UPower.displayDevice && UPower.displayDevice.isLaptopBattery) {
|
||||
return UPower.displayDevice;
|
||||
}
|
||||
|
||||
// 3. Any other device marked as a laptop battery
|
||||
for (var j = 0; j < devices.length; j++) {
|
||||
var dev = devices[j];
|
||||
if (dev && !isBluetoothDevice(dev) && dev.isLaptopBattery) {
|
||||
return dev;
|
||||
var device = devices[j];
|
||||
if (device && device.type === UPowerDeviceType.Battery && device.isLaptopBattery) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the first available device
|
||||
return devices[0];
|
||||
if (UPower.displayDevice.isPresent) {
|
||||
return UPower.displayDevice;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Whether the primary device is a laptop battery
|
||||
readonly property bool isLaptopBattery: primaryDevice !== null && !isBluetoothDevice(primaryDevice) && primaryDevice.isLaptopBattery
|
||||
|
||||
// Global properties for the Primary Device (used by LockScreen etc)
|
||||
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)
|
||||
|
||||
// Exposed subsets of devices
|
||||
readonly property var laptopBatteries: devices.filter(d => !isBluetoothDevice(d))
|
||||
readonly property var externalBatteries: devices.filter(d => isBluetoothDevice(d))
|
||||
|
||||
property bool healthAvailable: false
|
||||
property int healthPercent: -1
|
||||
|
||||
// Initialization state
|
||||
property bool initializationComplete: false
|
||||
readonly property bool ready: initializationComplete
|
||||
|
||||
Timer {
|
||||
interval: 500
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: root.initializationComplete = true
|
||||
readonly property var _bluetoothBattery: {
|
||||
if (externalBatteries.length > 0)
|
||||
return externalBatteries[0];
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. Helper to resolve a device by path, or return primary if path is empty/invalid
|
||||
// MARK: resolveDevice
|
||||
function resolveDevice(nativePath) {
|
||||
if (!nativePath || nativePath === "") {
|
||||
return primaryDevice;
|
||||
}
|
||||
|
||||
// Check for DisplayDevice explicitly if requested via "DisplayDevice" or empty string
|
||||
if (nativePath === "DisplayDevice" && UPower.displayDevice) {
|
||||
// Check for DisplayDevice explicitly (Literal key OR actual native path)
|
||||
if ((nativePath === "DisplayDevice" || (UPower.displayDevice && nativePath === UPower.displayDevice.nativePath)) && UPower.displayDevice) {
|
||||
return UPower.displayDevice;
|
||||
}
|
||||
|
||||
// Search in our cached list
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
var d = devices[i];
|
||||
if (isBluetoothDevice(d)) {
|
||||
if (d.address && d.address.toUpperCase() === nativePath.toUpperCase())
|
||||
return d;
|
||||
// Try matching MAC in path string if passed format differs
|
||||
if (nativePath.includes(d.address.toUpperCase()))
|
||||
return d;
|
||||
} else {
|
||||
if (d.nativePath === nativePath)
|
||||
return d;
|
||||
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;
|
||||
}
|
||||
|
||||
if (!UPower.devices) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// MARK: findBluetoothDevice
|
||||
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;
|
||||
}
|
||||
|
||||
// MARK: isDevicePresent
|
||||
function isDevicePresent(device) {
|
||||
if (!device)
|
||||
return false;
|
||||
|
||||
// Handle Bluetooth devices
|
||||
// Handle Bluetooth devices (identified by having batteryAvailable property)
|
||||
if (device.batteryAvailable !== undefined) {
|
||||
return device.connected === true;
|
||||
}
|
||||
@@ -154,11 +147,15 @@ Singleton {
|
||||
if (device.type === UPowerDeviceType.Battery && device.isPresent !== undefined) {
|
||||
return device.isPresent === true;
|
||||
}
|
||||
|
||||
// Fallback for non-battery UPower devices or if isPresent is missing
|
||||
return device.ready && device.percentage !== undefined;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// MARK: isDeviceReady
|
||||
function isDeviceReady(device) {
|
||||
if (!isDevicePresent(device))
|
||||
return false;
|
||||
@@ -166,9 +163,11 @@ Singleton {
|
||||
if (device.batteryAvailable !== undefined) {
|
||||
return device.battery !== undefined;
|
||||
}
|
||||
|
||||
return device.ready && device.percentage !== undefined;
|
||||
}
|
||||
|
||||
// MARK: getPercentage
|
||||
function getPercentage(device) {
|
||||
if (!device)
|
||||
return 0;
|
||||
@@ -178,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 "";
|
||||
@@ -214,70 +225,9 @@ Singleton {
|
||||
return "";
|
||||
}
|
||||
|
||||
function getTimeRemainingText(device) {
|
||||
if (!ready || !isDevicePresent(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");
|
||||
}
|
||||
|
||||
function getDeviceOptionsModel() {
|
||||
var model = [
|
||||
{
|
||||
"key": "",
|
||||
"name": I18n.tr("bar.battery.device-default")
|
||||
}
|
||||
];
|
||||
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
var d = devices[i];
|
||||
var name = "";
|
||||
|
||||
// Determine friendly name
|
||||
if (isBluetoothDevice(d)) {
|
||||
name = d.name || "Bluetooth Device";
|
||||
} else if (d === UPower.displayDevice) {
|
||||
name = I18n.tr("common.battery-aggregate") || "Display Device";
|
||||
} else {
|
||||
name = d.model || I18n.tr("common.battery");
|
||||
}
|
||||
|
||||
// Determine ID/Path
|
||||
var key = isBluetoothDevice(d) ? d.address : d.nativePath;
|
||||
if (!key && d === UPower.displayDevice)
|
||||
key = "DisplayDevice";
|
||||
|
||||
// Format: "Model (ID)"
|
||||
var displayName = name;
|
||||
if (key && key !== "DisplayDevice") {
|
||||
displayName = `${name} (${key})`;
|
||||
}
|
||||
|
||||
model.push({
|
||||
"key": key || "",
|
||||
"name": displayName
|
||||
});
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// MARK: refreshHealth
|
||||
function refreshHealth() {
|
||||
if (!isLaptopBattery || !primaryDevice) {
|
||||
if (!isLaptopBattery) {
|
||||
healthAvailable = false;
|
||||
healthPercent = -1;
|
||||
return;
|
||||
@@ -287,7 +237,6 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: healthProcess
|
||||
// Dynamically target the primary device if possible, otherwise fall back to first battery
|
||||
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"
|
||||
@@ -314,7 +263,7 @@ Singleton {
|
||||
Qt.callLater(refreshHealth);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: getIcon
|
||||
function getIcon(percent, charging, pluggedIn, isReady) {
|
||||
if (!isReady) {
|
||||
return "battery-exclamation";
|
||||
@@ -343,7 +292,117 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!query || query.trim() === "") {
|
||||
// Return popular/recently used emojis, fallback to all emojis sorted by usage
|
||||
return _getPopularEmojis(50);
|
||||
return emojis;
|
||||
}
|
||||
|
||||
const terms = query.toLowerCase().split(" ").filter(t => t);
|
||||
|
||||
@@ -11,7 +11,7 @@ Singleton {
|
||||
id: root
|
||||
|
||||
// Version properties
|
||||
readonly property string baseVersion: "4.2.4"
|
||||
readonly property string baseVersion: "4.2.6"
|
||||
readonly property bool isDevelopment: true
|
||||
readonly property string developmentSuffix: "-git"
|
||||
readonly property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + developmentSuffix}`
|
||||
|
||||
@@ -187,7 +187,8 @@ Singleton {
|
||||
"NotificationHistory": {
|
||||
"showUnreadBadge": true,
|
||||
"hideWhenZero": false,
|
||||
"hideWhenZeroUnread": false
|
||||
"hideWhenZeroUnread": false,
|
||||
"unreadBadgeColor": "primary"
|
||||
},
|
||||
"SessionMenu": {
|
||||
"colorName": "error"
|
||||
|
||||
@@ -88,12 +88,22 @@ Singleton {
|
||||
return null;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (pendingTooltip) {
|
||||
pendingTooltip.hide();
|
||||
}
|
||||
if (activeTooltip) {
|
||||
activeTooltip.hide();
|
||||
function hide(target) {
|
||||
// If target is provided, only hide if tooltip belongs to that target
|
||||
if (target) {
|
||||
if (pendingTooltip && pendingTooltip.targetItem === target) {
|
||||
pendingTooltip.hide();
|
||||
}
|
||||
if (activeTooltip && activeTooltip.targetItem === target) {
|
||||
activeTooltip.hide();
|
||||
}
|
||||
} else {
|
||||
if (pendingTooltip) {
|
||||
pendingTooltip.hide();
|
||||
}
|
||||
if (activeTooltip) {
|
||||
activeTooltip.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,13 +103,13 @@ Item {
|
||||
onExited: {
|
||||
hovering = false;
|
||||
if (tooltipText) {
|
||||
TooltipService.hide();
|
||||
TooltipService.hide(root);
|
||||
}
|
||||
root.exited();
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (tooltipText) {
|
||||
TooltipService.hide();
|
||||
TooltipService.hide(root);
|
||||
}
|
||||
if (!root.enabled && !allowClickWhenDisabled) {
|
||||
return;
|
||||
|
||||
@@ -40,9 +40,16 @@ Item {
|
||||
property real scrollCycleDuration: Math.max(4000, root.text.length * 120)
|
||||
property real resettingDuration: 300
|
||||
|
||||
readonly property real contentWidth: {
|
||||
if (!titleText.item)
|
||||
return 0;
|
||||
const implicit = titleText.item.implicitWidth;
|
||||
return implicit > 0 ? implicit : titleText.item.width;
|
||||
}
|
||||
readonly property real measuredWidth: scrollContainer.width
|
||||
|
||||
clip: true
|
||||
implicitWidth: alwaysMaxWidth ? maxWidth : Math.min(maxWidth, contentWidth)
|
||||
implicitHeight: titleText.height
|
||||
|
||||
enum ScrollState {
|
||||
@@ -63,13 +70,10 @@ Item {
|
||||
resetState();
|
||||
}
|
||||
onMaxWidthChanged: resetState()
|
||||
onContentWidthChanged: root.updateState()
|
||||
onForcedHoverChanged: updateState()
|
||||
|
||||
function resetState() {
|
||||
root.implicitWidth = Math.min(root.maxWidth, titleText.width);
|
||||
if (alwaysMaxWidth) {
|
||||
root.implicitWidth = root.maxWidth;
|
||||
}
|
||||
root.state = NScrollText.ScrollState.None;
|
||||
scrollContainer.x = 0;
|
||||
scrollTimer.restart();
|
||||
@@ -101,7 +105,7 @@ Item {
|
||||
}
|
||||
|
||||
function updateState() {
|
||||
if (titleText.width <= root.maxWidth || scrollMode === NScrollText.ScrollMode.Never) {
|
||||
if (contentWidth <= root.maxWidth || scrollMode === NScrollText.ScrollMode.Never) {
|
||||
state = NScrollText.ScrollState.None;
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user