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/multi_job.cpp
helpers/multi_job.h
helpers/rate_limited_progress_dialog.cpp
helpers/rate_limited_progress_dialog.h
helpers/simple_job.cpp
helpers/simple_job.h
cia_build_dialog.cpp
+67 -66
View File
@@ -1,66 +1,67 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <chrono>
#include "frontend/helpers/multi_job.h"
MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_,
std::vector<Core::ContentSpecifier> contents_, ExecuteFunc execute_func_,
AbortFunc abort_func_)
: QThread(parent), importer(importer_), contents(std::move(contents_)),
execute_func(std::move(execute_func_)), abort_func(abort_func_) {}
MultiJob::~MultiJob() = default;
void MultiJob::run() {
u64 total_size = 0;
for (const auto& content : contents) {
total_size += content.maximum_size;
}
std::size_t count = 0;
int eta = -1;
const auto initial_time = std::chrono::steady_clock::now();
const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) {
if (size_imported < 10 * 1024 * 1024) { // 10M Threshold
return;
}
using namespace std::chrono;
const u64 time_elapsed =
duration_cast<milliseconds>(steady_clock::now() - initial_time).count();
eta =
static_cast<int>(time_elapsed * (total_size - size_imported) / (size_imported) / 1000);
};
const auto Callback = [this, &eta, &UpdateETA](u64 current_imported_size,
u64 total_imported_size, u64 /*total_size*/) {
UpdateETA(total_imported_size);
emit ProgressUpdated(current_imported_size, total_imported_size, eta);
};
Common::ProgressCallbackWrapper wrapper{total_size};
for (const auto& content : contents) {
emit NextContent(count + 1, content, eta);
if (!execute_func(importer, content, wrapper.Wrap(Callback))) {
if (!cancelled) {
failed_contents.emplace_back(content);
}
}
count++;
if (cancelled) {
break;
}
}
emit Completed();
}
void MultiJob::Cancel() {
cancelled.store(true);
abort_func(importer);
}
std::vector<Core::ContentSpecifier> MultiJob::GetFailedContents() const {
return failed_contents;
}
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <chrono>
#include "frontend/helpers/multi_job.h"
MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_,
std::vector<Core::ContentSpecifier> contents_, ExecuteFunc execute_func_,
AbortFunc abort_func_)
: QThread(parent), importer(importer_), contents(std::move(contents_)),
execute_func(std::move(execute_func_)), abort_func(abort_func_) {}
MultiJob::~MultiJob() = default;
void MultiJob::run() {
u64 total_size = 0;
for (const auto& content : contents) {
total_size += content.maximum_size;
}
std::size_t count = 0;
int eta = -1;
const auto initial_time = std::chrono::steady_clock::now();
const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) {
if (size_imported < 10 * 1024 * 1024) { // 10M Threshold
return;
}
using namespace std::chrono;
const u64 time_elapsed =
duration_cast<milliseconds>(steady_clock::now() - initial_time).count();
eta =
static_cast<int>(time_elapsed * (total_size - size_imported) / (size_imported) / 1000);
};
const auto Callback = [this, &eta, &UpdateETA](u64 current_imported_size,
u64 total_imported_size, u64 /*total_size*/) {
UpdateETA(total_imported_size);
emit ProgressUpdated(current_imported_size, total_imported_size, eta);
};
Common::ProgressCallbackWrapper wrapper{total_size};
for (const auto& content : contents) {
emit NextContent(count + 1, wrapper.current_done_size + wrapper.current_pending_size,
content, eta);
if (!execute_func(importer, content, wrapper.Wrap(Callback))) {
if (!cancelled) {
failed_contents.emplace_back(content);
}
}
count++;
if (cancelled) {
break;
}
}
emit Completed();
}
void MultiJob::Cancel() {
cancelled.store(true);
abort_func(importer);
}
std::vector<Core::ContentSpecifier> MultiJob::GetFailedContents() const {
return failed_contents;
}
+56 -55
View File
@@ -1,55 +1,56 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <atomic>
#include <functional>
#include <QThread>
#include "common/progress_callback.h"
#include "core/importer.h"
class MultiJob : public QThread {
Q_OBJECT
public:
using ExecuteFunc = std::function<bool(Core::SDMCImporter&, const Core::ContentSpecifier&,
const Common::ProgressCallback&)>;
using AbortFunc = std::function<void(Core::SDMCImporter&)>;
explicit MultiJob(QObject* parent, Core::SDMCImporter& importer,
std::vector<Core::ContentSpecifier> contents, ExecuteFunc execute_func,
AbortFunc abort_func);
~MultiJob() override;
void run() override;
void Cancel();
std::vector<Core::ContentSpecifier> GetFailedContents() const;
signals:
/**
* Called when progress is updated on 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
* consideration.
* @param eta ETA in seconds, 0 when not determined.
*/
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.
void NextContent(std::size_t count, const Core::ContentSpecifier& next_content, int eta);
void Completed();
private:
std::atomic_bool cancelled{false};
Core::SDMCImporter& importer;
std::vector<Core::ContentSpecifier> contents;
std::vector<Core::ContentSpecifier> failed_contents;
ExecuteFunc execute_func;
AbortFunc abort_func;
};
Q_DECLARE_METATYPE(Core::ContentSpecifier)
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <atomic>
#include <functional>
#include <QThread>
#include "common/progress_callback.h"
#include "core/importer.h"
class MultiJob : public QThread {
Q_OBJECT
public:
using ExecuteFunc = std::function<bool(Core::SDMCImporter&, const Core::ContentSpecifier&,
const Common::ProgressCallback&)>;
using AbortFunc = std::function<void(Core::SDMCImporter&)>;
explicit MultiJob(QObject* parent, Core::SDMCImporter& importer,
std::vector<Core::ContentSpecifier> contents, ExecuteFunc execute_func,
AbortFunc abort_func);
~MultiJob() override;
void run() override;
void Cancel();
std::vector<Core::ContentSpecifier> GetFailedContents() const;
signals:
/**
* Called when progress is updated on 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
* consideration.
* @param eta ETA in seconds, 0 when not determined.
*/
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.
void NextContent(std::size_t count, u64 total_imported_size,
const Core::ContentSpecifier& next_content, int eta);
void Completed();
private:
std::atomic_bool cancelled{false};
Core::SDMCImporter& importer;
std::vector<Core::ContentSpecifier> contents;
std::vector<Core::ContentSpecifier> failed_contents;
ExecuteFunc execute_func;
AbortFunc abort_func;
};
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.
#include <QMessageBox>
#include <QProgressBar>
#include <QProgressDialog>
#include "frontend/helpers/frontend_common.h"
#include "frontend/helpers/rate_limited_progress_dialog.h"
#include "frontend/helpers/simple_job.h"
SimpleJob::SimpleJob(QObject* parent, ExecuteFunc execute_, AbortFunc abort_)
@@ -30,34 +29,25 @@ void SimpleJob::Cancel() {
}
void SimpleJob::StartWithProgressDialog(QWidget* widget) {
// We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's
// event handling.
auto* bar = new QProgressBar(widget);
bar->setRange(0, 100);
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) {
auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, widget);
connect(this, &SimpleJob::ProgressUpdated, this, [dialog](u64 current, u64 total) {
if (dialog->wasCanceled()) {
return;
}
// Try to map total to int range
// This is equal to ceil(total / INT_MAX)
const u64 multiplier =
(total + std::numeric_limits<int>::max() - 1) / std::numeric_limits<int>::max();
bar->setMaximum(static_cast<int>(total / multiplier));
bar->setValue(static_cast<int>(current / multiplier));
dialog->setLabelText(
tr("%1 / %2").arg(ReadableByteSize(current)).arg(ReadableByteSize(total)));
dialog->setMaximum(static_cast<int>(total / multiplier));
dialog->Update(static_cast<int>(current / multiplier),
tr("%1 / %2").arg(ReadableByteSize(current), ReadableByteSize(total)));
});
connect(this, &SimpleJob::ErrorOccured, this, [widget, dialog] {
QMessageBox::critical(widget, tr("threeSD"),
tr("Operation failed. Please refer to the log."));
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);
start();
+39 -48
View File
@@ -12,8 +12,6 @@
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QProgressBar>
#include <QProgressDialog>
#include <QStorageInfo>
#include <QtConcurrent/QtConcurrentRun>
#include "common/assert.h"
@@ -23,6 +21,7 @@
#include "frontend/cia_build_dialog.h"
#include "frontend/helpers/frontend_common.h"
#include "frontend/helpers/multi_job.h"
#include "frontend/helpers/rate_limited_progress_dialog.h"
#include "frontend/helpers/simple_job.h"
#include "frontend/import_dialog.h"
#include "frontend/title_info_dialog.h"
@@ -128,12 +127,9 @@ void ImportDialog::SetContentSizes(int previous_width, int previous_height) {
}
void ImportDialog::RelistContent() {
auto* dialog = new QProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this);
dialog->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint));
dialog->setWindowModality(Qt::WindowModal);
auto* dialog =
new RateLimitedProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this);
dialog->setCancelButton(nullptr);
dialog->setMinimumDuration(0);
dialog->setValue(0);
using FutureWatcher = QFutureWatcher<void>;
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->setFixedWidth(600);
// We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's
// event handling.
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);
auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0,
static_cast<int>(total_size / multiplier), this);
dialog->setLabel(label);
dialog->setMinimumDuration(0);
connect(job, &MultiJob::NextContent, this,
[this, dialog, total_count](std::size_t count,
const Core::ContentSpecifier& next_content, int eta) {
dialog->setLabelText(
tr("<p>(%1/%2) %3 (%4)</p><p>&nbsp;</p><p align=\"right\">%5</p>")
.arg(count)
.arg(total_count)
.arg(GetContentName(next_content))
.arg(GetContentTypeName<false>(next_content.type))
.arg(FormatETA(eta)));
[this, dialog, multiplier, total_count](std::size_t count, u64 total_imported_size,
const Core::ContentSpecifier& next_content,
int eta) {
if (dialog->wasCanceled()) {
return;
}
dialog->Update(static_cast<int>(total_imported_size / multiplier),
tr("<p>(%1/%2) %3 (%4)</p><p>&nbsp;</p><p align=\"right\">%5</p>")
.arg(count)
.arg(total_count)
.arg(GetContentName(next_content))
.arg(GetContentTypeName<false>(next_content.type))
.arg(FormatETA(eta)));
current_content = next_content;
current_count = count;
});
connect(job, &MultiJob::ProgressUpdated, this,
[this, bar, dialog, multiplier, total_count](u64 current_imported_size,
u64 total_imported_size, int eta) {
bar->setValue(static_cast<int>(total_imported_size / multiplier));
dialog->setLabelText(tr("<p>(%1/%2) %3 (%4)</p><p align=\"center\">%5 "
"/ %6</p><p align=\"right\">%7</p>")
.arg(current_count)
.arg(total_count)
.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::ProgressUpdated, this,
[this, dialog, multiplier, total_count](u64 current_imported_size, u64 total_imported_size,
int eta) {
if (dialog->wasCanceled()) {
return;
}
dialog->Update(
static_cast<int>(total_imported_size / multiplier),
tr("<p>(%1/%2) %3 (%4)</p><p align=\"center\">%5 / %6</p><p align=\"right\">%7</p>")
.arg(current_count)
.arg(total_count)
.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] {
dialog->setValue(dialog->maximum());
dialog->hide();
const auto failed_contents = job->GetFailedContents();
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] {
// Add yet-another-ProgressDialog to indicate cancel progress
auto* cancel_dialog = new QProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this);
cancel_dialog->setWindowFlags(cancel_dialog->windowFlags() &
(~Qt::WindowContextHelpButtonHint));
cancel_dialog->setWindowModality(Qt::WindowModal);
auto* cancel_dialog =
new RateLimitedProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this);
cancel_dialog->setCancelButton(nullptr);
cancel_dialog->setMinimumDuration(0);
cancel_dialog->setValue(0);
connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide);
job->Cancel();
});