feat(qt): Library Explorer columns select a run with Shift

A facet column already took Ctrl as "add or remove one" and a plain click
as "only this". Shift now picks the run from the last clicked value and
Ctrl+Shift adds that run, the file-list rule (SelectionModel.nextExclusive),
each column over its values as listed. A column whose values are rebuilt by
a change upstream starts without an anchor.

cc
This commit is contained in:
vicrodh committed 2026-09-14 10:31:48 -06:00
1 parent cf3069386b
commit ad07d8c14e
3 files changed
+84 -14

No files matched your search

@@ -118,4 +118,39 @@ QtObject {
root.anchorId = key
return out
}
/// The spreadsheet / file-manager rule, for lists where a plain click
/// REPLACES the selection (the Library Explorer facets):
///
/// plain click only this row; it becomes the anchor
/// Ctrl / Cmd toggle this row, keep the rest; it becomes the anchor
/// Shift only the range anchor..row; the anchor stays
/// Ctrl+Shift add the range anchor..row to the selection
///
/// Without a visible anchor Shift behaves like the same click without it.
function nextExclusive(current, id, rows, modifiers) {
var mods = modifiers || 0
var shift = (mods & Qt.ShiftModifier) !== 0
var additive = (mods & (Qt.ControlModifier | Qt.MetaModifier)) !== 0
var key = String(id)
var anchorAt = root.anchorId !== "" ? root._indexOf(rows, root.anchorId) : -1
if (shift && anchorAt >= 0) {
var here = root._indexOf(rows, id)
if (here >= 0)
return root._fillRange(additive ? Object.assign({}, current) : {},
rows, anchorAt, here)
}
root.anchorId = key
if (!additive) {
var only = {}
only[key] = true
return only
}
var out = Object.assign({}, current)
if (out[key] === true) delete out[key]
else out[key] = true
return out
}
}
+30 -14
View File
@@ -1187,23 +1187,42 @@ Rectangle {
return sortRows(rows, genresSort)
}
// The Explorer facets follow the spreadsheet rule — a plain click picks
// one value, Ctrl adds or removes one, Shift picks the run from the last
// click, Ctrl+Shift adds that run (SelectionModel.nextExclusive) — each
// column over its values AS LISTED (sorted, and filtered by its own
// search box). One anchor per column; a column whose values are rebuilt
// by a change upstream starts without one.
SelectionModel { id: genreFacetSel; idKey: "key" }
SelectionModel { id: yearFacetSel; idKey: "key" }
SelectionModel { id: artistFacetSel; idKey: "key" }
SelectionModel { id: albumFacetSel; idKey: "key" }
function toggleGenre(key, modifiers) {
selectedGenres = nextFacetSelection(selectedGenres, key, modifiers)
selectedGenres = nextFacetSelection(genreFacetSel, selectedGenres, key, modifiers, genreNames)
selectedGenreYears = ({})
selectedGenreArtists = ({})
selectedGenreAlbums = ({})
yearFacetSel.anchorId = ""
artistFacetSel.anchorId = ""
albumFacetSel.anchorId = ""
}
function toggleGenreYear(key, modifiers) {
selectedGenreYears = nextFacetSelection(selectedGenreYears, key, modifiers)
selectedGenreYears = nextFacetSelection(yearFacetSel, selectedGenreYears, key, modifiers,
genreYearOptions)
selectedGenreArtists = ({})
selectedGenreAlbums = ({})
artistFacetSel.anchorId = ""
albumFacetSel.anchorId = ""
}
function toggleGenreArtist(key, modifiers) {
selectedGenreArtists = nextFacetSelection(selectedGenreArtists, key, modifiers)
selectedGenreArtists = nextFacetSelection(artistFacetSel, selectedGenreArtists, key,
modifiers, genreArtistOptions)
selectedGenreAlbums = ({})
albumFacetSel.anchorId = ""
}
function toggleGenreAlbum(key, modifiers) {
selectedGenreAlbums = nextFacetSelection(selectedGenreAlbums, key, modifiers)
selectedGenreAlbums = nextFacetSelection(albumFacetSel, selectedGenreAlbums, key, modifiers,
genreAlbumOptions)
}
function setExplorerColumns(mode) {
if (["genre", "year", "both"].indexOf(mode) < 0 || mode === explorerColumns)
@@ -1219,17 +1238,14 @@ Rectangle {
readonly property int explorerFacetCount: explorerColumns === "both" ? 4 : 3
readonly property int explorerLeadingCount: explorerColumns === "year"
? genreYearOptions.length : genreNames.length
function nextFacetSelection(current, key, modifiers) {
if (key === "") return ({})
var additive = (modifiers & Qt.ControlModifier) !== 0
|| (modifiers & Qt.MetaModifier) !== 0
if (!additive) {
var only = {}; only[key] = true; return only
/// `key` "" is the column's "All" row: it clears the column.
function nextFacetSelection(rule, current, key, modifiers, options) {
if (key === "") {
rule.anchorId = ""
return ({})
}
var out = Object.assign({}, current)
if (out[key]) delete out[key]
else out[key] = true
return out
return rule.nextExclusive(current, key, options || [],
modifiers === undefined ? Qt.NoModifier : modifiers)
}
// The selected artist's albums.
+19
View File
@@ -432,5 +432,24 @@ Item {
m = keyedRule.next(m, 7, rows, Qt.ShiftModifier)
compare(Object.keys(m).sort().join(","), "7,8,9")
}
function test_exclusive_rule() {
var rows = [{ "id": "g1" }, { "id": "g2" }, { "id": "g3" }, { "id": "g4" }, { "id": "g5" }]
var m = rule.nextExclusive({}, "g2", rows, Qt.NoModifier)
compare(Object.keys(m).sort().join(","), "g2", "a plain click selects only that row")
m = rule.nextExclusive(m, "g4", rows, Qt.ShiftModifier)
compare(Object.keys(m).sort().join(","), "g2,g3,g4", "Shift selects the range")
m = rule.nextExclusive(m, "g1", rows, Qt.ShiftModifier)
compare(Object.keys(m).sort().join(","), "g1,g2", "the anchor stays, Shift replaces the range")
m = rule.nextExclusive(m, "g5", rows, Qt.ControlModifier)
compare(Object.keys(m).sort().join(","), "g1,g2,g5", "Ctrl adds one")
m = rule.nextExclusive(m, "g5", rows, Qt.ControlModifier)
compare(Object.keys(m).sort().join(","), "g1,g2", "Ctrl on a selected row removes it")
m = rule.nextExclusive(m, "g3", rows, Qt.ControlModifier)
m = rule.nextExclusive(m, "g5", rows, Qt.ControlModifier | Qt.ShiftModifier)
compare(Object.keys(m).sort().join(","), "g1,g2,g3,g4,g5", "Ctrl+Shift adds a range")
m = rule.nextExclusive(m, "g4", rows, Qt.NoModifier)
compare(Object.keys(m).sort().join(","), "g4", "a plain click starts over")
}
}
}