From d1d9008ffeb6eb252cb10dafa83257adcb80845c Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 30 Nov 2025 19:05:00 +1000 Subject: [PATCH] Qt: Add QtAsyncTaskWithProgress --- src/duckstation-qt/qtprogresscallback.cpp | 100 ++++++++++++++++++++++ src/duckstation-qt/qtprogresscallback.h | 46 ++++++++++ 2 files changed, 146 insertions(+) diff --git a/src/duckstation-qt/qtprogresscallback.cpp b/src/duckstation-qt/qtprogresscallback.cpp index 8125c33c7..01aae01ec 100644 --- a/src/duckstation-qt/qtprogresscallback.cpp +++ b/src/duckstation-qt/qtprogresscallback.cpp @@ -77,6 +77,106 @@ void QtProgressCallback::connectWidgets(QLabel* const status_label, QProgressBar } } +QtAsyncTaskWithProgress::QtAsyncTaskWithProgress() : QObject() +{ +} + +QtAsyncTaskWithProgress::~QtAsyncTaskWithProgress() = default; + +bool QtAsyncTaskWithProgress::IsCancelled() const +{ + return m_ts_cancelled.load(std::memory_order_acquire); +} + +void QtAsyncTaskWithProgress::SetTitle(const std::string_view title) +{ + emit titleUpdated(QtUtils::StringViewToQString(title)); +} + +void QtAsyncTaskWithProgress::SetStatusText(const std::string_view text) +{ + ProgressCallback::SetStatusText(text); + emit statusTextUpdated(QtUtils::StringViewToQString(text)); +} + +void QtAsyncTaskWithProgress::SetProgressRange(u32 range) +{ + const u32 prev_range = m_progress_range; + ProgressCallback::SetProgressRange(range); + if (m_progress_range == prev_range) + return; + + emit progressRangeUpdated(0, static_cast(m_progress_range)); +} + +void QtAsyncTaskWithProgress::SetProgressValue(u32 value) +{ + const u32 prev_value = m_progress_value; + ProgressCallback::SetProgressValue(value); + if (m_progress_value == prev_value) + return; + + emit progressValueUpdated(static_cast(m_progress_value)); +} + +void QtAsyncTaskWithProgress::connectWidgets(QLabel* const status_label, QProgressBar* const progress_bar, + QAbstractButton* const cancel_button) +{ + if (status_label) + connect(this, &QtAsyncTaskWithProgress::statusTextUpdated, status_label, &QLabel::setText); + if (progress_bar) + { + connect(this, &QtAsyncTaskWithProgress::progressRangeUpdated, progress_bar, &QProgressBar::setRange); + connect(this, &QtAsyncTaskWithProgress::progressValueUpdated, progress_bar, &QProgressBar::setValue); + } + if (cancel_button) + { + // force direct connection so it executes on the calling thread + connect( + cancel_button, &QAbstractButton::clicked, this, + [this]() { m_ts_cancelled.store(true, std::memory_order_release); }, Qt::DirectConnection); + } +} + +QtAsyncTaskWithProgress* QtAsyncTaskWithProgress::create(QWidget* const callback_parent, WorkCallback callback) +{ + QtAsyncTaskWithProgress* self = new QtAsyncTaskWithProgress(); + self->m_callback = std::move(callback); + + connect(self, &QtAsyncTaskWithProgress::completed, callback_parent, [self]() { + const CompletionCallback& cb = std::get(self->m_callback); + if (cb) + cb(); + }); + + return self; +} + +void QtAsyncTaskWithProgress::start() +{ + // Disconnect from the calling thread, so it can be pulled by the async task. + moveToThread(nullptr); + + System::QueueAsyncTask([this]() mutable { + QThread* const worker_thread = QThread::currentThread(); + moveToThread(worker_thread); + + m_callback = std::get(m_callback)(this); + moveToThread(nullptr); + + Host::RunOnUIThread([self = this]() { + self->moveToThread(QThread::currentThread()); + emit self->completed(); + delete self; + }); + }); +} + +void QtAsyncTaskWithProgress::cancel() +{ + m_ts_cancelled.store(true, std::memory_order_release); +} + QtAsyncTaskWithProgressDialog::QtAsyncTaskWithProgressDialog(const QString& initial_title, const QString& initial_status_text, bool cancellable, int range, int value, float show_delay, diff --git a/src/duckstation-qt/qtprogresscallback.h b/src/duckstation-qt/qtprogresscallback.h index 5d13ec8be..a8df39925 100644 --- a/src/duckstation-qt/qtprogresscallback.h +++ b/src/duckstation-qt/qtprogresscallback.h @@ -42,6 +42,52 @@ private: std::atomic_bool m_ts_cancelled{false}; }; +class QtAsyncTaskWithProgress final : public QObject, private ProgressCallback +{ + Q_OBJECT + +public: + using CompletionCallback = std::function; + using WorkCallback = std::function; + + ~QtAsyncTaskWithProgress() override; + + /// Creates a task, the task is still in the pending state until start() is called. + static QtAsyncTaskWithProgress* create(QWidget* const callback_parent, WorkCallback callback); + + /// Starts the task asynchronously. The pointer is only guaranteed to be valid until the + /// completion callback has executed. + void start(); + + /// Cancel the task asynchronously. If the callback_parent that was set in start() is no + /// longer valid, the completion handler will not execute. + void cancel(); + + /// Connects progress updates to a typical set of widgets. + void connectWidgets(QLabel* const status_label, QProgressBar* const progress_bar, + QAbstractButton* const cancel_button); + +Q_SIGNALS: + void titleUpdated(const QString& title); + void statusTextUpdated(const QString& status); + void progressRangeUpdated(int min, int max); + void progressValueUpdated(int value); + void completed(); + +protected: + bool IsCancelled() const override; + void SetTitle(const std::string_view title) override; + void SetStatusText(const std::string_view text) override; + void SetProgressRange(u32 range) override; + void SetProgressValue(u32 value) override; + +private: + QtAsyncTaskWithProgress(); + + std::variant m_callback; + std::atomic_bool m_ts_cancelled{false}; +}; + class QtAsyncTaskWithProgressDialog final : public QObject, private ProgressCallback { Q_OBJECT