Files
qbz/crates/qbz-qt/qml/views/local/TreeRow.qml
T
vicrodh b43771cc3e feat(qt): Shift ranges in the Local Library folder rail
The rail's selection lives in Rust as a set of file paths, so it had no
anchor and stayed toggle-only (750c59034 said as much). The rail now keeps
the anchor path and, on Shift, sends every drawn row from it to the clicked
row to a new treeSelectRange, which SELECTS: a folder with its whole
subtree, a track with its own record, so a range only ever adds. In select
mode a Shift or Ctrl click on a row body selects it instead of opening the
folder; a plain click still opens it.

The rail's selection mutations used to be spawned side by side. With ranges
that became a race: a range that finished before the click that set its
anchor would be undone by that late toggle. They now run one at a time in
click order, with select-all, clear and leaving select mode in the same
queue.

cc
2026-09-14 10:31:22 -06:00

152 lines
5.7 KiB
QML

// One row of the FLATTENED folder tree (LocalLibraryView.slint:444 TreeRow).
//
// 26px, radius 6, depth indent 8 + depth*16, select-mode checkbox (folder =
// tri-state from node.selectState 0/1/2, track = node.selected), an expand
// chevron with its OWN hit target (so toggling never selects), the folder /
// music glyph and the segment name. The selection cue is the 3px accent bar
// on the left edge — ADR-008: never a pill.
// Right-click opens a folder queue menu. DIVERGENCE: LocalLibraryView.slint
// gives the tree rail no per-row menu (only the detail pane's Play button and
// the bulk bar), but the owner asked for working context menus on the local
// rows and every entry here rides a seam that already exists
// (QbzLocal.playFolder / enqueue("folder", path, mode) — local_playback.rs
// `list_folder_tracks_recursive`). TRACK leaves get no menu: their `path` is
// a file path, not a track id, so no track arm accepts it.
import QtQuick
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
Rectangle {
id: root
property var node: ({})
property bool selected: false
property bool selectMode: false
signal toggled()
signal activated()
/// `modifiers` rides straight off the mouse event: Shift is what turns
/// a click into a range (controls/SelectionModel.qml).
signal toggleSelect(int modifiers)
QbzTheme { id: theme }
height: 26
radius: 6
color: selected ? (theme.ambientOn ? theme.surfaceElevatedA50 : theme.surfaceElevated)
: rowArea.containsMouse ? theme.surfaceHover : "transparent"
// Row body — declared FIRST so the chevron / checkbox win their clicks.
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: root.node.isFolder ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
if (root.node.isFolder) folderMenu.openAtCursor(rowArea, mouse.x, mouse.y)
return
}
// In select mode a Shift or Ctrl click on the body is a selection
// gesture, as in any file list: it ticks (or ranges) the row
// instead of opening the folder. A plain click still opens it.
if (root.selectMode
&& (mouse.modifiers & (Qt.ShiftModifier | Qt.ControlModifier
| Qt.MetaModifier)) !== 0) {
root.toggleSelect(mouse.modifiers)
return
}
if (root.node.isFolder) root.activated()
}
}
CardMenu {
id: folderMenu
menuWidth: 200
entries: [
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
]
onPicked: function (a) {
if (a === "play") QbzLocal.playFolder(root.node.path)
else QbzLocal.enqueue("folder", root.node.path, a)
}
}
Rectangle {
visible: root.selected
x: 0
y: 4
width: 3
height: parent.height - 8
radius: 1.5
color: theme.accent
}
Row {
anchors.fill: parent
anchors.leftMargin: 8 + (root.node.depth || 0) * 16
anchors.rightMargin: 10
spacing: 6
// Select-mode checkbox (folder tri-state / track boolean).
Item {
visible: root.selectMode
width: visible ? 13 : 0
height: parent.height
SelectCheck {
anchors.verticalCenter: parent.verticalCenter
on: root.node.isFolder ? root.node.selectState === 2
: root.node.selected === true
partial: root.node.isFolder === true && root.node.selectState === 1
onToggled: function (mods) { root.toggleSelect(mods) }
}
}
// Expand chevron — its own hit target.
Item {
width: 18
height: parent.height
QbzIcon {
visible: root.node.canExpand === true
name: root.node.expanded ? "chevron-down" : "chevron-right"
width: 11
height: 11
anchors.centerIn: parent
tintName: "muted"
}
MouseArea {
anchors.fill: parent
enabled: root.node.canExpand === true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.toggled()
}
}
QbzIcon {
name: root.node.isFolder
? (root.node.expanded ? "folder-open" : "folder") : "music"
width: 14
height: 14
anchors.verticalCenter: parent.verticalCenter
tintName: root.node.isFolder ? "accent" : "muted"
}
Text {
width: Math.max(0, parent.width - 50 - (root.node.depth || 0) * 16
- (root.selectMode ? 19 : 0))
height: parent.height
text: root.node.segment || ""
color: root.selected ? theme.textPrimary : theme.textSecondary
font.pixelSize: 13
font.weight: root.selected ? theme.weightSemibold : theme.weightRegular
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
}