feat(qt): Excel-style multi-select — Shift ranges across every surface
Selection existed but had no keyboard: every click was a single toggle,
so picking 40 tracks meant 40 clicks. Slint and Tauri have had ranges
since 2026-06; this is that port.
NINE surfaces, not the five the track views suggested: Album, Artist,
Playlist, Label, Local Album, plus Library's tracks and albums and Local
Library's tracks and albums. Each carried its own hand-copied `selected`
map and its own `toggleSelected`, so the rule went into
controls/SelectionModel.qml and the maps stayed where they were — the
smallest change that stops it being copied a tenth time.
The rules are Tauri's `applyShiftRange`, unchanged: a plain click
toggles one row without clearing the rest, Shift fills the range from
the anchor ADDITIVELY (so re-dragging a range cannot eat the set) and
leaves the anchor where it is, and Ctrl/Cmd is the same as a plain click
— not a shortcut, but the behaviour both references already have,
because an accumulating click is what "add without disturbing" means.
Qt makes this CHEAPER than Slint, and the difference is worth recording:
Slint had no modifiers at click time and had to read a thread-local set
from a ModifiersChanged handler, with a documented stale-read edge case
when the window lost focus mid-chord. A QML MouseArea delivers
`mouse.modifiers` on the event itself, so the modifier and the click can
never disagree and no such seam exists here.
Three judgement calls, stated because they are not forced:
- Library gets TWO models, one per kind. Its rows are mixed, and one
anchor would let a Shift-click on a track sweep up the albums drawn
between it and the anchor.
- Ranges run over the VISIBLE rows. Local Library's search box is a
view filter, and a range over rows the user cannot see is not the
range they asked for — the same call `select-all` already made.
- The Local Library tree rail keeps plain toggles. Its selection lives
in Rust (QbzLocal.treeToggle*Select), so there is no QML map to hang
an anchor off; teaching the Rust side about an anchor is its own
change, and the file says so where a reader would expect otherwise.
The anchor is an ID, not an index, because every one of these views can
re-sort or filter under a live selection — an index anchor silently
points at a different track the moment the order changes. Slint's
selection.rs made the same call for the same reason.
Verified by driving the app: in Abbey Road, click row 1 then Shift-click
row 5 selects exactly rows 1-5 and leaves 6-8 untouched, with the bar
reading "5 selected"; the same in a playlist, additively over a row that
was already selected.
cc
This commit is contained in:
19 files changed
+231
-94
No files matched your search
@@ -96,15 +96,16 @@ Rectangle {
|
||||
readonly property bool multiSelectOn: root.multiSelect
|
||||
function setMultiSelect(on) {
|
||||
root.multiSelect = on
|
||||
if (!on) root.selected = ({})
|
||||
if (!on) { root.selected = ({}); sel.anchorId = "" }
|
||||
}
|
||||
function toggleSelected(id) {
|
||||
var m = root.selected
|
||||
if (m[id] === true) delete m[id]
|
||||
else m[id] = true
|
||||
// A rebind needs a NEW object reference — mutating in place notifies
|
||||
// nothing.
|
||||
root.selected = Object.assign({}, m)
|
||||
/// Excel-style selection lives in ONE place — controls/SelectionModel.qml
|
||||
/// holds the anchor and the Shift-range rule; this view keeps owning its
|
||||
/// map. `mods` is the mouse event's modifiers, forwarded by the row; a
|
||||
/// caller with no event (the checkbox, a keyboard path) may omit it.
|
||||
SelectionModel { id: sel }
|
||||
function toggleSelected(id, mods) {
|
||||
root.selected = sel.next(root.selected, id, root.topTracks,
|
||||
mods === undefined ? Qt.NoModifier : mods)
|
||||
}
|
||||
function selectedIdsInOrder() {
|
||||
var rows = root.topTracks
|
||||
@@ -121,7 +122,7 @@ Rectangle {
|
||||
root.selected = m
|
||||
return
|
||||
}
|
||||
if (action === "clear") { root.selected = ({}); return }
|
||||
if (action === "clear") { root.selected = ({}); sel.anchorId = ""; return }
|
||||
var ids = root.selectedIdsInOrder()
|
||||
if (ids.length === 0) return
|
||||
QbzPlayer.bulkTracksAction(JSON.stringify(ids), action, "label", String(doc.id || ""))
|
||||
@@ -630,7 +631,7 @@ Rectangle {
|
||||
showAlbum: true
|
||||
selectMode: root.multiSelect
|
||||
checked: root.selected[item.id] === true
|
||||
onToggleSelect: root.toggleSelected(item.id)
|
||||
onToggleSelect: function (mods) { root.toggleSelected(item.id, mods) }
|
||||
onPlayRequested: QbzHome.labelPlayTrack(modelData.id)
|
||||
onEnqueueRequested: function (mode) {
|
||||
QbzHome.labelEnqueueTrack(modelData.id, mode)
|
||||
|
||||
Reference in new issue
Block a user