You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
duckstation/src/duckstation-qt/qtprogresscallback.cpp

519 lines
16 KiB
C++

// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "qtprogresscallback.h"
#include "qthost.h"
#include "qtutils.h"
#include "common/assert.h"
#include "common/log.h"
#include <QtWidgets/QDialogButtonBox>
#include <QtWidgets/QLabel>
#include <QtWidgets/QProgressBar>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QVBoxLayout>
#include <array>
#include "moc_qtprogresscallback.cpp"
LOG_CHANNEL(Host);
QtProgressCallback::QtProgressCallback(QObject* parent /* = nullptr */) : QObject(parent)
{
}
QtProgressCallback::~QtProgressCallback() = default;
bool QtProgressCallback::IsCancelled() const
{
return m_ts_cancelled.load(std::memory_order_acquire);
}
void QtProgressCallback::SetTitle(const std::string_view title)
{
emit titleUpdated(QtUtils::StringViewToQString(title));
}
void QtProgressCallback::SetStatusText(const std::string_view text)
{
ProgressCallback::SetStatusText(text);
emit statusTextUpdated(QtUtils::StringViewToQString(text));
if (!text.empty())
INFO_LOG(text);
}
void QtProgressCallback::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<int>(m_progress_range));
}
void QtProgressCallback::SetProgressValue(u32 value)
{
const u32 prev_value = m_progress_value;
ProgressCallback::SetProgressValue(value);
if (m_progress_value == prev_value)
return;
emit progressValueUpdated(static_cast<int>(m_progress_value));
}
void QtProgressCallback::connectWidgets(QLabel* const status_label, QProgressBar* const progress_bar,
QAbstractButton* const cancel_button)
{
if (status_label)
connect(this, &QtProgressCallback::statusTextUpdated, status_label, &QLabel::setText);
if (progress_bar)
{
connect(this, &QtProgressCallback::progressRangeUpdated, progress_bar, &QProgressBar::setRange);
connect(this, &QtProgressCallback::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() : 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));
if (!text.empty())
INFO_LOG(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<int>(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<int>(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<CompletionCallback>(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<WorkCallback>(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,
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);
m_cancellable = cancellable;
m_progress_range = range;
m_progress_value = value;
if (show_delay <= 0.0f)
{
m_shown = true;
m_dialog->open();
}
}
QtAsyncTaskWithProgressDialog::~QtAsyncTaskWithProgressDialog()
{
if (m_dialog)
{
// should null out itself
delete m_dialog;
DebugAssert(!m_dialog);
}
}
QtAsyncTaskWithProgressDialog::ProgressDialog::ProgressDialog(const QString& initial_title,
const QString& initial_status_text, bool cancellable,
int range, int value, QtAsyncTaskWithProgressDialog& 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);
}
QtAsyncTaskWithProgressDialog::ProgressDialog::~ProgressDialog()
{
DebugAssert(m_task.m_dialog == this);
m_task.m_dialog = nullptr;
}
void QtAsyncTaskWithProgressDialog::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 QtAsyncTaskWithProgressDialog::ProgressDialog::closeEvent(QCloseEvent* event)
{
cancelled();
QDialog::closeEvent(event);
}
void QtAsyncTaskWithProgressDialog::ProgressDialog::cancelled()
{
m_task.m_ts_cancelled.store(true, std::memory_order_release);
}
QtAsyncTaskWithProgressDialog* QtAsyncTaskWithProgressDialog::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.
QtAsyncTaskWithProgressDialog* task = new QtAsyncTaskWithProgressDialog(
QtUtils::StringViewToQString(initial_title), QtUtils::StringViewToQString(initial_status_text), cancellable, range,
value, show_delay, parent, std::move(callback));
connect(task, &QtAsyncTaskWithProgressDialog::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;
});
});
return task;
}
QtAsyncTaskWithProgressDialog* QtAsyncTaskWithProgressDialog::create(QWidget* parent, float show_delay,
WorkCallback callback)
{
return create(parent, {}, {}, false, 0, 1, show_delay, std::move(callback));
}
void QtAsyncTaskWithProgressDialog::cancel()
{
m_ts_cancelled.store(true, std::memory_order_release);
}
bool QtAsyncTaskWithProgressDialog::IsCancelled() const
{
return m_ts_cancelled.load(std::memory_order_acquire);
}
void QtAsyncTaskWithProgressDialog::SetCancellable(bool cancellable)
{
if (m_cancellable == cancellable)
return;
ProgressCallback::SetCancellable(cancellable);
Host::RunOnUIThread([this, cancellable]() {
if (m_dialog)
m_dialog->setCancellable(cancellable);
});
}
void QtAsyncTaskWithProgressDialog::SetTitle(const std::string_view title)
{
Host::RunOnUIThread([this, title = QtUtils::StringViewToQString(title)]() {
if (m_dialog)
m_dialog->setWindowTitle(title);
});
}
void QtAsyncTaskWithProgressDialog::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();
}
if (!text.empty())
INFO_LOG(text);
}
void QtAsyncTaskWithProgressDialog::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 QtAsyncTaskWithProgressDialog::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();
}
}
static QMessageBox::Icon ConvertPromptIcon(ProgressCallbackWithPrompt::PromptIcon icon)
{
switch (icon)
{
case ProgressCallbackWithPrompt::PromptIcon::Error:
return QMessageBox::Critical;
case ProgressCallbackWithPrompt::PromptIcon::Warning:
return QMessageBox::Warning;
case ProgressCallbackWithPrompt::PromptIcon::Question:
return QMessageBox::Question;
case ProgressCallbackWithPrompt::PromptIcon::Information:
default:
return QMessageBox::Information;
}
}
void QtAsyncTaskWithProgressDialog::AlertPrompt(PromptIcon icon, std::string_view message)
{
m_prompt_waiting.test_and_set(std::memory_order_release);
Host::RunOnUIThread([this, icon, message = QtUtils::StringViewToQString(message)]() {
if (!m_dialog)
{
// dialog closed :(
m_prompt_waiting.clear(std::memory_order_release);
m_prompt_waiting.notify_one();
return;
}
EnsureShown();
QMessageBox* msgbox =
QtUtils::NewMessageBox(m_dialog, ConvertPromptIcon(icon), m_dialog->windowTitle(), message, QMessageBox::Ok);
connect(msgbox, &QMessageBox::finished, [this]() {
m_prompt_waiting.clear(std::memory_order_release);
m_prompt_waiting.notify_one();
});
msgbox->open();
});
m_prompt_waiting.wait(true, std::memory_order_acquire);
}
bool QtAsyncTaskWithProgressDialog::ConfirmPrompt(PromptIcon icon, std::string_view message,
std::string_view yes_text /*= {}*/, std::string_view no_text /*= {}*/)
{
m_prompt_result.store(false, std::memory_order_relaxed);
m_prompt_waiting.test_and_set(std::memory_order_release);
Host::RunOnUIThread([this, icon, message = QtUtils::StringViewToQString(message),
yes_text = QtUtils::StringViewToQString(yes_text),
no_text = QtUtils::StringViewToQString(no_text)]() {
if (!m_dialog)
{
// dialog closed :(
m_prompt_waiting.clear(std::memory_order_release);
m_prompt_waiting.notify_one();
return;
}
EnsureShown();
QMessageBox* msgbox = QtUtils::NewMessageBox(m_dialog, ConvertPromptIcon(icon), m_dialog->windowTitle(), message,
QMessageBox::NoButton);
QAbstractButton* yes_button;
if (!yes_text.isEmpty())
yes_button = msgbox->addButton(yes_text, QMessageBox::YesRole);
else
yes_button = msgbox->addButton(QMessageBox::Yes);
if (!no_text.isEmpty())
msgbox->addButton(no_text, QMessageBox::NoRole);
else
msgbox->addButton(QMessageBox::No);
connect(msgbox, &QMessageBox::finished, [this, msgbox, yes_button]() {
m_prompt_result.store((msgbox->clickedButton() == yes_button), std::memory_order_relaxed);
m_prompt_waiting.clear(std::memory_order_release);
m_prompt_waiting.notify_one();
});
msgbox->open();
});
m_prompt_waiting.wait(true, std::memory_order_acquire);
return m_prompt_result.load(std::memory_order_relaxed);
}
void QtAsyncTaskWithProgressDialog::EnsureShown()
{
if (!m_shown)
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();
});
}
void QtAsyncTaskWithProgressDialog::CheckForDelayedShow()
{
DebugAssert(!m_shown);
if (m_show_timer.GetTimeSeconds() < m_show_delay)
return;
EnsureShown();
}