Qt: Add QtAsyncTaskWithProgress class

Async work item with a progress dialog that doesn't require a nested
event loop.
This commit is contained in:
Stenzek
2025-11-22 23:55:21 +10:00
parent 82a44b19bf
commit 3f882cf254
2 changed files with 305 additions and 0 deletions

View File

@@ -8,6 +8,10 @@
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtWidgets/QLabel>
#include <QtWidgets/QProgressBar>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QVBoxLayout>
#include <array>
#include "moc_qtprogresscallback.cpp"
@@ -174,3 +178,226 @@ QWidget* QtAsyncProgressThread::parentWidget() const
{
return qobject_cast<QWidget*>(parent());
}
QtAsyncTaskWithProgress::QtAsyncTaskWithProgress(const QString& initial_title, const QString& initial_status_text,
bool cancellable, int range, int value, float show_delay,
QWidget* dialog_parent, WorkCallback callback)
: m_callback(std::move(callback)), m_show_delay(show_delay)
{
m_dialog = new ProgressDialog(initial_title, initial_status_text, cancellable, range, value, *this, dialog_parent);
if (show_delay <= 0.0f)
{
m_shown = true;
m_dialog->open();
}
}
QtAsyncTaskWithProgress::~QtAsyncTaskWithProgress()
{
if (m_dialog)
{
// should null out itself
delete m_dialog;
DebugAssert(!m_dialog);
}
}
QtAsyncTaskWithProgress::ProgressDialog::ProgressDialog(const QString& initial_title,
const QString& initial_status_text, bool cancellable, int range,
int value, QtAsyncTaskWithProgress& task, QWidget* parent)
: QDialog(parent), m_task(task)
{
if (!initial_title.isEmpty())
setWindowTitle(initial_title);
else
setWindowTitle(QStringLiteral("DuckStation"));
setWindowFlag(Qt::CustomizeWindowHint, true);
setWindowFlag(Qt::WindowCloseButtonHint, cancellable);
setWindowModality(Qt::WindowModal);
setMinimumSize(MINIMUM_WIDTH, cancellable ? MINIMUM_HEIGHT_WITH_CANCEL : MINIMUM_HEIGHT_WITHOUT_CANCEL);
m_progress_bar = new QProgressBar(this);
m_progress_bar->setRange(0, range);
m_progress_bar->setValue(value);
m_status_label = new QLabel(this);
m_status_label->setAlignment(Qt::AlignCenter);
if (!initial_status_text.isEmpty())
m_status_label->setText(initial_status_text);
m_button_box = new QDialogButtonBox(QDialogButtonBox::Cancel, this);
connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::close);
m_button_box->setVisible(cancellable);
QVBoxLayout* const layout = new QVBoxLayout(this);
layout->setSpacing(8);
layout->addWidget(m_status_label);
layout->addWidget(m_progress_bar);
layout->addWidget(m_button_box);
}
QtAsyncTaskWithProgress::ProgressDialog::~ProgressDialog()
{
DebugAssert(m_task.m_dialog == this);
m_task.m_dialog = nullptr;
}
void QtAsyncTaskWithProgress::ProgressDialog::setCancellable(bool cancellable)
{
if (cancellable == m_button_box->isVisible())
return;
setWindowFlag(Qt::WindowCloseButtonHint, cancellable);
setMinimumHeight(cancellable ? MINIMUM_HEIGHT_WITH_CANCEL : MINIMUM_HEIGHT_WITHOUT_CANCEL);
m_button_box->setVisible(cancellable);
}
void QtAsyncTaskWithProgress::ProgressDialog::closeEvent(QCloseEvent* event)
{
cancelled();
QDialog::closeEvent(event);
}
void QtAsyncTaskWithProgress::ProgressDialog::cancelled()
{
m_task.m_ts_cancelled.store(true, std::memory_order_release);
}
void QtAsyncTaskWithProgress::create(QWidget* parent, std::string_view initial_title,
std::string_view initial_status_text, bool cancellable, int range, int value,
float show_delay, WorkCallback callback)
{
DebugAssert(parent);
// NOTE: Must get connected before queuing, because otherwise you risk a race.
QtAsyncTaskWithProgress* task = new QtAsyncTaskWithProgress(
QtUtils::StringViewToQString(initial_title), QtUtils::StringViewToQString(initial_status_text), cancellable, range,
value, show_delay, parent, std::move(callback));
connect(task, &QtAsyncTaskWithProgress::completed, parent,
[task]() { std::get<CompletionCallback>(task->m_callback)(); });
System::QueueAsyncTask([task]() {
task->m_callback = std::get<WorkCallback>(task->m_callback)(task);
Host::RunOnUIThread([task]() {
emit task->completed(task);
delete task;
});
});
}
void QtAsyncTaskWithProgress::create(QWidget* parent, float show_delay, WorkCallback callback)
{
create(parent, {}, {}, false, 0, 1, show_delay, std::move(callback));
}
bool QtAsyncTaskWithProgress::IsCancelled() const
{
return m_ts_cancelled.load(std::memory_order_acquire);
}
void QtAsyncTaskWithProgress::SetCancellable(bool cancellable)
{
if (m_cancellable == cancellable)
return;
ProgressCallback::SetCancellable(cancellable);
Host::RunOnUIThread([this, cancellable]() {
if (m_dialog)
m_dialog->setCancellable(cancellable);
});
}
void QtAsyncTaskWithProgress::SetTitle(const std::string_view title)
{
Host::RunOnUIThread([this, title = QtUtils::StringViewToQString(title)]() {
if (m_dialog)
m_dialog->setWindowTitle(title);
});
}
void QtAsyncTaskWithProgress::SetStatusText(const std::string_view text)
{
if (m_status_text == text)
return;
ProgressCallback::SetStatusText(text);
if (m_shown)
{
Host::RunOnUIThread([this, text = QtUtils::StringViewToQString(text)]() {
if (m_dialog)
m_dialog->m_status_label->setText(text);
});
}
else
{
CheckForDelayedShow();
}
}
void QtAsyncTaskWithProgress::SetProgressRange(u32 range)
{
const u32 prev_range = m_progress_range;
ProgressCallback::SetProgressRange(range);
if (m_progress_range == prev_range)
return;
if (m_shown)
{
Host::RunOnUIThread([this, range = static_cast<int>(m_progress_range)]() {
if (m_dialog)
m_dialog->m_progress_bar->setRange(0, range);
});
}
else
{
CheckForDelayedShow();
}
}
void QtAsyncTaskWithProgress::SetProgressValue(u32 value)
{
const u32 prev_value = m_progress_value;
ProgressCallback::SetProgressValue(value);
if (m_progress_value == prev_value)
return;
if (m_shown)
{
Host::RunOnUIThread([this, value = static_cast<int>(m_progress_value)]() {
if (m_dialog)
m_dialog->m_progress_bar->setValue(value);
});
}
else
{
CheckForDelayedShow();
}
}
void QtAsyncTaskWithProgress::CheckForDelayedShow()
{
DebugAssert(!m_shown);
if (m_show_timer.GetTimeSeconds() < m_show_delay)
return;
m_shown = true;
Host::RunOnUIThread([this, status_text = QtUtils::StringViewToQString(m_status_text),
range = static_cast<int>(m_progress_range), value = static_cast<int>(m_progress_value),
cancellable = m_cancellable]() {
if (!m_dialog)
return;
if (!status_text.isEmpty())
m_dialog->m_status_label->setText(status_text);
m_dialog->m_progress_bar->setRange(0, range);
m_dialog->m_progress_bar->setValue(value);
m_dialog->setCancellable(cancellable);
m_dialog->open();
});
}

