242 lines
7.5 KiB
QML
242 lines
7.5 KiB
QML
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Bluetooth
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.components
|
|
import qs.services
|
|
|
|
// The bluetooth menu: adapter power and the devices bluetoothd already knows
|
|
// about. Same full-screen layer surface as the launcher, so click-away and
|
|
// Escape work on any wlroots compositor.
|
|
//
|
|
// Deliberately not a pairing UI — discovery is never turned on, so nothing
|
|
// transient appears in the list and the panel stays a one-click connect.
|
|
PanelWindow {
|
|
id: root
|
|
|
|
required property var bar
|
|
|
|
readonly property string screenName: root.bar.screen ? root.bar.screen.name : ""
|
|
|
|
visible: Session.isOpen("bluetooth", root.screenName)
|
|
|
|
screen: root.bar.screen
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.namespace: Config.namespace + "-bluetooth"
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
|
|
anchors {
|
|
left: true
|
|
right: true
|
|
top: true
|
|
bottom: true
|
|
}
|
|
exclusionMode: ExclusionMode.Ignore
|
|
color: "transparent"
|
|
|
|
// ---- Data ------------------------------------------------------------
|
|
|
|
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
|
|
readonly property bool adapterOn: root.adapter ? root.adapter.enabled : false
|
|
readonly property bool adapterBusy: root.adapter ? root.adapter.state === BluetoothAdapterState.Enabling || root.adapter.state === BluetoothAdapterState.Disabling : false
|
|
|
|
// Known devices only: paired or bonded, i.e. what bluetoothd remembers.
|
|
// Connected ones first, then alphabetical, so the list does not reshuffle
|
|
// under the cursor for any other reason.
|
|
readonly property var devices: {
|
|
if (!root.adapter)
|
|
return [];
|
|
|
|
return [...root.adapter.devices.values].filter(d => d.paired || d.bonded).sort((a, b) => {
|
|
if (a.connected !== b.connected)
|
|
return a.connected ? -1 : 1;
|
|
return (a.name || "").localeCompare(b.name || "");
|
|
});
|
|
}
|
|
|
|
function toggleDevice(device: BluetoothDevice): void {
|
|
if (!device)
|
|
return;
|
|
|
|
if (device.connected)
|
|
device.disconnect();
|
|
else
|
|
device.connect();
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (root.visible) {
|
|
list.currentIndex = 0;
|
|
keyHandler.forceActiveFocus();
|
|
}
|
|
}
|
|
|
|
// ---- Chrome ----------------------------------------------------------
|
|
|
|
// Click-away scrim.
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: Session.close()
|
|
}
|
|
|
|
Item {
|
|
id: keyHandler
|
|
|
|
anchors.fill: parent
|
|
focus: true
|
|
|
|
Keys.onEscapePressed: Session.close()
|
|
Keys.onDownPressed: list.currentIndex = Math.min(list.currentIndex + 1, list.count - 1)
|
|
Keys.onUpPressed: list.currentIndex = Math.max(list.currentIndex - 1, 0)
|
|
Keys.onReturnPressed: root.toggleDevice(root.devices[list.currentIndex])
|
|
Keys.onEnterPressed: root.toggleDevice(root.devices[list.currentIndex])
|
|
}
|
|
|
|
Rectangle {
|
|
id: menu
|
|
|
|
// Anchored to the panel's right edge, under the button that opens it.
|
|
anchors {
|
|
right: parent.right
|
|
bottom: parent.bottom
|
|
rightMargin: Theme.padding
|
|
bottomMargin: Theme.barHeight + Theme.padding
|
|
}
|
|
|
|
readonly property int maxHeight: Math.min(Config.bluetoothMenuHeight, root.height - Theme.barHeight - Theme.padding * 2)
|
|
readonly property int bodyHeight: root.adapterOn && list.count > 0 ? list.contentHeight : 48
|
|
|
|
width: Math.min(Config.bluetoothMenuWidth, root.width - Theme.padding * 2)
|
|
height: Math.min(menu.maxHeight, Theme.padding * 4 + header.height + Theme.padding * 2 + menu.bodyHeight)
|
|
|
|
color: Theme.surface
|
|
radius: Theme.radius * 2
|
|
border.width: Theme.borderWidth
|
|
border.color: Theme.border
|
|
|
|
// Swallow clicks so they do not reach the scrim behind.
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
}
|
|
|
|
Item {
|
|
id: header
|
|
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: parent.top
|
|
margins: Theme.padding * 2
|
|
}
|
|
height: 24
|
|
|
|
BluetoothGlyph {
|
|
id: headerGlyph
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: root.adapterOn ? Theme.text : Theme.textDim
|
|
struck: !root.adapterOn
|
|
size: 18
|
|
}
|
|
|
|
Text {
|
|
anchors {
|
|
left: headerGlyph.right
|
|
right: toggle.left
|
|
leftMargin: Theme.padding
|
|
rightMargin: Theme.padding
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
elide: Text.ElideRight
|
|
text: "Bluetooth"
|
|
color: Theme.text
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.bold: true
|
|
}
|
|
|
|
ToggleSwitch {
|
|
id: toggle
|
|
|
|
anchors {
|
|
right: parent.right
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
checked: root.adapterOn
|
|
busy: root.adapterBusy
|
|
enabled: root.adapter !== null
|
|
|
|
// BlueZ reports the new state back, so `checked` stays bound to
|
|
// it rather than to what was clicked.
|
|
onToggled: on => {
|
|
if (root.adapter)
|
|
root.adapter.enabled = on;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: separator
|
|
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: header.bottom
|
|
topMargin: Theme.padding * 2
|
|
}
|
|
height: Theme.borderWidth
|
|
color: Theme.border
|
|
}
|
|
|
|
ListView {
|
|
id: list
|
|
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: separator.bottom
|
|
bottom: parent.bottom
|
|
margins: Theme.padding
|
|
topMargin: Theme.padding
|
|
}
|
|
visible: root.adapterOn
|
|
clip: true
|
|
spacing: 1
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
keyNavigationEnabled: false
|
|
model: root.devices
|
|
|
|
delegate: BluetoothDeviceEntry {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
device: modelData
|
|
width: list.width
|
|
selected: list.currentIndex === index
|
|
|
|
onActivated: root.toggleDevice(modelData)
|
|
onEntered: list.currentIndex = index
|
|
}
|
|
}
|
|
|
|
Text {
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: separator.bottom
|
|
bottom: parent.bottom
|
|
margins: Theme.padding * 2
|
|
}
|
|
visible: !root.adapterOn || list.count === 0
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
wrapMode: Text.WordWrap
|
|
text: !root.adapter ? "No bluetooth adapter" : !root.adapterOn ? "Bluetooth is off" : "No paired devices"
|
|
color: Theme.textDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
}
|
|
}
|
|
}
|