diff --git a/CMakeLists.txt b/CMakeLists.txt index 06cffe66201..16c3f4c950b 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 cbc8a7a88be..4ab669f9fdd 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1048,6 +1048,9 @@ def sources(self, build): "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 @@ def sources(self, build): "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 db266a751ed..9a0ad81c47d 100644 --- a/src/library/coverartutils.cpp +++ b/src/library/coverartutils.cpp @@ -223,20 +223,6 @@ void guessTrackCoverInfoConcurrently( } } -void guessTrackCoverInfoConcurrently( - QList 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 af67e881c78..b185dcfde3c 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 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 00000000000..b198c1e9184 --- /dev/null +++ b/src/library/trackcollectioniterator.cpp @@ -0,0 +1,21 @@ +#include "library/trackcollectioniterator.h" + +#include "library/trackcollection.h" + +namespace mixxx { + +std::optional 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 00000000000..0ef2f9f4ffd --- /dev/null +++ b/src/library/trackcollectioniterator.h @@ -0,0 +1,43 @@ +/// Utilities for iterating through a selection or collection +/// of tracks. + +#pragma once + +#include + +#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 estimateItemsRemaining() override { + return m_trackIdListIter.estimateItemsRemaining(); + } + + std::optional 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 00000000000..b5a0cd4a5db --- /dev/null +++ b/src/library/trackmodeliterator.cpp @@ -0,0 +1,35 @@ +#include "library/trackmodeliterator.h" + +#include "library/trackmodel.h" + +namespace mixxx { + +std::optional 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 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 00000000000..f9eb478d777 --- /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 + +#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 estimateItemsRemaining() override { + return m_modelIndexListIter.estimateItemsRemaining(); + } + + std::optional nextItem() override; + + private: + const TrackModel* const m_pTrackModel; + ListItemIterator 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 estimateItemsRemaining() override { + return m_modelIndexListIter.estimateItemsRemaining(); + } + + std::optional nextItem() override; + + private: + const TrackModel* const m_pTrackModel; + ListItemIterator m_modelIndexListIter; +}; + +} // namespace mixxx diff --git a/src/library/trackprocessing.cpp b/src/library/trackprocessing.cpp new file mode 100644 index 00000000000..6f5c0103aee --- /dev/null +++ b/src/library/trackprocessing.cpp @@ -0,0 +1,120 @@ +#include "library/trackprocessing.h" + +#include + +#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( + 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 00000000000..1d6e20cb968 --- /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 + +#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/track/trackiterator.h b/src/track/trackiterator.h new file mode 100644 index 00000000000..f95c5e6efdd --- /dev/null +++ b/src/track/trackiterator.h @@ -0,0 +1,19 @@ +/// Utilities for iterating through a selection or collection +/// of tracks. + +#pragma once + +#include + +#include "track/track.h" +#include "util/itemiterator.h" + +namespace mixxx { + +typedef ItemIterator TrackIdIterator; +typedef ListItemIterator TrackIdListIterator; + +typedef ItemIterator TrackPointerIterator; +typedef ListItemIterator TrackPointerListIterator; + +} // namespace mixxx diff --git a/src/util/itemiterator.h b/src/util/itemiterator.h new file mode 100644 index 00000000000..5fa0825b633 --- /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 +#include + +#include "util/assert.h" + +namespace mixxx { + +/// A generic iterator interface. +/// +/// The iterator needs to be resettable to allow repeated application. +template +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 estimateItemsRemaining() = 0; + + /// Returns the next item or std::nullopt when done. + virtual std::optional nextItem() = 0; +}; + +/// Generic class for iterating over an indexed Qt collection +/// of known size. +template +class IndexedCollectionIterator final + : public virtual ItemIterator { + public: + explicit IndexedCollectionIterator( + const T& itemCollection) + : m_itemCollection(itemCollection), + m_nextIndex(0) { + } + ~IndexedCollectionIterator() override = default; + + void reset() override { + m_nextIndex = 0; + } + + std::optional estimateItemsRemaining() override { + DEBUG_ASSERT(m_nextIndex <= m_itemCollection.size()); + return std::make_optional( + m_itemCollection.size() - m_nextIndex); + } + + std::optional 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 +using ListItemIterator = IndexedCollectionIterator>; + +/// Generic class for iterating over QVector. +template +using VectorItemIterator = IndexedCollectionIterator>; + +} // namespace mixxx diff --git a/src/util/taskmonitor.cpp b/src/util/taskmonitor.cpp new file mode 100644 index 00000000000..9db07b34c6a --- /dev/null +++ b/src/util/taskmonitor.cpp @@ -0,0 +1,189 @@ +#include "util/taskmonitor.h" + +#include +#include + +#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(sender()); + DEBUG_ASSERT(pTask); + DEBUG_ASSERT(dynamic_cast(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( + 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 00000000000..3f7b70ef731 --- /dev/null +++ b/src/util/taskmonitor.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include + +#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 + + public: + /// Default minimum duration that needs to pass before showing + /// the progress dialog. + /// + /// For short tasks no progress dialog is shown. Only if their + /// processing time exceeds the minimum duration the modal + /// progress dialog is shown. As a side-effect this closes + /// any open context menu. We want to prevent this distraction + /// whenever possible while still providing progress feedback + /// for long running tasks. + static constexpr Duration kDefaultMinimumProgressDuration = + Duration::fromMillis(2000); + + explicit TaskMonitor( + const QString& labelText, + Duration minimumProgressDuration = kDefaultMinimumProgressDuration, + QObject* parent = nullptr); + ~TaskMonitor() override; + + void registerTask( + Task* pTask, + const QString& title = QString()); + void unregisterTask( + Task* pTask); + + void reportTaskProgress( + Task* pTask, + PercentageOfCompletion estimatedPercentageOfCompletion, + const QString& progressMessage = QString()); + + void abortAllTasks(); + + PercentageOfCompletion sumEstimatedPercentageOfCompletion() const; + PercentageOfCompletion avgEstimatedPercentageOfCompletion() const; + + public slots: + void slotRegisterTask( + const QString& title); + void slotUnregisterTask(); + + void slotReportTaskProgress( + PercentageOfCompletion estimatedPercentageOfCompletion, + const QString& progressMessage); + + private slots: + void slotCanceled(); + + private: + Task* senderTask() const; + void updateProgress(); + + const QString m_labelText; + const Duration m_minimumProgressDuration; + + struct TaskInfo { + QString title; + PercentageOfCompletion estimatedPercentageOfCompletion; + QString progressMessage; + }; + QMap m_taskInfos; + + std::unique_ptr m_pProgressDlg; +}; + +} // namespace mixxx diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index 3b5206a9584..bd5f21784af 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -19,6 +19,8 @@ #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" #include "library/trackmodel.h" +#include "library/trackmodeliterator.h" +#include "library/trackprocessing.h" #include "mixer/playermanager.h" #include "preferences/colorpalettesettings.h" #include "sources/soundsourceproxy.h" @@ -185,19 +187,31 @@ void WTrackMenu::createActions() { } if (featureIsEnabled(Feature::Metadata)) { - m_pImportMetadataFromFileAct = new QAction(tr("Import From File Tags"), m_pMetadataMenu); - connect(m_pImportMetadataFromFileAct, &QAction::triggered, this, &WTrackMenu::slotImportTrackMetadataFromFileTags); + m_pImportMetadataFromFileAct = + new QAction(tr("Import From File Tags"), m_pMetadataMenu); + connect(m_pImportMetadataFromFileAct, + &QAction::triggered, + this, + &WTrackMenu::slotImportMetadataFromFileTags); + + m_pImportMetadataFromMusicBrainzAct = + new QAction(tr("Import From MusicBrainz"), m_pMetadataMenu); + connect(m_pImportMetadataFromMusicBrainzAct, + &QAction::triggered, + this, + &WTrackMenu::slotShowDlgTagFetcher); - m_pImportMetadataFromMusicBrainzAct = new QAction(tr("Import From MusicBrainz"), m_pMetadataMenu); - connect(m_pImportMetadataFromMusicBrainzAct, &QAction::triggered, this, &WTrackMenu::slotShowDlgTagFetcher); + m_pExportMetadataAct = + new QAction(tr("Export To File Tags"), m_pMetadataMenu); + connect(m_pExportMetadataAct, + &QAction::triggered, + this, + &WTrackMenu::slotExportMetadataIntoFileTags); // Give a nullptr parent because otherwise it inherits our style which can // make it unreadable. Bug #673411 m_pTagFetcher.reset(new DlgTagFetcher(nullptr, m_pTrackModel)); - m_pExportMetadataAct = new QAction(tr("Export To File Tags"), m_pMetadataMenu); - connect(m_pExportMetadataAct, &QAction::triggered, this, &WTrackMenu::slotExportTrackMetadataIntoFileTags); - for (const auto& externalTrackCollection : m_pTrackCollectionManager->externalCollections()) { UpdateExternalTrackCollection updateInExternalTrackCollection; updateInExternalTrackCollection.externalTrackCollection = externalTrackCollection; @@ -755,23 +769,6 @@ QList WTrackMenu::getTrackRefs() const { return trackRefs; } -TrackPointerList WTrackMenu::getTrackPointers() const { - if (!m_pTrackModel) { - return m_trackPointerList; - } - TrackPointerList trackPointers; - trackPointers.reserve(m_trackIndexList.size()); - for (const auto& index : m_trackIndexList) { - const auto pTrack = m_pTrackModel->getTrack(index); - if (!pTrack) { - // Skip unavailable tracks - continue; - } - trackPointers.push_back(pTrack); - } - return trackPointers; -} - TrackPointer WTrackMenu::getFirstTrackPointer() const { if (m_pTrackModel) { for (const auto& index : m_trackIndexList) { @@ -791,6 +788,41 @@ TrackPointer WTrackMenu::getFirstTrackPointer() const { } } +std::unique_ptr WTrackMenu::newTrackPointerIterator() const { + if (m_pTrackModel) { + if (m_trackIndexList.isEmpty()) { + return nullptr; + } + return std::make_unique( + m_pTrackModel, + m_trackIndexList); + } else { + if (m_trackPointerList.isEmpty()) { + return nullptr; + } + return std::make_unique( + m_trackPointerList); + } +} + +int WTrackMenu::applyTrackPointerOperation( + const QString& progressLabelText, + const mixxx::TrackPointerOperation* pTrackPointerOperation, + mixxx::ModalTrackBatchOperationProcessor::Mode operationMode) const { + const auto pTrackPointerIter = newTrackPointerIterator(); + if (!pTrackPointerIter) { + // Empty, i.e. nothing to do + return 0; + } + mixxx::ModalTrackBatchOperationProcessor modalOperation( + pTrackPointerOperation, + operationMode); + return modalOperation.processTracks( + progressLabelText, + m_pTrackCollectionManager, + pTrackPointerIter.get()); +} + const QModelIndexList& WTrackMenu::getTrackIndices() const { // Indices are associated with a TrackModel. Can only be obtained // if a TrackModel is available. @@ -808,27 +840,64 @@ void WTrackMenu::slotOpenInFileBrowser() { mixxx::DesktopHelper::openInFileBrowser(locations); } -void WTrackMenu::slotImportTrackMetadataFromFileTags() { - for (const auto& pTrack : getTrackPointers()) { +namespace { + +class ImportMetadataFromFileTagsTrackPointerOperation : public mixxx::TrackPointerOperation { + private: + void doApply( + const TrackPointer& pTrack) const override { // The user has explicitly requested to reload metadata from the file // to override the information within Mixxx! Custom cover art must be // reloaded separately. SoundSourceProxy(pTrack).updateTrackFromSource( SoundSourceProxy::ImportTrackMetadataMode::Again); } +}; + +} // anonymous namespace + +void WTrackMenu::slotImportMetadataFromFileTags() { + const auto progressLabelText = + tr("Importing metadata of %n track(s) from file tags", "", getTrackCount()); + const auto trackOperator = + ImportMetadataFromFileTagsTrackPointerOperation(); + applyTrackPointerOperation( + progressLabelText, + &trackOperator, + // Update the database to reflect the recent changes. This is + // crucial for additional metadata like custom tags that are + // directly fetched from the database for certain use cases! + mixxx::ModalTrackBatchOperationProcessor::Mode::ApplyAndSave); } -void WTrackMenu::slotExportTrackMetadataIntoFileTags() { - mixxx::DlgTrackMetadataExport::showMessageBoxOncePerSession(); +namespace { - for (const auto& pTrack : getTrackPointers()) { - // Export of metadata is deferred until all references to the - // corresponding track object have been dropped. Otherwise - // writing to files that are still used for playback might - // cause crashes or at least audible glitches! - mixxx::DlgTrackMetadataExport::showMessageBoxOncePerSession(); +class ExportMetadataIntoFileTagsTrackPointerOperation : public mixxx::TrackPointerOperation { + private: + void doApply( + const TrackPointer& pTrack) const override { pTrack->markForMetadataExport(); } +}; + +} // anonymous namespace + +void WTrackMenu::slotExportMetadataIntoFileTags() { + // Export of metadata is deferred until all references to the + // corresponding track object have been dropped. Otherwise + // writing to files that are still used for playback might + // cause crashes or at least audible glitches! + mixxx::DlgTrackMetadataExport::showMessageBoxOncePerSession(); + + const auto progressLabelText = + tr("Marking metadata of %n track(s) to be exported into file tags", + "", + getTrackCount()); + const auto trackOperator = + ExportMetadataIntoFileTagsTrackPointerOperation(); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } void WTrackMenu::slotUpdateExternalTrackCollection( @@ -1045,32 +1114,101 @@ void WTrackMenu::slotUnlockBpm() { lockBpm(false); } -void WTrackMenu::slotScaleBpm(int scale) { - for (const auto& pTrack : getTrackPointers()) { +namespace { + +class ScaleBpmTrackPointerOperation : public mixxx::TrackPointerOperation { + public: + explicit ScaleBpmTrackPointerOperation(mixxx::Beats::BPMScale bpmScale) + : m_bpmScale(bpmScale) { + } + + private: + void doApply( + const TrackPointer& pTrack) const override { if (pTrack->isBpmLocked()) { - continue; + return; } mixxx::BeatsPointer pBeats = pTrack->getBeats(); if (!pBeats) { - continue; + return; } - pBeats->scale(static_cast(scale)); + pBeats->scale(m_bpmScale); } + + const mixxx::Beats::BPMScale m_bpmScale; +}; + +} // anonymous namespace + +void WTrackMenu::slotScaleBpm(int scale) { + const auto progressLabelText = + tr("Scaling BPM of %n track(s)", "", getTrackCount()); + const auto trackOperator = + ScaleBpmTrackPointerOperation( + static_cast(scale)); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } -void WTrackMenu::lockBpm(bool lock) { - // TODO: This should be done in a thread for large selections - for (const auto& pTrack : getTrackPointers()) { - pTrack->setBpmLocked(lock); +namespace { + +class LockBpmTrackPointerOperation : public mixxx::TrackPointerOperation { + public: + explicit LockBpmTrackPointerOperation(bool lock) + : m_lock(lock) { + } + + private: + void doApply( + const TrackPointer& pTrack) const override { + pTrack->setBpmLocked(m_lock); } + + const bool m_lock; +}; + +} // anonymous namespace + +void WTrackMenu::lockBpm(bool lock) { + const auto progressLabelText = lock + ? tr("Locking BPM of %n track(s)", "", getTrackCount()) + : tr("Unlocking BPM of %n track(s)", "", getTrackCount()); + const auto trackOperator = + LockBpmTrackPointerOperation(lock); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } -void WTrackMenu::slotColorPicked(mixxx::RgbColor::optional_t color) { - // TODO: This should be done in a thread for large selections - for (const auto& pTrack : getTrackPointers()) { - pTrack->setColor(color); +namespace { + +class SetColorTrackPointerOperation : public mixxx::TrackPointerOperation { + public: + explicit SetColorTrackPointerOperation(mixxx::RgbColor::optional_t color) + : m_color(color) { + } + + private: + void doApply( + const TrackPointer& pTrack) const override { + pTrack->setColor(m_color); } + const mixxx::RgbColor::optional_t m_color; +}; + +} // anonymous namespace + +void WTrackMenu::slotColorPicked(mixxx::RgbColor::optional_t color) { + const auto progressLabelText = + tr("Setting color of %n track(s)", "", getTrackCount()); + const auto trackOperator = + SetColorTrackPointerOperation(color); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); + hide(); } @@ -1097,85 +1235,250 @@ void WTrackMenu::loadSelectionToGroup(QString group, bool play) { emit loadTrackToPlayer(pTrack, group, play); } -//slot for reset played count, sets count to 0 of one or more tracks -void WTrackMenu::slotClearPlayCount() { - for (const auto& pTrack : getTrackPointers()) { +namespace { + +class ResetPlayCounterTrackPointerOperation : public mixxx::TrackPointerOperation { + private: + void doApply( + const TrackPointer& pTrack) const override { pTrack->resetPlayCounter(); } +}; + +} // anonymous namespace + +//slot for reset played count, sets count to 0 of one or more tracks +void WTrackMenu::slotClearPlayCount() { + const auto progressLabelText = + tr("Resetting play count of %n track(s)", "", getTrackCount()); + const auto trackOperator = + ResetPlayCounterTrackPointerOperation(); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } -void WTrackMenu::slotClearBeats() { - // TODO: This should be done in a thread for large selections - for (const auto& pTrack : getTrackPointers()) { +namespace { + +class ResetBeatsTrackPointerOperation : public mixxx::TrackPointerOperation { + private: + void doApply( + const TrackPointer& pTrack) const override { if (pTrack->isBpmLocked()) { - continue; + return; } pTrack->setBeats(mixxx::BeatsPointer()); } +}; + +} // anonymous namespace + +void WTrackMenu::slotClearBeats() { + const auto progressLabelText = + tr("Resetting beats of %n track(s)", "", getTrackCount()); + const auto trackOperator = + ResetBeatsTrackPointerOperation(); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } -void WTrackMenu::slotClearMainCue() { - for (const auto& pTrack : getTrackPointers()) { - pTrack->removeCuesOfType(mixxx::CueType::MainCue); +namespace { + +class RemoveCuesOfTypeTrackPointerOperation : public mixxx::TrackPointerOperation { + public: + explicit RemoveCuesOfTypeTrackPointerOperation(mixxx::CueType cueType) + : m_cueType(cueType) { } + + private: + void doApply( + const TrackPointer& pTrack) const override { + pTrack->removeCuesOfType(m_cueType); + } + + const mixxx::CueType m_cueType; +}; + +} // anonymous namespace + +void WTrackMenu::slotClearMainCue() { + const auto progressLabelText = + tr("Removing main cue from %n track(s)", "", getTrackCount()); + const auto trackOperator = + RemoveCuesOfTypeTrackPointerOperation(mixxx::CueType::MainCue); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } void WTrackMenu::slotClearOutroCue() { - for (const auto& pTrack : getTrackPointers()) { - pTrack->removeCuesOfType(mixxx::CueType::Outro); - } + const auto progressLabelText = + tr("Removing outro cue from %n track(s)", "", getTrackCount()); + const auto trackOperator = + RemoveCuesOfTypeTrackPointerOperation(mixxx::CueType::Outro); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } void WTrackMenu::slotClearIntroCue() { - for (const auto& pTrack : getTrackPointers()) { - pTrack->removeCuesOfType(mixxx::CueType::Intro); - } + const auto progressLabelText = + tr("Removing intro cue from %n track(s)", "", getTrackCount()); + const auto trackOperator = + RemoveCuesOfTypeTrackPointerOperation(mixxx::CueType::Intro); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } -void WTrackMenu::slotClearKey() { - for (const auto& pTrack : getTrackPointers()) { +void WTrackMenu::slotClearLoop() { + const auto progressLabelText = + tr("Removing loop cues from %n track(s)", "", getTrackCount()); + const auto trackOperator = + RemoveCuesOfTypeTrackPointerOperation(mixxx::CueType::Loop); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); +} + +void WTrackMenu::slotClearHotCues() { + const auto progressLabelText = + tr("Removing hot cues from %n track(s)", "", getTrackCount()); + const auto trackOperator = + RemoveCuesOfTypeTrackPointerOperation(mixxx::CueType::HotCue); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); +} + +namespace { + +class ResetKeysTrackPointerOperation : public mixxx::TrackPointerOperation { + private: + void doApply( + const TrackPointer& pTrack) const override { pTrack->resetKeys(); } +}; + +} // anonymous namespace + +void WTrackMenu::slotClearKey() { + const auto progressLabelText = + tr("Resetting keys of %n track(s)", "", getTrackCount()); + const auto trackOperator = + ResetKeysTrackPointerOperation(); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } -void WTrackMenu::slotClearReplayGain() { - for (const auto& pTrack : getTrackPointers()) { +namespace { + +class ResetReplayGainTrackPointerOperation : public mixxx::TrackPointerOperation { + private: + void doApply( + const TrackPointer& pTrack) const override { pTrack->setReplayGain(mixxx::ReplayGain()); } +}; + +} // anonymous namespace + +void WTrackMenu::slotClearReplayGain() { + const auto progressLabelText = + tr("Resetting replay gain of %n track(s)", "", getTrackCount()); + const auto trackOperator = + ResetReplayGainTrackPointerOperation(); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } -void WTrackMenu::slotClearWaveform() { - AnalysisDao& analysisDao = m_pTrackCollectionManager->internalCollection()->getAnalysisDAO(); - for (const auto& pTrack : getTrackPointers()) { - analysisDao.deleteAnalysesForTrack(pTrack->getId()); +namespace { + +class ResetWaveformTrackPointerOperation : public mixxx::TrackPointerOperation { + public: + explicit ResetWaveformTrackPointerOperation(AnalysisDao& analysisDao) + : m_analysisDao(analysisDao) { + } + + private: + void doApply( + const TrackPointer& pTrack) const override { + m_analysisDao.deleteAnalysesForTrack(pTrack->getId()); pTrack->setWaveform(WaveformPointer()); pTrack->setWaveformSummary(WaveformPointer()); } -} -void WTrackMenu::slotClearLoop() { - for (const auto& pTrack : getTrackPointers()) { - pTrack->removeCuesOfType(mixxx::CueType::Loop); - } -} + AnalysisDao& m_analysisDao; +}; -void WTrackMenu::slotClearHotCues() { - for (const auto& pTrack : getTrackPointers()) { - pTrack->removeCuesOfType(mixxx::CueType::HotCue); - } +} // anonymous namespace + +void WTrackMenu::slotClearWaveform() { + const auto progressLabelText = + tr("Resetting waveform of %n track(s)", "", getTrackCount()); + const auto trackOperator = + ResetReplayGainTrackPointerOperation(); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } +namespace { + +class ClearAllPerformanceMetadataTrackPointerOperation : public mixxx::TrackPointerOperation { + public: + explicit ClearAllPerformanceMetadataTrackPointerOperation(AnalysisDao& analysisDao) + : m_removeMainCue(mixxx::CueType::MainCue), + m_removeIntroCue(mixxx::CueType::Intro), + m_removeOutroCue(mixxx::CueType::Outro), + m_removeHotCues(mixxx::CueType::HotCue), + m_removeLoopCues(mixxx::CueType::Loop), + m_resetWaveform(analysisDao) { + } + + private: + void doApply( + const TrackPointer& pTrack) const override { + m_resetBeats.apply(pTrack); + m_resetPlayCounter.apply(pTrack); + m_removeMainCue.apply(pTrack); + m_removeIntroCue.apply(pTrack); + m_removeOutroCue.apply(pTrack); + m_removeHotCues.apply(pTrack); + m_removeLoopCues.apply(pTrack); + m_resetKeys.apply(pTrack); + m_resetReplayGain.apply(pTrack); + m_resetWaveform.apply(pTrack); + } + + const ResetBeatsTrackPointerOperation m_resetBeats; + const ResetPlayCounterTrackPointerOperation m_resetPlayCounter; + const RemoveCuesOfTypeTrackPointerOperation m_removeMainCue; + const RemoveCuesOfTypeTrackPointerOperation m_removeIntroCue; + const RemoveCuesOfTypeTrackPointerOperation m_removeOutroCue; + const RemoveCuesOfTypeTrackPointerOperation m_removeHotCues; + const RemoveCuesOfTypeTrackPointerOperation m_removeLoopCues; + const ResetKeysTrackPointerOperation m_resetKeys; + const ResetReplayGainTrackPointerOperation m_resetReplayGain; + const ResetWaveformTrackPointerOperation m_resetWaveform; +}; + +} // anonymous namespace + void WTrackMenu::slotClearAllMetadata() { - slotClearBeats(); - slotClearPlayCount(); - slotClearMainCue(); - slotClearHotCues(); - slotClearIntroCue(); - slotClearOutroCue(); - slotClearLoop(); - slotClearKey(); - slotClearReplayGain(); - slotClearWaveform(); + const auto progressLabelText = + tr("Resetting all performance metadata of %n track(s)", "", getTrackCount()); + AnalysisDao& analysisDao = + m_pTrackCollectionManager->internalCollection()->getAnalysisDAO(); + const auto trackOperator = + ClearAllPerformanceMetadataTrackPointerOperation(analysisDao); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } void WTrackMenu::slotShowTrackInfo() { @@ -1231,14 +1534,57 @@ void WTrackMenu::addToAutoDJ(PlaylistDAO::AutoDJSendLoc loc) { playlistDao.addTracksToAutoDJQueue(trackIds, loc); } -void WTrackMenu::slotCoverInfoSelected(const CoverInfoRelative& coverInfo) { - for (const auto& pTrack : getTrackPointers()) { - pTrack->setCoverInfo(coverInfo); +namespace { + +class SetCoverInfoTrackPointerOperation : public mixxx::TrackPointerOperation { + public: + explicit SetCoverInfoTrackPointerOperation(CoverInfoRelative&& coverInfo) + : m_coverInfo(std::move(coverInfo)) { + } + + private: + void doApply( + const TrackPointer& pTrack) const override { + pTrack->setCoverInfo(m_coverInfo); } + + const CoverInfoRelative m_coverInfo; +}; + +} // anonymous namespace + +void WTrackMenu::slotCoverInfoSelected(CoverInfoRelative coverInfo) { + const auto progressLabelText = + tr("Setting cover art of %n track(s)", "", getTrackCount()); + const auto trackOperator = + SetCoverInfoTrackPointerOperation(std::move(coverInfo)); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } +namespace { + +class ReloadCoverInfoTrackPointerOperation : public mixxx::TrackPointerOperation { + private: + void doApply( + const TrackPointer& pTrack) const override { + m_coverInfoGuesser.guessAndSetCoverInfoForTrack(*pTrack); + } + + mutable CoverInfoGuesser m_coverInfoGuesser; +}; + +} // anonymous namespace + void WTrackMenu::slotReloadCoverArt() { - guessTrackCoverInfoConcurrently(getTrackPointers()); + const auto progressLabelText = + tr("Reloading cover art of %n track(s)", "", getTrackCount()); + const auto trackOperator = + ReloadCoverInfoTrackPointerOperation(); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); } void WTrackMenu::slotRemove() { diff --git a/src/widget/wtrackmenu.h b/src/widget/wtrackmenu.h index d7ef5341590..af760a153a1 100644 --- a/src/widget/wtrackmenu.h +++ b/src/widget/wtrackmenu.h @@ -3,10 +3,11 @@ #include #include #include +#include #include "library/dao/playlistdao.h" +#include "library/trackprocessing.h" #include "preferences/usersettings.h" -#include "track/track.h" #include "track/trackref.h" class ControlProxy; @@ -98,8 +99,8 @@ class WTrackMenu : public QMenu { // Info and metadata void slotShowTrackInfo(); void slotShowDlgTagFetcher(); - void slotImportTrackMetadataFromFileTags(); - void slotExportTrackMetadataIntoFileTags(); + void slotImportMetadataFromFileTags(); + void slotExportMetadataIntoFileTags(); void slotUpdateExternalTrackCollection(ExternalTrackCollection* externalTrackCollection); // Playlist and crate @@ -113,7 +114,7 @@ class WTrackMenu : public QMenu { void slotAddToAutoDJReplace(); // Cover - void slotCoverInfoSelected(const CoverInfoRelative& coverInfo); + void slotCoverInfoSelected(CoverInfoRelative coverInfo); void slotReloadCoverArt(); // Library management @@ -130,13 +131,16 @@ class WTrackMenu : public QMenu { TrackIdList getTrackIds() const; QList getTrackRefs() const; - // TODO: This function desperately needs to be replaced - // by an iterator pattern that loads (and drops) tracks - // lazily one-by-one during the traversal!! - TrackPointerList getTrackPointers() const; - TrackPointer getFirstTrackPointer() const; + std::unique_ptr newTrackPointerIterator() const; + + int applyTrackPointerOperation( + const QString& progressLabelText, + const mixxx::TrackPointerOperation* pTrackPointerOperation, + mixxx::ModalTrackBatchOperationProcessor::Mode operationMode = + mixxx::ModalTrackBatchOperationProcessor::Mode::Apply) const; + bool isEmpty() const { return getTrackCount() == 0; }