diff options
-rw-r--r-- | CMakeLists.txt | 4 | ||||
-rw-r--r-- | build/depends.py | 4 | ||||
-rw-r--r-- | src/library/coverartutils.cpp | 14 | ||||
-rw-r--r-- | src/library/coverartutils.h | 1 | ||||
-rw-r--r-- | src/library/trackcollectioniterator.cpp | 21 | ||||
-rw-r--r-- | src/library/trackcollectioniterator.h | 43 | ||||
-rw-r--r-- | src/library/trackmodeliterator.cpp | 35 | ||||
-rw-r--r-- | src/library/trackmodeliterator.h | 72 | ||||
-rw-r--r-- | src/library/trackprocessing.cpp | 120 | ||||
-rw-r--r-- | src/library/trackprocessing.h | 135 | ||||
-rw-r--r-- | src/test/analyserwaveformtest.cpp | 25 | ||||
-rw-r--r-- | src/track/trackiterator.h | 19 | ||||
-rw-r--r-- | src/util/itemiterator.h | 82 | ||||
-rw-r--r-- | src/util/taskmonitor.cpp | 189 | ||||
-rw-r--r-- | src/util/taskmonitor.h | 102 | ||||
-rw-r--r-- | src/waveform/widgets/glslwaveformwidget.cpp | 6 | ||||
-rw-r--r-- | src/waveform/widgets/glslwaveformwidget.h | 8 | ||||
-rw-r--r-- | src/widget/wtrackmenu.cpp | 536 | ||||
-rw-r--r-- | src/widget/wtrackmenu.h | 22 |
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 + |