mirror of https://github.com/stenzek/duckstation
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.
531 lines
17 KiB
C++
531 lines
17 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/small_string.h"
|
|
#include "common/log.h"
|
|
|
|
#include <QtWidgets/QDialogButtonBox>
|
|
#include <QtWidgets/QLabel>
|
|
#include <QtWidgets/QPlainTextEdit>
|
|
#include <QtWidgets/QProgressBar>
|
|
#include <QtWidgets/QPushButton>
|
|
#include <QtWidgets/QScrollBar>
|
|
#include <QtWidgets/QVBoxLayout>
|
|
#include <array>
|
|
|
|
#include "moc_qtprogresscallback.cpp"
|
|
|
|
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::StateChanged(StateChange changed)
|
|
{
|
|
if (changed & STATE_CHANGE_STATUS_TEXT)
|
|
emit statusTextUpdated(QtUtils::StringViewToQString(m_status_text));
|
|
if (changed & STATE_CHANGE_PROGRESS)
|
|
emit progressUpdated(static_cast<int>(m_progress_value), static_cast<int>(m_progress_range));
|
|
}
|
|
|
|
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::progressUpdated, progress_bar, [progress_bar](int value, int range) {
|
|
// qt checks if the value has changed
|
|
progress_bar->setMaximum(range);
|
|
progress_bar->setValue(value);
|
|
});
|
|
}
|
|
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::StateChanged(StateChange changed)
|
|
{
|
|
if (changed & STATE_CHANGE_STATUS_TEXT)
|
|
emit statusTextUpdated(QtUtils::StringViewToQString(m_status_text));
|
|
if (changed & STATE_CHANGE_PROGRESS)
|
|
emit progressUpdated(static_cast<int>(m_progress_value), static_cast<int>(m_progress_range));
|
|
}
|
|
|
|
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::progressUpdated, progress_bar, [progress_bar](int value, int range) {
|
|
// qt checks if the value has changed
|
|
progress_bar->setMaximum(range);
|
|
progress_bar->setValue(value);
|
|
});
|
|
}
|
|
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]() {
|
|
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);
|
|
|
|
Host::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 initial_message_log, bool initial_cancellable,
|
|
int initial_range, int initial_value, float show_delay,
|
|
bool auto_close, QWidget* dialog_parent,
|
|
WorkCallback callback)
|
|
: m_callback(std::move(callback)), m_show_delay(show_delay), m_auto_close(auto_close)
|
|
{
|
|
m_dialog = new ProgressDialog(initial_title, initial_status_text, initial_message_log, initial_cancellable,
|
|
initial_range, initial_value, this, dialog_parent);
|
|
m_cancellable = initial_cancellable;
|
|
m_progress_range = initial_range;
|
|
m_progress_value = initial_value;
|
|
|
|
if (show_delay <= 0.0f)
|
|
{
|
|
m_shown = true;
|
|
m_dialog->open();
|
|
}
|
|
}
|
|
|
|
QtAsyncTaskWithProgressDialog::~QtAsyncTaskWithProgressDialog()
|
|
{
|
|
if (m_dialog)
|
|
{
|
|
if (m_auto_close)
|
|
{
|
|
// should null out itself
|
|
delete m_dialog;
|
|
DebugAssert(!m_dialog);
|
|
}
|
|
else
|
|
{
|
|
m_dialog->taskFinished();
|
|
}
|
|
}
|
|
}
|
|
|
|
QtAsyncTaskWithProgressDialog::ProgressDialog::ProgressDialog(const QString& initial_title,
|
|
const QString& initial_status_text,
|
|
bool initial_message_log, bool initial_cancellable,
|
|
int initial_range, int initial_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, initial_cancellable);
|
|
setWindowModality(Qt::WindowModal);
|
|
setMinimumWidth(MINIMUM_WIDTH);
|
|
|
|
m_progress_bar = new QProgressBar(this);
|
|
m_progress_bar->setRange(0, initial_range);
|
|
m_progress_bar->setValue(initial_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(initial_cancellable);
|
|
|
|
m_layout = new QVBoxLayout(this);
|
|
m_layout->setSpacing(8);
|
|
m_layout->addWidget(m_status_label);
|
|
m_layout->addWidget(m_progress_bar);
|
|
m_layout->addWidget(m_button_box);
|
|
|
|
if (initial_message_log)
|
|
addMessageLog();
|
|
|
|
updateMinimumHeight();
|
|
}
|
|
|
|
QtAsyncTaskWithProgressDialog::ProgressDialog::~ProgressDialog()
|
|
{
|
|
if (m_task)
|
|
{
|
|
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);
|
|
updateMinimumHeight();
|
|
|
|
m_button_box->setVisible(cancellable);
|
|
}
|
|
|
|
void QtAsyncTaskWithProgressDialog::ProgressDialog::closeEvent(QCloseEvent* event)
|
|
{
|
|
cancelled();
|
|
QDialog::closeEvent(event);
|
|
}
|
|
|
|
void QtAsyncTaskWithProgressDialog::ProgressDialog::updateMinimumHeight()
|
|
{
|
|
setMinimumHeight((m_button_box->isVisible() ? MINIMUM_HEIGHT_WITH_CANCEL : MINIMUM_HEIGHT_WITHOUT_CANCEL) +
|
|
(m_message_log ? MESSAGE_LOG_HEIGHT : 0));
|
|
}
|
|
|
|
void QtAsyncTaskWithProgressDialog::ProgressDialog::cancelled()
|
|
{
|
|
if (m_task)
|
|
m_task->m_ts_cancelled.store(true, std::memory_order_release);
|
|
}
|
|
|
|
void QtAsyncTaskWithProgressDialog::ProgressDialog::taskFinished()
|
|
{
|
|
DebugAssert(m_task);
|
|
m_task = nullptr;
|
|
|
|
m_button_box->setStandardButtons(QDialogButtonBox::Close);
|
|
if (!m_button_box->isVisible())
|
|
{
|
|
m_button_box->setVisible(true);
|
|
updateMinimumHeight();
|
|
}
|
|
}
|
|
|
|
void QtAsyncTaskWithProgressDialog::ProgressDialog::addMessageLog()
|
|
{
|
|
DebugAssert(!m_message_log);
|
|
|
|
m_message_log = new QPlainTextEdit(this);
|
|
m_message_log->setReadOnly(true);
|
|
m_message_log->setMinimumHeight(100);
|
|
m_layout->insertWidget(2, m_message_log);
|
|
}
|
|
|
|
QtAsyncTaskWithProgressDialog* QtAsyncTaskWithProgressDialog::create(QWidget* parent, std::string_view initial_title,
|
|
std::string_view initial_status_text,
|
|
bool initial_message_log, bool initial_cancellable,
|
|
int initial_range, int initial_value,
|
|
float show_delay, bool auto_close,
|
|
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), initial_message_log,
|
|
initial_cancellable, initial_range, initial_value, show_delay, auto_close, parent, std::move(callback));
|
|
connect(task, &QtAsyncTaskWithProgressDialog::completed, parent, [task]() {
|
|
CompletionCallback& cb = std::get<CompletionCallback>(task->m_callback);
|
|
if (cb)
|
|
cb();
|
|
});
|
|
|
|
Host::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, false, 0, 1, show_delay, true, 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::SetTitle(const std::string_view title)
|
|
{
|
|
Host::RunOnUIThread([this, title = QtUtils::StringViewToQString(title)]() {
|
|
if (m_dialog)
|
|
m_dialog->setWindowTitle(title);
|
|
});
|
|
}
|
|
|
|
void QtAsyncTaskWithProgressDialog::StateChanged(StateChange changed)
|
|
{
|
|
if (changed & (STATE_CHANGE_STATUS_TEXT | STATE_CHANGE_PROGRESS))
|
|
{
|
|
if (m_shown)
|
|
{
|
|
if (changed & STATE_CHANGE_STATUS_TEXT)
|
|
{
|
|
Host::RunOnUIThread([this, text = QtUtils::StringViewToQString(m_status_text)]() {
|
|
if (m_dialog)
|
|
m_dialog->m_status_label->setText(text);
|
|
});
|
|
}
|
|
if (changed & STATE_CHANGE_PROGRESS)
|
|
{
|
|
Host::RunOnUIThread(
|
|
[this, value = static_cast<int>(m_progress_value), range = static_cast<int>(m_progress_range)]() {
|
|
if (m_dialog)
|
|
m_dialog->m_progress_bar->setRange(0, range);
|
|
if (m_dialog)
|
|
m_dialog->m_progress_bar->setValue(value);
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CheckForDelayedShow();
|
|
}
|
|
}
|
|
if (changed & STATE_CHANGE_CANCELLABLE)
|
|
{
|
|
Host::RunOnUIThread([this, cancellable = m_cancellable]() {
|
|
if (m_dialog)
|
|
m_dialog->setCancellable(cancellable);
|
|
});
|
|
}
|
|
}
|
|
|
|
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::AppendMessage(std::string_view message)
|
|
{
|
|
Log::Write(Log::PackCategory(Log::Channel::Host, Log::Level::Info, Log::Color::StrongOrange), message);
|
|
|
|
EnsureShown();
|
|
|
|
Host::RunOnUIThread([this, message = QtUtils::StringViewToQString(message)]() {
|
|
if (!m_dialog)
|
|
return;
|
|
|
|
if (!m_dialog->m_message_log)
|
|
m_dialog->addMessageLog();
|
|
|
|
m_dialog->m_message_log->appendPlainText(message);
|
|
|
|
QScrollBar* const scrollbar = m_dialog->m_message_log->verticalScrollBar();
|
|
const bool cursor_at_end = m_dialog->m_message_log->textCursor().atEnd();
|
|
const bool scroll_at_end = scrollbar->sliderPosition() == scrollbar->maximum();
|
|
if (cursor_at_end && scroll_at_end)
|
|
m_dialog->m_message_log->centerCursor();
|
|
});
|
|
}
|
|
|
|
void QtAsyncTaskWithProgressDialog::SetAutoClose(bool enabled)
|
|
{
|
|
m_auto_close = enabled;
|
|
}
|
|
|
|
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();
|
|
}
|