colour -> color, behaviour -> behavior, centre -> center, favourite -> favorite, and about twenty other pairs, applied consistently across comments, docs, error/UI copy, and a handful of QML identifiers that used the British spelling as their actual name: SystemSettings' serialiseValue/serialiseTable/normaliseGradient, Displays' normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's _normalise helper, and ShortcutCapture's cancelled signal (with its onCancelled handler in ShortcutsPage.qml). Every call site and the two tests that assert on the literal source text (settings-ownership and settings-backup-live contracts) were updated in lockstep. Left untouched: config/dot/espanso/match/packages/misspell-en/ is a vendored third-party autocorrect dictionary -- its entries are typo corrections, not our prose, and rewriting them would fight the package's own purpose (and any future re-sync from upstream). The already-American `favorites` property (Home page pinned accessories) was never actually misspelled -- only nearby comments and error strings said "favourites" -- so no data migration was needed there. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
140 lines
4.5 KiB
QML
140 lines
4.5 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
|
|
|
|
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
|
|
property bool discoveryOwned: false
|
|
|
|
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;
|
|
}
|
|
|
|
function syncDiscovery(): void {
|
|
if (!root.adapter)
|
|
return;
|
|
const shouldDiscover = root.active && root.adapter.enabled;
|
|
if (shouldDiscover && !root.adapter.discovering) {
|
|
root.adapter.discovering = true;
|
|
root.discoveryOwned = true;
|
|
} else if (!shouldDiscover && root.discoveryOwned && root.adapter.discovering) {
|
|
root.adapter.discovering = false;
|
|
root.discoveryOwned = false;
|
|
}
|
|
}
|
|
|
|
onActiveChanged: root.syncDiscovery()
|
|
onAdapterChanged: {
|
|
if (root.active)
|
|
root.syncDiscovery();
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
if (root.adapter && root.discoveryOwned && root.adapter.discovering)
|
|
root.adapter.discovering = false;
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|