diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index 338c9287c7e0..5d7ccf5ee16e 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,25 @@ 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; + // 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( + mixxx::BeatsPointer pBeats, + mixxx::audio::FramePos end, + mixxx::audio::FramePos start = mixxx::audio::FramePos(0)) { + return pBeats->numBeatsInRange(start, end); +} + } // anonymous namespace // static @@ -610,6 +631,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 +1265,157 @@ 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) { + // sort by position + return a->getPosition() < b->getPosition(); + }); + + 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(); + 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(""); + } 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(""); // ▸ + } + + // Add label if present, else add --- placeholder + const QString label = pHotcue->getLabel(); + if (label.isEmpty()) { + tooltip += QStringLiteral(""); + } else { + tooltip += QStringLiteral("").arg(label.toHtmlEscaped()); + } + + // Cue position - End is jump position, position is target + // Show in beats if track has beats + if (pBeats) { + mixxx::audio::FramePos position = type == mixxx::CueType::Jump + ? posEnd + : pos; + tooltip += QStringLiteral("") + .arg(posOrLengthToBeats(pBeats, position)); + } else { + double position = type == mixxx::CueType::Jump + ? posEnd.value() + : pos.value(); + tooltip += QStringLiteral("") + .arg(posOrLengthToSeconds(position, sampleRate)); + } + + // Add duration for saved loops/jumps + if (type == mixxx::CueType::Loop) { + // VERIFY ?? + const auto length = pHotcue->getLengthFrames(); + if (length > 0) { + if (pBeats) { + tooltip += QStringLiteral("") + .arg(posOrLengthToBeats(pBeats, posEnd, pos)); + } else { + tooltip += QStringLiteral("") + .arg(posOrLengthToSeconds(length, sampleRate)); + } + } + } else if (type == mixxx::CueType::Jump) { // Jump target + if (pBeats) { + tooltip += QStringLiteral("") + .arg(posOrLengthToBeats(pBeats, pos)); + } else { + 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][%1]\u2192 %1\u2192 %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; 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) { diff --git a/src/util/duration.cpp b/src/util/duration.cpp index 683d9d57b333..2ca00b93df4d 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,9 +55,12 @@ 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); + } else if (precision == Precision::DECISECONDS) { + DEBUG_ASSERT(durationString.length() >= 2); + durationString = durationString.left(durationString.length() - 2); } return durationString; @@ -72,9 +75,11 @@ QString DurationBase::formatSeconds(double dSeconds, Precision precision) { QString durationString; - if (Precision::CENTISECONDS == precision) { + if (precision == Precision::DECISECONDS) { + durationString = QString::number(dSeconds, 'f', 1); + } else 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 +97,10 @@ QString DurationBase::formatSecondsLong(double dSeconds, Precision precision) { QString durationString; - if (Precision::CENTISECONDS == precision) { + 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) { @@ -122,15 +130,18 @@ 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); + } 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 };