From 81b8ff31d845d31bc8116c6a3cb3702c8a85be70 Mon Sep 17 00:00:00 2001 From: Kaj Date: Sat, 26 Jul 2025 06:16:52 +0200 Subject: config: enable/disable status icons (#243) * Adds ability to disable/enable status icons Improves the hover calculations so that it's not hardcoded. This should make it easier to add/remove status icons. Also adds an optional audio status icon. * status: move config to barconfig * fixes * fix merge * loader icons * fix audio popout --------- Co-authored-by: Kaj Giesbers Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- config/BarConfig.qml | 8 ++ modules/bar/Bar.qml | 40 +++---- modules/bar/components/StatusIcons.qml | 208 ++++++++++++++++++++------------- modules/bar/popouts/Audio.qml | 57 +++++++++ modules/bar/popouts/Content.qml | 7 ++ 5 files changed, 219 insertions(+), 101 deletions(-) create mode 100644 modules/bar/popouts/Audio.qml diff --git a/config/BarConfig.qml b/config/BarConfig.qml index c8a8bba..3f9f94d 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -5,6 +5,7 @@ JsonObject { property bool showOnHover: true property int dragThreshold: 20 property Workspaces workspaces: Workspaces {} + property Status status: Status {} property Sizes sizes: Sizes {} component Workspaces: JsonObject { @@ -19,6 +20,13 @@ JsonObject { property string activeLabel: "󰮯 " } + component Status: JsonObject { + property bool showAudio: false + property bool showNetwork: true + property bool showBluetooth: true + property bool showBattery: true + } + component Sizes: JsonObject { property int innerHeight: 30 property int windowPreviewSize: 400 diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 9c84793..8d6f2bb 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -23,14 +23,24 @@ Item { const th = tray.implicitHeight; const trayItems = tray.items; - const n = statusIconsInner.network; - const ny = statusIcons.y + statusIconsInner.y + n.y - spacing / 2; - - const bls = statusIcons.y + statusIconsInner.y + statusIconsInner.bs - spacing / 2; - const ble = statusIcons.y + statusIconsInner.y + statusIconsInner.be + spacing / 2; - - const b = statusIconsInner.battery; - const by = statusIcons.y + statusIconsInner.y + b.y - spacing / 2; + // Check status icons hover areas + let statusIconFound = false; + for (const area of statusIconsInner.hoverAreas) { + if (!area.enabled) + continue; + + const item = area.item; + const itemY = statusIcons.y + statusIconsInner.y + item.y - spacing / 2; + const itemHeight = item.implicitHeight + spacing; + + if (y >= itemY && y <= itemY + itemHeight) { + popouts.currentName = area.name; + popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + item.y + item.implicitHeight / 2); + popouts.hasCurrent = true; + statusIconFound = true; + break; + } + } if (y >= awy && y <= awy + aw.implicitHeight) { popouts.currentName = "activewindow"; @@ -43,19 +53,7 @@ Item { popouts.currentName = `traymenu${index}`; popouts.currentCenter = Qt.binding(() => tray.y + item.y + item.implicitHeight / 2); popouts.hasCurrent = true; - } else if (y >= ny && y <= ny + n.implicitHeight + spacing) { - popouts.currentName = "network"; - popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + n.y + n.implicitHeight / 2); - popouts.hasCurrent = true; - } else if (y >= bls && y <= ble) { - popouts.currentName = "bluetooth"; - popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + statusIconsInner.bs + (statusIconsInner.be - statusIconsInner.bs) / 2); - popouts.hasCurrent = true; - } else if (y >= by && y <= by + b.implicitHeight + spacing) { - popouts.currentName = "battery"; - popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + b.y + b.implicitHeight / 2); - popouts.hasCurrent = true; - } else { + } else if (!statusIconFound) { popouts.hasCurrent = false; } } diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 971ec4d..45f943e 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import qs.widgets import qs.services import qs.utils @@ -6,117 +8,163 @@ import Quickshell import Quickshell.Bluetooth import Quickshell.Services.UPower import QtQuick +import QtQuick.Layouts Item { id: root property color colour: Colours.palette.m3secondary - readonly property Item network: network - readonly property real bs: bluetooth.y - readonly property real be: repeater.count > 0 ? devices.y + devices.implicitHeight : bluetooth.y + bluetooth.implicitHeight - readonly property Item battery: battery + readonly property list hoverAreas: [ + { + name: "audio", + item: audioIcon, + enabled: Config.bar.status.showAudio + }, + { + name: "network", + item: networkIcon, + enabled: Config.bar.status.showNetwork + }, + { + name: "bluetooth", + item: bluetoothGroup, + enabled: Config.bar.status.showBluetooth + }, + { + name: "battery", + item: batteryIcon, + enabled: Config.bar.status.showBattery + } + ] clip: true - implicitWidth: Math.max(network.implicitWidth, bluetooth.implicitWidth, devices.implicitWidth, battery.implicitWidth) - implicitHeight: network.implicitHeight + bluetooth.implicitHeight + bluetooth.anchors.topMargin + (repeater.count > 0 ? devices.implicitHeight + devices.anchors.topMargin : 0) + battery.implicitHeight + battery.anchors.topMargin + implicitWidth: iconColumn.implicitWidth + implicitHeight: iconColumn.implicitHeight - MaterialIcon { - id: network - - animate: true - text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off" - color: root.colour + ColumnLayout { + id: iconColumn anchors.horizontalCenter: parent.horizontalCenter - } - - MaterialIcon { - id: bluetooth - - anchors.horizontalCenter: network.horizontalCenter - anchors.top: network.bottom - anchors.topMargin: Appearance.spacing.smaller / 2 + spacing: Appearance.spacing.smaller / 2 - animate: true - text: Bluetooth.defaultAdapter?.enabled ? "bluetooth" : "bluetooth_disabled" - color: root.colour - } + // Audio icon + Loader { + id: audioIcon - Column { - id: devices + asynchronous: true + active: Config.bar.status.showAudio + visible: active - anchors.horizontalCenter: bluetooth.horizontalCenter - anchors.top: bluetooth.bottom - anchors.topMargin: Appearance.spacing.smaller / 2 + sourceComponent: MaterialIcon { + animate: true + text: Audio.muted ? "volume_off" : Audio.volume >= 0.66 ? "volume_up" : Audio.volume >= 0.33 ? "volume_down" : "volume_mute" + color: root.colour + } + } - spacing: Appearance.spacing.smaller / 2 + // Network icon + Loader { + id: networkIcon - Repeater { - id: repeater + asynchronous: true + active: Config.bar.status.showNetwork + visible: active - model: ScriptModel { - values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) + sourceComponent: MaterialIcon { + animate: true + text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off" + color: root.colour } + } - MaterialIcon { - id: device + // Bluetooth section (grouped for hover area) + Loader { + id: bluetoothGroup - required property BluetoothDevice modelData + asynchronous: true + active: Config.bar.status.showBluetooth + visible: active - animate: true - text: Icons.getBluetoothIcon(modelData.icon) - color: root.colour - fill: 1 + sourceComponent: ColumnLayout { + spacing: Appearance.spacing.smaller / 2 - SequentialAnimation on opacity { - running: device.modelData.state !== BluetoothDeviceState.Connected - alwaysRunToEnd: true - loops: Animation.Infinite + // Bluetooth icon + MaterialIcon { + animate: true + text: Bluetooth.defaultAdapter?.enabled ? "bluetooth" : "bluetooth_disabled" + color: root.colour + } - Anim { - from: 1 - to: 0 - easing.bezierCurve: Appearance.anim.curves.standardAccel + // Connected bluetooth devices + Repeater { + model: ScriptModel { + values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) } - Anim { - from: 0 - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + + MaterialIcon { + id: device + + required property BluetoothDevice modelData + + animate: true + text: Icons.getBluetoothIcon(modelData.icon) + color: root.colour + fill: 1 + + SequentialAnimation on opacity { + running: device.modelData.state !== BluetoothDeviceState.Connected + alwaysRunToEnd: true + loops: Animation.Infinite + + Anim { + from: 1 + to: 0 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + from: 0 + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } } } } } - } - MaterialIcon { - id: battery - - anchors.horizontalCenter: devices.horizontalCenter - anchors.top: repeater.count > 0 ? devices.bottom : bluetooth.bottom - anchors.topMargin: Appearance.spacing.smaller / 2 - - animate: true - text: { - if (!UPower.displayDevice.isLaptopBattery) { - if (PowerProfiles.profile === PowerProfile.PowerSaver) - return "energy_savings_leaf"; - if (PowerProfiles.profile === PowerProfile.Performance) - return "rocket_launch"; - return "balance"; - } + // Battery icon + Loader { + id: batteryIcon - const perc = UPower.displayDevice.percentage; - const charging = !UPower.onBattery; - if (perc === 1) - return charging ? "battery_charging_full" : "battery_full"; - let level = Math.floor(perc * 7); - if (charging && (level === 4 || level === 1)) - level--; - return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; + asynchronous: true + active: Config.bar.status.showBattery + visible: active + + sourceComponent: MaterialIcon { + animate: true + text: { + if (!UPower.displayDevice.isLaptopBattery) { + if (PowerProfiles.profile === PowerProfile.PowerSaver) + return "energy_savings_leaf"; + if (PowerProfiles.profile === PowerProfile.Performance) + return "rocket_launch"; + return "balance"; + } + + const perc = UPower.displayDevice.percentage; + const charging = !UPower.onBattery; + if (perc === 1) + return charging ? "battery_charging_full" : "battery_full"; + let level = Math.floor(perc * 7); + if (charging && (level === 4 || level === 1)) + level--; + return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; + } + color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error + fill: 1 + } } - color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error - fill: 1 } Behavior on implicitHeight { diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml new file mode 100644 index 0000000..28667ec --- /dev/null +++ b/modules/bar/popouts/Audio.qml @@ -0,0 +1,57 @@ +import qs.widgets +import qs.services +import qs.config +import QtQuick.Layouts +import Quickshell + +ColumnLayout { + id: root + + required property var wrapper + + spacing: Appearance.spacing.normal + + VerticalSlider { + id: volumeSlider + + icon: { + if (Audio.muted) + return "no_sound"; + if (value >= 0.5) + return "volume_up"; + if (value > 0) + return "volume_down"; + return "volume_mute"; + } + + value: Audio.volume + onMoved: Audio.setVolume(value) + + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + } + + StyledRect { + id: pavuButton + + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surfaceContainer + + StateLayer { + function onClicked(): void { + root.wrapper.hasCurrent = false; + Quickshell.execDetached(["pavucontrol"]); + } + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "settings" + } + } +} diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 42e138d..5b396a9 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -45,6 +45,13 @@ Item { source: "Battery.qml" } + Popout { + name: "audio" + sourceComponent: Audio { + wrapper: root.wrapper + } + } + Repeater { model: ScriptModel { values: [...SystemTray.items.values] -- cgit v1.2.3-freya