Add a rate limiter to Qt's progress dialog

To work around a macOS specific issue where theh progress bar is not updated. With this the previous workaround for other OSes also isn't needed.
This commit is contained in:
Pengfei
2021-08-18 20:40:53 +08:00
parent a30dfd7e34
commit adb4325d79
7 changed files with 229 additions and 189 deletions
+2
View File
@@ -15,6 +15,8 @@ add_executable(threeSD
helpers/frontend_common.h helpers/frontend_common.h
helpers/multi_job.cpp helpers/multi_job.cpp
helpers/multi_job.h helpers/multi_job.h
helpers/rate_limited_progress_dialog.cpp
helpers/rate_limited_progress_dialog.h
helpers/simple_job.cpp helpers/simple_job.cpp
helpers/simple_job.h helpers/simple_job.h
cia_build_dialog.cpp cia_build_dialog.cpp
+67 -66
View File
@@ -1,66 +1,67 @@
// Copyright 2019 threeSD Project // Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
#include <chrono> #include <chrono>
#include "frontend/helpers/multi_job.h" #include "frontend/helpers/multi_job.h"
MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_, MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_,
std::vector<Core::ContentSpecifier> contents_, ExecuteFunc execute_func_, std::vector<Core::ContentSpecifier> contents_, ExecuteFunc execute_func_,
AbortFunc abort_func_) AbortFunc abort_func_)
: QThread(parent), importer(importer_), contents(std::move(contents_)), : QThread(parent), importer(importer_), contents(std::move(contents_)),
execute_func(std::move(execute_func_)), abort_func(abort_func_) {} execute_func(std::move(execute_func_)), abort_func(abort_func_) {}
MultiJob::~MultiJob() = default; MultiJob::~MultiJob() = default;
void MultiJob::run() { void MultiJob::run() {
u64 total_size = 0; u64 total_size = 0;
for (const auto& content : contents) { for (const auto& content : contents) {
total_size += content.maximum_size; total_size += content.maximum_size;
} }
std::size_t count = 0; std::size_t count = 0;
int eta = -1; int eta = -1;
const auto initial_time = std::chrono::steady_clock::now(); const auto initial_time = std::chrono::steady_clock::now();
const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) { const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) {
if (size_imported < 10 * 1024 * 1024) { // 10M Threshold if (size_imported < 10 * 1024 * 1024) { // 10M Threshold
return; return;
} }
using namespace std::chrono; using namespace std::chrono;
const u64 time_elapsed = const u64 time_elapsed =
duration_cast<milliseconds>(steady_clock::now() - initial_time).count(); duration_cast<milliseconds>(steady_clock::now() - initial_time).count();
eta = eta =
static_cast<int>(time_elapsed * (total_size - size_imported) / (size_imported) / 1000); static_cast<int>(time_elapsed * (total_size - size_imported) / (size_imported) / 1000);
}; };
const auto Callback = [this, &eta, &UpdateETA](u64 current_imported_size, const auto Callback = [this, &eta, &UpdateETA](u64 current_imported_size,
u64 total_imported_size, u64 /*total_size*/) { u64 total_imported_size, u64 /*total_size*/) {
UpdateETA(total_imported_size); UpdateETA(total_imported_size);
emit ProgressUpdated(current_imported_size, total_imported_size, eta); emit ProgressUpdated(current_imported_size, total_imported_size, eta);
}; };
Common::ProgressCallbackWrapper wrapper{total_size}; Common::ProgressCallbackWrapper wrapper{total_size};
for (const auto& content : contents) { for (const auto& content : contents) {
emit NextContent(count + 1, content, eta); emit NextContent(count + 1, wrapper.current_done_size + wrapper.current_pending_size,
if (!execute_func(importer, content, wrapper.Wrap(Callback))) { content, eta);
if (!cancelled) { if (!execute_func(importer, content, wrapper.Wrap(Callback))) {
failed_contents.emplace_back(content); if (!cancelled) {
} failed_contents.emplace_back(content);
} }
count++; }
count++;
if (cancelled) {
break; if (cancelled) {
} break;
} }
emit Completed(); }
} emit Completed();
}
void MultiJob::Cancel() {
cancelled.store(true); void MultiJob::Cancel() {
abort_func(importer); cancelled.store(true);
} abort_func(importer);
}
std::vector<Core::ContentSpecifier> MultiJob::GetFailedContents() const {
return failed_contents; std::vector<Core::ContentSpecifier> MultiJob::GetFailedContents() const {
} return failed_contents;
}
+56 -55
View File
@@ -1,55 +1,56 @@
// Copyright 2019 threeSD Project // Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <functional> #include <functional>
#include <QThread> #include <QThread>
#include "common/progress_callback.h" #include "common/progress_callback.h"
#include "core/importer.h" #include "core/importer.h"
class MultiJob : public QThread { class MultiJob : public QThread {
Q_OBJECT Q_OBJECT
public: public:
using ExecuteFunc = std::function<bool(Core::SDMCImporter&, const Core::ContentSpecifier&, using ExecuteFunc = std::function<bool(Core::SDMCImporter&, const Core::ContentSpecifier&,
const Common::ProgressCallback&)>; const Common::ProgressCallback&)>;
using AbortFunc = std::function<void(Core::SDMCImporter&)>; using AbortFunc = std::function<void(Core::SDMCImporter&)>;
explicit MultiJob(QObject* parent, Core::SDMCImporter& importer, explicit MultiJob(QObject* parent, Core::SDMCImporter& importer,
std::vector<Core::ContentSpecifier> contents, ExecuteFunc execute_func, std::vector<Core::ContentSpecifier> contents, ExecuteFunc execute_func,
AbortFunc abort_func); AbortFunc abort_func);
~MultiJob() override; ~MultiJob() override;
void run() override; void run() override;
void Cancel(); void Cancel();
std::vector<Core::ContentSpecifier> GetFailedContents() const; std::vector<Core::ContentSpecifier> GetFailedContents() const;
signals: signals:
/** /**
* Called when progress is updated on the current content. * Called when progress is updated on the current content.
* @param current_imported_size Imported size of the current content. * @param current_imported_size Imported size of the current content.
* @param total_imported_size Total imported size taking all previous contents into * @param total_imported_size Total imported size taking all previous contents into
* consideration. * consideration.
* @param eta ETA in seconds, 0 when not determined. * @param eta ETA in seconds, 0 when not determined.
*/ */
void ProgressUpdated(u64 current_imported_size, u64 total_imported_size, int eta); void ProgressUpdated(u64 current_imported_size, u64 total_imported_size, int eta);
/// Dumping of a content has been finished, go on to the next. Called at start as well. /// Dumping of a content has been finished, go on to the next. Called at start as well.
void NextContent(std::size_t count, const Core::ContentSpecifier& next_content, int eta); void NextContent(std::size_t count, u64 total_imported_size,
const Core::ContentSpecifier& next_content, int eta);
void Completed();
void Completed();
private:
std::atomic_bool cancelled{false}; private:
Core::SDMCImporter& importer; std::atomic_bool cancelled{false};
std::vector<Core::ContentSpecifier> contents; Core::SDMCImporter& importer;
std::vector<Core::ContentSpecifier> failed_contents; std::vector<Core::ContentSpecifier> contents;
ExecuteFunc execute_func; std::vector<Core::ContentSpecifier> failed_contents;
AbortFunc abort_func; ExecuteFunc execute_func;
}; AbortFunc abort_func;
};
Q_DECLARE_METATYPE(Core::ContentSpecifier)
Q_DECLARE_METATYPE(Core::ContentSpecifier)
@@ -0,0 +1,34 @@
// Copyright 2021 Pengfei Zhu
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "frontend/helpers/rate_limited_progress_dialog.h"
RateLimitedProgressDialog::RateLimitedProgressDialog(const QString& label_text,
const QString& cancel_button_text, int minimum,
int maximum, QWidget* parent)
: QProgressDialog(label_text, cancel_button_text, minimum, maximum, parent) {
setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));
setWindowModality(Qt::WindowModal);
setMinimumDuration(0);
setValue(0);
}
RateLimitedProgressDialog::~RateLimitedProgressDialog() = default;
void RateLimitedProgressDialog::Update(int progress, const QString& label_text) {
if (progress == maximum()) { // always set the maximum
setValue(progress);
return;
}
const auto current_time = std::chrono::steady_clock::now();
if (current_time - last_update_time < MinimumInterval) {
return;
}
setValue(progress);
setLabelText(label_text);
last_update_time = current_time;
}
@@ -0,0 +1,21 @@
// Copyright 2021 Pengfei Zhu
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <chrono>
#include <QProgressDialog>
class RateLimitedProgressDialog : public QProgressDialog {
public:
explicit RateLimitedProgressDialog(const QString& label_text, const QString& cancel_button_text,
int minimum, int maximum, QWidget* parent = nullptr);
~RateLimitedProgressDialog() override;
void Update(int progress, const QString& label_text);
private:
std::chrono::steady_clock::time_point last_update_time = std::chrono::steady_clock::now();
static constexpr auto MinimumInterval = std::chrono::milliseconds{100};
};
+10 -20
View File
@@ -3,9 +3,8 @@
// Refer to the license.txt file included. // Refer to the license.txt file included.
#include <QMessageBox> #include <QMessageBox>
#include <QProgressBar>
#include <QProgressDialog>
#include "frontend/helpers/frontend_common.h" #include "frontend/helpers/frontend_common.h"
#include "frontend/helpers/rate_limited_progress_dialog.h"
#include "frontend/helpers/simple_job.h" #include "frontend/helpers/simple_job.h"
SimpleJob::SimpleJob(QObject* parent, ExecuteFunc execute_, AbortFunc abort_) SimpleJob::SimpleJob(QObject* parent, ExecuteFunc execute_, AbortFunc abort_)
@@ -30,34 +29,25 @@ void SimpleJob::Cancel() {
} }
void SimpleJob::StartWithProgressDialog(QWidget* widget) { void SimpleJob::StartWithProgressDialog(QWidget* widget) {
// We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, widget);
// event handling. connect(this, &SimpleJob::ProgressUpdated, this, [dialog](u64 current, u64 total) {
auto* bar = new QProgressBar(widget); if (dialog->wasCanceled()) {
bar->setRange(0, 100); return;
bar->setValue(0); }
auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, widget);
dialog->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint));
dialog->setWindowModality(Qt::WindowModal);
dialog->setBar(bar);
dialog->setMinimumDuration(0);
connect(this, &SimpleJob::ProgressUpdated, this, [bar, dialog](u64 current, u64 total) {
// Try to map total to int range // Try to map total to int range
// This is equal to ceil(total / INT_MAX) // This is equal to ceil(total / INT_MAX)
const u64 multiplier = const u64 multiplier =
(total + std::numeric_limits<int>::max() - 1) / std::numeric_limits<int>::max(); (total + std::numeric_limits<int>::max() - 1) / std::numeric_limits<int>::max();
bar->setMaximum(static_cast<int>(total / multiplier)); dialog->setMaximum(static_cast<int>(total / multiplier));
bar->setValue(static_cast<int>(current / multiplier)); dialog->Update(static_cast<int>(current / multiplier),
dialog->setLabelText( tr("%1 / %2").arg(ReadableByteSize(current), ReadableByteSize(total)));
tr("%1 / %2").arg(ReadableByteSize(current)).arg(ReadableByteSize(total)));
}); });
connect(this, &SimpleJob::ErrorOccured, this, [widget, dialog] { connect(this, &SimpleJob::ErrorOccured, this, [widget, dialog] {
QMessageBox::critical(widget, tr("threeSD"), QMessageBox::critical(widget, tr("threeSD"),
tr("Operation failed. Please refer to the log.")); tr("Operation failed. Please refer to the log."));
dialog->hide(); dialog->hide();
}); });
connect(this, &SimpleJob::Completed, this, [dialog] { dialog->setValue(dialog->maximum()); }); connect(this, &SimpleJob::Completed, dialog, &QProgressDialog::hide);
connect(dialog, &QProgressDialog::canceled, this, &SimpleJob::Cancel); connect(dialog, &QProgressDialog::canceled, this, &SimpleJob::Cancel);
start(); start();
+39 -48
View File
@@ -12,8 +12,6 @@
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
#include <QMouseEvent> #include <QMouseEvent>
#include <QProgressBar>
#include <QProgressDialog>
#include <QStorageInfo> #include <QStorageInfo>
#include <QtConcurrent/QtConcurrentRun> #include <QtConcurrent/QtConcurrentRun>
#include "common/assert.h" #include "common/assert.h"
@@ -23,6 +21,7 @@
#include "frontend/cia_build_dialog.h" #include "frontend/cia_build_dialog.h"
#include "frontend/helpers/frontend_common.h" #include "frontend/helpers/frontend_common.h"
#include "frontend/helpers/multi_job.h" #include "frontend/helpers/multi_job.h"
#include "frontend/helpers/rate_limited_progress_dialog.h"
#include "frontend/helpers/simple_job.h" #include "frontend/helpers/simple_job.h"
#include "frontend/import_dialog.h" #include "frontend/import_dialog.h"
#include "frontend/title_info_dialog.h" #include "frontend/title_info_dialog.h"
@@ -128,12 +127,9 @@ void ImportDialog::SetContentSizes(int previous_width, int previous_height) {
} }
void ImportDialog::RelistContent() { void ImportDialog::RelistContent() {
auto* dialog = new QProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this); auto* dialog =
dialog->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint)); new RateLimitedProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this);
dialog->setWindowModality(Qt::WindowModal);
dialog->setCancelButton(nullptr); dialog->setCancelButton(nullptr);
dialog->setMinimumDuration(0);
dialog->setValue(0);
using FutureWatcher = QFutureWatcher<void>; using FutureWatcher = QFutureWatcher<void>;
auto* future_watcher = new FutureWatcher(this); auto* future_watcher = new FutureWatcher(this);
@@ -601,48 +597,47 @@ void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total
label->setWordWrap(true); label->setWordWrap(true);
label->setFixedWidth(600); label->setFixedWidth(600);
// We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0,
// event handling. static_cast<int>(total_size / multiplier), this);
auto* bar = new QProgressBar(this);
bar->setRange(0, static_cast<int>(total_size / multiplier));
bar->setValue(0);
auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, this);
dialog->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint));
dialog->setWindowModality(Qt::WindowModal);
dialog->setBar(bar);
dialog->setLabel(label); dialog->setLabel(label);
dialog->setMinimumDuration(0);
connect(job, &MultiJob::NextContent, this, connect(job, &MultiJob::NextContent, this,
[this, dialog, total_count](std::size_t count, [this, dialog, multiplier, total_count](std::size_t count, u64 total_imported_size,
const Core::ContentSpecifier& next_content, int eta) { const Core::ContentSpecifier& next_content,
dialog->setLabelText( int eta) {
tr("<p>(%1/%2) %3 (%4)</p><p>&nbsp;</p><p align=\"right\">%5</p>") if (dialog->wasCanceled()) {
.arg(count) return;
.arg(total_count) }
.arg(GetContentName(next_content)) dialog->Update(static_cast<int>(total_imported_size / multiplier),
.arg(GetContentTypeName<false>(next_content.type)) tr("<p>(%1/%2) %3 (%4)</p><p>&nbsp;</p><p align=\"right\">%5</p>")
.arg(FormatETA(eta))); .arg(count)
.arg(total_count)
.arg(GetContentName(next_content))
.arg(GetContentTypeName<false>(next_content.type))
.arg(FormatETA(eta)));
current_content = next_content; current_content = next_content;
current_count = count; current_count = count;
}); });
connect(job, &MultiJob::ProgressUpdated, this, connect(
[this, bar, dialog, multiplier, total_count](u64 current_imported_size, job, &MultiJob::ProgressUpdated, this,
u64 total_imported_size, int eta) { [this, dialog, multiplier, total_count](u64 current_imported_size, u64 total_imported_size,
bar->setValue(static_cast<int>(total_imported_size / multiplier)); int eta) {
dialog->setLabelText(tr("<p>(%1/%2) %3 (%4)</p><p align=\"center\">%5 " if (dialog->wasCanceled()) {
"/ %6</p><p align=\"right\">%7</p>") return;
.arg(current_count) }
.arg(total_count) dialog->Update(
.arg(GetContentName(current_content)) static_cast<int>(total_imported_size / multiplier),
.arg(GetContentTypeName<false>(current_content.type)) tr("<p>(%1/%2) %3 (%4)</p><p align=\"center\">%5 / %6</p><p align=\"right\">%7</p>")
.arg(ReadableByteSize(current_imported_size)) .arg(current_count)
.arg(ReadableByteSize(current_content.maximum_size)) .arg(total_count)
.arg(FormatETA(eta))); .arg(GetContentName(current_content))
}); .arg(GetContentTypeName<false>(current_content.type))
.arg(ReadableByteSize(current_imported_size))
.arg(ReadableByteSize(current_content.maximum_size))
.arg(FormatETA(eta)));
});
connect(job, &MultiJob::Completed, this, [this, dialog, job] { connect(job, &MultiJob::Completed, this, [this, dialog, job] {
dialog->setValue(dialog->maximum()); dialog->hide();
const auto failed_contents = job->GetFailedContents(); const auto failed_contents = job->GetFailedContents();
if (failed_contents.empty()) { if (failed_contents.empty()) {
@@ -662,13 +657,9 @@ void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total
}); });
connect(dialog, &QProgressDialog::canceled, this, [this, job] { connect(dialog, &QProgressDialog::canceled, this, [this, job] {
// Add yet-another-ProgressDialog to indicate cancel progress // Add yet-another-ProgressDialog to indicate cancel progress
auto* cancel_dialog = new QProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this); auto* cancel_dialog =
cancel_dialog->setWindowFlags(cancel_dialog->windowFlags() & new RateLimitedProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this);
(~Qt::WindowContextHelpButtonHint));
cancel_dialog->setWindowModality(Qt::WindowModal);
cancel_dialog->setCancelButton(nullptr); cancel_dialog->setCancelButton(nullptr);
cancel_dialog->setMinimumDuration(0);
cancel_dialog->setValue(0);
connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide); connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide);
job->Cancel(); job->Cancel();
}); });