summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt4
-rw-r--r--build/depends.py4
-rw-r--r--src/library/coverartutils.cpp14
-rw-r--r--src/library/coverartutils.h1
-rw-r--r--src/library/trackcollectioniterator.cpp21
-rw-r--r--src/library/trackcollectioniterator.h43
-rw-r--r--src/library/trackmodeliterator.cpp35
-rw-r--r--src/library/trackmodeliterator.h72
-rw-r--r--src/library/trackprocessing.cpp120
-rw-r--r--src/library/trackprocessing.h135
-rw-r--r--src/test/analyserwaveformtest.cpp25
-rw-r--r--src/track/trackiterator.h19
-rw-r--r--src/util/itemiterator.h82
-rw-r--r--src/util/taskmonitor.cpp189
-rw-r--r--src/util/taskmonitor.h102
-rw-r--r--src/waveform/widgets/glslwaveformwidget.cpp6
-rw-r--r--src/waveform/widgets/glslwaveformwidget.h8
-rw-r--r--src/widget/wtrackmenu.cpp536
-rw-r--r--src/widget/wtrackmenu.h22
19 files changed, 1283 insertions, 155 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 13e2ad3e98..f84ee7cf4b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -599,8 +599,11 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL
src/library/starrating.cpp
src/library/tableitemdelegate.cpp
src/library/trackcollection.cpp
+ src/library/trackcollectioniterator.cpp
src/library/trackcollectionmanager.cpp
src/library/trackloader.cpp
+ src/library/trackmodeliterator.cpp
+ src/library/trackprocessing.cpp
src/library/traktor/traktorfeature.cpp
src/library/treeitem.cpp
src/library/treeitemmodel.cpp
@@ -779,6 +782,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL
src/util/statsmanager.cpp
src/util/tapfilter.cpp
src/util/task.cpp
+ src/util/taskmonitor.cpp
src/util/threadcputimer.cpp
src/util/time.cpp
src/util/timer.cpp
diff --git a/build/depends.py b/build/depends.py
index cbc8a7a88b..4ab669f9fd 100644
--- a/build/depends.py
+++ b/build/depends.py
@@ -1048,6 +1048,9 @@ class MixxxCore(Feature):
"src/library/coverart.cpp",
"src/library/coverartcache.cpp",
"src/library/coverartutils.cpp",
+ "src/library/trackcollectioniterator.cpp",
+ "src/library/trackmodeliterator.cpp",
+ "src/library/trackprocessing.cpp",
"src/library/crate/cratestorage.cpp",
"src/library/crate/cratefeature.cpp",
@@ -1302,6 +1305,7 @@ class MixxxCore(Feature):
"src/util/file.cpp",
"src/util/mac.cpp",
"src/util/task.cpp",
+ "src/util/taskmonitor.cpp",
"src/util/experiment.cpp",
"src/util/xml.cpp",
"src/util/tapfilter.cpp",
diff --git a/src/library/coverartutils.cpp b/src/library/coverartutils.cpp
index db266a751e..9a0ad81c47 100644
--- a/src/library/coverartutils.cpp
+++ b/src/library/coverartutils.cpp
@@ -223,20 +223,6 @@ void guessTrackCoverInfoConcurrently(
}
}
-void guessTrackCoverInfoConcurrently(
- QList<TrackPointer> tracks) {
- if (tracks.isEmpty()) {
- return;
- }
- if (s_enableConcurrentGuessingOfTrackCoverInfo) {
- QtConcurrent::run([tracks] {
- CoverInfoGuesser().guessAndSetCoverInfoForTracks(tracks);
- });
- } else {
- CoverInfoGuesser().guessAndSetCoverInfoForTracks(tracks);
- }
-}
-
void disableConcurrentGuessingOfTrackCoverInfoDuringTests() {
s_enableConcurrentGuessingOfTrackCoverInfo = false;
}
diff --git a/src/library/coverartutils.h b/src/library/coverartutils.h
index af67e881c7..b185dcfde3 100644
--- a/src/library/coverartutils.h
+++ b/src/library/coverartutils.h
@@ -93,7 +93,6 @@ class CoverInfoGuesser {
// metadata and folders for image files. All I/O is done in a separate
// thread.
void guessTrackCoverInfoConcurrently(TrackPointer pTrack);
-void guessTrackCoverInfoConcurrently(QList<TrackPointer> tracks);
// Concurrent guessing of track covers during short running
// tests may cause spurious test failures due to timing issues.
diff --git a/src/library/trackcollectioniterator.cpp b/src/library/trackcollectioniterator.cpp
new file mode 100644
index 0000000000..b198c1e918
--- /dev/null
+++ b/src/library/trackcollectioniterator.cpp
@@ -0,0 +1,21 @@
+#include "library/trackcollectioniterator.h"
+
+#include "library/trackcollection.h"
+
+namespace mixxx {
+
+std::optional<TrackPointer> TrackByIdCollectionIterator::nextItem() {
+ const auto nextTrackId =
+ m_trackIdListIter.nextItem();
+ if (!nextTrackId) {
+ return std::nullopt;
+ }
+ const auto trackPtr =
+ m_pTrackCollection->getTrackById(*nextTrackId);
+ if (!trackPtr) {
+ return std::nullopt;
+ }
+ return std::make_optional(trackPtr);
+}
+
+} // namespace mixxx
diff --git a/src/library/trackcollectioniterator.h b/src/library/trackcollectioniterator.h
new file mode 100644
index 0000000000..0ef2f9f4ff
--- /dev/null
+++ b/src/library/trackcollectioniterator.h
@@ -0,0 +1,43 @@
+/// Utilities for iterating through a selection or collection
+/// of tracks.
+
+#pragma once
+
+#include <QModelIndex>
+
+#include "track/trackiterator.h"
+
+class TrackCollection;
+
+namespace mixxx {
+
+/// Iterate over selected and valid(!) track pointers in a TrackModel.
+/// Invalid (= nullptr) track pointers are skipped silently.
+class TrackByIdCollectionIterator final
+ : public virtual TrackPointerIterator {
+ public:
+ TrackByIdCollectionIterator(
+ const TrackCollection* pTrackCollection,
+ const TrackIdList& trackIds)
+ : m_pTrackCollection(pTrackCollection),
+ m_trackIdListIter(trackIds) {
+ DEBUG_ASSERT(m_pTrackCollection);
+ }
+ ~TrackByIdCollectionIterator() override = default;
+
+ void reset() override {
+ m_trackIdListIter.reset();
+ }
+
+ std::optional<int> estimateItemsRemaining() override {
+ return m_trackIdListIter.estimateItemsRemaining();
+ }
+
+ std::optional<TrackPointer> nextItem() override;
+
+ private:
+ const TrackCollection* const m_pTrackCollection;
+ TrackIdListIterator m_trackIdListIter;
+};
+
+} // namespace mixxx
diff --git a/src/library/trackmodeliterator.cpp b/src/library/trackmodeliterator.cpp
new file mode 100644
index 0000000000..b5a0cd4a5d
--- /dev/null
+++ b/src/library/trackmodeliterator.cpp
@@ -0,0 +1,35 @@
+#include "library/trackmodeliterator.h"
+
+#include "library/trackmodel.h"
+
+namespace mixxx {
+
+std::optional<TrackId> TrackIdModelIterator::nextItem() {
+ const auto nextModelIndex =
+ m_modelIndexListIter.nextItem();
+ if (!nextModelIndex) {
+ return std::nullopt;
+ }
+ const auto trackId =
+ m_pTrackModel->getTrackId(*nextModelIndex);
+ if (!trackId.isValid()) {
+ return std::nullopt;
+ }
+ return std::make_optional(trackId);
+}
+
+std::optional<TrackPointer> TrackPointerModelIterator::nextItem() {
+ const auto nextModelIndex =
+ m_modelIndexListIter.nextItem();
+ if (!nextModelIndex) {
+ return std::nullopt;
+ }
+ const auto trackPtr =
+ m_pTrackModel->getTrack(*nextModelIndex);
+ if (!trackPtr) {
+ return std::nullopt;
+ }
+ return std::make_optional(trackPtr);
+}
+
+} // namespace mixxx
diff --git a/src/library/trackmodeliterator.h b/src/library/trackmodeliterator.h
new file mode 100644
index 0000000000..f9eb478d77
--- /dev/null
+++ b/src/library/trackmodeliterator.h
@@ -0,0 +1,72 @@
+/// Utilities for iterating through a selection or collection
+/// of tracks identified by QModelIndex.
+
+#pragma once
+
+#include <QModelIndex>
+
+#include "track/trackiterator.h"
+
+class TrackModel;
+
+namespace mixxx {
+
+/// Iterate over selected, valid track ids in a TrackModel.
+/// Invalid track ids are skipped silently.
+class TrackIdModelIterator final
+ : public virtual TrackIdIterator {
+ public:
+ TrackIdModelIterator(
+ const TrackModel* pTrackModel,
+ const QModelIndexList& indexList)
+ : m_pTrackModel(pTrackModel),
+ m_modelIndexListIter(indexList) {
+ DEBUG_ASSERT(m_pTrackModel);
+ }
+ ~TrackIdModelIterator() override = default;
+
+ void reset() override {
+ m_modelIndexListIter.reset();
+ }
+
+ std::optional<int> estimateItemsRemaining() override {
+ return m_modelIndexListIter.estimateItemsRemaining();
+ }
+
+ std::optional<TrackId> nextItem() override;
+
+ private:
+ const TrackModel* const m_pTrackModel;
+ ListItemIterator<QModelIndex> m_modelIndexListIter;
+};
+
+/// Iterate over selected, valid track pointers in a TrackModel.
+/// Invalid (= nullptr) track pointers are skipped silently.
+class TrackPointerModelIterator final
+ : public virtual TrackPointerIterator {
+ public:
+ TrackPointerModelIterator(
+ const TrackModel* pTrackModel,
+ const QModelIndexList& indexList)
+ : m_pTrackModel(pTrackModel),
+ m_modelIndexListIter(indexList) {
+ DEBUG_ASSERT(m_pTrackModel);
+ }
+ ~TrackPointerModelIterator() override = default;
+
+ void reset() override {
+ m_modelIndexListIter.reset();
+ }
+
+ std::optional<int> estimateItemsRemaining() override {
+ return m_modelIndexListIter.estimateItemsRemaining();
+ }
+
+ std::optional<TrackPointer> nextItem() override;
+
+ private:
+ const TrackModel* const m_pTrackModel;
+ ListItemIterator<QModelIndex> m_modelIndexListIter;
+};
+
+} // namespace mixxx
diff --git a/src/library/trackprocessing.cpp b/src/library/trackprocessing.cpp
new file mode 100644
index 0000000000..6f5c0103ae
--- /dev/null
+++ b/src/library/trackprocessing.cpp
@@ -0,0 +1,120 @@
+#include "library/trackprocessing.h"
+
+#include <QThread>
+
+#include "library/trackcollection.h"
+#include "library/trackcollectionmanager.h"
+#include "util/logger.h"
+
+namespace mixxx {
+
+namespace {
+
+const Logger kLogger("ModalTrackBatchProcessor");
+
+} // anonymous namespace
+
+int ModalTrackBatchProcessor::processTracks(
+ const QString& progressLabelText,
+ TrackCollectionManager* pTrackCollectionManager,
+ TrackPointerIterator* pTrackPointerIterator) {
+ DEBUG_ASSERT(pTrackCollectionManager);
+ DEBUG_ASSERT(pTrackPointerIterator);
+ DEBUG_ASSERT(QThread::currentThread() ==
+ pTrackCollectionManager->thread());
+ int finishedTrackCount = 0;
+ // The total count is initialized with the remaining count
+ // before starting the iteration. If this value is unknown
+ // we use 0 as the default until an estimation is available
+ // (see update below).
+ int estimatedTotalCount =
+ pTrackPointerIterator->estimateItemsRemaining().value_or(0);
+ m_bAborted = false;
+ TaskMonitor taskMonitor(
+ progressLabelText,
+ m_minimumProgressDuration,
+ this);
+ taskMonitor.registerTask(this);
+ while (auto nextTrackPointer = pTrackPointerIterator->nextItem()) {
+ const auto pTrack = *nextTrackPointer;
+ VERIFY_OR_DEBUG_ASSERT(pTrack) {
+ kLogger.warning()
+ << progressLabelText
+ << "failed to load next track for processing";
+ continue;
+ }
+ if (m_bAborted) {
+ kLogger.info()
+ << "Aborting"
+ << progressLabelText
+ << "after processing"
+ << finishedTrackCount
+ << "of"
+ << estimatedTotalCount
+ << "track(s)";
+ return finishedTrackCount;
+ }
+ switch (doProcessNextTrack(pTrack)) {
+ case ProcessNextTrackResult::AbortProcessing:
+ kLogger.info()
+ << progressLabelText
+ << "aborted while processing"
+ << finishedTrackCount + 1
+ << "of"
+ << estimatedTotalCount
+ << "track(s)";
+ return finishedTrackCount;
+ case ProcessNextTrackResult::ContinueProcessing:
+ break;
+ case ProcessNextTrackResult::SaveTrackAndContinueProcessing:
+ pTrackCollectionManager->saveTrack(pTrack);
+ break;
+ }
+ ++finishedTrackCount;
+ if (finishedTrackCount > estimatedTotalCount) {
+ // Update the total count which cannot be less than the
+ // number of already finished items plus the estimated number
+ // of remaining items.
+ auto estimatedRemainingCount =
+ pTrackPointerIterator->estimateItemsRemaining().value_or(0);
+ estimatedTotalCount = finishedTrackCount + estimatedRemainingCount;
+ }
+ DEBUG_ASSERT(finishedTrackCount <= estimatedTotalCount);
+ taskMonitor.reportTaskProgress(
+ this,
+ kPercentageOfCompletionMin +
+ (kPercentageOfCompletionMax -
+ kPercentageOfCompletionMin) *
+ finishedTrackCount /
+ static_cast<PercentageOfCompletion>(
+ estimatedTotalCount));
+ }
+ return finishedTrackCount;
+}
+
+ModalTrackBatchOperationProcessor::ModalTrackBatchOperationProcessor(
+ const TrackPointerOperation* pTrackPointerOperation,
+ Mode mode,
+ Duration progressGracePeriod,
+ QObject* parent)
+ : ModalTrackBatchProcessor(progressGracePeriod, parent),
+ m_pTrackPointerOperation(pTrackPointerOperation),
+ m_mode(mode) {
+ DEBUG_ASSERT(m_pTrackPointerOperation);
+}
+
+ModalTrackBatchProcessor::ProcessNextTrackResult
+ModalTrackBatchOperationProcessor::doProcessNextTrack(
+ const TrackPointer& pTrack) {
+ m_pTrackPointerOperation->apply(pTrack);
+ switch (m_mode) {
+ case Mode::Apply:
+ return ProcessNextTrackResult::ContinueProcessing;
+ case Mode::ApplyAndSave:
+ return ProcessNextTrackResult::SaveTrackAndContinueProcessing;
+ }
+ DEBUG_ASSERT(!"unreachable");
+ return ProcessNextTrackResult::AbortProcessing;
+}
+
+} // namespace mixxx
diff --git a/src/library/trackprocessing.h b/src/library/trackprocessing.h
new file mode 100644
index 0000000000..1d6e20cb96
--- /dev/null
+++ b/src/library/trackprocessing.h
@@ -0,0 +1,135 @@
+/// Utilities for executing operations on a selection of multiple
+/// tracks while displaying an application modal progress dialog.
+
+#pragma once
+
+#include <QObject>
+
+#include "track/trackiterator.h"
+#include "util/duration.h"
+#include "util/taskmonitor.h"
+
+class TrackCollectionManager;
+
+namespace mixxx {
+
+/// Processes a selection of tracks in the foreground.
+///
+/// Shows a modal progress dialog while processing. This dialog
+/// only appears if processing takes longer than the given grace
+/// period. This avoids that an open context menu gets closed
+/// while processing only a few tracks.
+class ModalTrackBatchProcessor
+ : public Task {
+ Q_OBJECT
+
+ public:
+ virtual ~ModalTrackBatchProcessor() = default;
+
+ /// Subsequently load and process a list of tracks.
+ ///
+ /// Returns the number of processed tracks.
+ int processTracks(
+ const QString& progressLabelText,
+ TrackCollectionManager* pTrackCollectionManager,
+ TrackPointerIterator* pTrackPointerIterator);
+
+ protected:
+ explicit ModalTrackBatchProcessor(
+ Duration minimumProgressDuration =
+ TaskMonitor::kDefaultMinimumProgressDuration,
+ QObject* parent = nullptr)
+ : Task(parent),
+ m_minimumProgressDuration(minimumProgressDuration) {
+ }
+
+ enum class ProcessNextTrackResult {
+ AbortProcessing,
+ ContinueProcessing,
+ SaveTrackAndContinueProcessing,
+ };
+
+ private slots:
+ void slotAbortTask() override {
+ m_bAborted = true;
+ }
+
+ private:
+ ModalTrackBatchProcessor(const ModalTrackBatchProcessor&) = delete;
+ ModalTrackBatchProcessor(ModalTrackBatchProcessor&&) = delete;
+
+ /// Template method to process the next available track.
+ virtual ProcessNextTrackResult doProcessNextTrack(
+ const TrackPointer& pTrack) = 0;
+
+ const Duration m_minimumProgressDuration;
+
+ bool m_bAborted;
+};
+
+/// Apply an operation on individual track pointers.
+//
+/// The operation is supposed to be applied subsequently to multiple
+/// tracks in a batch. The order of tracks should not matter. The
+/// `const` classifier in the function signature indicates that all
+/// internal state mutations should not affect the actual processing.
+//
+/// Derived classes may store results of the last invocation or any
+/// kind of internal state (i.e. for caching) in a mutable member if
+/// needed.
+class TrackPointerOperation {
+ public:
+ virtual ~TrackPointerOperation() = default;
+
+ /// Non-overridable public method.
+ ///
+ /// Future extension: Might contain pre/post-processing actions.
+ void apply(
+ const TrackPointer& pTrack) const {
+ doApply(pTrack);
+ }
+
+ private:
+ /// Overridable template method that is supposed to handle or
+ /// modify the given track object.
+ virtual void doApply(
+ const TrackPointer& pTrack) const = 0;
+};
+
+class ModalTrackBatchOperationProcessor
+ : public ModalTrackBatchProcessor {
+ Q_OBJECT
+
+ public:
+ enum class Mode {
+ /// Apply the operation. Modified track objects will
+ /// only be saved implicitly when their pointer goes
+ /// out of scope.
+ Apply,
+
+ /// Explicitly save modified track objects after
+ /// applying the operation.
+ ApplyAndSave,
+ };
+
+ /// Construct a new processing instance.
+ ///
+ /// The pointer to the actual track operation must be valid
+ /// for the whole lifetime of the created instance.
+ ModalTrackBatchOperationProcessor(
+ const TrackPointerOperation* pTrackPointerOperation,
+ Mode mode,
+ Duration minimumProgressDuration =
+ TaskMonitor::kDefaultMinimumProgressDuration,
+ QObject* parent = nullptr);
+ ~ModalTrackBatchOperationProcessor() override = default;
+
+ private:
+ ProcessNextTrackResult doProcessNextTrack(
+ const TrackPointer& pTrack) override;
+
+ const TrackPointerOperation* const m_pTrackPointerOperation;
+ const Mode m_mode;
+};
+
+} // namespace mixxx
diff --git a/src/test/analyserwaveformtest.cpp b/src/test/analyserwaveformtest.cpp
index 47f2429fbd..bab8c44892 100644
--- a/src/test/analyserwaveformtest.cpp
+++ b/src/test/analyserwaveformtest.cpp
@@ -84,29 +84,4 @@ TEST_F(AnalyzerWaveformTest, canary) {
}
}
-//Test to make sure that if an incorrect totalSamples is passed to
-//initialize(..) and process(..) is told to process more samples than that,
-//that we don't step out of bounds.
-TEST_F(AnalyzerWaveformTest, wrongTotalSamples) {
- aw.initialize(tio, tio->getSampleRate(), BIGBUF_SIZE / 2);
- // Deliver double the expected samples
- int wrongTotalSamples = BIGBUF_SIZE;
- int blockSize = 2 * 32768;
- for (int i = CANARY_SIZE; i < CANARY_SIZE + wrongTotalSamples; i += blockSize) {
- aw.processSamples(&canaryBigBuf[i], blockSize);
- }
- aw.storeResults(tio);
- aw.cleanup();
- //Ensure the source buffer is intact
- for (int i = CANARY_SIZE; i < BIGBUF_SIZE; i++) {
- EXPECT_FLOAT_EQ(canaryBigBuf[i], MAGIC_FLOAT);
- }
- //Make sure our canaries are still OK
- for (int i = 0; i < CANARY_SIZE; i++) {
- EXPECT_FLOAT_EQ(canaryBigBuf[i], CANARY_FLOAT);
- }
- for (int i = CANARY_SIZE + BIGBUF_SIZE; i < 2 * CANARY_SIZE + BIGBUF_SIZE; i++) {
- EXPECT_FLOAT_EQ(canaryBigBuf[i], CANARY_FLOAT);
- }
-}
} // namespace
diff --git a/src/track/trackiterator.h b/src/track/trackiterator.h
new file mode 100644
index 0000000000..f95c5e6efd
--- /dev/null
+++ b/src/track/trackiterator.h
@@ -0,0 +1,19 @@
+/// Utilities for iterating through a selection or collection
+/// of tracks.
+
+#pragma once
+
+#include <QModelIndex>
+
+#include "track/track.h"
+#include "util/itemiterator.h"
+
+namespace mixxx {
+
+typedef ItemIterator<TrackId> TrackIdIterator;
+typedef ListItemIterator<TrackId> TrackIdListIterator;
+
+typedef ItemIterator<TrackPointer> TrackPointerIterator;
+typedef ListItemIterator<TrackPointer> TrackPointerListIterator;
+
+} // namespace mixxx
diff --git a/src/util/itemiterator.h b/src/util/itemiterator.h
new file mode 100644
index 0000000000..5fa0825b63
--- /dev/null
+++ b/src/util/itemiterator.h
@@ -0,0 +1,82 @@
+/// Utilities for iterating over a collection or selection
+/// of multiple items.
+
+#pragma once
+
+#include <QList>
+#include <QVector>
+
+#include "util/assert.h"
+
+namespace mixxx {
+
+/// A generic iterator interface.
+///
+/// The iterator needs to be resettable to allow repeated application.
+template<typename T>
+class ItemIterator {
+ public:
+ virtual ~ItemIterator() = default;
+
+ /// Resets the iterator to the first position before starting a
+ /// new iteration.
+ ///
+ /// This operation should be invoked regardless
+ /// either if the iterator has been newly created or already
+ /// been used for an preceding iteration.
+ virtual void reset() = 0;
+
+ /// Returns a best-effort guess of the number of items that
+ /// are remaining for the iteration or std::nullopt if unknown.
+ virtual std::optional<int> estimateItemsRemaining() = 0;
+
+ /// Returns the next item or std::nullopt when done.
+ virtual std::optional<T> nextItem() = 0;
+};
+
+/// Generic class for iterating over an indexed Qt collection
+/// of known size.
+template<typename T>
+class IndexedCollectionIterator final
+ : public virtual ItemIterator<typename T::value_type> {
+ public:
+ explicit IndexedCollectionIterator(
+ const T& itemCollection)
+ : m_itemCollection(itemCollection),
+ m_nextIndex(0) {
+ }
+ ~IndexedCollectionIterator() override = default;
+
+ void reset() override {
+ m_nextIndex = 0;
+ }
+
+ std::optional<int> estimateItemsRemaining() override {
+ DEBUG_ASSERT(m_nextIndex <= m_itemCollection.size());
+ return std::make_optional(
+ m_itemCollection.size() - m_nextIndex);
+ }
+
+ std::optional<typename T::value_type> nextItem() override {
+ DEBUG_ASSERT(m_nextIndex <= m_itemCollection.size());
+ if (m_nextIndex < m_itemCollection.size()) {
+ return std::make_optional(m_itemCollection[m_nextIndex++]);
+ } else {
+ return std::nullopt;
+ }
+ }
+
+ private:
+ const T m_itemCollection;
+ int m_nextIndex;
+};
+
+/// Generic class for iterating over QList.
+template<typename T>
+using ListItemIterator = IndexedCollectionIterator<QList<T>>;
+
+/// Generic class for iterating over QVector.
+template<typename T>
+using VectorItemIterator = IndexedCollectionIterator<QVector<T>>;
+
+} // namespace mixxx
diff --git a/src/util/taskmonitor.cpp b/src/util/taskmonitor.cpp
new file mode 100644
index 0000000000..9db07b34c6
--- /dev/null
+++ b/src/util/taskmonitor.cpp
@@ -0,0 +1,189 @@
+#include "util/taskmonitor.h"
+
+#include <QCoreApplication>
+#include <QThread>
+
+#include "util/assert.h"
+#include "util/math.h"
+
+namespace mixxx {
+
+TaskMonitor::TaskMonitor(
+ const QString& labelText,
+ Duration minimumProgressDuration,
+ QObject* parent)
+ : QObject(parent),
+ m_labelText(labelText),
+ m_minimumProgressDuration(minimumProgressDuration) {
+}
+
+TaskMonitor::~TaskMonitor() {
+ VERIFY_OR_DEBUG_ASSERT(m_taskInfos.isEmpty()) {
+ // All tasks should have finished now!
+ qWarning()
+ << "Aborting"
+ << m_taskInfos.size()
+ << "pending tasks";
+ abortAllTasks();
+ }
+}
+
+Task* TaskMonitor::senderTask() const {
+ DEBUG_ASSERT(thread() == QThread::currentThread());
+ auto* pTask = static_cast<Task*>(sender());
+ DEBUG_ASSERT(pTask);
+ DEBUG_ASSERT(dynamic_cast<Task*>(sender()));
+ return pTask;
+}
+
+void TaskMonitor::slotRegisterTask(
+ const QString& title) {
+ auto* pTask = senderTask();
+ registerTask(pTask, title);
+}
+
+void TaskMonitor::slotUnregisterTask() {
+ auto* pTask = senderTask();
+ unregisterTask(pTask);
+}
+
+void TaskMonitor::slotReportTaskProgress(
+ PercentageOfCompletion estimatedPercentageOfCompletion,
+ const QString& progressMessage) {
+ auto* pTask = senderTask();
+ reportTaskProgress(
+ pTask,
+ estimatedPercentageOfCompletion,
+ progressMessage);
+}
+
+void TaskMonitor::slotCanceled() {
+ DEBUG_ASSERT(m_pProgressDlg);
+ abortAllTasks();
+}
+
+void TaskMonitor::registerTask(
+ Task* pTask,
+ const QString& title) {
+ DEBUG_ASSERT(thread() == QThread::currentThread());
+ DEBUG_ASSERT(pTask);
+ VERIFY_OR_DEBUG_ASSERT(!m_taskInfos.contains(pTask)) {
+ return;
+ }
+ auto taskInfo = TaskInfo{
+ title,
+ kPercentageOfCompletionMin,
+ QString(),
+ };
+ m_taskInfos.insert(
+ pTask,
+ std::move(taskInfo));
+ connect(
+ pTask,
+ &QObject::destroyed,
+ this,
+ &TaskMonitor::slotUnregisterTask);
+ updateProgress();
+}
+
+void TaskMonitor::unregisterTask(
+ Task* pTask) {
+ DEBUG_ASSERT(thread() == QThread::currentThread());
+ DEBUG_ASSERT(pTask);
+ if (m_taskInfos.remove(pTask) > 0) {
+ updateProgress();
+ }
+}
+
+void TaskMonitor::reportTaskProgress(
+ Task* pTask,
+ PercentageOfCompletion estimatedPercentageOfCompletion,
+ const QString& progressMessage) {
+ DEBUG_ASSERT(thread() == QThread::currentThread());
+ DEBUG_ASSERT(pTask);
+ VERIFY_OR_DEBUG_ASSERT(estimatedPercentageOfCompletion >= kPercentageOfCompletionMin) {
+ estimatedPercentageOfCompletion = kPercentageOfCompletionMin;
+ }
+ VERIFY_OR_DEBUG_ASSERT(estimatedPercentageOfCompletion <= kPercentageOfCompletionMax) {
+ estimatedPercentageOfCompletion = kPercentageOfCompletionMax;
+ }
+ if (estimatedPercentageOfCompletion == kPercentageOfCompletionMax) {
+ // Unregister immediately when finished
+ unregisterTask(pTask);
+ return;
+ }
+ const auto iTaskInfo = m_taskInfos.find(pTask);
+ if (iTaskInfo == m_taskInfos.end()) {
+ // Silently ignore (delayed?) progress signals from unregistered tasks
+ return;
+ }
+ iTaskInfo.value().estimatedPercentageOfCompletion = estimatedPercentageOfCompletion;
+ iTaskInfo.value().progressMessage = progressMessage;
+ updateProgress();
+}
+
+void TaskMonitor::abortAllTasks() {
+ for (auto* pTask : m_taskInfos.keys()) {
+ QMetaObject::invokeMethod(
+ pTask,
+#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
+ "slotAbortTask",
+ Qt::AutoConnection
+#else
+ &Task::slotAbortTask
+#endif
+ );
+ }
+ m_taskInfos.clear();
+ updateProgress();
+}
+
+void TaskMonitor::updateProgress() {
+ DEBUG_ASSERT(thread() == QThread::currentThread());
+ DEBUG_ASSERT(thread() == QCoreApplication::instance()->thread());
+ if (m_taskInfos.isEmpty()) {
+ m_pProgressDlg.reset();
+ return;
+ }
+ const auto currentProgress =
+ std::round(sumEstimatedPercentageOfCompletion());
+ if (m_pProgressDlg) {
+ m_pProgressDlg->setMaximum(
+ kPercentageOfCompletionMax * m_taskInfos.size());
+ m_pProgressDlg->setValue(
+ currentProgress);
+ } else {
+ m_pProgressDlg = std::make_unique<QProgressDialog>(
+ m_labelText,
+ tr("Abort"),
+ currentProgress,
+ kPercentageOfCompletionMax * m_taskInfos.size());
+ m_pProgressDlg->setWindowModality(
+ Qt::ApplicationModal);
+ m_pProgressDlg->setMinimumDuration(
+ m_minimumProgressDuration.toIntegerMillis());
+ }
+ // TODO: Display the title and optional progress message of each
+ // task. Maybe also the individual progress and an option to abort
+ // selected tasks.
+}
+
+PercentageOfCompletion TaskMonitor::sumEstimatedPercentageOfCompletion() const {
+ DEBUG_ASSERT(thread() == QThread::currentThread());
+ PercentageOfCompletion sumPercentageOfCompletion = kPercentageOfCompletionMin;
+ for (const auto& taskInfo : m_taskInfos) {
+ sumPercentageOfCompletion += taskInfo.estimatedPercentageOfCompletion;
+ }
+ return sumPercentageOfCompletion;
+}
+
+PercentageOfCompletion TaskMonitor::avgEstimatedPercentageOfCompletion() const {
+ DEBUG_ASSERT(thread() == QThread::currentThread());
+ if (m_taskInfos.size() > 0) {
+ return sumEstimatedPercentageOfCompletion() / m_taskInfos.size();
+ } else {
+ return kPercentageOfCompletionMin;
+ }
+}
+
+} // namespace mixxx
diff --git a/src/util/taskmonitor.h b/src/util/taskmonitor.h
new file mode 100644
index 0000000000..3f7b70ef73
--- /dev/null
+++ b/src/util/taskmonitor.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include <QMap>
+#include <QObject>
+#include <QProgressDialog>
+#include <memory>
+
+#include "util/duration.h"
+
+namespace mixxx {
+
+typedef double PercentageOfCompletion;
+
+constexpr PercentageOfCompletion kPercentageOfCompletionMin = 0.0; // not started
+constexpr PercentageOfCompletion kPercentageOfCompletionMax = 100.0; // finished
+
+class Task
+ : public QObject {
+ Q_OBJECT
+
+ public:
+ ~Task() override = default;
+
+ protected:
+ explicit Task(
+ QObject* parent = nullptr)
+ : QObject(parent) {
+ }
+
+ public slots:
+ virtual void slotAbortTask() = 0;
+};
+
+class TaskMonitor
+ : public QObject {
+ Q_OBJECT
+