No forgetting, deleting or clearing on a single press, anywhere

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 00:41:39 -04:00
parent 88371d19f0
commit f9e5d3f470
8 changed files with 267 additions and 84 deletions
@@ -41,11 +41,36 @@ Item {
return list;
}
// Forgetting is armed in place rather than through ConfirmAction: a 44px
// row inside a 300px popup has no width for a Keep/Forget it pair beside
// the device name, so the trash glyph arms itself and the row's own
// sublabel becomes the question. The token is ShellState's, the same one
// ConfirmAction uses, so arming here disarms whatever was armed elsewhere
// and only one confirm is ever live app-wide.
//
// The token is keyed by device address rather than held on the delegate,
// because discovery rebuilds this list -- and every delegate in it -- every
// time BlueZ reports something new. An arm parked on the row object would
// be dropped by the next scan result; keyed by address it survives the
// rebuild and is only ever cleared by a press or by the section closing.
readonly property string forgetPrefix: "bluetooth-forget-quick:"
function forgetId(device): string {
return root.forgetPrefix + (device.address || device.name || "");
}
function disarmForget(): void {
if (ShellState.armedConfirm.indexOf(root.forgetPrefix) === 0)
ShellState.armedConfirm = "";
}
onActiveChanged: {
if (root.active)
if (root.active) {
Connectivity.acquireDiscovery(root.scanHold);
else
} else {
Connectivity.releaseDiscovery(root.scanHold);
root.disarmForget();
}
}
Component.onDestruction: Connectivity.releaseDiscovery(root.scanHold)
@@ -106,18 +131,36 @@ Item {
required property var modelData
readonly property bool forgetArmed:
ShellState.armedConfirm === root.forgetId(deviceRow.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)
// Armed, the row states the question and what it costs, since
// the glyph alone is too small to carry either.
sublabel: deviceRow.forgetArmed
? "Press again to drop the pairing"
: root.stateText(deviceRow.modelData)
selected: deviceRow.modelData.connected
onClicked: root.activate(deviceRow.modelData)
// While armed the row body is the way out: a press anywhere
// else on it takes the arming back instead of connecting, so
// the escape is the largest target on screen.
onClicked: {
if (deviceRow.forgetArmed) {
root.disarmForget();
return;
}
root.activate(deviceRow.modelData);
}
// Forgetting is destructive enough that it gets its own
// control rather than sharing the row click.
// control rather than sharing the row click -- and its own
// two presses. See root.forgetPrefix for why the arming
// happens in place here rather than through ConfirmAction.
IconButton {
anchors.verticalCenter: parent.verticalCenter
visible: deviceRow.modelData.paired
@@ -125,7 +168,28 @@ Item {
iconSize: 13
icon: "user-trash-symbolic"
iconFallback: "window-close-symbolic"
onClicked: deviceRow.modelData.forget()
tint: deviceRow.forgetArmed ? Theme.danger : Theme.fg
onClicked: {
if (!deviceRow.forgetArmed) {
ShellState.armedConfirm = root.forgetId(deviceRow.modelData);
return;
}
root.disarmForget();
deviceRow.modelData.forget();
}
// Behind the parent's own fill, so the hover tint still
// reads on top of it. Danger appears only once armed --
// the first press must not look like the last one.
Rectangle {
anchors.fill: parent
z: -1
radius: parent.radius
visible: deviceRow.forgetArmed
color: Theme.alpha(Theme.danger, 0.3)
border.width: 1
border.color: Theme.alpha(Theme.danger, 0.6)
}
}
}
}