Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions src/library/basetracktablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<CuePointer> cues = pTrack->getCuePoints();
// Collect only hotcues
QList<CuePointer> 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("<html><body>");

// Always show the column value (title, artist, composer, etc.)
if (!columnValue.isEmpty()) {
tooltip += QStringLiteral("<b>%1</b><br><br>").arg(columnValue.toHtmlEscaped());
}
tooltip += QStringLiteral("<b>%1:</b>").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 <td></td> tags.
// Use padding-right to apply horizontal cell spacing.
tooltip += QStringLiteral("<table><style>td {padding-right: 0.2em;}</style>");

for (const auto& pHotcue : std::as_const(hotcues)) {
tooltip += QStringLiteral("<tr>"); // 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(
"<td align=right>"
"<span style='"
"background-color: %1; "
"color: %2; "
"font-weight: bold; "
"vertical-align: middle; "
// border-radius is also not supported unfortunately
"border: 1px solid #444;'>"
// 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.
"&nbsp;%3&nbsp;"
"</span></td>")
.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("<td align=center><b>\u21BA</b></td>");
} else if (type == mixxx::CueType::Jump) {
// End is jump position, position is target
if (posEnd < pos) {
// forward →
tooltip += QStringLiteral("<td align=center><b>\u2192</b></td>");
} else {
// backward ←
tooltip += QStringLiteral("<td align=center><b>\u2190</b></td>");
}
} else {
tooltip += QStringLiteral("<td align=center>\u25B8</td>"); // ▸
}

// Add label if present, else add --- placeholder
const QString label = pHotcue->getLabel();
if (label.isEmpty()) {
tooltip += QStringLiteral("<td>---</td>");
} else {
tooltip += QStringLiteral("<td>%1</td>").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("<td>%1</td>")
.arg(posOrLengthToBeats(pBeats, position));
} else {
double position = type == mixxx::CueType::Jump
? posEnd.value()
: pos.value();
tooltip += QStringLiteral("<td>%1</td>")
.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("<td>[%1]</td>")
.arg(posOrLengthToBeats(pBeats, posEnd, pos));
} else {
tooltip += QStringLiteral("<td>[%1]</td>")
.arg(posOrLengthToSeconds(length, sampleRate));
}
}
} else if (type == mixxx::CueType::Jump) { // Jump target
if (pBeats) {
tooltip += QStringLiteral("<td>\u2192 %1</td>")
.arg(posOrLengthToBeats(pBeats, pos));
} else {
tooltip += QStringLiteral("<td>\u2192 %1</td>")
.arg(posOrLengthToSeconds(pos.value(), sampleRate));
}
}

tooltip += QStringLiteral("</tr>"); // end table row
}

// Close table and HTML
tooltip += QStringLiteral("</table></body></html>");

return tooltip;
}
2 changes: 2 additions & 0 deletions src/library/basetracktablemodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 6 additions & 0 deletions src/track/beats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
31 changes: 21 additions & 10 deletions src/util/duration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/util/duration.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class DurationBase {

enum class Precision {
SECONDS,
DECISECONDS,
CENTISECONDS,
MILLISECONDS
};
Expand Down