From 6f95109294da135d81cd0f1f8595a856051a7471 Mon Sep 17 00:00:00 2001 From: ronso0 Date: Mon, 13 Oct 2025 16:38:35 +0200 Subject: [PATCH 1/8] show hotcue tooltip in library Co-authored-by: Evelynne --- src/library/basetracktablemodel.cpp | 159 ++++++++++++++++++++++++++++ src/library/basetracktablemodel.h | 2 + 2 files changed, 161 insertions(+) diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index 338c9287c7e0..c7bb9387f9ca 100644 --- a/src/library/basetracktablemodel.cpp +++ b/src/library/basetracktablemodel.cpp @@ -26,10 +26,12 @@ #include "mixer/playerinfo.h" #include "mixer/playermanager.h" #include "moc_basetracktablemodel.cpp" +#include "track/cue.h" #include "track/keyutils.h" #include "track/track.h" #include "util/assert.h" #include "util/clipboard.h" +#include "util/color/color.h" #include "util/color/colorpalette.h" #include "util/color/predefinedcolorpalettes.h" #include "util/datetime.h" @@ -74,6 +76,16 @@ QSqlDatabase cloneDatabase( pTrackCollectionManager->internalCollection()->database()); } +// For hotcue tooltip +inline QString posOrLengthToSeconds(double posOrLength, double sampleRate) { + if (sampleRate <= 0) { + // if no sampleRate -> 44.1kHz + sampleRate = 44100.0; + } + double seconds = posOrLength / sampleRate; + return mixxx::Duration::formatTime(seconds, mixxx::Duration::Precision::CENTISECONDS); +} + } // anonymous namespace // static @@ -610,6 +622,19 @@ QVariant BaseTrackTableModel::roleValue( case ColumnCache::COLUMN_LIBRARYTABLE_RATING: case ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED: return rawValue; + // Eve: show the hotcue overview for in tooltips for following fields: + case ColumnCache::COLUMN_LIBRARYTABLE_TITLE: + case ColumnCache::COLUMN_LIBRARYTABLE_ARTIST: + case ColumnCache::COLUMN_LIBRARYTABLE_ALBUM: + case ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST: + case ColumnCache::COLUMN_LIBRARYTABLE_GENRE: + case ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER: + case ColumnCache::COLUMN_LIBRARYTABLE_GROUPING: + case ColumnCache::COLUMN_LIBRARYTABLE_COMMENT: + if (role == Qt::ToolTipRole) { + return composeHotCueTooltip(index, rawValue.toString()); + } + break; default: // Same value as for Qt::DisplayRole (see below) break; @@ -1231,3 +1256,137 @@ QString BaseTrackTableModel::getFieldString( const QModelIndex& index, ColumnCache::Column column) const { return getFieldVariant(index, column).toString(); } + +/// This appends a list of the track's hotcues to the column string. +QString BaseTrackTableModel::composeHotCueTooltip( + const QModelIndex& index, const QString& columnValue) const { + TrackPointer pTrack = getTrack(index); + if (!pTrack) { + return columnValue; + } + + const QList cues = pTrack->getCuePoints(); + // Collect only hotcues + QList hotcues; + for (const auto& pCue : std::as_const(cues)) { + if (pCue && pCue->getHotCue() != Cue::kNoHotCue) { + hotcues.append(pCue); + } + } + + // No hotcues: show only column value with default formatting + if (hotcues.isEmpty()) { + return columnValue; + } + + // Sort hotcues by number + std::sort(hotcues.begin(), hotcues.end(), [](const CuePointer& a, const CuePointer& b) { + return a->getHotCue() < b->getHotCue(); + }); + + double sampleRate = pTrack->getSampleRate(); + + // Start HTML ////////////////////////////////////////////////////////////// + QString tooltip = QStringLiteral(""); + + // Always show the column value (title, artist, composer, etc.) + if (!columnValue.isEmpty()) { + tooltip += QStringLiteral("%1

