fix(qt): Shift and Ctrl work on every select-mode click target, not only the row body

Excel-style selection (750c59034) read the modifiers only where a row BODY
took the click. Every other target a select-mode surface draws dropped them,
so a Shift-click there was a plain toggle:

  - the track row's checkbox disc passed Qt.NoModifier;
  - QbzCheckbox and SelectCheck emitted toggled() with no argument, so the
    Library album rows, the local album rows and the folder rows, which
    already expected `function (mods)`, received undefined;
  - AlbumCard emitted selectToggled() with no argument, and the paged local
    albums grid (299dcde5b) spelled the drop out as Qt.NoModifier.

Some targets were never selection targets at all: in select mode the artist
and album names on a track row, the artist and label on a Library album row
and the artist line of an album card still navigated, leaving the page and
the selection mid-gesture. The row links stand down in select mode so the
click reaches the row body with its modifiers, and the card's artist line
toggles like its artwork and title. A hover-only tooltip on the pulled but
cached glyph no longer eats the row's click.

Reproduced on the release binary in a nested KWin with injected input: in
Local Library Tracks, the checkbox discs of rows 1, Shift 4 and Ctrl 6
selected 3 rows; the same clicks on the row bodies selected 5. No list
view, Flickable or wheel code changes; tst_wheel passes unchanged.

tst_multi_select clicks each of these targets through the real TrackRow,
AlbumListRow, AlbumCard, QbzCheckbox and SelectCheck.

cc
This commit is contained in:
vicrodh committed 2026-09-14 10:30:34 -06:00
1 parent 7d1a144c89
commit 7c9bf8043f
12 files changed
+360 -23

No files matched your search

