frontend: Allow selection of views

This commit is contained in:
zhupengfei
2019-09-28 23:03:06 +08:00
parent 479dd327df
commit 9f9d1d85db
4 changed files with 171 additions and 111 deletions
+2 -1
View File
@@ -46,7 +46,8 @@ Make sure the SD card is properly recognized and shows up as a disk.
1. Launch threeSD. You should see a small dialog, which has your SD card as an auto-detected configuration.
* If it does not show up and the combo box says `None`, you should check if you can really find your SD card in the explorer (aka. `My Computer`), whether the drive for your SD card is accessible, and whether it contains the `Nintendo 3DS` and `threeSD` folders.
1. Click `OK`. After a few seconds of loading, you should see the `Select Contents` dialog. Select the contents you would like to import. By default, contents that do not currently exist is selected. Make sure the total size of your selected contents do not exceed the available space on your disk.
* `System Data` (which resides in `Ungrouped`) contains important data that is necessary for your imported games to run. You should definitely import the contents there, if they do not exist yet.
* You can select between `Title View` which organizes contents by title, and `Group View` which organizes contents by type (application, save data, etc).
* `System Data` (which resides in `Ungrouped` when using `Title View`) contains important data that is necessary for your imported games to run. You should definitely import the contents there, if they do not exist yet.
1. After you've finished your selection, click `OK`. You should now see a progress dialog; wait a while until your contents are imported.
* The time will depend on how big your contents are, as well as your CPU processing power and (mainly) disk I/O speeds.
+140 -110
View File
@@ -31,14 +31,14 @@ QString ReadableByteSize(qulonglong size) {
.arg(QObject::tr(units[digit_groups], "ImportDialog"));
}
static const std::map<Core::ContentType, const char*> ContentTypeMap{
static constexpr std::array<std::pair<Core::ContentType, const char*>, 6> ContentTypeMap{{
{Core::ContentType::Application, QT_TR_NOOP("Application")},
{Core::ContentType::Update, QT_TR_NOOP("Update")},
{Core::ContentType::DLC, QT_TR_NOOP("DLC")},
{Core::ContentType::Savegame, QT_TR_NOOP("Save Data")},
{Core::ContentType::Extdata, QT_TR_NOOP("Extra Data")},
{Core::ContentType::Sysdata, QT_TR_NOOP("System Data")},
};
}};
QString GetContentName(const Core::ContentSpecifier& specifier) {
return specifier.name.empty()
@@ -46,6 +46,10 @@ QString GetContentName(const Core::ContentSpecifier& specifier) {
: QString::fromStdString(specifier.name);
}
QString GetContentTypeName(Core::ContentType type) {
return QObject::tr(ContentTypeMap.at(static_cast<std::size_t>(type)).second, "ImportDialog");
}
ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
: QDialog(parent), ui(std::make_unique<Ui::ImportDialog>()), user_path(config.user_path),
importer(config) {
@@ -61,6 +65,8 @@ ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
reject();
}
ui->title_view_button->setChecked(true);
ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)->setText(tr("Refresh"));
connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) {
if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) {
@@ -72,6 +78,8 @@ ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
}
});
connect(ui->title_view_button, &QRadioButton::toggled, this, &ImportDialog::RepopulateContent);
RelistContent();
UpdateSizeDisplay();
@@ -91,134 +99,156 @@ void ImportDialog::RelistContent() {
dialog->setMinimumDuration(0);
dialog->setValue(0);
using FutureWatcher = QFutureWatcher<std::vector<Core::ContentSpecifier>>;
using FutureWatcher = QFutureWatcher<void>;
auto* future_watcher = new FutureWatcher(this);
connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] {
dialog->hide();
RepopulateContent();
});
auto future =
QtConcurrent::run([& importer = this->importer] { return importer.ListContent(); });
auto future = QtConcurrent::run([& contents = this->contents, &importer = this->importer] {
contents = importer.ListContent();
});
future_watcher->setFuture(future);
}
void ImportDialog::InsertTopLevelItem(const QString& text) {
auto* checkBox = new QCheckBox();
checkBox->setText(text);
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
checkBox->setTristate(true);
checkBox->setProperty("previousState", static_cast<int>(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<Qt::CheckState>(state = Qt::Checked));
} else {
checkBox->setCheckState(static_cast<Qt::CheckState>(state = Qt::Unchecked));
}
return;
}
program_trigger = true;
for (int i = 0; i < item->childCount(); ++i) {
static_cast<QCheckBox*>(ui->main->itemWidget(item->child(i), 0))
->setCheckState(static_cast<Qt::CheckState>(state));
}
program_trigger = false;
});
ui->main->setItemWidget(item, 0, checkBox);
}
void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpecifier& content,
std::size_t id) {
auto* checkBox = new QCheckBox();
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
// HACK: The checkbox is used to record ID. Is there a better way?
checkBox->setProperty("id", id);
const QString name = (row == 0 ? QStringLiteral("%1 (%2)")
.arg(GetContentName(content))
.arg(GetContentTypeName(content.type))
: GetContentTypeName(content.type));
auto* item = new QTreeWidgetItem{
{QString{}, name, ReadableByteSize(content.maximum_size),
content.already_exists ? QStringLiteral("Yes") : QStringLiteral("No")}};
ui->main->invisibleRootItem()->child(row)->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());
}
});
if (!content.already_exists) {
checkBox->setChecked(true);
}
}
void ImportDialog::RepopulateContent() {
total_size = 0;
contents = importer.ListContent();
ui->main->clear();
ui->main->setSortingEnabled(false);
std::map<u64, QString> title_name_map; // title ID -> title name
std::unordered_map<u64, u64> extdata_id_map; // extdata ID -> title ID
for (const auto& content : contents) {
if (content.type == Core::ContentType::Application) {
title_name_map.emplace(content.id, GetContentName(content));
extdata_id_map.emplace(content.extdata_id, content.id);
const bool use_title_view = ui->title_view_button->isChecked();
if (use_title_view) {
std::map<u64, QString> title_name_map; // title ID -> title name
std::unordered_map<u64, u64> extdata_id_map; // extdata ID -> title ID
for (const auto& content : contents) {
if (content.type == Core::ContentType::Application) {
title_name_map.emplace(content.id, GetContentName(content));
extdata_id_map.emplace(content.extdata_id, content.id);
}
}
}
title_name_map.insert_or_assign(0, tr("Ungrouped"));
title_name_map.insert_or_assign(0, tr("Ungrouped"));
std::unordered_map<u64, u64> title_row_map;
for (const auto& [id, name] : title_name_map) {
auto* checkBox = new QCheckBox();
checkBox->setText(name);
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
checkBox->setTristate(true);
checkBox->setProperty("previousState", static_cast<int>(Qt::Unchecked));
std::unordered_map<u64, u64> title_row_map;
for (const auto& [id, name] : title_name_map) {
InsertTopLevelItem(name);
title_row_map[id] = ui->main->invisibleRootItem()->childCount() - 1;
}
auto* item = new QTreeWidgetItem;
item->setFirstColumnSpanned(true);
ui->main->invisibleRootItem()->addChild(item);
title_row_map[id] = ui->main->invisibleRootItem()->childCount() - 1;
for (std::size_t i = 0; i < contents.size(); ++i) {
const auto& content = contents[i];
connect(checkBox, &QCheckBox::stateChanged, [this, checkBox, item](int state) {
SCOPE_EXIT({ checkBox->setProperty("previousState", state); });
if (program_trigger) {
program_trigger = false;
return;
std::size_t row = title_row_map.at(0);
switch (content.type) {
case Core::ContentType::Application:
case Core::ContentType::Update:
case Core::ContentType::DLC:
case Core::ContentType::Savegame: {
// Fix the id
const auto real_id = content.id & 0xffffff00ffffffff;
row =
title_row_map.count(real_id) ? title_row_map.at(real_id) : title_row_map.at(0);
break;
}
case Core::ContentType::Extdata: {
const auto real_id =
extdata_id_map.count(content.id) ? extdata_id_map.at(content.id) : 0;
row = title_row_map.at(real_id);
break;
}
case Core::ContentType::Sysdata: {
row = title_row_map.at(0);
break;
}
}
if (state == Qt::PartiallyChecked) {
if (checkBox->property("previousState").toInt() == Qt::Unchecked) {
checkBox->setCheckState(static_cast<Qt::CheckState>(state = Qt::Checked));
} else {
checkBox->setCheckState(static_cast<Qt::CheckState>(state = Qt::Unchecked));
}
return;
}
program_trigger = true;
for (int i = 0; i < item->childCount(); ++i) {
static_cast<QCheckBox*>(ui->main->itemWidget(item->child(i), 0))
->setCheckState(static_cast<Qt::CheckState>(state));
}
program_trigger = false;
});
ui->main->setItemWidget(item, 0, checkBox);
}
for (std::size_t i = 0; i < contents.size(); ++i) {
const auto& content = contents[i];
auto* checkBox = new QCheckBox();
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
// HACK: The checkbox is used to record ID. Is there a better way?
checkBox->setProperty("id", i);
std::size_t row = title_row_map.at(0);
switch (content.type) {
case Core::ContentType::Application:
case Core::ContentType::Update:
case Core::ContentType::DLC:
case Core::ContentType::Savegame: {
// Fix the id
const auto real_id = content.id & 0xffffff00ffffffff;
row = title_row_map.count(real_id) ? title_row_map.at(real_id) : title_row_map.at(0);
break;
}
case Core::ContentType::Extdata: {
const auto real_id =
extdata_id_map.count(content.id) ? extdata_id_map.at(content.id) : 0;
row = title_row_map.at(real_id);
break;
}
case Core::ContentType::Sysdata: {
row = title_row_map.at(0);
break;
InsertSecondLevelItem(row, content, i);
}
} else {
for (const auto& [type, name] : ContentTypeMap) {
InsertTopLevelItem(tr(name));
}
const QString name = (row == 0 ? QStringLiteral("%1 (%2)")
.arg(GetContentName(content))
.arg(tr(ContentTypeMap.at(content.type)))
: tr(ContentTypeMap.at(content.type)));
auto* item = new QTreeWidgetItem{
{QString{}, name, ReadableByteSize(content.maximum_size),
content.already_exists ? QStringLiteral("Yes") : QStringLiteral("No")}};
ui->main->invisibleRootItem()->child(row)->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());
}
});
if (!content.already_exists) {
checkBox->setChecked(true);
for (std::size_t i = 0; i < contents.size(); ++i) {
const auto& content = contents[i];
InsertSecondLevelItem(static_cast<std::size_t>(content.type), content, i);
}
}
@@ -318,7 +348,7 @@ void ImportDialog::StartImporting() {
.arg(count)
.arg(total_count)
.arg(GetContentName(next_content))
.arg(tr(ContentTypeMap.at(next_content.type))));
.arg(GetContentTypeName(next_content.type)));
current_content = next_content;
current_count = count;
});
@@ -330,7 +360,7 @@ void ImportDialog::StartImporting() {
.arg(current_count)
.arg(total_count)
.arg(GetContentName(current_content))
.arg(tr(ContentTypeMap.at(current_content.type)))
.arg(GetContentTypeName(current_content.type))
.arg(ReadableByteSize(current_size_imported))
.arg(ReadableByteSize(current_content.maximum_size)));
});
@@ -339,7 +369,7 @@ void ImportDialog::StartImporting() {
QMessageBox::critical(this, tr("Error"),
tr("Failed to import content %1 (%2)!")
.arg(GetContentName(current_content))
.arg(tr(ContentTypeMap.at(current_content.type))));
.arg(GetContentTypeName(current_content.type)));
dialog->hide();
});
connect(job, &ImportJob::Completed, this, [this, dialog] {
+4
View File
@@ -31,6 +31,10 @@ private:
std::vector<Core::ContentSpecifier> GetSelectedContentList();
void StartImporting();
void InsertTopLevelItem(const QString& text);
void InsertSecondLevelItem(std::size_t row, const Core::ContentSpecifier& content,
std::size_t id);
std::unique_ptr<Ui::ImportDialog> ui;
std::string user_path;
+25
View File
@@ -14,6 +14,31 @@
<string>Select Contents</string>
</property>
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</spacer>
</item>
<item>
<widget class="QRadioButton" name="title_view_button">
<property name="text">
<string>Title View</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="group_view_button">
<property name="text">
<string>Group View</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeWidget" name="main">
<column>