feat(qt): Copy submenus open on hover and hold their parent open

CardMenu entries declare a submenu as data (`submenu: [ {label, icon,
action} ]`); the row builds a child CardMenu on first use, opens it when
the pointer rests on the entry (or clicks it), and closes it when the
pointer moves to another entry. While either menu is hovered the pair
stays open; leaving both for 350 ms, pressing Escape or clicking outside
closes both. Picking in the child closes the parent too.

The track row, the Popular Tracks row and the player bar's "+" menu
declare their Copy entries this way instead of re-opening a second
CardMenu at the row's saved click position.

cc
This commit is contained in:
vicrodh committed 2026-09-13 23:31:59 -06:00
1 parent 119fab64ee
commit 7f609a55e5
5 files changed
+143 -82

No files matched your search

+15 -24
View File
@@ -657,7 +657,7 @@ Rectangle {
favorite: root.npFavorite
ephemeral: root.npEphemeral
tooltip: root.tooltip
onAddRequested: function (anchorItem) { root.addAnchor = anchorItem; addMenu.openBelowRight(anchorItem) }
onAddRequested: function (anchorItem) { addMenu.openBelowRight(anchorItem) }
onTrackInfoRequested: root.openTrackInfo()
}
}
@@ -680,7 +680,7 @@ Rectangle {
playCircle: true
favorite: root.npFavorite
tooltip: root.tooltip
onAddRequested: function (anchorItem) { root.addAnchor = anchorItem; addMenu.openBelowRight(anchorItem) }
onAddRequested: function (anchorItem) { addMenu.openBelowRight(anchorItem) }
onTrackInfoRequested: root.openTrackInfo()
}
}
@@ -908,25 +908,8 @@ Rectangle {
// "Add to…" flyout behind the transport "+" (TransportControls.slint's
// add-menu), on the shared CardMenu surface. Same seven entries, same
// order, same icons.
// The "+" button that opened the add-menu: the Copy submenu reopens on it.
property Item addAnchor: null
// The Copy submenu of the add-menu writes through this carrier.
QbzClipboard { id: npClipboard }
CardMenu {
id: npCopyMenu
menuWidth: 232
entries: [
{ "label": QbzSession.tr("Track name", QbzSession.trRev), "icon": "copy", "action": "copy-title" },
{ "label": QbzSession.tr("Track - Album - Artist", QbzSession.trRev), "icon": "clipboard", "action": "copy-full" }
]
onPicked: function (a) {
var parts = [QbzPlayer.npTitle || ""]
if (a === "copy-full") {
if ((QbzPlayer.npAlbum || "") !== "") parts.push(QbzPlayer.npAlbum)
if ((QbzPlayer.npArtist || "") !== "") parts.push(QbzPlayer.npArtist)
}
npClipboard.copy(parts.join(" - "))
}
}
CardMenu {
id: addMenu
@@ -992,13 +975,16 @@ Rectangle {
"action": "buy"
})
}
// Copy (2026-09-13): a submenu — the track name, or
// Copy (2026-09-13): a hover-opened submenu — the track name, or
// "Track - Album - Artist".
m.push({
"label": QbzSession.tr("Copy", QbzSession.trRev),
"icon": "copy",
"action": "copy",
"submenu": true
"submenu": [
{ "label": QbzSession.tr("Track name", QbzSession.trRev), "icon": "copy", "action": "copy-title" },
{ "label": QbzSession.tr("Track - Album - Artist", QbzSession.trRev), "icon": "clipboard", "action": "copy-full" }
]
})
return m.concat(root.abEntries())
}
@@ -1017,8 +1003,13 @@ Rectangle {
QbzLibrary.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
} else if (a === "buy") {
if (id !== "") QbzAlbum.buyTrack(id)
} else if (a === "copy") {
if (root.addAnchor) npCopyMenu.openBelowRight(root.addAnchor)
} else if (a === "copy-title" || a === "copy-full") {
var parts = [QbzPlayer.npTitle || ""]
if (a === "copy-full") {
if ((QbzPlayer.npAlbum || "") !== "") parts.push(QbzPlayer.npAlbum)
if ((QbzPlayer.npArtist || "") !== "") parts.push(QbzPlayer.npArtist)
}
npClipboard.copy(parts.join(" - "))
} else if (a === "ab-mark") {
QbzPlayer.abMark()
} else if (a === "ab-clear") {