diff --git a/crates/qbz-qt/qml/controls/SelectionModel.qml b/crates/qbz-qt/qml/controls/SelectionModel.qml index 8551010ed..1563602bc 100644 --- a/crates/qbz-qt/qml/controls/SelectionModel.qml +++ b/crates/qbz-qt/qml/controls/SelectionModel.qml @@ -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 + } } diff --git a/crates/qbz-qt/qml/views/LocalLibraryView.qml b/crates/qbz-qt/qml/views/LocalLibraryView.qml index a7a6fed69..468a06e88 100644 --- a/crates/qbz-qt/qml/views/LocalLibraryView.qml +++ b/crates/qbz-qt/qml/views/LocalLibraryView.qml @@ -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. diff --git a/scripts/qml-tests/tst_multi_select.qml b/scripts/qml-tests/tst_multi_select.qml index 7b353011a..1ebd62e35 100644 --- a/scripts/qml-tests/tst_multi_select.qml +++ b/scripts/qml-tests/tst_multi_select.qml @@ -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") + } } }