").arg(columnValue.toHtmlEscaped()); + } + tooltip += QStringLiteral("%1:").arg(tr("Hotcues")); + + // In order to keep all properties aligned vertically we construct a html table. + // That means we need to wrap each field in tags. + // Use padding-right to apply horizontal cell spacing. + tooltip += QStringLiteral(""); + + for (const auto& pHotcue : std::as_const(hotcues)) { + tooltip += QStringLiteral(""); // start table row + + // Make the cue appear like a colored hotcue button: + // * colored box with grey outline, slightly rounded corners + // * bold number label with contrasting color + QColor cueColor = mixxx::RgbColor::toQColor(pHotcue->getColor()); + // FIXME Use the skin's custom dark/bright threshold? Probably overkill.. + const QColor textColor = Color::chooseColorByBrightness( + cueColor, + Qt::white, + Qt::black, + 127); + tooltip += QStringLiteral( + "") + .arg(cueColor.name(), + textColor.name(), + QString::number(pHotcue->getHotCue() + 1)); + // Add icon (unicode char) to indicate the cue type + // We first need the position and optional jump target position in order + // to pick the correct icon + const auto type = pHotcue->getType(); + const auto pos = pHotcue->getPosition(); + const auto posEnd = pHotcue->getEndPosition(); + // Note: unicode arrow and loop chars may be rendered bold if font supports it + if (type == mixxx::CueType::Loop) { // ↺ + tooltip += QStringLiteral(""); + } else if (type == mixxx::CueType::Jump) { + // End is jump position, position is target + if (posEnd < pos) { + // forward → + tooltip += QStringLiteral(""); + } else { + // backward ← + tooltip += QStringLiteral(""); + } + } else { + tooltip += QStringLiteral(""); // ▸ + } + + // Cue position - End is jump position, position is target + double position = type == mixxx::CueType::Jump + ? posEnd.value() + : pos.value(); + tooltip += QStringLiteral("") + .arg(posOrLengthToSeconds(position, sampleRate)); + + // Add label if present + // FIXME Add empty cell or --- placeholder if label is mepty + // in order to keep loop/jump durations aligned? + // Or show label in last column? + const QString label = pHotcue->getLabel(); + if (!label.isEmpty()) { + tooltip += QStringLiteral("").arg(label.toHtmlEscaped()); + } + + // Add duration for saved loops/jumps + if (type == mixxx::CueType::Loop) { + // VERIFY ?? + const auto length = pHotcue->getLengthFrames(); + if (length > 0) { + tooltip += QStringLiteral("") + .arg(posOrLengthToSeconds(length, sampleRate)); + } + } else if (type == mixxx::CueType::Jump) { // Jump target + tooltip += QStringLiteral("") + .arg(posOrLengthToSeconds(pos.value(), sampleRate)); + } + + tooltip += QStringLiteral(""); // end table row + } + + // Close table and HTML + tooltip += QStringLiteral("
" + "" + // Qt RichText/Html only supports a subset of Html, so we need + // to fake left/right padding inside the box with whitespaces. + // Note: this will produce very wide boxes if the skin uses a + // monospace font for tooltips. + " %3 " + "\u21BA\u2192\u2190\u25B8%1%1(%1)(%1)
"); + + return tooltip; +} diff --git a/src/library/basetracktablemodel.h b/src/library/basetracktablemodel.h index eb95d4bff0b0..c369d7967144 100644 --- a/src/library/basetracktablemodel.h +++ b/src/library/basetracktablemodel.h @@ -276,6 +276,8 @@ class BaseTrackTableModel : public QAbstractTableModel, public TrackModel { QVariant composeCoverArtToolTipHtml( const QModelIndex& index) const; + QString composeHotCueTooltip(const QModelIndex& index, const QString& columnValue) const; + Qt::ItemFlags defaultItemFlags( const QModelIndex& index) const; From df98585a3ad6d960fa7281142c0e4e637c24b44c Mon Sep 17 00:00:00 2001 From: ronso0 Date: Tue, 14 Oct 2025 00:52:29 +0200 Subject: [PATCH 2/8] Library hotcue tooltip: show position / loop length in beats if available --- src/library/basetracktablemodel.cpp | 45 +++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index c7bb9387f9ca..21f6a5bc3299 100644 --- a/src/library/basetracktablemodel.cpp +++ b/src/library/basetracktablemodel.cpp @@ -86,6 +86,13 @@ inline QString posOrLengthToSeconds(double posOrLength, double sampleRate) { return mixxx::Duration::formatTime(seconds, mixxx::Duration::Precision::CENTISECONDS); } +inline int posOrLengthToBeats( + mixxx::BeatsPointer pBeats, + mixxx::audio::FramePos end, + mixxx::audio::FramePos start = mixxx::audio::FramePos(0)) { + return pBeats->numBeatsInRange(start, end); +} + } // anonymous namespace // static @@ -1337,6 +1344,7 @@ QString BaseTrackTableModel::composeHotCueTooltip( const auto type = pHotcue->getType(); const auto pos = pHotcue->getPosition(); const auto posEnd = pHotcue->getEndPosition(); + auto pBeats = pTrack->getBeats(); // Note: unicode arrow and loop chars may be rendered bold if font supports it if (type == mixxx::CueType::Loop) { // ↺ tooltip += QStringLiteral("\u21BA"); @@ -1354,11 +1362,20 @@ QString BaseTrackTableModel::composeHotCueTooltip( } // Cue position - End is jump position, position is target - double position = type == mixxx::CueType::Jump - ? posEnd.value() - : pos.value(); - tooltip += QStringLiteral("%1") - .arg(posOrLengthToSeconds(position, sampleRate)); + // Show in beats if track has beats + if (pBeats) { + mixxx::audio::FramePos position = type == mixxx::CueType::Jump + ? posEnd + : pos; + tooltip += QStringLiteral("%1") + .arg(posOrLengthToBeats(pBeats, position)); + } else { + double position = type == mixxx::CueType::Jump + ? posEnd.value() + : pos.value(); + tooltip += QStringLiteral("%1") + .arg(posOrLengthToSeconds(position, sampleRate)); + } // Add label if present // FIXME Add empty cell or --- placeholder if label is mepty @@ -1374,12 +1391,22 @@ QString BaseTrackTableModel::composeHotCueTooltip( // VERIFY ?? const auto length = pHotcue->getLengthFrames(); if (length > 0) { - tooltip += QStringLiteral("(%1)") - .arg(posOrLengthToSeconds(length, sampleRate)); + if (pBeats) { + tooltip += QStringLiteral("(%1)") + .arg(posOrLengthToBeats(pBeats, pos, posEnd)); + } else { + tooltip += QStringLiteral("(%1)") + .arg(posOrLengthToSeconds(length, sampleRate)); + } } } else if (type == mixxx::CueType::Jump) { // Jump target - tooltip += QStringLiteral("(%1)") - .arg(posOrLengthToSeconds(pos.value(), sampleRate)); + if (pBeats) { + tooltip += QStringLiteral("(%1)") + .arg(posOrLengthToBeats(pBeats, pos)); + } else { + tooltip += QStringLiteral("(%1)") + .arg(posOrLengthToSeconds(pos.value(), sampleRate)); + } } tooltip += QStringLiteral(""); // end table row From 165e23b06636b2b0fedf886c15eb616f28db25c5 Mon Sep 17 00:00:00 2001 From: ronso0 Date: Thu, 16 Oct 2025 11:32:02 +0200 Subject: [PATCH 3/8] change column order: [#] [type icon] Label Position (loop length / jump target) --- src/library/basetracktablemodel.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index 21f6a5bc3299..bb52fda3e92e 100644 --- a/src/library/basetracktablemodel.cpp +++ b/src/library/basetracktablemodel.cpp @@ -1288,7 +1288,8 @@ QString BaseTrackTableModel::composeHotCueTooltip( // Sort hotcues by number std::sort(hotcues.begin(), hotcues.end(), [](const CuePointer& a, const CuePointer& b) { - return a->getHotCue() < b->getHotCue(); + // sort by position + return a->getPosition() < b->getPosition(); }); double sampleRate = pTrack->getSampleRate(); @@ -1361,6 +1362,14 @@ QString BaseTrackTableModel::composeHotCueTooltip( tooltip += QStringLiteral("\u25B8"); // ▸ } + // Add label if present, else add --- placeholder + const QString label = pHotcue->getLabel(); + if (label.isEmpty()) { + tooltip += QStringLiteral("---"); + } else { + tooltip += QStringLiteral("%1").arg(label.toHtmlEscaped()); + } + // Cue position - End is jump position, position is target // Show in beats if track has beats if (pBeats) { @@ -1377,15 +1386,6 @@ QString BaseTrackTableModel::composeHotCueTooltip( .arg(posOrLengthToSeconds(position, sampleRate)); } - // Add label if present - // FIXME Add empty cell or --- placeholder if label is mepty - // in order to keep loop/jump durations aligned? - // Or show label in last column? - const QString label = pHotcue->getLabel(); - if (!label.isEmpty()) { - tooltip += QStringLiteral("%1").arg(label.toHtmlEscaped()); - } - // Add duration for saved loops/jumps if (type == mixxx::CueType::Loop) { // VERIFY ?? From 9ed673c5ee131daf086372b979a555f06c11ffbd Mon Sep 17 00:00:00 2001 From: ronso0 Date: Thu, 16 Oct 2025 11:35:50 +0200 Subject: [PATCH 4/8] Duration: revert yoda style --- src/util/duration.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/util/duration.cpp b/src/util/duration.cpp index 683d9d57b333..7fc9bb2ad096 100644 --- a/src/util/duration.cpp +++ b/src/util/duration.cpp @@ -42,7 +42,7 @@ QString DurationBase::formatTime(double dSeconds, Precision precision) { QString formatString = (t.hour() > 0 && days < 1 ? QStringLiteral("hh:mm:ss") : QStringLiteral("mm:ss")) + - (Precision::SECONDS == precision ? QString() : QStringLiteral(".zzz")); + (precision == Precision::SECONDS ? QString() : QStringLiteral(".zzz")); QString durationString = t.toString(formatString); if (days > 0) { @@ -55,8 +55,8 @@ QString DurationBase::formatTime(double dSeconds, Precision precision) { // The format string gives us milliseconds but we want // centiseconds. Slice one character off. - if (Precision::CENTISECONDS == precision) { - DEBUG_ASSERT(1 <= durationString.length()); + if (precision == Precision::CENTISECONDS) { + DEBUG_ASSERT(durationString.length() >= 1); durationString = durationString.left(durationString.length() - 1); } @@ -72,9 +72,9 @@ QString DurationBase::formatSeconds(double dSeconds, Precision precision) { QString durationString; - if (Precision::CENTISECONDS == precision) { + if (precision == Precision::CENTISECONDS) { durationString = QString::number(dSeconds, 'f', 2); - } else if (Precision::MILLISECONDS == precision) { + } else if (precision == Precision::MILLISECONDS) { durationString = QString::number(dSeconds, 'f', 3); } else { durationString = QString::number(dSeconds, 'f', 0); @@ -92,7 +92,7 @@ QString DurationBase::formatSecondsLong(double dSeconds, Precision precision) { QString durationString; - if (Precision::CENTISECONDS == precision) { + if (precision == Precision::CENTISECONDS) { durationString = QString::number(dSeconds, 'f', 2) .rightJustified(6, QLatin1Char('0')); } else if (Precision::MILLISECONDS == precision) { @@ -122,14 +122,14 @@ QString DurationBase::formatKiloSeconds(double dSeconds, Precision precision) { QString::number(kilos), QString(kDecimalSeparator), QString::number(seconds).rightJustified(3, QLatin1Char('0'))); - if (Precision::SECONDS != precision) { - durationString += kKiloGroupSeparator % QString::number(subs, 'f', 3).right(3); + if (precision != Precision::SECONDS) { + durationString += kKiloGroupSeparator % QString::number(subs, 'f', 3).right(3); } // The format string gives us milliseconds but we want // centiseconds. Slice one character off. - if (Precision::CENTISECONDS == precision) { - DEBUG_ASSERT(1 <= durationString.length()); + if (precision == Precision::CENTISECONDS) { + DEBUG_ASSERT(durationString.length() >= 1); durationString = durationString.left(durationString.length() - 1); } From 9d7f76b3548f7a70f060bae4add04dc94775fafa Mon Sep 17 00:00:00 2001 From: ronso0 Date: Thu, 16 Oct 2025 11:36:25 +0200 Subject: [PATCH 5/8] Duration: add DECISECONDS format --- src/util/duration.cpp | 15 +++++++++++++-- src/util/duration.h | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/util/duration.cpp b/src/util/duration.cpp index 7fc9bb2ad096..2ca00b93df4d 100644 --- a/src/util/duration.cpp +++ b/src/util/duration.cpp @@ -58,6 +58,9 @@ QString DurationBase::formatTime(double dSeconds, Precision precision) { if (precision == Precision::CENTISECONDS) { DEBUG_ASSERT(durationString.length() >= 1); durationString = durationString.left(durationString.length() - 1); + } else if (precision == Precision::DECISECONDS) { + DEBUG_ASSERT(durationString.length() >= 2); + durationString = durationString.left(durationString.length() - 2); } return durationString; @@ -72,7 +75,9 @@ QString DurationBase::formatSeconds(double dSeconds, Precision precision) { QString durationString; - if (precision == Precision::CENTISECONDS) { + if (precision == Precision::DECISECONDS) { + durationString = QString::number(dSeconds, 'f', 1); + } else if (precision == Precision::CENTISECONDS) { durationString = QString::number(dSeconds, 'f', 2); } else if (precision == Precision::MILLISECONDS) { durationString = QString::number(dSeconds, 'f', 3); @@ -92,7 +97,10 @@ QString DurationBase::formatSecondsLong(double dSeconds, Precision precision) { QString durationString; - if (precision == Precision::CENTISECONDS) { + if (precision == Precision::DECISECONDS) { + durationString = QString::number(dSeconds, 'f', 1) + .rightJustified(6, QLatin1Char('0')); + } else if (precision == Precision::CENTISECONDS) { durationString = QString::number(dSeconds, 'f', 2) .rightJustified(6, QLatin1Char('0')); } else if (Precision::MILLISECONDS == precision) { @@ -131,6 +139,9 @@ QString DurationBase::formatKiloSeconds(double dSeconds, Precision precision) { if (precision == Precision::CENTISECONDS) { DEBUG_ASSERT(durationString.length() >= 1); durationString = durationString.left(durationString.length() - 1); + } else if (precision == Precision::DECISECONDS) { + DEBUG_ASSERT(durationString.length() >= 2); + durationString = durationString.left(durationString.length() - 2); } return durationString; diff --git a/src/util/duration.h b/src/util/duration.h index 313511334fa7..2d5f5289f418 100644 --- a/src/util/duration.h +++ b/src/util/duration.h @@ -65,6 +65,7 @@ class DurationBase { enum class Precision { SECONDS, + DECISECONDS, CENTISECONDS, MILLISECONDS }; From f1d6b59de0808652491973b83efcc65b45129795 Mon Sep 17 00:00:00 2001 From: ronso0 Date: Thu, 16 Oct 2025 11:36:53 +0200 Subject: [PATCH 6/8] Library hotcue tooltip: use deciseconds for position / loop length display --- src/library/basetracktablemodel.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index bb52fda3e92e..b41474443c2a 100644 --- a/src/library/basetracktablemodel.cpp +++ b/src/library/basetracktablemodel.cpp @@ -83,7 +83,9 @@ inline QString posOrLengthToSeconds(double posOrLength, double sampleRate) { sampleRate = 44100.0; } double seconds = posOrLength / sampleRate; - return mixxx::Duration::formatTime(seconds, mixxx::Duration::Precision::CENTISECONDS); + // We don't need centiseconds precision in the overview, it just adds noise. + // Deciseconds are sufficient to distinguish very close hotcues. + return mixxx::Duration::formatTime(seconds, mixxx::Duration::Precision::DECISECONDS); } inline int posOrLengthToBeats( From 5c9da06d03dd6fac6235b5ece5ca86c0223262c3 Mon Sep 17 00:00:00 2001 From: ronso0 Date: Tue, 20 Jan 2026 14:01:34 +0100 Subject: [PATCH 7/8] fixup! Library hotcue tooltip: show position / loop length in beats if available --- src/track/beats.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/track/beats.cpp b/src/track/beats.cpp index 83dfe73994ab..14ca6d3057ad 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -742,6 +742,12 @@ audio::FramePos Beats::snapPosToNearBeat(audio::FramePos position) const { int Beats::numBeatsInRange(audio::FramePos startPosition, audio::FramePos endPosition) const { startPosition = snapPosToNearBeat(startPosition); + if (startPosition > endPosition) { + // May happen if arguments are in the wrong order. + // Also helps with Jumpcues so caller doesn't have to swap + // positions in case it's a backward jump. + std::swap(startPosition, endPosition); + } audio::FramePos lastPosition = audio::kStartFramePos; int i = 1; while (lastPosition < endPosition) { From 7ef66efb27a2f88a3ca856ae4f611a3aa42a2714 Mon Sep 17 00:00:00 2001 From: ronso0 Date: Tue, 20 Jan 2026 14:02:13 +0100 Subject: [PATCH 8/8] Library hotcue tooltip: use [] for loop length, use arrow for jump target --- src/library/basetracktablemodel.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index b41474443c2a..5d7ccf5ee16e 100644 --- a/src/library/basetracktablemodel.cpp +++ b/src/library/basetracktablemodel.cpp @@ -1394,19 +1394,19 @@ QString BaseTrackTableModel::composeHotCueTooltip( const auto length = pHotcue->getLengthFrames(); if (length > 0) { if (pBeats) { - tooltip += QStringLiteral("(%1)") - .arg(posOrLengthToBeats(pBeats, pos, posEnd)); + tooltip += QStringLiteral("[%1]") + .arg(posOrLengthToBeats(pBeats, posEnd, pos)); } else { - tooltip += QStringLiteral("(%1)") + tooltip += QStringLiteral("[%1]") .arg(posOrLengthToSeconds(length, sampleRate)); } } } else if (type == mixxx::CueType::Jump) { // Jump target if (pBeats) { - tooltip += QStringLiteral("(%1)") + tooltip += QStringLiteral("\u2192 %1") .arg(posOrLengthToBeats(pBeats, pos)); } else { - tooltip += QStringLiteral("(%1)") + tooltip += QStringLiteral("\u2192 %1") .arg(posOrLengthToSeconds(pos.value(), sampleRate)); } }