From 64d75e497a9e22d547d271ed2e9ef9cda484b207 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 14 Sep 2026 01:16:52 -0600 Subject: [PATCH] fix(qt): featured artist names in track rows are plain spans, not underlined links StyledText underlines every `` anchor unconditionally and nothing else in the app underlines a link. The names are now colour spans in the same StyledText (elision kept), and the click and hover hit test measures the prefix up to each name with the row's own font, so each name still opens its page or its search on its own. cc --- crates/qbz-qt/qml/rows/TrackRow.qml | 90 ++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/crates/qbz-qt/qml/rows/TrackRow.qml b/crates/qbz-qt/qml/rows/TrackRow.qml index f602df425..4b699b2d0 100644 --- a/crates/qbz-qt/qml/rows/TrackRow.qml +++ b/crates/qbz-qt/qml/rows/TrackRow.qml @@ -990,13 +990,28 @@ Rectangle { // Main artist first, then the featured performers, one comma // separated list. Local / media-server rows carry no // `featured` and stay as they are. With links enabled and - // featured names present, every name is its own StyledText - // link: the main artist opens its page as before; a featured + // featured names present, every name is its own clickable + // SPAN: the main artist opens its page as before; a featured // name opens its page when the album credits gave it an id - // and the search for that name otherwise. StyledText elides - // like plain text (probed: truncated stays true on one line). + // and the search for that name otherwise. Spans, not `` + // anchors: StyledText underlines every anchor unconditionally + // (qquickstyledtext.cpp, setFontUnderline(true)) and nothing + // else in the app underlines a link, so the hit test is done + // by measuring the prefix up to each name with the same font + // (TextMetrics below). StyledText still elides like plain + // text (probed: truncated stays true on one line). readonly property var featured: root.showFeaturedArtists ? (root.item.featured || []) : [] readonly property bool linkMode: root.artistLink && featured.length > 0 + /// The names as displayed, in order: {name, href} — href "" + /// = the main artist with no page to open. + readonly property var spans: { + var out = [{ "name": String(root.item.artist || ""), "href": root.item.artistId ? "main" : "" }] + for (var i = 0; i < featured.length; i++) + out.push({ "name": String(featured[i].name || ""), "href": "f:" + i }) + return out + } + /// The span under the pointer (-1 = none). + property int hotSpan: -1 function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">") } @@ -1005,51 +1020,84 @@ Rectangle { return ("0" + Math.round(v * 255).toString(16)).slice(-2) }).join("") } - function link(href, name) { - var hot = artistLine.hoveredLink === href - return '' + esc(name) + '' + function span(i) { + var s = artistLine.spans[i] + var hot = artistLine.hotSpan === i && s.href !== "" + return '' + esc(s.name) + '' } textFormat: linkMode ? Text.StyledText : Text.PlainText text: { var names = featured.map(function (f) { return f.name }) if (!linkMode) return (root.item.artist || "") + (names.length ? ", " + names.join(", ") : "") - var parts = [root.item.artistId ? link("main", root.item.artist) : esc(root.item.artist)] - for (var i = 0; i < featured.length; i++) - parts.push(link("f:" + i, featured[i].name)) + var parts = [] + for (var i = 0; i < spans.length; i++) + parts.push(span(i)) return parts.join(", ") } - linkColor: theme.textMuted color: root.artistLink && root.item.artistId && artistLinkArea.containsMouse ? theme.textPrimary : theme.textMuted font.pixelSize: 12 elide: Text.ElideRight - onLinkActivated: function (href) { - if (href === "main") { + /// Which span sits at `x` (-1 = a separator or past the text): + /// the prefix up to each name's end is measured with this + /// Text's own font, so the edges match what is drawn. + function spanAt(x) { + if (!linkMode || x < 0) + return -1 + var prefix = "" + for (var i = 0; i < spans.length; i++) { + spanMetrics.text = prefix + var from = spanMetrics.advanceWidth + prefix += spans[i].name + spanMetrics.text = prefix + if (x >= from && x < spanMetrics.advanceWidth) + return i + prefix += ", " + } + return -1 + } + function activate(i) { + var s = artistLine.spans[i] + if (!s || s.href === "") + return + if (s.href === "main") { if (root.routeGoToExternally) root.goToRequested("artist") else QbzArtist.openArtist(root.item.artistId) return } - var f = featured[parseInt(href.slice(2))] + var f = featured[parseInt(s.href.slice(2))] if (!f) return if (f.id) QbzArtist.openArtist(f.id) else QbzSearch.searchSubmit(f.name) } - HoverHandler { - enabled: artistLine.linkMode - cursorShape: artistLine.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor + TextMetrics { + id: spanMetrics + font: artistLine.font } MouseArea { id: artistLinkArea anchors.fill: parent - enabled: root.artistLink && !!root.item.artistId && !artistLine.linkMode + enabled: root.artistLink && (artistLine.linkMode || !!root.item.artistId) hoverEnabled: true - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + cursorShape: (artistLine.linkMode + ? (artistLine.hotSpan >= 0 && artistLine.spans[artistLine.hotSpan].href !== "") + : enabled) ? Qt.PointingHandCursor : Qt.ArrowCursor + onPositionChanged: function (mouse) { + if (artistLine.linkMode) + artistLine.hotSpan = artistLine.spanAt(mouse.x) + } + onExited: artistLine.hotSpan = -1 // Same routing as the menu's "Go to artist" — Slint drives // both through one handler, so gating only the menu would // leave the LINK navigating to the track's own artist. - onClicked: { + onClicked: function (mouse) { + if (artistLine.linkMode) { + var i = artistLine.spanAt(mouse.x) + if (i >= 0) + artistLine.activate(i) + return + } if (root.routeGoToExternally) root.goToRequested("artist") else QbzArtist.openArtist(root.item.artistId) }