add Bluetooth to quickshell bar

This commit is contained in:
Johannes Knopp
2026-06-22 15:46:38 +02:00
parent 9bd023ce2c
commit 1c32b40669
6 changed files with 282 additions and 2 deletions

View File

@ -65,6 +65,10 @@ PanelWindow {
NetworkStatus { NetworkStatus {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
BluetoothControl {
anchors.verticalCenter: parent.verticalCenter
onClickedLeft: root.activePopup = root.activePopup === "bluetooth" ? "" : "bluetooth"
}
VolumeControl { VolumeControl {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onClickedLeft: root.activePopup = root.activePopup === "mixer" ? "" : "mixer" onClickedLeft: root.activePopup = root.activePopup === "mixer" ? "" : "mixer"
@ -118,6 +122,28 @@ PanelWindow {
} }
} }
// ── Bluetooth popup ───────────────────────────────────────────────
PopoutWindow {
id: btPopup
popupName: "bluetooth"
activePopup: root.activePopup
anchor.window: root
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
readonly property real pw: Math.max(rightRow.width + 2 * root.pad,
btContent.implicitWidth + 2 * root.bw)
anchor.rect.x: root.width - pw
implicitWidth: pw
implicitHeight: btContent.implicitHeight + root.bw + Theme.radius
BluetoothContent {
id: btContent
anchors { left: parent.left; right: parent.right }
scanning: btPopup._open
}
}
// ── Volume mixer popup ──────────────────────────────────────────── // ── Volume mixer popup ────────────────────────────────────────────
PopoutWindow { PopoutWindow {
popupName: "mixer" popupName: "mixer"

View File

@ -0,0 +1,187 @@
import Quickshell.Bluetooth
import QtQuick
import QtQuick.Layouts
import "../components"
// Bluetooth panel a plain Item so it can live inside an expanding section border.
Item {
id: root
implicitWidth: 300
implicitHeight: btCol.implicitHeight + 24
// Driven by the popup's open state — only scan for nearby devices while visible.
property bool scanning: false
readonly property var adapter: Bluetooth.defaultAdapter
readonly property bool powered: adapter?.enabled ?? false
// Run device discovery only while the popup is open and the adapter is on.
Binding {
target: root.adapter
property: "discovering"
value: root.scanning && root.powered
when: root.adapter !== null
}
// \uf293 = FA bluetooth-b, \uf294 = FA bluetooth
function devIcon(dev) {
return dev.connected ? "\uf293" : "\uf294"
}
ColumnLayout {
id: btCol
anchors { fill: parent; margins: 12 }
spacing: 4
RowLayout {
Layout.fillWidth: true
Layout.bottomMargin: 2
Text {
text: "Bluetooth"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
}
Text {
text: root.powered ? "On" : "Off"
font.pixelSize: 10
color: root.powered ? Theme.accent : Theme.textDim
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: if (root.adapter) root.adapter.enabled = !root.adapter.enabled
}
}
}
Text {
visible: !root.powered
text: "Adapter disabled"
color: Theme.textDim
font.pixelSize: 11
Layout.fillWidth: true
}
// ── Paired devices ────────────────────────────────────────────
Text {
visible: root.powered
text: "Paired"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.topMargin: 6
Layout.bottomMargin: 2
}
Repeater {
model: Bluetooth.devices
delegate: DeviceRow {
required property var modelData
Layout.fillWidth: true
device: modelData
visible: root.powered && modelData.bonded
}
}
// ── Available (discoverable) devices ──────────────────────────
RowLayout {
visible: root.powered
Layout.fillWidth: true
Layout.topMargin: 6
Layout.bottomMargin: 2
Text {
text: "Available"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
}
Text {
visible: root.adapter?.discovering ?? false
text: "scanning…"
color: Theme.textDim
font.pixelSize: 10
}
}
Repeater {
model: Bluetooth.devices
delegate: DeviceRow {
required property var modelData
Layout.fillWidth: true
device: modelData
// Only named devices — hides the swarm of random-address BLE beacons.
visible: root.powered && !modelData.bonded
&& (modelData.deviceName ?? "").length > 0
}
}
}
component DeviceRow: RowLayout {
id: drow
required property var device
spacing: 8
implicitHeight: 28
// Once a freshly-paired device bonds, trust it (so it can reconnect
// without an agent) and bring up the connection automatically.
Connections {
target: drow.device
function onBondedChanged() {
if (!drow.device.bonded) return
if (!drow.device.trusted) drow.device.trusted = true
if (!drow.device.connected) drow.device.connect()
}
}
Text {
text: root.devIcon(drow.device)
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 13
color: drow.device.connected ? Theme.accent : Theme.textDim
}
Text {
text: drow.device.name || drow.device.address || "?"
font.pixelSize: 11
color: drow.device.connected ? Theme.text : Theme.textDim
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
visible: drow.device.batteryAvailable
text: Math.round(drow.device.battery * 100) + "%"
font.pixelSize: 10
color: Theme.textDim
Layout.preferredWidth: 30
horizontalAlignment: Text.AlignRight
}
Text {
text: drow.device.pairing ? "pairing…"
: drow.device.state === BluetoothDeviceState.Connecting ? "…"
: drow.device.state === BluetoothDeviceState.Disconnecting ? "…"
: !drow.device.bonded ? "Pair"
: drow.device.connected ? "Disconnect"
: "Connect"
font.pixelSize: 10
color: drow.device.connected ? Theme.text : Theme.textDim
Layout.preferredWidth: 70
horizontalAlignment: Text.AlignRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const d = drow.device
if (d.pairing) d.cancelPair()
else if (!d.bonded) d.pair()
else if (d.connected) d.disconnect()
else d.connect()
}
}
}
}
}

View File

@ -0,0 +1,63 @@
import Quickshell.Bluetooth
import QtQuick
import "../components"
Item {
id: root
implicitWidth: chip.implicitWidth + 10
implicitHeight: 24
signal clickedLeft
readonly property var adapter: Bluetooth.defaultAdapter
readonly property bool powered: adapter?.enabled ?? false
readonly property var connectedDevices:
Bluetooth.devices.values.filter(d => d.connected)
// \uf293 = FA bluetooth-b (on), \uf294 = FA bluetooth (off / no connection)
readonly property string btIcon: root.powered ? "\uf293" : "\uf294"
function label() {
const n = root.connectedDevices.length
if (n === 0) return ""
if (n === 1) {
const name = root.connectedDevices[0].name || ""
return name.length > 16 ? name.substring(0, 15) + "…" : name
}
return n + " devices"
}
Row {
id: chip
anchors.centerIn: parent
spacing: 5
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.btIcon
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 14
color: (root.powered && root.connectedDevices.length > 0) ? Theme.text : Theme.textDim
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.label()
font.pixelSize: 11
color: Theme.textDim
visible: text !== ""
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (root.adapter) root.adapter.enabled = !root.adapter.enabled
} else {
root.clickedLeft()
}
}
}
}

