Files
Panama/config/dot/quickshell/modules/quicksettings/BluetoothList.qml
T

134 lines
4.6 KiB
QML

// Bluetooth device picker. Paired devices first (that's what you actually
// reconnect to), then whatever discovery has turned up.
import QtQuick
import Quickshell
import Quickshell.Widgets
import Quickshell.Bluetooth
import qs.config
import qs.services
Item {
id: root
// True while the section is open. Discovery is expensive, so it only runs
// while the list is on screen — same as the GNOME panel.
property bool active: false
property alias maxHeight: list.maxHeight
readonly property var adapter: Bluetooth.defaultAdapter
// Discovery is held, not switched. This picker and the settings page both
// list the same adapter, and each writing adapter.discovering from its own
// visibility flag meant the last one to change its mind decided for both --
// closing the settings page stopped discovery under this panel, which then
// said "Searching…" over a radio that had stopped. Connectivity counts the
// holds, owns the BlueZ write, and never stops a scan it did not start;
// nothing here touches the adapter.
readonly property string scanHold: "quicksettings-bluetooth"
implicitHeight: list.implicitHeight
readonly property var devices: {
const list = Bluetooth.devices.values.slice();
list.sort((a, b) => {
if (a.connected !== b.connected)
return a.connected ? -1 : 1;
if (a.paired !== b.paired)
return a.paired ? -1 : 1;
return (a.name || "").localeCompare(b.name || "");
});
return list;
}
onActiveChanged: {
if (root.active)
Connectivity.acquireDiscovery(root.scanHold);
else
Connectivity.releaseDiscovery(root.scanHold);
}
Component.onDestruction: Connectivity.releaseDiscovery(root.scanHold)
function stateText(device): string {
if (device.pairing)
return "Pairing…";
switch (device.state) {
case BluetoothDeviceState.Connecting:
return "Connecting…";
case BluetoothDeviceState.Disconnecting:
return "Disconnecting…";
case BluetoothDeviceState.Connected:
return device.batteryAvailable ? "Connected · " + Math.round(device.battery * 100) + "%" : "Connected";
default:
return device.paired ? "Paired" : "";
}
}
// Click cycles the obvious next step: pair, connect, disconnect.
function activate(device): void {
if (device.connected)
device.disconnect();
else if (device.paired)
device.connect();
else
device.pair();
}
ScrollColumn {
id: list
anchors.fill: parent
maxHeight: 300
Text {
width: parent.width
visible: root.devices.length === 0
topPadding: 12
bottomPadding: 12
horizontalAlignment: Text.AlignHCenter
text: {
if (!root.adapter)
return "No Bluetooth adapter";
if (!root.adapter.enabled)
return "Bluetooth is off";
return "Searching…";
}
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
Repeater {
model: root.devices
RowButton {
id: deviceRow
required property var modelData
width: parent.width
// BlueZ reports a plain freedesktop name ("audio-headphones");
// the symbolic variant is the one that can be recolored.
icon: deviceRow.modelData.icon !== "" ? deviceRow.modelData.icon + "-symbolic" : "bluetooth-symbolic"
iconFallback: "bluetooth-symbolic"
label: deviceRow.modelData.name || deviceRow.modelData.address
sublabel: root.stateText(deviceRow.modelData)
selected: deviceRow.modelData.connected
onClicked: root.activate(deviceRow.modelData)
// Forgetting is destructive enough that it gets its own
// control rather than sharing the row click.
IconButton {
anchors.verticalCenter: parent.verticalCenter
visible: deviceRow.modelData.paired
size: 26
iconSize: 13
icon: "user-trash-symbolic"
iconFallback: "window-close-symbolic"
onClicked: deviceRow.modelData.forget()
}
}
}
}
}