View File

@@ -3,14 +3,21 @@
#pragma once
#include "qthost.h"
#include "common/progress_callback.h"
#include "common/timer.h"
#include <QtCore/QSemaphore>
#include <QtCore/QThread>
#include <QtWidgets/QDialog>
#include <QtWidgets/QProgressDialog>
#include <atomic>
class QLabel;
class QProgressBar;
class QPushButton;
class QtModalProgressCallback final : public QObject, public ProgressCallback
{
Q_OBJECT
@@ -79,3 +86,74 @@ private:
QSemaphore m_start_semaphore;
QThread* m_starting_thread = nullptr;
};
class QtAsyncTaskWithProgress final : public QObject, private ProgressCallback
{
Q_OBJECT
public:
using CompletionCallback = std::function<void()>;
using WorkCallback = std::function<CompletionCallback(ProgressCallback*)>;
~QtAsyncTaskWithProgress();
static void create(QWidget* parent, std::string_view initial_title, std::string_view initial_status_text,
bool cancellable, int range, int value, float show_delay, WorkCallback callback);
static void create(QWidget* parent, float show_delay, WorkCallback callback);
Q_SIGNALS:
void completed(QtAsyncTaskWithProgress* self);
private:
// can't use QProgressDialog, it starts an event in setValue()...
class ProgressDialog final : public QDialog
{
friend QtAsyncTaskWithProgress;
public:
ProgressDialog(const QString& initial_title, const QString& initial_status_text, bool cancellable, int range,
int value, QtAsyncTaskWithProgress& task, QWidget* parent);
~ProgressDialog() override;
void setCancellable(bool cancellable);
protected:
void closeEvent(QCloseEvent* event) override;
private:
static constexpr int MINIMUM_WIDTH = 500;
static constexpr int MINIMUM_HEIGHT_WITHOUT_CANCEL = 70;
static constexpr int MINIMUM_HEIGHT_WITH_CANCEL = 100;
void cancelled();
QtAsyncTaskWithProgress& m_task;
QLabel* m_status_label = nullptr;
QProgressBar* m_progress_bar = nullptr;
QDialogButtonBox* m_button_box = nullptr;
};
friend ProgressDialog;
// constructor hidden, clients should not be creating this directly
QtAsyncTaskWithProgress(const QString& initial_title, const QString& initial_status_text, bool cancellable, int range,
int value, float show_delay, QWidget* dialog_parent, WorkCallback callback);
// progress callback overrides
bool IsCancelled() const override;
void SetCancellable(bool cancellable) 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;
void CheckForDelayedShow();
std::variant<WorkCallback, CompletionCallback> m_callback;
ProgressDialog* m_dialog = nullptr;
Timer m_show_timer;
float m_show_delay;
std::atomic_bool m_ts_cancelled{false};
bool m_shown = false;
};