From 92316b3ca9af0f6edd64c13cdceede12970bd419 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Tue, 4 Nov 2025 19:31:38 -0500 Subject: [PATCH] feat: bluetooth widget --- shell/bar/Bar.qml | 7 + shell/bar/bluetooth/BluetoothCard.qml | 98 ++++++++++++++ shell/bar/bluetooth/BluetoothMenu.qml | 187 ++++++++++++++++++++++++++ shell/bar/volume/VolumeCard.qml | 2 + shell/bar/volume/VolumeIndicator.qml | 6 +- shell/widgets/StyledSlider.qml | 5 +- 6 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 shell/bar/bluetooth/BluetoothCard.qml create mode 100644 shell/bar/bluetooth/BluetoothMenu.qml diff --git a/shell/bar/Bar.qml b/shell/bar/Bar.qml index d30bdad..0d7c2c6 100644 --- a/shell/bar/Bar.qml +++ b/shell/bar/Bar.qml @@ -4,6 +4,7 @@ import Quickshell import "power" import "volume" import "systray" +import "bluetooth" // import qs.widgets import qs @@ -79,6 +80,12 @@ Variants { Layout.fillHeight: true } + BluetoothMenu { + bar: root + Layout.preferredWidth: this.height + Layout.fillHeight: true + } + PowerMenu { bar: root Layout.fillHeight: true diff --git a/shell/bar/bluetooth/BluetoothCard.qml b/shell/bar/bluetooth/BluetoothCard.qml new file mode 100644 index 0000000..725ea93 --- /dev/null +++ b/shell/bar/bluetooth/BluetoothCard.qml @@ -0,0 +1,98 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Widgets +import qs.widgets +import qs + +Item { + id: root + + required property BluetoothDevice device + + RowLayout { + spacing: 2 + anchors.fill: parent + + IconImage { + source: Quickshell.iconPath(root.device.icon) + Layout.preferredWidth: this.height + Layout.fillHeight: true + Layout.margins: 6 + } + + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + + Text { + text: root.device.name + color: ShellSettings.colors.active + elide: Text.ElideRight + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + } + + Text { + text: root.device.connected ? "Connected" : "Disconnected" + color: ShellSettings.colors.active.darker(1.5) + elide: Text.ElideRight + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + } + } + + RowLayout { + spacing: 2 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 4 + + StyledMouseArea { + Layout.preferredWidth: this.height + Layout.fillHeight: true + + onClicked: { + if (root.device.connected) { + root.device.disconnect(); + } else { + root.device.connect(); + } + } + + IconImage { + source: { + if (root.device.connected) { + return "image://icon/network-disconnect-symbolic"; + } else { + return "image://icon/network-connect-symbolic"; + } + } + + anchors { + fill: parent + margins: 2 + } + } + } + + StyledMouseArea { + onClicked: root.device.forget() + Layout.preferredWidth: this.height + Layout.fillHeight: true + + IconImage { + source: "image://icon/albumfolder-user-trash" + + anchors { + fill: parent + margins: 2 + } + } + } + } + } +} diff --git a/shell/bar/bluetooth/BluetoothMenu.qml b/shell/bar/bluetooth/BluetoothMenu.qml new file mode 100644 index 0000000..51eb333 --- /dev/null +++ b/shell/bar/bluetooth/BluetoothMenu.qml @@ -0,0 +1,187 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Quickshell.Bluetooth +import qs.widgets +import qs.bar +import qs + +StyledMouseArea { + id: root + onClicked: showMenu = !showMenu + + required property var bar + property bool showMenu: false + + IconImage { + anchors.fill: parent + source: { + if (Bluetooth.defaultAdapter.enabled) { + return "image://icon/bluetooth-online"; + } else { + return "image://icon/bluetooth-offline"; + } + } + } + + property PopupItem menu: PopupItem { + id: menu + owner: root + popup: root.bar.popup + show: root.showMenu + onClosed: root.showMenu = false + implicitWidth: 300 + implicitHeight: container.implicitHeight + (2 * container.anchors.margins) + + property var entryHeight: 35 + + ColumnLayout { + id: container + spacing: 2 + + anchors { + fill: parent + margins: 4 + } + + // Adapter + RowLayout { + spacing: 2 + Layout.fillWidth: true + Layout.preferredHeight: menu.entryHeight + + IconImage { + Layout.preferredWidth: this.height + Layout.fillHeight: true + // Layout.margins: 5 + + source: { + if (Bluetooth.defaultAdapter.enabled) { + return "image://icon/bluetooth-online"; + } else { + return "image://icon/bluetooth-offline"; + } + } + } + + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + + Text { + text: `Bluetooth(${Bluetooth.defaultAdapter.adapterId})` + color: ShellSettings.colors.active + elide: Text.ElideRight + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + } + + Text { + text: Bluetooth.defaultAdapter.enabled ? "Enabled" : "Disabled" + color: ShellSettings.colors.active.darker(1.5) + elide: Text.ElideRight + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + } + } + + RowLayout { + spacing: 2 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 4 + + StyledMouseArea { + Layout.preferredWidth: this.height + Layout.fillHeight: true + + onClicked: { + Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled; + } + + IconImage { + source: { + if (Bluetooth.defaultAdapter.enabled) { + return "image://icon/bluetooth-offline"; + } else { + return "image://icon/bluetooth-online"; + } + } + + anchors { + fill: parent + margins: 2 + } + } + } + + StyledMouseArea { + Layout.preferredWidth: this.height + Layout.fillHeight: true + + onClicked: { + Bluetooth.defaultAdapter.discovering = !Bluetooth.defaultAdapter.discovering; + } + + IconImage { + id: searchIcon + transformOrigin: Item.Center + + source: { + if (Bluetooth.defaultAdapter.discovering) { + return "image://icon/reload"; + } else { + return "image://icon/cm_search"; + } + } + + anchors { + fill: parent + margins: 2 + } + + NumberAnimation on rotation { + from: 0 + to: 360 + duration: 900 + loops: Animation.Infinite + running: Bluetooth.defaultAdapter.discovering + onRunningChanged: { + if (!running) + searchIcon.rotation = 0; + } + } + } + } + } + } + + // Devices + StyledListView { + id: appList + spacing: 2 + model: Bluetooth.devices + clip: true + + Layout.fillWidth: true + Layout.preferredHeight: { + const entryHeight = Math.min(8, Bluetooth.devices.values.length); + + return entryHeight * (menu.entryHeight + appList.spacing); + } + + delegate: BluetoothCard { + device: modelData + width: ListView.view.width + height: menu.entryHeight + + required property BluetoothDevice modelData + } + } + } + } +} diff --git a/shell/bar/volume/VolumeCard.qml b/shell/bar/volume/VolumeCard.qml index 9a47ba4..84c9336 100644 --- a/shell/bar/volume/VolumeCard.qml +++ b/shell/bar/volume/VolumeCard.qml @@ -47,6 +47,8 @@ Loader { } StyledSlider { + implicitHeight: 7 + handleHeight: 12 value: root.node.audio.volume ?? 0 onValueChanged: { diff --git a/shell/bar/volume/VolumeIndicator.qml b/shell/bar/volume/VolumeIndicator.qml index f4b6785..3aa6f64 100644 --- a/shell/bar/volume/VolumeIndicator.qml +++ b/shell/bar/volume/VolumeIndicator.qml @@ -38,9 +38,9 @@ StyledMouseArea { onClosed: root.showMenu = false implicitWidth: 275 - implicitHeight: container.implicitHeight + (2 * 8) + implicitHeight: container.implicitHeight + (2 * container.anchors.margins) - property real entryHeight: 40 + property real entryHeight: 38 ColumnLayout { id: container @@ -91,7 +91,7 @@ StyledMouseArea { Layout.fillWidth: true Layout.preferredHeight: { - const entryHeight = Math.min(5, linkTracker.linkGroups.length); + const entryHeight = Math.min(6, linkTracker.linkGroups.length); return entryHeight * (menu.entryHeight + appList.spacing); } diff --git a/shell/widgets/StyledSlider.qml b/shell/widgets/StyledSlider.qml index d7b59c8..30ee62e 100644 --- a/shell/widgets/StyledSlider.qml +++ b/shell/widgets/StyledSlider.qml @@ -10,6 +10,7 @@ Slider { implicitHeight: 7 property var accentColor: ShellSettings.colors.active + property real handleHeight: 16 background: Rectangle { id: sliderContainer @@ -50,8 +51,8 @@ Slider { id: handleRect x: slider.visualPosition * (slider.availableWidth - width) y: slider.topPadding + slider.availableHeight / 2 - height / 2 - width: 16 - height: 16 + width: slider.handleHeight + height: slider.handleHeight radius: width / 2 color: slider.pressed ? Qt.color(slider.accentColor ?? "purple").darker(1.5) : slider.accentColor ?? "purple" }