From 8acfe9f304912d970d14e7521280289ae963f124 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 28 Aug 2019 23:02:30 +0800 Subject: [PATCH] UI updates - add import dialog this is more complex than I thought - added scope exit grammar sugar - main dialog is now linked to import dialog - importer citra file path is fixed - importer now reports maximum size - file util is improved with GetDirectoryTreeSize --- src/common/CMakeLists.txt | 1 + src/common/file_util.cpp | 31 ++++++ src/common/file_util.h | 3 + src/common/logging/log.h | 2 +- src/common/scope_exit.h | 44 +++++++++ src/core/importer.cpp | 68 ++++++++----- src/core/importer.h | 2 + src/frontend/CMakeLists.txt | 6 ++ src/frontend/import_dialog.cpp | 174 +++++++++++++++++++++++++++++++++ src/frontend/import_dialog.h | 41 ++++++++ src/frontend/import_dialog.ui | 77 +++++++++++++++ src/frontend/main.cpp | 25 ++++- src/frontend/main.h | 1 + 13 files changed, 448 insertions(+), 27 deletions(-) create mode 100644 src/common/scope_exit.h create mode 100644 src/frontend/import_dialog.cpp create mode 100644 src/frontend/import_dialog.h create mode 100644 src/frontend/import_dialog.ui diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index f8b32f7..bc70cd6 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(common STATIC file_util.h logging/log.h misc.cpp + scope_exit.h string_util.cpp string_util.h swap.h diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 9d092b8..75ea5f2 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -367,6 +367,37 @@ u64 GetSize(FILE* f) { return size; } +u64 GetDirectoryTreeSize(const std::string& path, unsigned int recursion) { + if (!IsDirectory(path)) { + LOG_ERROR(Common_FileSystem, "failed {}: is a file", path); + return 0; + } + + std::string real_path = path; + if (real_path.back() != '/' && real_path.back() != '\\') { + real_path += '/'; + } + + u64 total_size = 0; + const auto callback = [recursion, &total_size](u64* /*num_entries_out*/, + const std::string& directory, + const std::string& virtual_name) -> bool { + if (IsDirectory(directory + virtual_name)) { + if (recursion == 0) { + LOG_WARNING(Common_FileSystem, "directory tree too deep"); + return true; + } + total_size += GetDirectoryTreeSize(directory + virtual_name, recursion - 1); + } else { // is a file + total_size += GetSize(directory + virtual_name); + } + + return true; + }; + + return ForeachDirectoryEntry(nullptr, real_path, callback) ? total_size : 0; +} + bool CreateEmptyFile(const std::string& filename) { LOG_TRACE(Common_Filesystem, "{}", filename); diff --git a/src/common/file_util.h b/src/common/file_util.h index ad44c0f..0fe24b9 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -59,6 +59,9 @@ u64 GetSize(const int fd); // Overloaded GetSize, accepts FILE* u64 GetSize(FILE* f); +// Returns the size of a directory tree +u64 GetDirectoryTreeSize(const std::string& path, unsigned int recursion = 256); + // Returns true if successful, or path already exists. bool CreateDir(const std::string& filename); diff --git a/src/common/logging/log.h b/src/common/logging/log.h index 4aa6838..bd42654 100644 --- a/src/common/logging/log.h +++ b/src/common/logging/log.h @@ -30,7 +30,7 @@ void PrintLog(std::FILE* f, const std::string& log_class, const std::string& lev us / 1000000.0, real_class, level, file, line, func, args...); fflush(stderr); } catch (...) { - std::cerr << "FMT failed with exception" << std::endl; + std::cerr << "(unexpected) fmt failed with exception" << std::endl; } } diff --git a/src/common/scope_exit.h b/src/common/scope_exit.h new file mode 100644 index 0000000..1176a72 --- /dev/null +++ b/src/common/scope_exit.h @@ -0,0 +1,44 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/common_funcs.h" + +namespace detail { +template +struct ScopeExitHelper { + explicit ScopeExitHelper(Func&& func) : func(std::move(func)) {} + ~ScopeExitHelper() { + func(); + } + + Func func; +}; + +template +ScopeExitHelper ScopeExit(Func&& func) { + return ScopeExitHelper(std::forward(func)); +} +} // namespace detail + +/** + * This macro allows you to conveniently specify a block of code that will run on scope exit. Handy + * for doing ad-hoc clean-up tasks in a function with multiple returns. + * + * Example usage: + * \code + * const int saved_val = g_foo; + * g_foo = 55; + * SCOPE_EXIT({ g_foo = saved_val; }); + * + * if (Bar()) { + * return 0; + * } else { + * return 20; + * } + * \endcode + */ +#define SCOPE_EXIT(body) auto CONCAT2(scope_exit_helper_, __LINE__) = detail::ScopeExit([&]() body) diff --git a/src/core/importer.cpp b/src/core/importer.cpp index 04213b9..70372bb 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -80,7 +80,10 @@ bool SDMCImporter::ImportTitle(u64 id) { } return decryptor->DecryptAndWriteFile( "/" + path + virtual_name, - FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + path + virtual_name); + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo " + "3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + + path + virtual_name); }); } @@ -91,7 +94,9 @@ bool SDMCImporter::ImportSavegame(u64 id) { return false; } - return save.Extract(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + path); + return save.Extract( + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + path); } bool SDMCImporter::ImportExtdata(u64 id) { @@ -101,7 +106,9 @@ bool SDMCImporter::ImportExtdata(u64 id) { return false; } - return extdata.Extract(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + path); + return extdata.Extract( + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + path); } bool SDMCImporter::ImportSysdata(u64 id) { @@ -130,7 +137,7 @@ bool SDMCImporter::ImportSysdata(u64 id) { } return FileUtil::Copy( directory + virtual_name, - fmt::format("{}title/00040138/{}/content/{}", + fmt::format("{}00000000000000000000000000000000/title/00040138/{}/content/{}", FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (is_new_3ds ? "20000003" : "00000003"), virtual_name)); }); @@ -167,16 +174,20 @@ void SDMCImporter::ListTitle(std::vector& out) const { nullptr, fmt::format("{}title/{:08x}/", sdmc_path, high_id), [type, high_id, &out](u64* /*num_entries_out*/, const std::string& directory, const std::string& virtual_name) { - if (!FileUtil::IsDirectory(directory + virtual_name)) { + if (!FileUtil::IsDirectory(directory + virtual_name + "/")) { return true; } const u64 id = (high_id << 32) + std::stoull(virtual_name, nullptr, 16); const auto citra_path = fmt::format( - "{}title/{:08x}/{}/", FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), - high_id, virtual_name); + "{}Nintendo " + "3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/" + "{:08x}/{}/", + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), high_id, virtual_name); if (FileUtil::Exists(directory + virtual_name + "/content/")) { - out.push_back({type, id, FileUtil::Exists(citra_path + "content/")}); + out.push_back( + {type, id, FileUtil::Exists(citra_path + "content/"), + FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/")}); } if (type != ContentType::Application) { @@ -184,7 +195,8 @@ void SDMCImporter::ListTitle(std::vector& out) const { } if (FileUtil::Exists(directory + virtual_name + "/data/")) { out.push_back( - {ContentType::Savegame, id, FileUtil::Exists(citra_path + "data/")}); + {ContentType::Savegame, id, FileUtil::Exists(citra_path + "data/"), + FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/data/")}); } return true; }); @@ -200,32 +212,38 @@ void SDMCImporter::ListExtdata(std::vector& out) const { nullptr, fmt::format("{}extdata/00000000/", config.sdmc_path), [&out](u64* /*num_entries_out*/, const std::string& directory, const std::string& virtual_name) { - if (!FileUtil::IsDirectory(directory + virtual_name)) { + if (!FileUtil::IsDirectory(directory + virtual_name + "/")) { return true; } const u64 id = std::stoull(virtual_name, nullptr, 16); const auto citra_path = - fmt::format("{}extdata/00000000/{}", + fmt::format("{}Nintendo " + "3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + "extdata/00000000/{}", FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), virtual_name); - out.push_back({ContentType::Extdata, id, FileUtil::Exists(citra_path)}); + out.push_back({ContentType::Extdata, id, FileUtil::Exists(citra_path), + FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/")}); return true; }); } void SDMCImporter::ListSysdata(std::vector& out) const { -#define CHECK_CONTENT(id, var_path, citra_path) \ +#define CHECK_CONTENT(id, var_path, citra_path, display_name) \ if (!var_path.empty()) { \ - out.push_back({ContentType::Sysdata, id, FileUtil::Exists(citra_path)}); \ + out.push_back({ContentType::Sysdata, id, FileUtil::Exists(citra_path), \ + FileUtil::GetSize(var_path), display_name}); \ } { const auto sysdata_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir); - CHECK_CONTENT(0, config.bootrom_path, sysdata_path + BOOTROM9); - CHECK_CONTENT(2, config.seed_db_path, sysdata_path + SEED_DB); - CHECK_CONTENT(3, config.secret_sector_path, sysdata_path + SECRET_SECTOR); + CHECK_CONTENT(0, config.bootrom_path, sysdata_path + BOOTROM9, BOOTROM9); + CHECK_CONTENT(2, config.seed_db_path, sysdata_path + SEED_DB, SEED_DB); + CHECK_CONTENT(3, config.secret_sector_path, sysdata_path + SECRET_SECTOR, SECRET_SECTOR); } +#undef CHECK_CONTENT + do { if (config.safe_mode_firm_path.empty()) { break; @@ -240,13 +258,15 @@ void SDMCImporter::ListSysdata(std::vector& out) const { break; } - const auto citra_path = fmt::format("{}title/00040138/{}/content/", - FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), - (is_new ? "20000003" : "00000003")); - CHECK_CONTENT(1, config.safe_mode_firm_path, citra_path); + const auto citra_path = fmt::format( + "{}00000000000000000000000000000000/title/00040138/{}/content/", + FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (is_new ? "20000003" : "00000003")); + if (!config.safe_mode_firm_path.empty()) { + out.push_back({ContentType::Sysdata, 1, FileUtil::Exists(citra_path), + FileUtil::GetDirectoryTreeSize(config.safe_mode_firm_path), + "Safe mode firm"}); + } } while (0); - -#undef CHECK_CONTENT } std::vector LoadPresetConfig(std::string mount_point) { @@ -288,7 +308,7 @@ std::vector LoadPresetConfig(std::string mount_point) { [&id_regex, &config_template, &out](u64* /*num_entries_out*/, const std::string& directory, const std::string& virtual_name) { - if (!FileUtil::IsDirectory(directory + virtual_name)) { + if (!FileUtil::IsDirectory(directory + virtual_name + "/")) { return true; } diff --git a/src/core/importer.h b/src/core/importer.h index 5a495db..2aa4fce 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -33,6 +33,8 @@ struct ContentSpecifier { ContentType type; u64 id; bool already_exists; ///< Tells whether a file already exists in target path. + u64 maximum_size; ///< The maximum size of the content. May be slightly bigger than real size. + std::string name; ///< Optional. The content's preferred display name. }; /** diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index 2a369f8..6300da7 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -7,6 +7,9 @@ if (POLICY CMP0071) endif() add_executable(threeSD + import_dialog.cpp + import_dialog.h + import_dialog.ui main.cpp main.h main.ui @@ -47,6 +50,9 @@ target_compile_definitions(threeSD PRIVATE # Disable implicit QString->QUrl conversions to enforce use of proper resolving functions. -DQT_NO_URL_CAST_FROM_STRING + + # Disable automatic conversions from 8-bit strings (char *) to unicode QStrings + -DQT_NO_CAST_FROM_ASCII ) if (MSVC) diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp new file mode 100644 index 0000000..1165cb6 --- /dev/null +++ b/src/frontend/import_dialog.cpp @@ -0,0 +1,174 @@ +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "common/scope_exit.h" +#include "frontend/import_dialog.h" +#include "ui_import_dialog.h" + +QString ReadableByteSize(qulonglong size) { + static const std::array units = {QT_TR_NOOP("B"), QT_TR_NOOP("KiB"), + QT_TR_NOOP("MiB"), QT_TR_NOOP("GiB"), + QT_TR_NOOP("TiB"), QT_TR_NOOP("PiB")}; + if (size == 0) + return QStringLiteral("0"); + int digit_groups = std::min(static_cast(std::log10(size) / std::log10(1024)), + static_cast(units.size())); + return QStringLiteral("%L1 %2") + .arg(size / std::pow(1024, digit_groups), 0, 'f', 1) + .arg(QObject::tr(units[digit_groups], "ImportDialog")); +} + +ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config) + : QDialog(parent), ui(std::make_unique()), user_path(config.user_path), + importer(config) { + + ui->setupUi(this); + if (!importer.IsGood()) { + QMessageBox::critical( + this, tr("Importer Error"), + tr("Failed to initalize the importer.\nRefer to the log for details.")); + reject(); + } + + PopulateContent(); + UpdateSizeDisplay(); +} + +ImportDialog::~ImportDialog() = default; + +void ImportDialog::PopulateContent() { + contents = importer.ListContent(); + ui->main->clear(); + ui->main->setSortingEnabled(false); + + const std::map content_type_map{ + {Core::ContentType::Application, QStringLiteral("Application")}, + {Core::ContentType::Update, QStringLiteral("Update")}, + {Core::ContentType::DLC, QStringLiteral("DLC (Add-on Content)")}, + {Core::ContentType::Savegame, QStringLiteral("Save Data")}, + {Core::ContentType::Extdata, QStringLiteral("Extra Data")}, + {Core::ContentType::Sysdata, QStringLiteral("System Data")}, + }; + + for (const auto& [type, name] : content_type_map) { + auto* checkBox = new QCheckBox(); + checkBox->setText(name); + checkBox->setStyleSheet(QStringLiteral("margin-left:7px")); + checkBox->setTristate(true); + checkBox->setProperty("previousState", static_cast(Qt::Unchecked)); + + auto* item = new QTreeWidgetItem; + item->setFirstColumnSpanned(true); + ui->main->invisibleRootItem()->addChild(item); + + connect(checkBox, &QCheckBox::stateChanged, [this, checkBox, item](int state) { + SCOPE_EXIT({ checkBox->setProperty("previousState", state); }); + + if (program_trigger) { + program_trigger = false; + return; + } + + if (state == Qt::PartiallyChecked) { + if (checkBox->property("previousState").toInt() == Qt::Unchecked) { + checkBox->setCheckState(static_cast(state = Qt::Checked)); + } else { + checkBox->setCheckState(static_cast(state = Qt::Unchecked)); + } + return; + } + + program_trigger = true; + for (int i = 0; i < item->childCount(); ++i) { + static_cast(ui->main->itemWidget(item->child(i), 0)) + ->setCheckState(static_cast(state)); + } + program_trigger = false; + }); + + ui->main->setItemWidget(item, 0, checkBox); + } + + for (const auto& content : contents) { + auto* checkBox = new QCheckBox(); + checkBox->setStyleSheet(QStringLiteral("margin-left:7px")); + + auto* item = new QTreeWidgetItem{ + {QString{}, + content.name.empty() ? QStringLiteral("0x%1").arg(content.id, 16, 16, QLatin1Char('0')) + : QString::fromStdString(content.name), + ReadableByteSize(content.maximum_size), + content.already_exists ? QStringLiteral("Yes") : QStringLiteral("No")}}; + + ui->main->invisibleRootItem()->child(static_cast(content.type))->addChild(item); + ui->main->setItemWidget(item, 0, checkBox); + + connect(checkBox, &QCheckBox::stateChanged, + [this, item, size = content.maximum_size](int state) { + if (state == Qt::Checked) { + total_size += size; + } else { + total_size -= size; + } + UpdateSizeDisplay(); + + if (!program_trigger) { + UpdateItemCheckState(item->parent()); + } + }); + } + + ui->main->setSortingEnabled(true); +} + +void ImportDialog::UpdateSizeDisplay() { + QStorageInfo storage(QString::fromStdString(user_path)); + if (!storage.isValid() || !storage.isReady()) { + LOG_ERROR(Frontend, "Storage {} is not good", user_path); + QMessageBox::critical( + this, tr("Bad Storage"), + tr("An error occured while trying to get available space for the storage.\nPlease " + "ensure that your SD card is well connected and try again.")); + reject(); + } + + ui->availableSpace->setText( + tr("Available Space: %1").arg(ReadableByteSize(storage.bytesAvailable()))); + ui->totalSize->setText(tr("Total Size: %1").arg(ReadableByteSize(total_size))); + + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) + ->setEnabled(total_size <= static_cast(storage.bytesAvailable())); +} + +void ImportDialog::UpdateItemCheckState(QTreeWidgetItem* item) { + bool has_checked = false, has_unchecked = false; + auto* item_checkBox = static_cast(ui->main->itemWidget(item, 0)); + for (int i = 0; i < item->childCount(); ++i) { + auto* checkBox = static_cast(ui->main->itemWidget(item->child(i), 0)); + if (checkBox->isChecked()) { + has_checked = true; + } else { + has_unchecked = true; + } + if (has_checked && has_unchecked) { + program_trigger = true; + item_checkBox->setCheckState(Qt::PartiallyChecked); + program_trigger = false; + return; + } + } + program_trigger = true; + if (has_checked) { + item_checkBox->setCheckState(Qt::Checked); + } else { + item_checkBox->setCheckState(Qt::Unchecked); + } + program_trigger = false; +} diff --git a/src/frontend/import_dialog.h b/src/frontend/import_dialog.h new file mode 100644 index 0000000..04f6394 --- /dev/null +++ b/src/frontend/import_dialog.h @@ -0,0 +1,41 @@ +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "core/importer.h" + +class QTreeWidgetItem; + +namespace Ui { +class ImportDialog; +} + +class ImportDialog : public QDialog { + Q_OBJECT; + +public: + explicit ImportDialog(QWidget* parent, const Core::Config& config); + ~ImportDialog() override; + +private: + void PopulateContent(); + void UpdateSizeDisplay(); + void UpdateItemCheckState(QTreeWidgetItem* item); + + std::unique_ptr ui; + + std::string user_path; + Core::SDMCImporter importer; + std::vector contents; + u64 total_size = 0; + + // HACK: To tell whether the checkbox state change is a programmatic trigger + // TODO: Is there a more elegant way of doing the same? + bool program_trigger = false; +}; diff --git a/src/frontend/import_dialog.ui b/src/frontend/import_dialog.ui new file mode 100644 index 0000000..0e06ee6 --- /dev/null +++ b/src/frontend/import_dialog.ui @@ -0,0 +1,77 @@ + + + ImportDialog + + + + 0 + 0 + 600 + 400 + + + + Select Contents + + + + + + + + + + + + Name + + + + + Size + + + + + Imported + + + + + + + + + + Available Space: + + + + + + + Qt::Horizontal + + + + + + + Total Size: + + + + + + + + + QDialogButtonBox::Ok|QDialogButtonBox::Cancel + + + + + + + + diff --git a/src/frontend/main.cpp b/src/frontend/main.cpp index b769aea..9fa835e 100644 --- a/src/frontend/main.cpp +++ b/src/frontend/main.cpp @@ -7,6 +7,7 @@ #include #include #include "common/file_util.h" +#include "frontend/import_dialog.h" #include "frontend/main.h" #include "ui_main.h" @@ -34,6 +35,9 @@ MainDialog::MainDialog(QWidget* parent) : QDialog(parent), ui(std::make_uniquebuttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) { if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)) { LoadPresetConfig(); + } else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) { + ImportDialog dialog(this, GetCurrentConfig()); + dialog.exec(); } }); @@ -124,10 +128,27 @@ void MainDialog::HideAdvanced() { adjustSize(); } +Core::Config MainDialog::GetCurrentConfig() { + if (ui->customGroupBox->isVisible()) { + Core::Config config{ + /*sdmc_path*/ ui->sdmcPath->text().toStdString(), + /*user_path*/ ui->userPath->text().toStdString(), + /*movable_sed_path*/ ui->movableSedPath->text().toStdString(), + /*bootrom_path*/ ui->bootrom9Path->text().toStdString(), + /*safe_mode_firm_path*/ ui->safeModeFirmPath->text().toStdString(), + /*seed_db_path*/ ui->seeddbPath->text().toStdString(), + /*secret_sector_path*/ ui->secretSectorPath->text().toStdString(), + }; + return config; + } else { + return preset_config_list[ui->configSelect->currentIndex()]; + } +} + int main(int argc, char* argv[]) { // Init settings params - QCoreApplication::setOrganizationName("zhaowenlan1779"); - QCoreApplication::setApplicationName("threeSD"); + QCoreApplication::setOrganizationName(QStringLiteral("zhaowenlan1779")); + QCoreApplication::setApplicationName(QStringLiteral("threeSD")); #ifdef __APPLE__ std::string bin_path = FileUtil::GetBundleDirectory() + DIR_SEP + ".."; diff --git a/src/frontend/main.h b/src/frontend/main.h index cc703c1..d33231b 100644 --- a/src/frontend/main.h +++ b/src/frontend/main.h @@ -23,6 +23,7 @@ private: void LoadPresetConfig(); void ShowAdvanced(); void HideAdvanced(); + Core::Config GetCurrentConfig(); std::vector preset_config_list; std::unique_ptr ui;