Simplify Bluetooth device actions
This commit is contained in:
@@ -29,6 +29,7 @@ declare -A GROUP_DESCRIPTIONS
|
|||||||
GROUP_DESCRIPTIONS[ac]="AC power detection"
|
GROUP_DESCRIPTIONS[ac]="AC power detection"
|
||||||
GROUP_DESCRIPTIONS[audio]="Audio input and output controls"
|
GROUP_DESCRIPTIONS[audio]="Audio input and output controls"
|
||||||
GROUP_DESCRIPTIONS[battery]="Battery status helpers"
|
GROUP_DESCRIPTIONS[battery]="Battery status helpers"
|
||||||
|
GROUP_DESCRIPTIONS[bluetooth]="Bluetooth device controls"
|
||||||
GROUP_DESCRIPTIONS[branch]="Omarchy git branch management"
|
GROUP_DESCRIPTIONS[branch]="Omarchy git branch management"
|
||||||
GROUP_DESCRIPTIONS[branding]="About and screensaver branding"
|
GROUP_DESCRIPTIONS[branding]="About and screensaver branding"
|
||||||
GROUP_DESCRIPTIONS[brightness]="Display and keyboard brightness"
|
GROUP_DESCRIPTIONS[brightness]="Display and keyboard brightness"
|
||||||
|
|||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Control a Bluetooth device
|
||||||
|
# omarchy:group=bluetooth
|
||||||
|
# omarchy:args=[pair|connect|disconnect|forget] <address>
|
||||||
|
# omarchy:examples=omarchy bluetooth device connect 00:11:22:33:44:55
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: omarchy-bluetooth-device [pair|connect|disconnect|forget] <address>" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
action=${1:-}
|
||||||
|
address=${2:-}
|
||||||
|
|
||||||
|
[[ $action == "pair" || $action == "connect" || $action == "disconnect" || $action == "forget" ]] || usage
|
||||||
|
[[ $address =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]] || usage
|
||||||
|
|
||||||
|
power_on() {
|
||||||
|
bluetoothctl power on >/dev/null 2>&1 || true
|
||||||
|
sleep 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
trust_device() {
|
||||||
|
bluetoothctl trust "$address" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
pair)
|
||||||
|
power_on
|
||||||
|
timeout 20s bluetoothctl pair "$address" >/dev/null 2>&1 || true
|
||||||
|
trust_device
|
||||||
|
timeout 20s bluetoothctl connect "$address" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
connect)
|
||||||
|
power_on
|
||||||
|
trust_device
|
||||||
|
timeout 20s bluetoothctl connect "$address" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
disconnect)
|
||||||
|
timeout 10s bluetoothctl disconnect "$address" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
forget)
|
||||||
|
power_on
|
||||||
|
timeout 10s bluetoothctl disconnect "$address" >/dev/null 2>&1 || true
|
||||||
|
timeout 10s bluetoothctl remove "$address" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
+206
-147
@@ -11,11 +11,10 @@ Panel {
|
|||||||
moduleName: "BluetoothPanel"
|
moduleName: "BluetoothPanel"
|
||||||
ipcTarget: "panels.bluetooth"
|
ipcTarget: "panels.bluetooth"
|
||||||
|
|
||||||
// Address -> true while we are waiting for a click-initiated pair to land
|
// Address -> "connecting" | "disconnecting" | "forgetting".
|
||||||
// so we can chain trust + connect at root scope. Doing this in the row's
|
// The actual Bluetooth sequencing lives in bin/omarchy-bluetooth-device;
|
||||||
// Connections is racy: the discovered Repeater destroys the delegate the
|
// this map only keeps the panel responsive while BlueZ catches up.
|
||||||
// moment `paired` flips, before the row's handler reliably fires.
|
property var pendingActions: ({})
|
||||||
property var pendingPairAddresses: ({})
|
|
||||||
|
|
||||||
readonly property var adapter: Bluetooth.defaultAdapter
|
readonly property var adapter: Bluetooth.defaultAdapter
|
||||||
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
|
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
|
||||||
@@ -46,8 +45,13 @@ Panel {
|
|||||||
|
|
||||||
readonly property var connectedDevices: {
|
readonly property var connectedDevices: {
|
||||||
var list = []
|
var list = []
|
||||||
for (var i = 0; i < devices.length; i++)
|
for (var i = 0; i < devices.length; i++) {
|
||||||
if (devices[i] && devices[i].connected && hasHumanName(devices[i])) list.push(devices[i])
|
var d = devices[i]
|
||||||
|
if (d && d.connected && hasHumanName(d)) list.push(d)
|
||||||
|
}
|
||||||
|
list.sort(function(a, b) {
|
||||||
|
return deviceLabel(a).localeCompare(deviceLabel(b))
|
||||||
|
})
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +59,9 @@ Panel {
|
|||||||
var list = []
|
var list = []
|
||||||
for (var i = 0; i < devices.length; i++) {
|
for (var i = 0; i < devices.length; i++) {
|
||||||
var d = devices[i]
|
var d = devices[i]
|
||||||
if (d && hasHumanName(d) && (d.paired || d.connected || d.bonded || d.trusted)) list.push(d)
|
if (d && hasHumanName(d) && !d.connected && (d.paired || d.bonded || d.trusted)) list.push(d)
|
||||||
}
|
}
|
||||||
list.sort(function(a, b) {
|
list.sort(function(a, b) {
|
||||||
if (a.connected !== b.connected) return a.connected ? -1 : 1
|
|
||||||
return deviceLabel(a).localeCompare(deviceLabel(b))
|
return deviceLabel(a).localeCompare(deviceLabel(b))
|
||||||
})
|
})
|
||||||
return list
|
return list
|
||||||
@@ -69,7 +72,7 @@ Panel {
|
|||||||
for (var i = 0; i < devices.length; i++) {
|
for (var i = 0; i < devices.length; i++) {
|
||||||
var d = devices[i]
|
var d = devices[i]
|
||||||
if (!d || !hasHumanName(d)) continue
|
if (!d || !hasHumanName(d)) continue
|
||||||
if (d.paired || d.connected || d.bonded || d.trusted) continue
|
if (d.connected || d.paired || d.bonded || d.trusted) continue
|
||||||
list.push(d)
|
list.push(d)
|
||||||
}
|
}
|
||||||
list.sort(function(a, b) {
|
list.sort(function(a, b) {
|
||||||
@@ -88,8 +91,9 @@ Panel {
|
|||||||
// Single cursor model shared by keyboard and mouse. Sections:
|
// Single cursor model shared by keyboard and mouse. Sections:
|
||||||
// "header" — 2 action pills (scan, toggle); h/l moves between
|
// "header" — 2 action pills (scan, toggle); h/l moves between
|
||||||
// them, Enter activates.
|
// them, Enter activates.
|
||||||
// "known" — paired/known device rows; Enter toggles connect.
|
// "connected" — currently connected devices; Enter disconnects.
|
||||||
// "discovered" — unpaired devices visible while scanning; Enter pairs.
|
// "known" — remembered devices; Enter connects.
|
||||||
|
// "discovered" — unremembered devices visible while scanning; Enter connects.
|
||||||
// Visuals always come from CursorSurface (hasCursor / current),
|
// Visuals always come from CursorSurface (hasCursor / current),
|
||||||
// never from containsMouse. Mouse hover updates root cursor state too,
|
// never from containsMouse. Mouse hover updates root cursor state too,
|
||||||
// guaranteeing one highlight on screen.
|
// guaranteeing one highlight on screen.
|
||||||
@@ -98,11 +102,10 @@ Panel {
|
|||||||
property bool cursorActive: false
|
property bool cursorActive: false
|
||||||
readonly property int headerPillCount: 2
|
readonly property int headerPillCount: 2
|
||||||
|
|
||||||
// Stable identity for the focused known device. The known list is sorted
|
// Stable identity for the focused device. Devices move between sections as
|
||||||
// (connected-first, then alphabetical) so activating a device can shift
|
// they connect, disconnect, pair, or get forgotten, so follow the BlueZ
|
||||||
// its index. We track the BlueZ address here so the cursor follows the
|
// address across section changes instead of preserving a stale row index.
|
||||||
// same device across reorders rather than the slot it used to occupy.
|
property string focusedDeviceAddress: ""
|
||||||
property string focusedKnownAddress: ""
|
|
||||||
|
|
||||||
readonly property color hoverFill: bar
|
readonly property color hoverFill: bar
|
||||||
? Style.hoverFillFor(bar.foreground, Color.accent)
|
? Style.hoverFillFor(bar.foreground, Color.accent)
|
||||||
@@ -113,6 +116,7 @@ Panel {
|
|||||||
|
|
||||||
function sectionCount(section) {
|
function sectionCount(section) {
|
||||||
if (section === "header") return headerPillCount
|
if (section === "header") return headerPillCount
|
||||||
|
if (section === "connected") return connectedDevices.length
|
||||||
if (section === "known") return knownDevices.length
|
if (section === "known") return knownDevices.length
|
||||||
if (section === "discovered") return discoveredDevices.length
|
if (section === "discovered") return discoveredDevices.length
|
||||||
return 0
|
return 0
|
||||||
@@ -120,6 +124,7 @@ Panel {
|
|||||||
|
|
||||||
function sectionVisible(section) {
|
function sectionVisible(section) {
|
||||||
if (section === "header") return true
|
if (section === "header") return true
|
||||||
|
if (section === "connected") return connectedDevices.length > 0
|
||||||
if (section === "known") return knownDevices.length > 0
|
if (section === "known") return knownDevices.length > 0
|
||||||
if (section === "discovered") return adapter && adapter.discovering && discoveredDevices.length > 0
|
if (section === "discovered") return adapter && adapter.discovering && discoveredDevices.length > 0
|
||||||
return false
|
return false
|
||||||
@@ -127,11 +132,102 @@ Panel {
|
|||||||
|
|
||||||
readonly property var visibleSections: {
|
readonly property var visibleSections: {
|
||||||
var list = ["header"]
|
var list = ["header"]
|
||||||
|
if (sectionVisible("connected")) list.push("connected")
|
||||||
if (sectionVisible("known")) list.push("known")
|
if (sectionVisible("known")) list.push("known")
|
||||||
if (sectionVisible("discovered")) list.push("discovered")
|
if (sectionVisible("discovered")) list.push("discovered")
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function devicesForSection(section) {
|
||||||
|
if (section === "connected") return connectedDevices
|
||||||
|
if (section === "known") return knownDevices
|
||||||
|
if (section === "discovered") return discoveredDevices
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceAt(section, index) {
|
||||||
|
var list = devicesForSection(section)
|
||||||
|
return index >= 0 && index < list.length ? list[index] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneMap(map) {
|
||||||
|
var next = ({})
|
||||||
|
for (var key in map) next[key] = map[key]
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
function pendingAction(address) {
|
||||||
|
return address && pendingActions[address] ? pendingActions[address] : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPendingAction(address, action) {
|
||||||
|
if (!address) return
|
||||||
|
var next = cloneMap(pendingActions)
|
||||||
|
if (action) next[address] = action
|
||||||
|
else delete next[address]
|
||||||
|
pendingActions = next
|
||||||
|
if (action) pendingTimeout.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceCommand(action, address) {
|
||||||
|
var command = root.bar && root.bar.omarchyPath
|
||||||
|
? root.bar.omarchyPath + "/bin/omarchy-bluetooth-device"
|
||||||
|
: "omarchy-bluetooth-device"
|
||||||
|
return [command, action, address]
|
||||||
|
}
|
||||||
|
|
||||||
|
function runDeviceAction(device, action, pending) {
|
||||||
|
if (!device || !device.address) return
|
||||||
|
setPendingAction(device.address, pending)
|
||||||
|
Quickshell.execDetached(deviceCommand(action, device.address))
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectDevice(device) {
|
||||||
|
if (!device || device.connected) return
|
||||||
|
if (device.paired || device.bonded || device.trusted) runDeviceAction(device, "connect", "connecting")
|
||||||
|
else runDeviceAction(device, "pair", "connecting")
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectDevice(device) {
|
||||||
|
if (!device || !device.address) return
|
||||||
|
if (!device.connected) return
|
||||||
|
setPendingAction(device.address, "disconnecting")
|
||||||
|
if (device.disconnect) device.disconnect()
|
||||||
|
Quickshell.execDetached(deviceCommand("disconnect", device.address))
|
||||||
|
}
|
||||||
|
|
||||||
|
function forgetDevice(device) {
|
||||||
|
if (!device || !device.address) return
|
||||||
|
runDeviceAction(device, "forget", "forgetting")
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncPendingActions() {
|
||||||
|
var next = cloneMap(pendingActions)
|
||||||
|
var changed = false
|
||||||
|
|
||||||
|
for (var address in next) {
|
||||||
|
var action = next[address]
|
||||||
|
var found = null
|
||||||
|
|
||||||
|
for (var i = 0; i < devices.length; i++) {
|
||||||
|
var d = devices[i]
|
||||||
|
if (d && d.address === address) {
|
||||||
|
found = d
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((action === "connecting" && found && found.connected)
|
||||||
|
|| (action === "disconnecting" && found && !found.connected)
|
||||||
|
|| (action === "forgetting" && (!found || (!found.paired && !found.bonded && !found.trusted)))) {
|
||||||
|
delete next[address]
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) pendingActions = next
|
||||||
|
}
|
||||||
|
|
||||||
// j/k navigates between sections row-by-row. The header is treated as a
|
// j/k navigates between sections row-by-row. The header is treated as a
|
||||||
// SINGLE row (its pills sit on one horizontal line), so j/k from devices
|
// SINGLE row (its pills sit on one horizontal line), so j/k from devices
|
||||||
// jumps to/from the header as a unit, and h/l moves between the three
|
// jumps to/from the header as a unit, and h/l moves between the three
|
||||||
@@ -185,19 +281,17 @@ Panel {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (focusSection === "known") {
|
if (focusSection === "connected" || focusSection === "known") {
|
||||||
var dev = knownDevices[selectedIndex]
|
var dev = deviceAt(focusSection, selectedIndex)
|
||||||
if (!dev) return
|
if (!dev) return
|
||||||
if (!dev.trusted) dev.trusted = true
|
if (dev.connected) disconnectDevice(dev)
|
||||||
if (dev.connected) dev.disconnect()
|
else connectDevice(dev)
|
||||||
else dev.connect()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (focusSection === "discovered") {
|
if (focusSection === "discovered") {
|
||||||
var d = discoveredDevices[selectedIndex]
|
var d = discoveredDevices[selectedIndex]
|
||||||
if (!d) return
|
if (!d) return
|
||||||
pendingPairAddresses[d.address] = true
|
connectDevice(d)
|
||||||
d.pair()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,52 +301,61 @@ Panel {
|
|||||||
// tooltip says "Disconnect" for connected rows, and the keybind has
|
// tooltip says "Disconnect" for connected rows, and the keybind has
|
||||||
// to agree.
|
// to agree.
|
||||||
function deleteSelected() {
|
function deleteSelected() {
|
||||||
if (focusSection !== "known") return
|
if (focusSection !== "connected" && focusSection !== "known") return
|
||||||
var dev = knownDevices[selectedIndex]
|
var dev = deviceAt(focusSection, selectedIndex)
|
||||||
if (!dev) return
|
if (!dev) return
|
||||||
if (dev.connected) dev.disconnect()
|
if (dev.connected) disconnectDevice(dev)
|
||||||
else if (dev.forget) dev.forget()
|
else forgetDevice(dev)
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpenedChanged: {
|
onOpenedChanged: {
|
||||||
if (opened) {
|
if (opened) {
|
||||||
if (adapter && adapter.enabled && !adapter.discovering) adapter.discovering = true
|
if (adapter && adapter.enabled && !adapter.discovering) adapter.discovering = true
|
||||||
if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
|
if (connectedDevices.length > 0) { focusSection = "connected"; selectedIndex = 0 }
|
||||||
|
else if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
|
||||||
else { focusSection = "header"; selectedIndex = 1 }
|
else { focusSection = "header"; selectedIndex = 1 }
|
||||||
cursorActive = false
|
cursorActive = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When `selectedIndex` changes inside the known section, remember which
|
function updateFocusedAddress() {
|
||||||
// address it points at. Updates from re-resolution (below) are idempotent
|
if (focusSection === "header") {
|
||||||
// because we end up setting the same address.
|
focusedDeviceAddress = ""
|
||||||
onSelectedIndexChanged: {
|
return
|
||||||
if (focusSection !== "known") return
|
}
|
||||||
if (selectedIndex < 0 || selectedIndex >= knownDevices.length) return
|
var d = deviceAt(focusSection, selectedIndex)
|
||||||
var d = knownDevices[selectedIndex]
|
focusedDeviceAddress = d ? (d.address || "") : ""
|
||||||
focusedKnownAddress = d ? (d.address || "") : ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocusSectionChanged: {
|
function reselectFocusedDevice() {
|
||||||
if (focusSection !== "known") focusedKnownAddress = ""
|
if (focusedDeviceAddress === "") {
|
||||||
}
|
clampCursor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
onKnownDevicesChanged: {
|
var sections = ["connected", "known", "discovered"]
|
||||||
// Try to follow the device by address before clamping. If we can't find
|
for (var s = 0; s < sections.length; s++) {
|
||||||
// the address (e.g. it was forgotten), fall through to clampCursor()
|
var section = sections[s]
|
||||||
// which will pull selectedIndex back into range.
|
if (!sectionVisible(section)) continue
|
||||||
if (focusSection === "known" && focusedKnownAddress !== "") {
|
var list = devicesForSection(section)
|
||||||
for (var i = 0; i < knownDevices.length; i++) {
|
for (var i = 0; i < list.length; i++) {
|
||||||
if (knownDevices[i] && knownDevices[i].address === focusedKnownAddress) {
|
if (list[i] && list[i].address === focusedDeviceAddress) {
|
||||||
if (selectedIndex !== i) selectedIndex = i
|
focusSection = section
|
||||||
|
selectedIndex = i
|
||||||
clampCursor()
|
clampCursor()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clampCursor()
|
clampCursor()
|
||||||
}
|
}
|
||||||
onDiscoveredDevicesChanged: clampCursor()
|
|
||||||
|
onSelectedIndexChanged: updateFocusedAddress()
|
||||||
|
onFocusSectionChanged: updateFocusedAddress()
|
||||||
|
onConnectedDevicesChanged: { reselectFocusedDevice(); syncPendingActions() }
|
||||||
|
onKnownDevicesChanged: { reselectFocusedDevice(); syncPendingActions() }
|
||||||
|
onDiscoveredDevicesChanged: { reselectFocusedDevice(); syncPendingActions() }
|
||||||
onVisibleSectionsChanged: clampCursor()
|
onVisibleSectionsChanged: clampCursor()
|
||||||
|
|
||||||
// Keep the keyboard-focused row inside the visible viewport of the device
|
// Keep the keyboard-focused row inside the visible viewport of the device
|
||||||
@@ -303,29 +406,11 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-visual lifecycle watchers, one per device. Survives popup open/close
|
Timer {
|
||||||
// and the discovered-known transition that destroys row delegates.
|
id: pendingTimeout
|
||||||
Repeater {
|
interval: 20000
|
||||||
model: root.devices
|
repeat: false
|
||||||
Item {
|
onTriggered: root.pendingActions = ({})
|
||||||
required property var modelData
|
|
||||||
visible: false
|
|
||||||
Connections {
|
|
||||||
target: modelData || null
|
|
||||||
function onPairedChanged() {
|
|
||||||
var d = modelData
|
|
||||||
if (!d || !d.paired) return
|
|
||||||
if (!root.pendingPairAddresses[d.address]) return
|
|
||||||
delete root.pendingPairAddresses[d.address]
|
|
||||||
// BlueZ pair() does not auto-trust or auto-connect. Without
|
|
||||||
// trusted, the daemon may drop the entry shortly after pairing,
|
|
||||||
// which makes a freshly-paired device flash "Connected" and then
|
|
||||||
// vanish from the model.
|
|
||||||
d.trusted = true
|
|
||||||
if (!d.connected) d.connect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetButton {
|
WidgetButton {
|
||||||
@@ -437,7 +522,35 @@ Panel {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Style.space(10)
|
spacing: Style.space(10)
|
||||||
|
|
||||||
// Paired / known devices.
|
// Connected devices.
|
||||||
|
PanelSectionHeader {
|
||||||
|
visible: root.connectedDevices.length > 0
|
||||||
|
text: "Connected"
|
||||||
|
foreground: root.bar.foreground
|
||||||
|
fontFamily: root.bar.fontFamily
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.connectedDevices
|
||||||
|
DeviceRow {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
width: deviceList.width
|
||||||
|
dev: modelData
|
||||||
|
rowIndex: index
|
||||||
|
sectionName: "connected"
|
||||||
|
isDiscovered: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remembered devices.
|
||||||
|
PanelSectionHeader {
|
||||||
|
visible: root.knownDevices.length > 0
|
||||||
|
text: "Paired"
|
||||||
|
foreground: root.bar.foreground
|
||||||
|
fontFamily: root.bar.fontFamily
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.knownDevices
|
model: root.knownDevices
|
||||||
DeviceRow {
|
DeviceRow {
|
||||||
@@ -446,6 +559,7 @@ Panel {
|
|||||||
width: deviceList.width
|
width: deviceList.width
|
||||||
dev: modelData
|
dev: modelData
|
||||||
rowIndex: index
|
rowIndex: index
|
||||||
|
sectionName: "known"
|
||||||
isDiscovered: false
|
isDiscovered: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,7 +567,7 @@ Panel {
|
|||||||
// Discovered (unpaired) devices, only shown while scanning.
|
// Discovered (unpaired) devices, only shown while scanning.
|
||||||
PanelSectionHeader {
|
PanelSectionHeader {
|
||||||
visible: root.adapter && root.adapter.discovering && root.discoveredDevices.length > 0
|
visible: root.adapter && root.adapter.discovering && root.discoveredDevices.length > 0
|
||||||
text: "Discovered"
|
text: "Available"
|
||||||
foreground: root.bar.foreground
|
foreground: root.bar.foreground
|
||||||
fontFamily: root.bar.fontFamily
|
fontFamily: root.bar.fontFamily
|
||||||
}
|
}
|
||||||
@@ -466,12 +580,14 @@ Panel {
|
|||||||
width: deviceList.width
|
width: deviceList.width
|
||||||
dev: modelData
|
dev: modelData
|
||||||
rowIndex: index
|
rowIndex: index
|
||||||
|
sectionName: "discovered"
|
||||||
isDiscovered: true
|
isDiscovered: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: root.knownDevices.length === 0
|
visible: root.connectedDevices.length === 0
|
||||||
|
&& root.knownDevices.length === 0
|
||||||
&& (!root.adapter || !root.adapter.discovering || root.discoveredDevices.length === 0)
|
&& (!root.adapter || !root.adapter.discovering || root.discoveredDevices.length === 0)
|
||||||
text: !root.adapter ? "No Bluetooth adapter"
|
text: !root.adapter ? "No Bluetooth adapter"
|
||||||
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
|
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
|
||||||
@@ -518,20 +634,18 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two-line device row showing name + live status (Connected, Connecting,
|
// Two-line device row showing name + live status. Pending state is owned
|
||||||
// Pairing, Failed). Tracks pending click attempts with a Timer so a
|
// by the panel so it survives rows moving between sections.
|
||||||
// connect that drops back to Disconnected within 10s surfaces as "Failed".
|
|
||||||
// Now a cursor target: hasCursor binds to root state, mouse hover updates
|
|
||||||
// root state. The X button on the right is a PanelActionButton.
|
|
||||||
component DeviceRow: CursorSurface {
|
component DeviceRow: CursorSurface {
|
||||||
id: row
|
id: row
|
||||||
required property var dev
|
required property var dev
|
||||||
required property int rowIndex
|
required property int rowIndex
|
||||||
|
required property string sectionName
|
||||||
required property bool isDiscovered
|
required property bool isDiscovered
|
||||||
|
|
||||||
readonly property bool isConnected: dev && dev.connected
|
readonly property bool isConnected: dev && dev.connected
|
||||||
readonly property int devState: dev && dev.state !== undefined ? dev.state : -1
|
readonly property int devState: dev && dev.state !== undefined ? dev.state : -1
|
||||||
readonly property string sectionName: isDiscovered ? "discovered" : "known"
|
readonly property string action: root.pendingAction(dev ? dev.address : "")
|
||||||
|
|
||||||
hasCursor: root.cursorActive && root.focusSection === sectionName && root.selectedIndex === rowIndex
|
hasCursor: root.cursorActive && root.focusSection === sectionName && root.selectedIndex === rowIndex
|
||||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(row)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(row)
|
||||||
@@ -540,61 +654,22 @@ Panel {
|
|||||||
fill: root.hoverFill
|
fill: root.hoverFill
|
||||||
currentFill: root.selectedFill
|
currentFill: root.selectedFill
|
||||||
|
|
||||||
// 0 idle, 1 connecting, 2 disconnecting, 3 pairing, 4 failed.
|
|
||||||
property int pendingAction: 0
|
|
||||||
property string failureReason: ""
|
|
||||||
|
|
||||||
// Heuristic: while pendingAction is set, the connect/pair attempt is
|
|
||||||
// expected to land within ~10s. If state stays Disconnected past that, we
|
|
||||||
// declare failure. Cleared as soon as state reaches Connected.
|
|
||||||
Timer {
|
|
||||||
id: failureTimer
|
|
||||||
interval: 10000
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (row.pendingAction === 1 && !row.isConnected) {
|
|
||||||
row.pendingAction = 4
|
|
||||||
row.failureReason = "Could not connect"
|
|
||||||
} else if (row.pendingAction === 3 && row.dev && !row.dev.paired) {
|
|
||||||
row.pendingAction = 4
|
|
||||||
row.failureReason = "Pairing failed"
|
|
||||||
} else {
|
|
||||||
row.pendingAction = 0
|
|
||||||
row.failureReason = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: row.dev || null
|
|
||||||
function onConnectedChanged() {
|
|
||||||
if (row.isConnected) { row.pendingAction = 0; row.failureReason = "" }
|
|
||||||
}
|
|
||||||
function onPairedChanged() {
|
|
||||||
if (row.dev && row.dev.paired && row.pendingAction === 3) {
|
|
||||||
row.pendingAction = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property string statusText: {
|
readonly property string statusText: {
|
||||||
if (!dev) return ""
|
if (!dev) return ""
|
||||||
if (pendingAction === 4) return failureReason || "Failed"
|
if (action === "forgetting") return "Forgetting…"
|
||||||
if (pendingAction === 1 || devState === 3) return "Connecting…"
|
if (action === "disconnecting" || devState === 2) return "Disconnecting…"
|
||||||
if (pendingAction === 2 || devState === 2) return "Disconnecting…"
|
|
||||||
if (pendingAction === 3 || (dev.pairing === true)) return "Pairing…"
|
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
if (dev.batteryAvailable) return "Connected · " + Math.round(dev.battery * 100) + "%"
|
if (dev.batteryAvailable) return Math.round(dev.battery * 100) + "%"
|
||||||
return "Connected"
|
return sectionName === "connected" ? "" : "Connected"
|
||||||
}
|
}
|
||||||
|
if (action === "connecting" || devState === 3 || dev.pairing === true) return "Connecting…"
|
||||||
if (isDiscovered) return ""
|
if (isDiscovered) return ""
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color statusColor: {
|
readonly property color statusColor: {
|
||||||
if (pendingAction === 4) return root.bar.urgent
|
|
||||||
if (isConnected) return root.bar.foreground
|
if (isConnected) return root.bar.foreground
|
||||||
if (pendingAction === 1 || devState === 3 || pendingAction === 3) return root.bar.foreground
|
if (action !== "" || devState === 3 || dev.pairing === true) return root.bar.foreground
|
||||||
return Qt.darker(root.bar.foreground, 1.5)
|
return Qt.darker(root.bar.foreground, 1.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,23 +691,12 @@ Panel {
|
|||||||
onClicked: function(mouse) {
|
onClicked: function(mouse) {
|
||||||
if (!row.dev) return
|
if (!row.dev) return
|
||||||
if (mouse.button === Qt.RightButton) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
if (row.dev.forget) row.dev.forget()
|
if (row.isConnected) root.disconnectDevice(row.dev)
|
||||||
|
else if (!row.isDiscovered) root.forgetDevice(row.dev)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (row.isDiscovered) {
|
|
||||||
row.pendingAction = 3
|
|
||||||
row.failureReason = ""
|
|
||||||
failureTimer.restart()
|
|
||||||
root.pendingPairAddresses[row.dev.address] = true
|
|
||||||
row.dev.pair()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!row.dev.trusted) row.dev.trusted = true
|
|
||||||
if (row.isConnected) return // use the X button to disconnect
|
if (row.isConnected) return // use the X button to disconnect
|
||||||
row.pendingAction = 1
|
root.connectDevice(row.dev)
|
||||||
row.failureReason = ""
|
|
||||||
failureTimer.restart()
|
|
||||||
row.dev.connect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,13 +733,8 @@ Panel {
|
|||||||
fontFamily: root.bar.fontFamily
|
fontFamily: root.bar.fontFamily
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!row.dev) return
|
if (!row.dev) return
|
||||||
if (row.isConnected) {
|
if (row.isConnected) root.disconnectDevice(row.dev)
|
||||||
row.pendingAction = 2
|
else root.forgetDevice(row.dev)
|
||||||
failureTimer.stop()
|
|
||||||
row.dev.disconnect()
|
|
||||||
} else if (row.dev.forget) {
|
|
||||||
row.dev.forget()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user