add Bluetooth to quickshell bar
This commit is contained in:
@ -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"
|
||||||
|
|||||||
187
roles/quickshell/files/bar/BluetoothContent.qml
Normal file
187
roles/quickshell/files/bar/BluetoothContent.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
roles/quickshell/files/bar/BluetoothControl.qml
Normal file
63
roles/quickshell/files/bar/BluetoothControl.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user