mirror of
https://github.com/stenzek/duckstation.git
synced 2026-02-04 05:04:33 +00:00
Qt: Add QtAsyncTaskWithProgress class
Async work item with a progress dialog that doesn't require a nested event loop.
This commit is contained in:
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user