View File

@ -68,8 +68,8 @@ Item {
required property var modelData required property var modelData
Layout.fillWidth: true Layout.fillWidth: true
node: modelData node: modelData
visible: modelData.isStream && !modelData.isSink && modelData.ready // Application playback streams (sink inputs) report isStream && isSink.
&& !(modelData.description ?? "").toLowerCase().includes("monitor") visible: modelData.isStream && modelData.isSink && modelData.ready
} }
} }
} }

View File

@ -1,5 +1,7 @@
Bar 1.0 Bar.qml Bar 1.0 Bar.qml
Battery 1.0 Battery.qml Battery 1.0 Battery.qml
BluetoothControl 1.0 BluetoothControl.qml
BluetoothContent 1.0 BluetoothContent.qml
PopoutWindow 1.0 PopoutWindow.qml PopoutWindow 1.0 PopoutWindow.qml
CalendarContent 1.0 CalendarContent.qml CalendarContent 1.0 CalendarContent.qml
Clock 1.0 Clock.qml Clock 1.0 Clock.qml

View File

@ -48,4 +48,6 @@
- { src: bar/VolumeMixerContent.qml, dest: bar/VolumeMixerContent.qml } - { src: bar/VolumeMixerContent.qml, dest: bar/VolumeMixerContent.qml }
- { src: bar/Workspaces.qml, dest: bar/Workspaces.qml } - { src: bar/Workspaces.qml, dest: bar/Workspaces.qml }
- { src: bar/WorkspaceButton.qml, dest: bar/WorkspaceButton.qml } - { src: bar/WorkspaceButton.qml, dest: bar/WorkspaceButton.qml }
- { src: bar/BluetoothContent.qml, dest: bar/BluetoothContent.qml }
- { src: bar/BluetoothControl.qml, dest: bar/BluetoothControl.qml }
- { src: bar/qmldir, dest: bar/qmldir } - { src: bar/qmldir, dest: bar/qmldir }