Fix bluetooth panel pair flow, scroll, and X-button click
- Discovered devices now populate via a new `discoveredDevices` filter; the Repeater was referencing a property that did not exist. - Pairing chains into trust + connect at root scope, so freshly-paired devices (especially audio headphones) persist in BlueZ and actually finish connecting instead of flashing "Connected" and disappearing. - Wrap the device list in a Flickable capped at 400px with a scroll bar so crowded RF environments do not grow the popup off-screen. - Reorder MouseAreas so the per-row X button receives clicks instead of the full-row click handler swallowing them.
This commit is contained in:
@@ -13,6 +13,12 @@ Item {
|
|||||||
|
|
||||||
property bool popupOpen: false
|
property bool popupOpen: false
|
||||||
|
|
||||||
|
// Address -> true while we are waiting for a click-initiated pair to land
|
||||||
|
// so we can chain trust + connect at root scope. Doing this in the row's
|
||||||
|
// Connections is racy: the discovered Repeater destroys the delegate the
|
||||||
|
// moment `paired` flips, before the row's handler reliably fires.
|
||||||
|
property var pendingPairAddresses: ({})
|
||||||
|
|
||||||
function closePopout() { popupOpen = false }
|
function closePopout() { popupOpen = false }
|
||||||
|
|
||||||
readonly property var adapter: Bluetooth.defaultAdapter
|
readonly property var adapter: Bluetooth.defaultAdapter
|
||||||
@@ -38,6 +44,20 @@ Item {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property var discoveredDevices: {
|
||||||
|
var list = []
|
||||||
|
for (var i = 0; i < devices.length; i++) {
|
||||||
|
var d = devices[i]
|
||||||
|
if (!d) continue
|
||||||
|
if (d.paired || d.connected || d.bonded || d.trusted) continue
|
||||||
|
list.push(d)
|
||||||
|
}
|
||||||
|
list.sort(function(a, b) {
|
||||||
|
return (a.name || a.deviceName || a.address || "").localeCompare(b.name || b.deviceName || b.address || "")
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
readonly property string icon: {
|
readonly property string icon: {
|
||||||
if (!adapter) return ""
|
if (!adapter) return ""
|
||||||
if (!adapter.enabled) return ""
|
if (!adapter.enabled) return ""
|
||||||
@@ -49,6 +69,31 @@ Item {
|
|||||||
implicitWidth: button.implicitWidth
|
implicitWidth: button.implicitWidth
|
||||||
implicitHeight: button.implicitHeight
|
implicitHeight: button.implicitHeight
|
||||||
|
|
||||||
|
// Non-visual lifecycle watchers, one per device. Survives popup open/close
|
||||||
|
// and the discovered↔known transition that destroys row delegates.
|
||||||
|
Repeater {
|
||||||
|
model: root.devices
|
||||||
|
Item {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Common.WidgetButton {
|
Common.WidgetButton {
|
||||||
id: button
|
id: button
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -141,49 +186,68 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paired / known devices.
|
// Scrollable device list — capped so a noisy neighborhood doesn't
|
||||||
Repeater {
|
// grow the popup past the screen.
|
||||||
model: root.knownDevices
|
Flickable {
|
||||||
DeviceRow {
|
|
||||||
required property var modelData
|
|
||||||
width: parent.width
|
|
||||||
dev: modelData
|
|
||||||
isDiscovered: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discovered (unpaired) devices, only shown while scanning.
|
|
||||||
Text {
|
|
||||||
visible: root.adapter && root.adapter.discovering && root.discoveredDevices.length > 0
|
|
||||||
text: "Discovered"
|
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.adapter && root.adapter.discovering ? root.discoveredDevices : []
|
|
||||||
DeviceRow {
|
|
||||||
required property var modelData
|
|
||||||
width: parent.width
|
|
||||||
dev: modelData
|
|
||||||
isDiscovered: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: root.knownDevices.length === 0
|
|
||||||
&& (!root.adapter || !root.adapter.discovering || root.discoveredDevices.length === 0)
|
|
||||||
text: !root.adapter ? "No Bluetooth adapter"
|
|
||||||
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
|
|
||||||
: root.adapter.discovering ? "Scanning for devices…"
|
|
||||||
: "No paired devices. Tap the scan icon to find new ones."
|
|
||||||
color: Qt.darker(root.bar.foreground, 1.5)
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: 11
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
height: Math.min(deviceList.implicitHeight, 400)
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: deviceList.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: deviceList
|
||||||
|
width: parent.width
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
// Paired / known devices.
|
||||||
|
Repeater {
|
||||||
|
model: root.knownDevices
|
||||||
|
DeviceRow {
|
||||||
|
required property var modelData
|
||||||
|
width: deviceList.width
|
||||||
|
dev: modelData
|
||||||
|
isDiscovered: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discovered (unpaired) devices, only shown while scanning.
|
||||||
|
Text {
|
||||||
|
visible: root.adapter && root.adapter.discovering && root.discoveredDevices.length > 0
|
||||||
|
text: "Discovered"
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.adapter && root.adapter.discovering ? root.discoveredDevices : []
|
||||||
|
DeviceRow {
|
||||||
|
required property var modelData
|
||||||
|
width: deviceList.width
|
||||||
|
dev: modelData
|
||||||
|
isDiscovered: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: root.knownDevices.length === 0
|
||||||
|
&& (!root.adapter || !root.adapter.discovering || root.discoveredDevices.length === 0)
|
||||||
|
text: !root.adapter ? "No Bluetooth adapter"
|
||||||
|
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
|
||||||
|
: root.adapter.discovering ? "Scanning for devices…"
|
||||||
|
: "No paired devices. Tap the scan icon to find new ones."
|
||||||
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: 11
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: deviceList.width
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,6 +329,36 @@ Item {
|
|||||||
|
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rowMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
cursorShape: row.dev ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
|
||||||
|
onClicked: function(mouse) {
|
||||||
|
if (!row.dev) return
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
if (row.dev.forget) row.dev.forget()
|
||||||
|
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
|
||||||
|
row.pendingAction = 1
|
||||||
|
row.failureReason = ""
|
||||||
|
failureTimer.restart()
|
||||||
|
row.dev.connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: rowContent
|
id: rowContent
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -382,32 +476,5 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rowMouse
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
cursorShape: row.dev ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
|
|
||||||
onClicked: function(mouse) {
|
|
||||||
if (!row.dev) return
|
|
||||||
if (mouse.button === Qt.RightButton) {
|
|
||||||
if (row.dev.forget) row.dev.forget()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (row.isDiscovered) {
|
|
||||||
row.pendingAction = 3
|
|
||||||
row.failureReason = ""
|
|
||||||
failureTimer.restart()
|
|
||||||
row.dev.pair()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (row.isConnected) return // use the X button to disconnect
|
|
||||||
row.pendingAction = 1
|
|
||||||
row.failureReason = ""
|
|
||||||
failureTimer.restart()
|
|
||||||
row.dev.connect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user