+16 -6
View File
@@ -213,7 +213,10 @@ Rectangle {
/// Park the select indicator bottom-right of the art instead of top-right. /// Park the select indicator bottom-right of the art instead of top-right.
property bool selectMarkBottom: false property bool selectMarkBottom: false
property bool selected: false property bool selected: false
signal selectToggled() /// `modifiers` rides straight off the click: a grid host turns Shift into
/// a range (controls/SelectionModel.qml). Every select-mode target on the
/// card — artwork, title, artist line — emits it.
signal selectToggled(int modifiers)
// Album blacklist ("Block this album") — LIVE since QbzBlacklist landed // Album blacklist ("Block this album") — LIVE since QbzBlacklist landed
// (`blockAlbum(id, title, artist, coverUrl)`). Kept as a property, not // (`blockAlbum(id, title, artist, coverUrl)`). Kept as a property, not
@@ -561,7 +564,7 @@ Rectangle {
} }
// .slint:169 — in select mode the card click TOGGLES. // .slint:169 — in select mode the card click TOGGLES.
if (root.selectMode) { if (root.selectMode) {
root.selectToggled() root.selectToggled(mouse.modifiers)
return return
} }
if (root.pulledDead) if (root.pulledDead)
@@ -993,7 +996,7 @@ Rectangle {
// .slint:465 — the title carries the SAME target // .slint:465 — the title carries the SAME target
// as the artwork, select mode included. // as the artwork, select mode included.
if (root.selectMode) { if (root.selectMode) {
root.selectToggled() root.selectToggled(mouse.modifiers)
return return
} }
if (root.pulledDead) if (root.pulledDead)
@@ -1009,7 +1012,8 @@ Rectangle {
width: parent.width width: parent.width
height: 18 height: 18
text: root.artist text: root.artist
color: (root.artistId !== "" || root.hostArtistLink) color: !root.selectMode
&& (root.artistId !== "" || root.hostArtistLink)
&& artistArea.containsMouse && artistArea.containsMouse
? theme.textPrimary : theme.textMuted ? theme.textPrimary : theme.textMuted
font.pixelSize: theme.fontLink - 1 font.pixelSize: theme.fontLink - 1
@@ -1019,9 +1023,15 @@ Rectangle {
id: artistArea id: artistArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: root.artistId !== "" || root.hostArtistLink cursorShape: root.selectMode || root.artistId !== "" || root.hostArtistLink
? Qt.PointingHandCursor : Qt.ArrowCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: { onClicked: function (mouse) {
// In select mode the artist line is the card too:
// opening the artist mid-selection would drop it.
if (root.selectMode) {
root.selectToggled(mouse.modifiers)
return
}
if (root.artistId !== "") if (root.artistId !== "")
QbzArtist.openArtist(root.artistId) QbzArtist.openArtist(root.artistId)
else if (root.hostArtistLink) else if (root.hostArtistLink)
+11 -6
View File
@@ -1,6 +1,11 @@
// QbzCheckbox (primitives/QbzCheckbox.slint) — 18px square, r4, accent fill // QbzCheckbox (primitives/QbzCheckbox.slint) — 18px square, r4, accent fill
// with an accent-text check when on, muted border when off. Emits toggled(); // with an accent-text check when on, muted border when off. Emits
// like the Slint original it never self-flips (the owner of the state does). // toggled(modifiers); like the Slint original it never self-flips (the owner of
// the state does).
//
// `modifiers` is the click's (or the key's), straight through: a multi-select
// host turns Shift into a range (controls/SelectionModel.qml). Hosts that only
// flip a flag keep a plain expression handler and never look at it.
// //
// Used by Settings > Local Library (folder selection) and the Plex library // Used by Settings > Local Library (folder selection) and the Plex library
// picker, i.e. every place the Slint uses its own QbzCheckbox. // picker, i.e. every place the Slint uses its own QbzCheckbox.
@@ -15,7 +20,7 @@ Rectangle {
id: root id: root
property bool checked: false property bool checked: false
signal toggled() signal toggled(int modifiers)
QbzTheme { id: theme } QbzTheme { id: theme }
@@ -30,14 +35,14 @@ Rectangle {
activeFocusOnTab: root.enabled activeFocusOnTab: root.enabled
Accessible.role: Accessible.CheckBox Accessible.role: Accessible.CheckBox
Accessible.checked: root.checked Accessible.checked: root.checked
Accessible.onToggleAction: if (root.enabled) root.toggled() Accessible.onToggleAction: if (root.enabled) root.toggled(Qt.NoModifier)
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if (root.enabled && !event.isAutoRepeat if (root.enabled && !event.isAutoRepeat
&& (event.key === Qt.Key_Space && (event.key === Qt.Key_Space
|| event.key === Qt.Key_Return || event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter)) { || event.key === Qt.Key_Enter)) {
root.toggled() root.toggled(event.modifiers)
event.accepted = true event.accepted = true
} }
} }
@@ -66,6 +71,6 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: root.forceActiveFocus() onPressed: root.forceActiveFocus()
onClicked: root.toggled() onClicked: function (mouse) { root.toggled(mouse.modifiers) }
} }
} }
+18 -3
View File
@@ -749,7 +749,10 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.toggleSelect(Qt.NoModifier) // The disc is where a select-mode click most often lands,
// so it hands over the modifiers exactly like the body:
// a plain `NoModifier` here made Shift-click a toggle.
onClicked: function (mouse) { root.toggleSelect(mouse.modifiers) }
} }
} }
Text { Text {
@@ -969,6 +972,9 @@ Rectangle {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
// Hover only: a tooltip must not eat the click the
// row body (play, or a select-mode toggle) owns.
acceptedButtons: Qt.NoButton
ToolTip.visible: containsMouse ToolTip.visible: containsMouse
ToolTip.delay: 300 ToolTip.delay: 300
// NEW msgid — the one string this state needs and // NEW msgid — the one string this state needs and
@@ -1078,7 +1084,12 @@ Rectangle {
MouseArea { MouseArea {
id: artistLinkArea id: artistLinkArea
anchors.fill: parent anchors.fill: parent
enabled: root.artistLink && (artistLine.linkMode || !!root.item.artistId) // In select mode the whole row is a selection target
// (TrackRow.slint:174): the names stop being links so the
// click — and its Shift / Ctrl — reaches the row body
// instead of opening an artist page mid-selection.
enabled: !root.selectMode
&& root.artistLink && (artistLine.linkMode || !!root.item.artistId)
hoverEnabled: true hoverEnabled: true
cursorShape: (artistLine.linkMode cursorShape: (artistLine.linkMode
? (artistLine.hotSpan >= 0 && artistLine.spans[artistLine.hotSpan].href !== "") ? (artistLine.hotSpan >= 0 && artistLine.spans[artistLine.hotSpan].href !== "")
@@ -1088,6 +1099,9 @@ Rectangle {
artistLine.hotSpan = artistLine.spanAt(mouse.x) artistLine.hotSpan = artistLine.spanAt(mouse.x)
} }
onExited: artistLine.hotSpan = -1 onExited: artistLine.hotSpan = -1
// Entering select mode under the pointer disables the area
// without an `exited`; the hot name must not stay lit.
onEnabledChanged: if (!enabled) artistLine.hotSpan = -1
// Same routing as the menu's "Go to artist" — Slint drives // Same routing as the menu's "Go to artist" — Slint drives
// both through one handler, so gating only the menu would // both through one handler, so gating only the menu would
// leave the LINK navigating to the track's own artist. // leave the LINK navigating to the track's own artist.
@@ -1116,7 +1130,8 @@ Rectangle {
MouseArea { MouseArea {
id: albumArea id: albumArea
anchors.fill: parent anchors.fill: parent
enabled: !!root.item.albumId && !root.releaseGone // Not a link in select mode, for the artist line's reason.
enabled: !root.selectMode && !!root.item.albumId && !root.releaseGone
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
// The names ride along so a release the catalog lost can // The names ride along so a release the catalog lost can
+7 -3
View File
@@ -304,8 +304,11 @@ Rectangle {
id: artistArea id: artistArea
anchors.fill: parent anchors.fill: parent
// Only a real artist id is clickable — otherwise the // Only a real artist id is clickable — otherwise the
// pointer promises a page that cannot open. // pointer promises a page that cannot open. In select
enabled: (root.item.artistId || "") !== "" // mode the whole row selects (Shift / Ctrl included), so
// the name stops being a link and the click falls to the
// row body underneath.
enabled: !root.selectMode && (root.item.artistId || "") !== ""
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: QbzArtist.openArtist(root.item.artistId) onClicked: QbzArtist.openArtist(root.item.artistId)
@@ -331,7 +334,8 @@ Rectangle {
MouseArea { MouseArea {
id: labelArea id: labelArea
anchors.fill: parent anchors.fill: parent
enabled: (root.item.labelId || "") !== "" // Not a link in select mode, for the artist name's reason.
enabled: !root.selectMode && (root.item.labelId || "") !== ""
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: QbzHome.openLabel(root.item.labelId) onClicked: QbzHome.openLabel(root.item.labelId)
@@ -593,11 +593,14 @@ Item {
selected: root.nativeActive selected: root.nativeActive
? cardCell.slot.selected === true ? cardCell.slot.selected === true
: root.selected[cardCell.slot.id] === true : root.selected[cardCell.slot.id] === true
onSelectToggled: { // The card's modifiers, straight through: the
// paged grid (299dcde5b) passed NoModifier here,
// so a Shift-click on a card was a toggle.
onSelectToggled: function (mods) {
if (root.nativeActive) if (root.nativeActive)
root.nativeToggleSelect(cardCell.slot.nativeIndex, Qt.NoModifier) root.nativeToggleSelect(cardCell.slot.nativeIndex, mods)
else else
root.toggleSelect(cardCell.slot.id, Qt.NoModifier) root.toggleSelect(cardCell.slot.id, mods)
} }
// Non-select mode only — the card routes a // Non-select mode only — the card routes a
// select-mode click to `selectToggled` and never // select-mode click to `selectToggled` and never
@@ -5,6 +5,10 @@
// Slint declares this inline inside TreeRow; the Qt port also needs it on // Slint declares this inline inside TreeRow; the Qt port also needs it on
// the album cards and the track rows in multi-select, so it is its own file // the album cards and the track rows in multi-select, so it is its own file
// (three call sites, one shape). // (three call sites, one shape).
//
// `toggled` carries the click's modifiers: this disc IS a select-mode click
// target, and one that dropped them turned every Shift-click on it into a
// plain toggle (controls/SelectionModel.qml).
import QtQuick import QtQuick
import com.blitzfc.qbz import com.blitzfc.qbz
@@ -16,7 +20,7 @@ Rectangle {
property bool on: false property bool on: false
property bool partial: false property bool partial: false
property int diameter: 13 property int diameter: 13
signal toggled() signal toggled(int modifiers)
QbzTheme { id: theme } QbzTheme { id: theme }
@@ -52,6 +56,6 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.toggled() onClicked: function (mouse) { root.toggled(mouse.modifiers) }
} }
} }
@@ -0,0 +1,7 @@
pragma Singleton
import QtQuick
QtObject {
property var opened: []
function openAlbum(id) { opened = opened.concat([String(id)]) }
function openAlbumFrom(id, title, artist) { opened = opened.concat([String(id)]) }
}
@@ -0,0 +1,6 @@
pragma Singleton
import QtQuick
QtObject {
property var opened: []
function openArtist(id) { opened = opened.concat([String(id)]) }
}
@@ -0,0 +1,6 @@
pragma Singleton
import QtQuick
QtObject {
property var labels: []
function openLabel(id) { labels = labels.concat([String(id)]) }
}
@@ -0,0 +1,6 @@
pragma Singleton
import QtQuick
QtObject {
property var submitted: []
function searchSubmit(text) { submitted = submitted.concat([String(text)]) }
}
@@ -9,3 +9,7 @@ singleton QbzPurchases 1.0 QbzPurchases.qml
singleton QbzOrbit 1.0 QbzOrbit.qml singleton QbzOrbit 1.0 QbzOrbit.qml
singleton QbzAbout 1.0 QbzAbout.qml singleton QbzAbout 1.0 QbzAbout.qml
singleton QbzArtist 1.0 QbzArtist.qml
singleton QbzAlbum 1.0 QbzAlbum.qml
singleton QbzSearch 1.0 QbzSearch.qml
singleton QbzHome 1.0 QbzHome.qml
+267
View File
@@ -8,11 +8,278 @@ import QtQuick
import QtTest import QtTest
import com.blitzfc.qbz import com.blitzfc.qbz
import "../../crates/qbz-qt/qml/controls" import "../../crates/qbz-qt/qml/controls"
import "../../crates/qbz-qt/qml/rows"
import "../../crates/qbz-qt/qml/cards"
import "../../crates/qbz-qt/qml/views" as Views
import "../../crates/qbz-qt/qml/views/local" as Local
Item { Item {
id: root id: root
width: 1200; height: 1400 width: 1200; height: 1400
function track(i) {
return { "id": "t" + i, "title": "Track " + i, "artist": "Artist " + i,
"artistId": "a" + i, "album": "Album " + i, "albumId": "al" + i,
"duration": "3:00", "qualityTier": "", "qualityDetail": "" }
}
function album(i) {
return { "id": "b" + i, "title": "Record " + i, "artist": "Band " + i,
"artistId": "band" + i, "label": "Label " + i, "labelId": "l" + i,
"year": "2001", "qualityTier": "", "qualityDetail": "" }
}
function range(n, make) {
var out = []
for (var i = 0; i < n; i++)
out.push(make(i))
return out
}
// ---- the track list (AlbumView / PlaylistView wiring) -------------------
property var tracks: range(8, track)
property var trackSelected: ({})
SelectionModel { id: trackSel }
function toggleTrack(id, mods) {
root.trackSelected = trackSel.next(root.trackSelected, id, root.tracks,
mods === undefined ? Qt.NoModifier : mods)
}
Column {
id: trackColumn
width: 1000
Repeater {
id: trackRepeater
model: root.tracks
delegate: TrackRow {
required property var modelData
required property int index
width: 1000
item: modelData
number: index + 1
artistLink: true
showAlbum: true
selectMode: true
checked: root.trackSelected[modelData.id] === true
onToggleSelect: function (mods) { root.toggleTrack(modelData.id, mods) }
}
}
}
// ---- the album list (Library albums list wiring) ------------------------
property var albums: range(6, album)
property var albumSelected: ({})
SelectionModel { id: albumSel }
function toggleAlbum(id, mods) {
root.albumSelected = albumSel.next(root.albumSelected, id, root.albums,
mods === undefined ? Qt.NoModifier : mods)
}
Column {
id: albumColumn
y: 420
width: 1000
Repeater {
id: albumRepeater
model: root.albums
delegate: Views.AlbumListRow {
required property var modelData
required property int index
width: 1000
item: modelData
rowIndex: index
selectMode: true
checked: root.albumSelected[modelData.id] === true
onToggleSelect: function (mods) { root.toggleAlbum(modelData.id, mods) }
}
}
}
// ---- one album card and the two checkbox controls ------------------------
property var cardMods: []
AlbumCard {
id: card
x: 1000; y: 0
albumId: "card"
title: "Card title"
artist: "Card artist"
artistId: "card-artist"
selectMode: true
onSelectToggled: function (mods) { root.cardMods = root.cardMods.concat([mods]) }
}
property var checkboxMods: []
QbzCheckbox {
id: checkbox
x: 1000; y: 320
onToggled: function (mods) { root.checkboxMods = root.checkboxMods.concat([mods]) }
}
property var selectCheckMods: []
Local.SelectCheck {
id: selectCheck
x: 1040; y: 320
onToggled: function (mods) { root.selectCheckMods = root.selectCheckMods.concat([mods]) }
}
TestCase {
name: "MultiSelect"
when: windowShown
function find(item, pred) {
if (!item)
return null
if (pred(item))
return item
var kids = item.children || []
for (var i = 0; i < kids.length; i++) {
var hit = find(kids[i], pred)
if (hit)
return hit
}
return null
}
function shown(item) {
for (var it = item; it; it = it.parent)
if (!it.visible)
return false
return true
}
function textItem(host, text) {
var t = find(host, function (it) {
return it.text === text && it.contentWidth !== undefined && shown(it)
})
verify(t !== null, "text '" + text + "' is drawn")
return t
}
// Clicks go through the window at the item's position, so whichever
// MouseArea really sits on top there receives them — the point of
// using the real components.
function clickOn(item, x, y, mods) {
var p = item.mapToItem(root, x, y)
mouseClick(root, p.x, p.y, Qt.LeftButton, mods === undefined ? Qt.NoModifier : mods)
wait(20)
}
function selectedIds(map) {
return Object.keys(map).sort().join(",")
}
function trackRow(i) { return trackRepeater.itemAt(i) }
function albumRow(i) { return albumRepeater.itemAt(i) }
function disc(row) {
var d = find(row, function (it) {
return it.width === 14 && it.height === 14 && it.hoverEnabled !== undefined
&& it.cursorShape !== undefined && shown(it)
})
verify(d !== null, "the select-mode disc is drawn")
return d
}
function init() {
root.trackSelected = ({})
trackSel.anchorId = ""
root.albumSelected = ({})
albumSel.anchorId = ""
root.cardMods = []
root.checkboxMods = []
root.selectCheckMods = []
QbzArtist.opened = []
QbzAlbum.opened = []
QbzHome.labels = []
}
// A point of the row with nothing drawn over it but the body: the
// duration cell.
function bodyPoint(row) {
var duration = textItem(row, "3:00")
return duration.mapToItem(row, duration.width / 2, duration.height / 2)
}
function clickBody(row, mods) {
var p = bodyPoint(row)
clickOn(row, p.x, p.y, mods)
}
function test_row_body_shift_and_ctrl() {
clickBody(trackRow(1))
clickBody(trackRow(4), Qt.ShiftModifier)
compare(selectedIds(root.trackSelected), "t1,t2,t3,t4")
clickBody(trackRow(6), Qt.ControlModifier)
compare(selectedIds(root.trackSelected), "t1,t2,t3,t4,t6")
}
function test_checkbox_disc_shift_and_ctrl() {
clickOn(disc(trackRow(0)), 7, 7)
clickOn(disc(trackRow(3)), 7, 7, Qt.ShiftModifier)
compare(selectedIds(root.trackSelected), "t0,t1,t2,t3",
"Shift on the disc selects the range")
clickOn(disc(trackRow(5)), 7, 7, Qt.ControlModifier)
clickOn(disc(trackRow(7)), 7, 7, Qt.ControlModifier | Qt.ShiftModifier)
compare(selectedIds(root.trackSelected), "t0,t1,t2,t3,t5,t6,t7",
"Ctrl adds a row and Ctrl+Shift adds a second group")
}
function test_artist_link_selects_in_select_mode() {
clickBody(trackRow(2))
var name = textItem(trackRow(5), "Artist 5")
clickOn(name, 10, name.height / 2, Qt.ShiftModifier)
compare(QbzArtist.opened.length, 0, "no artist page opens in select mode")
compare(selectedIds(root.trackSelected), "t2,t3,t4,t5")
}
function test_album_link_selects_in_select_mode() {
var name = textItem(trackRow(6), "Album 6")
clickOn(name, 10, name.height / 2)
compare(QbzAlbum.opened.length, 0, "no album page opens in select mode")
compare(selectedIds(root.trackSelected), "t6")
}
function test_album_row_checkbox_shift_range() {
var first = find(albumRow(0), function (it) {
return it.checked !== undefined && it.width === 18 && it.height === 18 && shown(it)
})
var fourth = find(albumRow(3), function (it) {
return it.checked !== undefined && it.width === 18 && it.height === 18 && shown(it)
})
verify(first !== null && fourth !== null, "the album row checkboxes are drawn")
clickOn(first, 9, 9)
clickOn(fourth, 9, 9, Qt.ShiftModifier)
compare(selectedIds(root.albumSelected), "b0,b1,b2,b3")
}
function test_album_row_links_select_in_select_mode() {
clickOn(albumRow(1), 600, 32)
var band = textItem(albumRow(4), "Band 4")
clickOn(band, 10, band.height / 2, Qt.ShiftModifier)
compare(QbzArtist.opened.length, 0, "no artist page opens in select mode")
var label = textItem(albumRow(5), "Label 5")
clickOn(label, 10, label.height / 2, Qt.ControlModifier)
compare(QbzHome.labels.length, 0, "no label page opens in select mode")
compare(selectedIds(root.albumSelected), "b1,b2,b3,b4,b5")
}
function test_album_card_forwards_modifiers() {
clickOn(card, 100, 100, Qt.ShiftModifier)
var title = textItem(card, "Card title")
clickOn(title, 10, title.height / 2, Qt.ControlModifier)
var artist = textItem(card, "Card artist")
clickOn(artist, 10, artist.height / 2, Qt.ShiftModifier)
compare(QbzArtist.opened.length, 0, "no artist page opens in select mode")
compare(root.cardMods.length, 3)
verify((root.cardMods[0] & Qt.ShiftModifier) !== 0, "artwork carries Shift")
verify((root.cardMods[1] & Qt.ControlModifier) !== 0, "title carries Ctrl")
verify((root.cardMods[2] & Qt.ShiftModifier) !== 0, "artist line carries Shift")
}
function test_checkbox_controls_forward_modifiers() {
clickOn(checkbox, 9, 9, Qt.ShiftModifier)
clickOn(selectCheck, 6, 6, Qt.ControlModifier)
compare(root.checkboxMods.length, 1)
verify((root.checkboxMods[0] & Qt.ShiftModifier) !== 0, "QbzCheckbox carries Shift")
compare(root.selectCheckMods.length, 1)
verify((root.selectCheckMods[0] & Qt.ControlModifier) !== 0, "SelectCheck carries Ctrl")
checkbox.forceActiveFocus()
keyClick(Qt.Key_Space)
compare(root.checkboxMods.length, 2)
compare(root.checkboxMods[1] & Qt.ShiftModifier, 0, "the keyboard toggle is a plain toggle")
}
}
// ---- the selection rule itself --------------------------------------------- // ---- the selection rule itself ---------------------------------------------
SelectionModel { id: rule } SelectionModel { id: rule }
SelectionModel { id: keyedRule; idKey: "trackId" } SelectionModel { id: keyedRule; idKey: "trackId" }