1 //
2 // C++ Implementation: woverview
3 //
4 // Description:
5 //
6 //
7 // Author: Tue Haste Andersen <haste@diku.dk>, (C) 2003
8 //
9 // Copyright: See COPYING file that comes with this distribution
10 //
11 //
12 
13 #include "woverview.h"
14 
15 #include <QBrush>
16 #include <QMimeData>
17 #include <QMouseEvent>
18 #include <QPaintEvent>
19 #include <QPainter>
20 #include <QUrl>
21 #include <QtDebug>
22 
23 #include "analyzer/analyzerprogress.h"
24 #include "control/controlobject.h"
25 #include "control/controlproxy.h"
26 #include "engine/engine.h"
27 #include "mixer/playermanager.h"
28 #include "moc_woverview.cpp"
29 #include "preferences/colorpalettesettings.h"
30 #include "track/track.h"
31 #include "util/color/color.h"
32 #include "util/compatibility.h"
33 #include "util/dnd.h"
34 #include "util/duration.h"
35 #include "util/math.h"
36 #include "util/painterscope.h"
37 #include "util/timer.h"
38 #include "waveform/waveform.h"
39 #include "waveform/waveformwidgetfactory.h"
40 #include "widget/controlwidgetconnection.h"
41 #include "wskincolor.h"
42 
WOverview(const QString & group,PlayerManager * pPlayerManager,UserSettingsPointer pConfig,QWidget * parent)43 WOverview::WOverview(
44         const QString& group,
45         PlayerManager* pPlayerManager,
46         UserSettingsPointer pConfig,
47         QWidget* parent)
48         : WWidget(parent),
49           m_actualCompletion(0),
50           m_pixmapDone(false),
51           m_waveformPeak(-1.0),
52           m_diffGain(0),
53           m_devicePixelRatio(1.0),
54           m_group(group),
55           m_pConfig(pConfig),
56           m_endOfTrack(false),
57           m_bPassthroughEnabled(false),
58           m_pCueMenuPopup(make_parented<WCueMenuPopup>(pConfig, this)),
59           m_bShowCueTimes(true),
60           m_iPosSeconds(0),
61           m_bLeftClickDragging(false),
62           m_iPickupPos(0),
63           m_iPlayPos(0),
64           m_pHoveredMark(nullptr),
65           m_bTimeRulerActive(false),
66           m_orientation(Qt::Horizontal),
67           m_iLabelFontSize(10),
68           m_a(1.0),
69           m_b(0.0),
70           m_analyzerProgress(kAnalyzerProgressUnknown),
71           m_trackLoaded(false),
72           m_scaleFactor(1.0) {
73     m_endOfTrackControl = new ControlProxy(
74             m_group, "end_of_track", this, ControlFlag::NoAssertIfMissing);
75     m_endOfTrackControl->connectValueChanged(this, &WOverview::onEndOfTrackChange);
76     m_pRateRatioControl = new ControlProxy(
77             m_group, "rate_ratio", this, ControlFlag::NoAssertIfMissing);
78     // Needed to recalculate range durations when rate slider is moved without the deck playing
79     m_pRateRatioControl->connectValueChanged(
80             this, &WOverview::onRateRatioChange);
81     m_trackSampleRateControl = new ControlProxy(
82             m_group, "track_samplerate", this, ControlFlag::NoAssertIfMissing);
83     m_trackSamplesControl = new ControlProxy(m_group, "track_samples", this);
84     m_playpositionControl = new ControlProxy(
85             m_group, "playposition", this, ControlFlag::NoAssertIfMissing);
86     m_pPassthroughControl =
87             new ControlProxy(m_group, "passthrough", this, ControlFlag::NoAssertIfMissing);
88     m_pPassthroughControl->connectValueChanged(this, &WOverview::onPassthroughChange);
89     m_bPassthroughEnabled = static_cast<bool>(m_pPassthroughControl->get());
90 
91     setAcceptDrops(true);
92 
93     setMouseTracking(true);
94 
95     connect(pPlayerManager, &PlayerManager::trackAnalyzerProgress,
96             this, &WOverview::onTrackAnalyzerProgress);
97 
98     connect(m_pCueMenuPopup.get(), &WCueMenuPopup::aboutToHide, this, &WOverview::slotCueMenuPopupAboutToHide);
99 
100     m_pPassthroughLabel = new QLabel(this);
101     m_pPassthroughLabel->setObjectName("PassthroughLabel");
102     m_pPassthroughLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
103     // Shown on the overview waveform when vinyl passthrough is enabled
104     m_pPassthroughLabel->setText(tr("Passthrough"));
105     m_pPassthroughLabel->hide();
106     QVBoxLayout *pPassthroughLayout = new QVBoxLayout(this);
107     pPassthroughLayout->setContentsMargins(0,0,0,0);
108     pPassthroughLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
109     pPassthroughLayout->addWidget(m_pPassthroughLabel);
110     setLayout(pPassthroughLayout);
111 }
112 
setup(const QDomNode & node,const SkinContext & context)113 void WOverview::setup(const QDomNode& node, const SkinContext& context) {
114     m_scaleFactor = context.getScaleFactor();
115     m_signalColors.setup(node, context);
116 
117     m_backgroundColor = m_signalColors.getBgColor();
118     m_axesColor = m_signalColors.getAxesColor();
119     m_playPosColor = m_signalColors.getPlayPosColor();
120     m_passthroughOverlayColor = m_signalColors.getPassthroughOverlayColor();
121     m_playedOverlayColor = m_signalColors.getPlayedOverlayColor();
122     m_lowColor = m_signalColors.getLowColor();
123     m_dimBrightThreshold = m_signalColors.getDimBrightThreshold();
124 
125     m_labelBackgroundColor = context.selectColor(node, "LabelBackgroundColor");
126     if (!m_labelBackgroundColor.isValid()) {
127         m_labelBackgroundColor = m_backgroundColor;
128         m_labelBackgroundColor.setAlpha(255 / 2); // 0 == fully transparent
129     }
130 
131     m_labelTextColor = context.selectColor(node, "LabelTextColor");
132     if (!m_labelTextColor.isValid()) {
133         m_labelTextColor = Qt::white;
134     }
135 
136     bool okay = false;
137     int labelFontSize = context.selectInt(node, "LabelFontSize", &okay);
138     if (okay) {
139         m_iLabelFontSize = labelFontSize;
140     }
141 
142     // Clear the background pixmap, if it exists.
143     m_backgroundPixmap = QPixmap();
144     m_backgroundPixmapPath = context.selectString(node, "BgPixmap");
145     if (!m_backgroundPixmapPath.isEmpty()) {
146         m_backgroundPixmap = *WPixmapStore::getPixmapNoCache(
147                 context.makeSkinPath(m_backgroundPixmapPath),
148                 m_scaleFactor);
149     }
150 
151     m_endOfTrackColor = QColor(200, 25, 20);
152     const QString endOfTrackColorName = context.selectString(node, "EndOfTrackColor");
153     if (!endOfTrackColorName.isNull()) {
154         m_endOfTrackColor.setNamedColor(endOfTrackColorName);
155         m_endOfTrackColor = WSkinColor::getCorrectColor(m_endOfTrackColor);
156     }
157 
158     // setup hotcues and cue and loop(s)
159     m_marks.setup(m_group, node, context, m_signalColors);
160 
161     ColorPaletteSettings colorPaletteSettings(m_pConfig);
162     auto colorPalette = colorPaletteSettings.getHotcueColorPalette();
163     m_pCueMenuPopup->setColorPalette(colorPalette);
164 
165     for (const auto& pMark: m_marks) {
166         if (pMark->isValid()) {
167             pMark->connectSamplePositionChanged(this,
168                     &WOverview::onMarkChanged);
169         }
170         if (pMark->hasVisible()) {
171             pMark->connectVisibleChanged(this,
172                     &WOverview::onMarkChanged);
173         }
174     }
175 
176     QDomNode child = node.firstChild();
177     while (!child.isNull()) {
178         if (child.nodeName() == "MarkRange") {
179             m_markRanges.push_back(WaveformMarkRange(m_group, child, context, m_signalColors));
180             WaveformMarkRange& markRange = m_markRanges.back();
181 
182             if (markRange.m_markEnabledControl) {
183                 markRange.m_markEnabledControl->connectValueChanged(
184                         this, &WOverview::onMarkRangeChange);
185             }
186             if (markRange.m_markVisibleControl) {
187                 markRange.m_markVisibleControl->connectValueChanged(
188                         this, &WOverview::onMarkRangeChange);
189             }
190             if (markRange.m_markStartPointControl) {
191                 markRange.m_markStartPointControl->connectValueChanged(
192                         this, &WOverview::onMarkRangeChange);
193             }
194             if (markRange.m_markEndPointControl) {
195                 markRange.m_markEndPointControl->connectValueChanged(
196                         this, &WOverview::onMarkRangeChange);
197             }
198         }
199         child = child.nextSibling();
200     }
201 
202     QString orientationString = context.selectString(node, "Orientation").toLower();
203     if (orientationString == "vertical") {
204         m_orientation = Qt::Vertical;
205     } else {
206         m_orientation = Qt::Horizontal;
207     }
208 
209     m_bShowCueTimes = context.selectBool(node, "ShowCueTimes", true);
210 
211     //qDebug() << "WOverview : m_marks" << m_marks.size();
212     //qDebug() << "WOverview : m_markRanges" << m_markRanges.size();
213     if (!m_connections.isEmpty()) {
214         ControlParameterWidgetConnection* defaultConnection = m_connections.at(0);
215         if (defaultConnection) {
216             if (defaultConnection->getEmitOption() &
217                     ControlParameterWidgetConnection::EMIT_DEFAULT) {
218                 // ON_PRESS means here value change on mouse move during press
219                 defaultConnection->setEmitOption(
220                         ControlParameterWidgetConnection::EMIT_ON_RELEASE);
221             }
222         }
223     }
224 
225     setFocusPolicy(Qt::NoFocus);
226 }
227 
onConnectedControlChanged(double dParameter,double dValue)228 void WOverview::onConnectedControlChanged(double dParameter, double dValue) {
229     // this is connected via skin to "playposition"
230     Q_UNUSED(dValue);
231 
232     // Calculate handle position. Clamp the value within 0-1 because that's
233     // all we represent with this widget.
234     dParameter = math_clamp(dParameter, 0.0, 1.0);
235 
236     bool redraw = false;
237     int oldPos = m_iPlayPos;
238     m_iPlayPos = valueToPosition(dParameter);
239     if (oldPos != m_iPlayPos) {
240         redraw = true;
241     }
242 
243     if (!m_bLeftClickDragging) {
244         // if not dragged the pick-up moves with the play position
245         m_iPickupPos = m_iPlayPos;
246     }
247 
248     // In case the user is hovering a cue point or holding right click, the
249     // calculated time between the playhead and cue/cursor should be updated at
250     // least once per second, regardless of m_iPos which depends on the length
251     // of the widget.
252     int oldPositionSeconds = m_iPosSeconds;
253     m_iPosSeconds = static_cast<int>(dParameter * m_trackSamplesControl->get());
254     if ((m_bTimeRulerActive || m_pHoveredMark != nullptr) && oldPositionSeconds != m_iPosSeconds) {
255         redraw = true;
256     }
257 
258     if (redraw) {
259         update();
260     }
261 }
262 
slotWaveformSummaryUpdated()263 void WOverview::slotWaveformSummaryUpdated() {
264     //qDebug() << "WOverview::slotWaveformSummaryUpdated()";
265 
266     TrackPointer pTrack(m_pCurrentTrack);
267     if (!pTrack) {
268         return;
269     }
270     m_pWaveform = pTrack->getWaveformSummary();
271     if (m_pWaveform) {
272         // If the waveform is already complete, just draw it.
273         if (m_pWaveform->getCompletion() == m_pWaveform->getDataSize()) {
274             m_actualCompletion = 0;
275             if (drawNextPixmapPart()) {
276                 update();
277             }
278         }
279     } else {
280         // Null waveform pointer means waveform was cleared.
281         m_waveformSourceImage = QImage();
282         m_analyzerProgress = kAnalyzerProgressUnknown;
283         m_actualCompletion = 0;
284         m_waveformPeak = -1.0;
285         m_pixmapDone = false;
286 
287         update();
288     }
289 }
290 
onTrackAnalyzerProgress(TrackId trackId,AnalyzerProgress analyzerProgress)291 void WOverview::onTrackAnalyzerProgress(TrackId trackId, AnalyzerProgress analyzerProgress) {
292     if (!m_pCurrentTrack || (m_pCurrentTrack->getId() != trackId)) {
293         return;
294     }
295 
296     bool updateNeeded = drawNextPixmapPart();
297     if (updateNeeded || (m_analyzerProgress != analyzerProgress)) {
298         m_analyzerProgress = analyzerProgress;
299         update();
300     }
301 }
302 
slotTrackLoaded(TrackPointer pTrack)303 void WOverview::slotTrackLoaded(TrackPointer pTrack) {
304     Q_UNUSED(pTrack); // only used in DEBUG_ASSERT
305     DEBUG_ASSERT(m_pCurrentTrack == pTrack);
306     m_trackLoaded = true;
307     if (m_pCurrentTrack) {
308         updateCues(m_pCurrentTrack->getCuePoints());
309     }
310     update();
311 }
312 
slotLoadingTrack(TrackPointer pNewTrack,TrackPointer pOldTrack)313 void WOverview::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
314     Q_UNUSED(pOldTrack); // only used in DEBUG_ASSERT
315     //qDebug() << this << "WOverview::slotLoadingTrack" << pNewTrack.get() << pOldTrack.get();
316     DEBUG_ASSERT(m_pCurrentTrack == pOldTrack);
317     if (m_pCurrentTrack != nullptr) {
318         disconnect(m_pCurrentTrack.get(),
319                 &Track::waveformSummaryUpdated,
320                 this,
321                 &WOverview::slotWaveformSummaryUpdated);
322     }
323 
324     m_waveformSourceImage = QImage();
325     m_analyzerProgress = kAnalyzerProgressUnknown;
326     m_actualCompletion = 0;
327     m_waveformPeak = -1.0;
328     m_pixmapDone = false;
329     m_trackLoaded = false;
330     m_endOfTrack = false;
331 
332     if (pNewTrack) {
333         m_pCurrentTrack = pNewTrack;
334         m_pWaveform = pNewTrack->getWaveformSummary();
335 
336         connect(pNewTrack.get(),
337                 &Track::waveformSummaryUpdated,
338                 this,
339                 &WOverview::slotWaveformSummaryUpdated);
340         slotWaveformSummaryUpdated();
341         connect(pNewTrack.get(), &Track::cuesUpdated, this, &WOverview::receiveCuesUpdated);
342     } else {
343         m_pCurrentTrack.reset();
344         m_pWaveform.clear();
345     }
346     update();
347 }
348 
onEndOfTrackChange(double v)349 void WOverview::onEndOfTrackChange(double v) {
350     //qDebug() << "WOverview::onEndOfTrackChange()" << v;
351     m_endOfTrack = v > 0.0;
352     update();
353 }
354 
onMarkChanged(double v)355 void WOverview::onMarkChanged(double v) {
356     Q_UNUSED(v);
357     //qDebug() << "WOverview::onMarkChanged()" << v;
358     if (m_pCurrentTrack) {
359         updateCues(m_pCurrentTrack->getCuePoints());
360         update();
361     }
362 }
363 
onMarkRangeChange(double v)364 void WOverview::onMarkRangeChange(double v) {
365     Q_UNUSED(v);
366     //qDebug() << "WOverview::onMarkRangeChange()" << v;
367     update();
368 }
369 
onRateRatioChange(double v)370 void WOverview::onRateRatioChange(double v) {
371     Q_UNUSED(v);
372     update();
373 }
374 
onPassthroughChange(double v)375 void WOverview::onPassthroughChange(double v) {
376     m_bPassthroughEnabled = static_cast<bool>(v);
377 
378     if (!m_bPassthroughEnabled) {
379         slotWaveformSummaryUpdated();
380     }
381 
382     // Always call this to trigger a repaint even if not track is loaded
383     update();
384 }
385 
updateCues(const QList<CuePointer> & loadedCues)386 void WOverview::updateCues(const QList<CuePointer> &loadedCues) {
387     m_marksToRender.clear();
388     for (const CuePointer& currentCue : loadedCues) {
389         const WaveformMarkPointer pMark = m_marks.getHotCueMark(currentCue->getHotCue());
390 
391         if (pMark != nullptr && pMark->isValid() && pMark->isVisible()
392             && pMark->getSamplePosition() != Cue::kNoPosition) {
393             QColor newColor = mixxx::RgbColor::toQColor(currentCue->getColor());
394             if (newColor != pMark->fillColor() || newColor != pMark->m_textColor) {
395                 pMark->setBaseColor(newColor, m_dimBrightThreshold);
396             }
397 
398             int hotcueNumber = currentCue->getHotCue();
399             if (currentCue->getType() == mixxx::CueType::HotCue && hotcueNumber != Cue::kNoHotCue) {
400                 // Prepend the hotcue number to hotcues' labels
401                 QString newLabel = currentCue->getLabel();
402                 if (newLabel.isEmpty()) {
403                     newLabel = QString::number(hotcueNumber + 1);
404                 } else {
405                     newLabel = QString("%1: %2").arg(hotcueNumber + 1).arg(newLabel);
406                 }
407 
408                 if (pMark->m_text != newLabel) {
409                     pMark->m_text = newLabel;
410                 }
411             }
412 
413             m_marksToRender.append(pMark);
414         }
415     }
416 
417     // The loop above only adds WaveformMarks for hotcues to m_marksToRender.
418     for (const auto& pMark : m_marks) {
419         if (!m_marksToRender.contains(pMark) && pMark->isValid() && pMark->getSamplePosition() != Cue::kNoPosition && pMark->isVisible()) {
420             m_marksToRender.append(pMark);
421         }
422     }
423     std::sort(m_marksToRender.begin(), m_marksToRender.end());
424 }
425 
426 // connecting the tracks cuesUpdated and onMarkChanged is not possible
427 // due to the incompatible signatures. This is a "wrapper" workaround
receiveCuesUpdated()428 void WOverview::receiveCuesUpdated() {
429     onMarkChanged(0);
430 }
431 
mouseMoveEvent(QMouseEvent * e)432 void WOverview::mouseMoveEvent(QMouseEvent* e) {
433     if (m_bLeftClickDragging) {
434         if (m_orientation == Qt::Horizontal) {
435             m_iPickupPos = math_clamp(e->x(), 0, width() - 1);
436         } else {
437             m_iPickupPos = math_clamp(e->y(), 0, height() - 1);
438         }
439     }
440 
441     // Do not activate cue hovering while right click is held down and the
442     // button down event was not on a cue.
443     if (m_bTimeRulerActive) {
444         // Prevent showing times beyond the boundaries of the track when the
445         // cursor is dragged outside this widget before releasing right click.
446         m_timeRulerPos.setX(math_clamp(e->pos().x(), 0, width()));
447         m_timeRulerPos.setY(math_clamp(e->pos().y(), 0, height()));
448         update();
449         return;
450     }
451 
452     m_pHoveredMark.clear();
453 
454     // Non-hotcue marks (intro/outro cues, main cue, loop in/out) are sorted
455     // before hotcues in m_marksToRender so if there is a hotcue in the same
456     // location, the hotcue gets rendered on top. When right clicking, the
457     // the hotcue rendered on top must be assigned to m_pHoveredMark to show
458     // the CueMenuPopup. To accomplish this, m_marksToRender is iterated in
459     // reverse and the loop breaks as soon as m_pHoveredMark is set.
460     for (int i = m_marksToRender.size() - 1; i >= 0; --i) {
461         WaveformMarkPointer pMark = m_marksToRender.at(i);
462         if (pMark->contains(e->pos(), m_orientation)) {
463             m_pHoveredMark = pMark;
464             break;
465         }
466     }
467 
468     //qDebug() << "WOverview::mouseMoveEvent" << e->pos() << m_iPos;
469     update();
470 }
471 
mouseReleaseEvent(QMouseEvent * e)472 void WOverview::mouseReleaseEvent(QMouseEvent* e) {
473     mouseMoveEvent(e);
474     //qDebug() << "WOverview::mouseReleaseEvent" << e->pos() << m_iPos << ">>" << dValue;
475 
476     if (e->button() == Qt::LeftButton) {
477         if (m_bLeftClickDragging) {
478             m_iPlayPos = m_iPickupPos;
479             double dValue = positionToValue(m_iPickupPos);
480             setControlParameterUp(dValue);
481             m_bLeftClickDragging = false;
482         }
483         m_bTimeRulerActive = false;
484     } else if (e->button() == Qt::RightButton) {
485         // Do not seek when releasing a right click. This is important to
486         // prevent accidental seeking when trying to right click a hotcue.
487         m_bTimeRulerActive = false;
488     }
489 }
490 
mousePressEvent(QMouseEvent * e)491 void WOverview::mousePressEvent(QMouseEvent* e) {
492     //qDebug() << "WOverview::mousePressEvent" << e->pos();
493     mouseMoveEvent(e);
494     if (m_pCurrentTrack == nullptr) {
495         return;
496     }
497     if (e->button() == Qt::LeftButton) {
498         if (m_orientation == Qt::Horizontal) {
499             m_iPickupPos = math_clamp(e->x(), 0, width() - 1);
500         } else {
501             m_iPickupPos = math_clamp(e->y(), 0, height() - 1);
502         }
503 
504         if (m_pHoveredMark != nullptr) {
505             double dValue = m_pHoveredMark->getSamplePosition() / m_trackSamplesControl->get();
506             m_iPickupPos = valueToPosition(dValue);
507             m_iPlayPos = m_iPickupPos;
508             setControlParameterUp(dValue);
509             m_bLeftClickDragging = false;
510         } else {
511             m_bLeftClickDragging = true;
512             m_bTimeRulerActive = true;
513             m_timeRulerPos = e->pos();
514         }
515     } else if (e->button() == Qt::RightButton) {
516         if (m_bLeftClickDragging) {
517             m_iPickupPos = m_iPlayPos;
518             m_bLeftClickDragging = false;
519             m_bTimeRulerActive = false;
520         } else if (m_pHoveredMark == nullptr) {
521             m_bTimeRulerActive = true;
522             m_timeRulerPos = e->pos();
523         } else if (m_pHoveredMark->getHotCue() != Cue::kNoHotCue) {
524             // Currently the only way WaveformMarks can be associated
525             // with their respective Cue objects is by using the hotcue
526             // number. If cues without assigned hotcue are drawn on
527             // WOverview in the future, another way to associate
528             // WaveformMarks with Cues will need to be implemented.
529             CuePointer pHoveredCue;
530             QList<CuePointer> cueList = m_pCurrentTrack->getCuePoints();
531             for (const auto& pCue : cueList) {
532                 if (pCue->getHotCue() == m_pHoveredMark->getHotCue()) {
533                     pHoveredCue = pCue;
534                     break;
535                 }
536             }
537             if (pHoveredCue != nullptr) {
538                 if (e->modifiers().testFlag(Qt::ShiftModifier)) {
539                     m_pCurrentTrack->removeCue(pHoveredCue);
540                     return;
541                 } else {
542                     m_pCueMenuPopup->setTrackAndCue(m_pCurrentTrack, pHoveredCue);
543                     m_pCueMenuPopup->popup(e->globalPos());
544                 }
545             }
546         }
547     }
548 }
549 
slotCueMenuPopupAboutToHide()550 void WOverview::slotCueMenuPopupAboutToHide() {
551     m_pHoveredMark.clear();
552     update();
553 }
554 
leaveEvent(QEvent * pEvent)555 void WOverview::leaveEvent(QEvent* pEvent) {
556     Q_UNUSED(pEvent);
557     if (!m_pCueMenuPopup->isVisible()) {
558         m_pHoveredMark.clear();
559     }
560     m_bLeftClickDragging = false;
561     m_bTimeRulerActive = false;
562     update();
563 }
564 
paintEvent(QPaintEvent * pEvent)565 void WOverview::paintEvent(QPaintEvent* pEvent) {
566     Q_UNUSED(pEvent);
567     ScopedTimer t("WOverview::paintEvent");
568 
569     QPainter painter(this);
570     painter.fillRect(rect(), m_backgroundColor);
571 
572     if (!m_backgroundPixmap.isNull()) {
573         painter.drawPixmap(rect(), m_backgroundPixmap);
574     }
575 
576     if (m_pCurrentTrack) {
577         // Refer to util/ScopePainter.h to understand the semantics of
578         // ScopePainter.
579         drawEndOfTrackBackground(&painter);
580         drawAxis(&painter);
581         drawWaveformPixmap(&painter);
582         drawPlayedOverlay(&painter);
583         drawPlayPosition(&painter);
584         drawEndOfTrackFrame(&painter);
585         drawAnalyzerProgress(&painter);
586 
587         double trackSamples = m_trackSamplesControl->get();
588         if (m_trackLoaded && trackSamples > 0) {
589             const float offset = 1.0f;
590             const auto gain = static_cast<CSAMPLE_GAIN>(length() - 2) /
591                     static_cast<CSAMPLE_GAIN>(m_trackSamplesControl->get());
592 
593             drawRangeMarks(&painter, offset, gain);
594             drawMarks(&painter, offset, gain);
595             drawPickupPosition(&painter);
596             drawTimeRuler(&painter);
597             drawMarkLabels(&painter, offset, gain);
598         }
599     }
600 
601     if (m_bPassthroughEnabled) {
602         drawPassthroughOverlay(&painter);
603         m_pPassthroughLabel->show();
604     } else {
605         m_pPassthroughLabel->hide();
606     }
607 }
608 
drawEndOfTrackBackground(QPainter * pPainter)609 void WOverview::drawEndOfTrackBackground(QPainter* pPainter) {
610     if (m_endOfTrack) {
611         PainterScope painterScope(pPainter);
612         pPainter->setOpacity(0.3);
613         pPainter->setBrush(m_endOfTrackColor);
614         pPainter->drawRect(rect().adjusted(1, 1, -2, -2));
615     }
616 }
617 
drawAxis(QPainter * pPainter)618 void WOverview::drawAxis(QPainter* pPainter) {
619     PainterScope painterScope(pPainter);
620     pPainter->setPen(QPen(m_axesColor, m_scaleFactor));
621     if (m_orientation == Qt::Horizontal) {
622         pPainter->drawLine(0, height() / 2, width(), height() / 2);
623     } else {
624         pPainter->drawLine(width() / 2, 0, width() / 2, height());
625     }
626 }
627 
drawWaveformPixmap(QPainter * pPainter)628 void WOverview::drawWaveformPixmap(QPainter* pPainter) {
629     WaveformWidgetFactory* widgetFactory = WaveformWidgetFactory::instance();
630     if (!m_waveformSourceImage.isNull()) {
631         PainterScope painterScope(pPainter);
632         float diffGain;
633         bool normalize = widgetFactory->isOverviewNormalized();
634         if (normalize && m_pixmapDone && m_waveformPeak > 1) {
635             diffGain = 255 - m_waveformPeak - 1;
636         } else {
637             const auto visualGain = static_cast<float>(
638                     widgetFactory->getVisualGain(WaveformWidgetFactory::All));
639             diffGain = 255.0f - (255.0f / visualGain);
640         }
641 
642         if (m_diffGain != diffGain || m_waveformImageScaled.isNull()) {
643             QRect sourceRect(0,
644                     static_cast<int>(diffGain),
645                     m_waveformSourceImage.width(),
646                     m_waveformSourceImage.height() -
647                             2 * static_cast<int>(diffGain));
648             QImage croppedImage = m_waveformSourceImage.copy(sourceRect);
649             if (m_orientation == Qt::Vertical) {
650                 // Rotate pixmap
651                 croppedImage = croppedImage.transformed(QTransform(0, 1, 1, 0, 0, 0));
652             }
653             m_waveformImageScaled = croppedImage.scaled(size() * m_devicePixelRatio,
654                     Qt::IgnoreAspectRatio,
655                     Qt::SmoothTransformation);
656             m_diffGain = diffGain;
657         }
658 
659         pPainter->drawImage(rect(), m_waveformImageScaled);
660     }
661 }
662 
drawPlayedOverlay(QPainter * pPainter)663 void WOverview::drawPlayedOverlay(QPainter* pPainter) {
664     // Overlay the played part of the overview-waveform with a skin defined color
665     if (!m_waveformSourceImage.isNull() && m_playedOverlayColor.alpha() > 0) {
666         if (m_orientation == Qt::Vertical) {
667             pPainter->fillRect(0,
668                     0,
669                     m_waveformImageScaled.width(),
670                     m_iPlayPos,
671                     m_playedOverlayColor);
672         } else {
673             pPainter->fillRect(0,
674                     0,
675                     m_iPlayPos,
676                     m_waveformImageScaled.height(),
677                     m_playedOverlayColor);
678         }
679     }
680 }
681 
drawPlayPosition(QPainter * pPainter)682 void WOverview::drawPlayPosition(QPainter* pPainter) {
683     // When the position line is currently dragged with the left mouse button
684     // draw a thin line -without triangles or shadow- at the playposition.
685     // The new playposition is drawn by drawPickupPosition()
686     if (m_bLeftClickDragging) {
687         PainterScope painterScope(pPainter);
688         QLineF line;
689         if (m_orientation == Qt::Horizontal) {
690             line.setLine(m_iPlayPos, 0.0, m_iPlayPos, height());
691         } else {
692             line.setLine(0.0, m_iPlayPos, width(), m_iPlayPos);
693         }
694         pPainter->setPen(QPen(m_playPosColor, m_scaleFactor));
695         pPainter->setOpacity(0.5);
696         pPainter->drawLine(line);
697     }
698 }
699 
drawEndOfTrackFrame(QPainter * pPainter)700 void WOverview::drawEndOfTrackFrame(QPainter* pPainter) {
701     if (m_endOfTrack) {
702         PainterScope painterScope(pPainter);
703         pPainter->setOpacity(0.8);
704         pPainter->setPen(QPen(QBrush(m_endOfTrackColor), 1.5 * m_scaleFactor));
705         pPainter->setBrush(QColor(0, 0, 0, 0));
706         pPainter->drawRect(rect().adjusted(0, 0, -1, -1));
707     }
708 }
709 
drawAnalyzerProgress(QPainter * pPainter)710 void WOverview::drawAnalyzerProgress(QPainter* pPainter) {
711     if ((m_analyzerProgress >= kAnalyzerProgressNone) &&
712             (m_analyzerProgress < kAnalyzerProgressDone)) {
713         PainterScope painterScope(pPainter);
714         pPainter->setPen(QPen(m_playPosColor, 3 * m_scaleFactor));
715 
716         if (m_analyzerProgress > kAnalyzerProgressNone) {
717             if (m_orientation == Qt::Horizontal) {
718                 pPainter->drawLine(QLineF(width() * m_analyzerProgress,
719                         height() / 2,
720                         width(),
721                         height() / 2));
722             } else {
723                 pPainter->drawLine(QLineF(width() / 2,
724                         height() * m_analyzerProgress,
725                         width() / 2,
726                         height()));
727             }
728         }
729 
730         if (m_analyzerProgress <= kAnalyzerProgressHalf) { // remove text after progress by wf is recognizable
731             if (m_trackLoaded) {
732                 //: Text on waveform overview when file is playable but no waveform is visible
733                 paintText(tr("Ready to play, analyzing..."), pPainter);
734             } else {
735                 //: Text on waveform overview when file is cached from source
736                 paintText(tr("Loading track..."), pPainter);
737             }
738         } else if (m_analyzerProgress >= kAnalyzerProgressFinalizing) {
739             //: Text on waveform overview during finalizing of waveform analysis
740             paintText(tr("Finalizing..."), pPainter);
741         }
742     } else if (!m_trackLoaded) {
743         // This happens if the track samples are not loaded, but we have
744         // a cached track
745         //: Text on waveform overview when file is cached from source
746         paintText(tr("Loading track..."), pPainter);
747     }
748 }
749 
drawRangeMarks(QPainter * pPainter,const float & offset,const float & gain)750 void WOverview::drawRangeMarks(QPainter* pPainter, const float& offset, const float& gain) {
751     for (auto&& markRange : m_markRanges) {
752         if (!markRange.active() || !markRange.visible()) {
753             continue;
754         }
755 
756         // Active mark ranges by definition have starts/ends that are not
757         // disabled.
758         const qreal startValue = markRange.start();
759         const qreal endValue = markRange.end();
760 
761         const qreal startPosition = offset + startValue * gain;
762         const qreal endPosition = offset + endValue * gain;
763 
764         if (startPosition < 0.0 && endPosition < 0.0) {
765             continue;
766         }
767 
768         PainterScope painterScope(pPainter);
769 
770         if (markRange.enabled()) {
771             pPainter->setOpacity(markRange.m_enabledOpacity);
772             pPainter->setPen(markRange.m_activeColor);
773             pPainter->setBrush(markRange.m_activeColor);
774         } else {
775             pPainter->setOpacity(markRange.m_disabledOpacity);
776             pPainter->setPen(markRange.m_disabledColor);
777             pPainter->setBrush(markRange.m_disabledColor);
778         }
779 
780         // let top and bottom of the rect out of the widget
781         if (m_orientation == Qt::Horizontal) {
782             pPainter->drawRect(QRectF(QPointF(startPosition, -2.0),
783                     QPointF(endPosition, height() + 1.0)));
784         } else {
785             pPainter->drawRect(QRectF(QPointF(-2.0, startPosition),
786                     QPointF(width() + 1.0, endPosition)));
787         }
788     }
789 }
790 
drawMarks(QPainter * pPainter,const float offset,const float gain)791 void WOverview::drawMarks(QPainter* pPainter, const float offset, const float gain) {
792     QFont markerFont = pPainter->font();
793     markerFont.setPixelSize(static_cast<int>(m_iLabelFontSize * m_scaleFactor));
794     QFontMetricsF fontMetrics(markerFont);
795 
796     // Text labels are rendered so they do not overlap with other WaveformMarks'
797     // labels. If the text would be too wide, it is elided. However, the user
798     // can hover the mouse cursor over a label to show the whole label text,
799     // temporarily hiding any following labels that would be drawn over it.
800     // This requires looping over the WaveformMarks twice and the marks must be
801     // sorted in the order they appear on the waveform.
802     // In the first loop, the lines are drawn and the text to render plus its
803     // location are calculated then stored in a WaveformMarkLabel. The text must
804     // be drawn in the second loop to prevent the lines of following
805     // WaveformMarks getting drawn over it. The second loop is in the separate
806     // drawMarkLabels function so it can be called after drawCurrentPosition so
807     // the view of labels is not obscured by the playhead.
808 
809     bool markHovered = false;
810     for (int i = 0; i < m_marksToRender.size(); ++i) {
811         WaveformMarkPointer pMark = m_marksToRender.at(i);
812         PainterScope painterScope(pPainter);
813 
814         const float markPosition = math_clamp(
815                 offset + static_cast<float>(m_marksToRender.at(i)->getSamplePosition()) * gain,
816                 0.0f,
817                 static_cast<float>(width()));
818         pMark->m_linePosition = markPosition;
819 
820         QLineF line;
821         QLineF bgLine;
822         if (m_orientation == Qt::Horizontal) {
823             line.setLine(markPosition, 0.0, markPosition, height());
824             bgLine.setLine(markPosition - 1.0, 0.0, markPosition - 1.0, height());
825         } else {
826             line.setLine(0.0, markPosition, width(), markPosition);
827             bgLine.setLine(0.0, markPosition - 1.0, width(), markPosition - 1.0);
828         }
829 
830         pPainter->setPen(pMark->borderColor());
831         pPainter->drawLine(bgLine);
832 
833         pPainter->setPen(pMark->fillColor());
834         pPainter->drawLine(line);
835 
836         if (!pMark->m_text.isEmpty()) {
837             Qt::Alignment halign = pMark->m_align & Qt::AlignHorizontal_Mask;
838             Qt::Alignment valign = pMark->m_align & Qt::AlignVertical_Mask;
839 
840             QString text = pMark->m_text;
841 
842             // Only allow the text to overlap the following mark if the mouse is
843             // hovering over it. Elide it if it would render over the next
844             // label, but do not elide it if the next mark's label is not at the
845             // same vertical position.
846             if (pMark != m_pHoveredMark && i < m_marksToRender.size() - 1) {
847                 float nextMarkPosition = -1.0f;
848                 for (int m = i + 1; m < m_marksToRender.size() - 1; ++m) {
849                     WaveformMarkPointer otherMark = m_marksToRender.at(m);
850                     bool otherAtSameHeight = valign == (otherMark->m_align & Qt::AlignVertical_Mask);
851                     // Hotcues always show at least their number.
852                     bool otherHasLabel = !otherMark->m_text.isEmpty() || otherMark->getHotCue() != Cue::kNoHotCue;
853                     if (otherAtSameHeight && otherHasLabel) {
854                         nextMarkPosition = offset +
855                                 static_cast<float>(
856                                         otherMark->getSamplePosition()) *
857                                         gain;
858                         break;
859                     }
860                 }
861                 if (nextMarkPosition != -1.0) {
862                     text = fontMetrics.elidedText(text, Qt::ElideRight, nextMarkPosition - markPosition - 5);
863                 }
864             }
865             // Sometimes QFontMetrics::elidedText turns the QString into just an
866             // ellipsis character, so always show at least the hotcue number if
867             // the label does not fit.
868             if ((text.isEmpty() || text == "…") && pMark->getHotCue() != Cue::kNoHotCue) {
869                 text = QString::number(pMark->getHotCue() + 1);
870             }
871 
872             QRectF textRect = fontMetrics.boundingRect(text);
873             QPointF textPoint;
874             if (m_orientation == Qt::Horizontal) {
875                 if (halign == Qt::AlignLeft) {
876                     textPoint.setX(markPosition - textRect.width() - 5.5);
877                 } else if (halign == Qt::AlignHCenter) {
878                     textPoint.setX(markPosition - textRect.width() / 2);
879                 } else { // AlignRight
880                     textPoint.setX(markPosition + 1.5);
881                 }
882 
883                 if (valign == Qt::AlignTop) {
884                     textPoint.setY(fontMetrics.height());
885                 } else if (valign == Qt::AlignVCenter) {
886                     textPoint.setY((textRect.height() + height()) / 2);
887                 } else { // AlignBottom
888                     textPoint.setY(float(height()) - 0.5f);
889                 }
890             } else { // Vertical
891                 if (halign == Qt::AlignLeft) {
892                     textPoint.setX(1.0f);
893                 } else if (halign == Qt::AlignHCenter) {
894                     textPoint.setX((width() - textRect.width()) / 2);
895                 } else { // AlignRight
896                     textPoint.setX(width() - textRect.width());
897                 }
898 
899                 if (valign == Qt::AlignTop) {
900                     textPoint.setY(markPosition - 1.0f);
901                 } else if (valign == Qt::AlignVCenter) {
902                     textPoint.setY(markPosition + textRect.height() / 2);
903                 } else { // AlignBottom
904                     textPoint.setY(markPosition + fontMetrics.ascent());
905                 }
906             }
907 
908             pMark->m_label.prerender(textPoint,
909                     QPixmap(),
910                     text,
911                     markerFont,
912                     m_labelTextColor,
913                     m_labelBackgroundColor,
914                     width(),
915                     getDevicePixelRatioF(this));
916         }
917 
918         // Show cue position when hovered
919         // The area it will be drawn in needs to be calculated here
920         // before drawMarkLabels so drawMarkLabels can avoid drawing
921         // labels over the cue position.
922         // This can happen for example if the user shows the cue position
923         // of a hotcue which is near the intro end position because the
924         // intro_end_position WaveformMark label is drawn at the top.
925         // However, the drawing of this text needs to happen in
926         // drawMarkLabels so none of the WaveformMark lines are drawn
927         // on top of the position text.
928 
929         // WaveformMark::m_align refers to the alignment of the label,
930         // so if the label is on bottom draw the position text on top and
931         // vice versa.
932         if (pMark == m_pHoveredMark) {
933             Qt::Alignment valign = pMark->m_align & Qt::AlignVertical_Mask;
934             QPointF positionTextPoint(markPosition + 1.5, 0);
935             if (valign == Qt::AlignTop) {
936                 positionTextPoint.setY(float(height()) - 0.5f);
937             } else {
938                 positionTextPoint.setY(fontMetrics.height());
939             }
940 
941             double markSamples = pMark->getSamplePosition();
942             double trackSamples = m_trackSamplesControl->get();
943             double currentPositionSamples = m_playpositionControl->get() * trackSamples;
944             double markTime = samplePositionToSeconds(markSamples);
945             double markTimeRemaining = samplePositionToSeconds(trackSamples - markSamples);
946             double markTimeDistance = samplePositionToSeconds(markSamples - currentPositionSamples);
947             QString cuePositionText = mixxx::Duration::formatTime(markTime) + " -" +
948                     mixxx::Duration::formatTime(markTimeRemaining);
949             QString cueTimeDistanceText = mixxx::Duration::formatTime(fabs(markTimeDistance));
950             // Cast to int to avoid confusingly switching from -0:00 to 0:00 as
951             // the playhead passes the cue
952             if (static_cast<int>(markTimeDistance) < 0) {
953                 cueTimeDistanceText = "-" + cueTimeDistanceText;
954             }
955 
956             m_cuePositionLabel.prerender(positionTextPoint,
957                     QPixmap(),
958                     cuePositionText,
959                     markerFont,
960                     m_labelTextColor,
961                     m_labelBackgroundColor,
962                     width(),
963                     getDevicePixelRatioF(this));
964 
965             QPointF timeDistancePoint(positionTextPoint.x(),
966                     (fontMetrics.height() + height()) / 2);
967 
968             m_cueTimeDistanceLabel.prerender(timeDistancePoint,
969                     QPixmap(),
970                     cueTimeDistanceText,
971                     markerFont,
972                     m_labelTextColor,
973                     m_labelBackgroundColor,
974                     width(),
975                     getDevicePixelRatioF(this));
976             markHovered = true;
977         }
978     }
979     if (!markHovered) {
980         m_cuePositionLabel.clear();
981         m_cueTimeDistanceLabel.clear();
982     }
983 }
984 
drawPickupPosition(QPainter * pPainter)985 void WOverview::drawPickupPosition(QPainter* pPainter) {
986     PainterScope painterScope(pPainter);
987 
988     if (m_orientation == Qt::Vertical) {
989         pPainter->setTransform(QTransform(0, 1, 1, 0, 0, 0));
990     }
991 
992     // draw dark play position outlines
993     pPainter->setPen(QPen(QBrush(m_backgroundColor), m_scaleFactor));
994     pPainter->setOpacity(0.5);
995     pPainter->drawLine(m_iPickupPos + 1, 0, m_iPickupPos + 1, breadth());
996     pPainter->drawLine(m_iPickupPos - 1, 0, m_iPickupPos - 1, breadth());
997 
998     // draw colored play position line
999     pPainter->setPen(QPen(m_playPosColor, m_scaleFactor));
1000     pPainter->setOpacity(1.0);
1001     pPainter->drawLine(m_iPickupPos, 0, m_iPickupPos, breadth());
1002 
1003     // draw triangle at the top
1004     pPainter->drawLine(m_iPickupPos - 2, 0, m_iPickupPos, 2);
1005     pPainter->drawLine(m_iPickupPos, 2, m_iPickupPos + 2, 0);
1006     pPainter->drawLine(m_iPickupPos - 2, 0, m_iPickupPos + 2, 0);
1007 
1008     // draw triangle at the bottom
1009     pPainter->drawLine(m_iPickupPos - 2, breadth() - 1, m_iPickupPos, breadth() - 3);
1010     pPainter->drawLine(m_iPickupPos, breadth() - 3, m_iPickupPos + 2, breadth() - 1);
1011     pPainter->drawLine(m_iPickupPos - 2, breadth() - 1, m_iPickupPos + 2, breadth() - 1);
1012 }
1013 
drawTimeRuler(QPainter * pPainter)1014 void WOverview::drawTimeRuler(QPainter* pPainter) {
1015     QFont markerFont = pPainter->font();
1016     markerFont.setPixelSize(static_cast<int>(m_iLabelFontSize * m_scaleFactor));
1017     QFontMetricsF fontMetrics(markerFont);
1018 
1019     QFont shadowFont = pPainter->font();
1020     shadowFont.setWeight(99);
1021     shadowFont.setPixelSize(static_cast<int>(m_iLabelFontSize * m_scaleFactor));
1022     QPen shadowPen(Qt::black, 2.5 * m_scaleFactor);
1023 
1024     if (m_bTimeRulerActive) {
1025         if (!m_bLeftClickDragging) {
1026             QLineF line;
1027             if (m_orientation == Qt::Horizontal) {
1028                 line.setLine(m_timeRulerPos.x(), 0.0, m_timeRulerPos.x(), height());
1029             } else {
1030                 line.setLine(0.0, m_timeRulerPos.x(), width(), m_timeRulerPos.x());
1031             }
1032             pPainter->setPen(shadowPen);
1033             pPainter->drawLine(line);
1034 
1035             pPainter->setPen(QPen(m_playPosColor, m_scaleFactor));
1036             pPainter->drawLine(line);
1037         }
1038 
1039         QPointF textPoint = m_timeRulerPos;
1040         QPointF textPointDistance = m_timeRulerPos;
1041         qreal widgetPositionFraction;
1042         qreal padding = 1.0; // spacing between line and text
1043         if (m_orientation == Qt::Horizontal) {
1044             textPoint = QPointF(textPoint.x() + padding, fontMetrics.height());
1045             textPointDistance = QPointF(textPointDistance.x() + padding,
1046                     (fontMetrics.height() + height()) / 2);
1047             widgetPositionFraction = m_timeRulerPos.x() / width();
1048         } else {
1049             textPoint.setX(0);
1050             textPointDistance.setX(0);
1051             widgetPositionFraction = m_timeRulerPos.y() / height();
1052         }
1053         qreal trackSamples = m_trackSamplesControl->get();
1054         qreal timePosition = samplePositionToSeconds(
1055                 widgetPositionFraction * trackSamples);
1056         qreal timePositionTillEnd = samplePositionToSeconds(
1057                 (1 - widgetPositionFraction) * trackSamples);
1058         qreal timeDistance = samplePositionToSeconds(
1059                 (widgetPositionFraction - m_playpositionControl->get()) * trackSamples);
1060 
1061         QString timeText = mixxx::Duration::formatTime(timePosition) + " -" + mixxx::Duration::formatTime(timePositionTillEnd);
1062 
1063         m_timeRulerPositionLabel.prerender(textPoint,
1064                 QPixmap(),
1065                 timeText,
1066                 markerFont,
1067                 m_labelTextColor,
1068                 m_labelBackgroundColor,
1069                 width(),
1070                 getDevicePixelRatioF(this));
1071         m_timeRulerPositionLabel.draw(pPainter);
1072 
1073         QString timeDistanceText = mixxx::Duration::formatTime(fabs(timeDistance));
1074         // Cast to int to avoid confusingly switching from -0:00 to 0:00 as
1075         // the playhead passes the point
1076         if (static_cast<int>(timeDistance) < 0) {
1077             timeDistanceText = "-" + timeDistanceText;
1078         }
1079         m_timeRulerDistanceLabel.prerender(textPointDistance,
1080                 QPixmap(),
1081                 timeDistanceText,
1082                 markerFont,
1083                 m_labelTextColor,
1084                 m_labelBackgroundColor,
1085                 width(),
1086                 getDevicePixelRatioF(this));
1087         m_timeRulerDistanceLabel.draw(pPainter);
1088     } else {
1089         m_timeRulerPositionLabel.clear();
1090         m_timeRulerDistanceLabel.clear();
1091     }
1092 }
1093 
drawMarkLabels(QPainter * pPainter,const float offset,const float gain)1094 void WOverview::drawMarkLabels(QPainter* pPainter, const float offset, const float gain) {
1095     QFont markerFont = pPainter->font();
1096     markerFont.setPixelSize(static_cast<int>(m_iLabelFontSize * m_scaleFactor));
1097     QFontMetricsF fontMetrics(markerFont);
1098 
1099     // Draw WaveformMark labels
1100     for (const auto& pMark : qAsConst(m_marksToRender)) {
1101         if (m_pHoveredMark != nullptr && pMark != m_pHoveredMark) {
1102             if (pMark->m_label.intersects(m_pHoveredMark->m_label)) {
1103                 continue;
1104             }
1105         }
1106         if (m_bShowCueTimes &&
1107                 (pMark->m_label.intersects(m_cuePositionLabel) || pMark->m_label.intersects(m_cueTimeDistanceLabel))) {
1108             continue;
1109         }
1110         if (pMark->m_label.intersects(m_timeRulerPositionLabel) || pMark->m_label.intersects(m_timeRulerDistanceLabel)) {
1111             continue;
1112         }
1113 
1114         pMark->m_label.draw(pPainter);
1115     }
1116 
1117     if (m_bShowCueTimes) {
1118         m_cuePositionLabel.draw(pPainter);
1119         m_cueTimeDistanceLabel.draw(pPainter);
1120     }
1121 
1122     // draw duration of WaveformMarkRanges
1123     for (auto&& markRange : m_markRanges) {
1124         if (markRange.showDuration() && markRange.active() && markRange.visible()) {
1125             // Active mark ranges by definition have starts/ends that are not
1126             // disabled.
1127             const qreal startValue = markRange.start();
1128             const qreal endValue = markRange.end();
1129 
1130             const qreal startPosition = offset + startValue * gain;
1131             const qreal endPosition = offset + endValue * gain;
1132 
1133             if (startPosition < 0.0 && endPosition < 0.0) {
1134                 continue;
1135             }
1136             QString duration = mixxx::Duration::formatTime(
1137                     samplePositionToSeconds(endValue - startValue));
1138 
1139             QRectF durationRect = fontMetrics.boundingRect(duration);
1140             qreal x;
1141 
1142             WaveformMarkRange::DurationTextLocation textLocation = markRange.durationTextLocation();
1143             if (textLocation == WaveformMarkRange::DurationTextLocation::Before) {
1144                 x = startPosition - durationRect.width() - 5.5;
1145             } else {
1146                 x = endPosition + 1.5;
1147             }
1148 
1149             QPointF durationBottomLeft(x, fontMetrics.height());
1150 
1151             markRange.m_durationLabel.prerender(durationBottomLeft,
1152                     QPixmap(),
1153                     duration,
1154                     markerFont,
1155                     m_labelTextColor,
1156                     m_labelBackgroundColor,
1157                     width(),
1158                     getDevicePixelRatioF(this));
1159 
1160             if (!(markRange.m_durationLabel.intersects(m_cuePositionLabel) || markRange.m_durationLabel.intersects(m_cueTimeDistanceLabel) || markRange.m_durationLabel.intersects(m_timeRulerPositionLabel) || markRange.m_durationLabel.intersects(m_timeRulerDistanceLabel))) {
1161                 markRange.m_durationLabel.draw(pPainter);
1162             }
1163         }
1164     }
1165 }
1166 
drawPassthroughOverlay(QPainter * pPainter)1167 void WOverview::drawPassthroughOverlay(QPainter* pPainter) {
1168     if (!m_waveformSourceImage.isNull() && m_passthroughOverlayColor.alpha() > 0) {
1169         // Overlay the entire overview-waveform with a skin defined color
1170         pPainter->fillRect(rect(), m_passthroughOverlayColor);
1171     }
1172 }
1173 
paintText(const QString & text,QPainter * pPainter)1174 void WOverview::paintText(const QString& text, QPainter* pPainter) {
1175     PainterScope painterScope(pPainter);
1176     m_lowColor.setAlphaF(0.5);
1177     QPen lowColorPen(
1178             QBrush(m_lowColor), 1.25 * m_scaleFactor, Qt::SolidLine, Qt::RoundCap);
1179     pPainter->setPen(lowColorPen);
1180     QFont font = pPainter->font();
1181     QFontMetrics fm(font);
1182 
1183     // TODO: The following use of QFontMetrics::width(const QString&, int) const
1184     // is deprecated and should be replaced with
1185     // QFontMetrics::horizontalAdvance(const QString&, int) const. However, the
1186     // proposed alternative has just been introduced in Qt 5.11.
1187     // Until the minimum required Qt version of Mixxx is increased, we need a
1188     // version check here.
1189     #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0))
1190     int textWidth = fm.width(text);
1191     #else
1192     int textWidth = fm.horizontalAdvance(text);
1193     #endif
1194 
1195     if (textWidth > length()) {
1196         qreal pointSize = font.pointSizeF();
1197         pointSize = pointSize * (length() - 5 * m_scaleFactor) / textWidth;
1198         if (pointSize < 6 * m_scaleFactor) {
1199             pointSize = 6 * m_scaleFactor;
1200         }
1201         font.setPointSizeF(pointSize);
1202         pPainter->setFont(font);
1203     }
1204     if (m_orientation == Qt::Vertical) {
1205         pPainter->setTransform(QTransform(0, 1, -1, 0, width(), 0));
1206     }
1207     pPainter->drawText(QPointF(10 * m_scaleFactor, 12 * m_scaleFactor), text);
1208     pPainter->resetTransform();
1209 }
1210 
samplePositionToSeconds(double sample)1211 double WOverview::samplePositionToSeconds(double sample) {
1212     double trackTime = sample /
1213             (m_trackSampleRateControl->get() * mixxx::kEngineChannelCount);
1214     return trackTime / m_pRateRatioControl->get();
1215 }
1216 
resizeEvent(QResizeEvent * pEvent)1217 void WOverview::resizeEvent(QResizeEvent* pEvent) {
1218     Q_UNUSED(pEvent);
1219     // Play-position potmeters range from 0 to 1 but they allow out-of-range
1220     // sets. This is to give VC access to the pre-roll area.
1221     const double kMaxPlayposRange = 1.0;
1222     const double kMinPlayposRange = 0.0;
1223 
1224     // Values of zero and one in normalized space.
1225     const double zero = (0.0 - kMinPlayposRange) / (kMaxPlayposRange - kMinPlayposRange);
1226     const double one = (1.0 - kMinPlayposRange) / (kMaxPlayposRange - kMinPlayposRange);
1227 
1228     // These coefficients convert between widget space and normalized value
1229     // space.
1230     m_a = (length() - 1) / (one - zero);
1231     m_b = zero * m_a;
1232 
1233     m_devicePixelRatio = getDevicePixelRatioF(this);
1234 
1235     m_waveformImageScaled = QImage();
1236     m_diffGain = 0;
1237     Init();
1238 }
1239 
dragEnterEvent(QDragEnterEvent * pEvent)1240 void WOverview::dragEnterEvent(QDragEnterEvent* pEvent) {
1241     DragAndDropHelper::handleTrackDragEnterEvent(pEvent, m_group, m_pConfig);
1242 }
dropEvent(QDropEvent * pEvent)1243 void WOverview::dropEvent(QDropEvent* pEvent) {
1244     DragAndDropHelper::handleTrackDropEvent(pEvent, *this, m_group, m_pConfig);
1245 }
1246