Merge pull request #8996 from AdmiralCurtiss/memcard-save-import-export-refactor

Various improvements for the Memory Card Manager.
This commit is contained in:
JMC47
2021-03-03 14:51:31 -05:00
committed by GitHub
8 changed files with 834 additions and 476 deletions

View File

@ -5,6 +5,11 @@
#include "DolphinQt/GCMemcardManager.h"
#include <algorithm>
#include <cassert>
#include <string>
#include <vector>
#include <fmt/format.h>
#include <QDialogButtonBox>
#include <QDir>
@ -15,25 +20,41 @@
#include <QImage>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QPixmap>
#include <QPushButton>
#include <QString>
#include <QStringList>
#include <QTableWidget>
#include <QTimer>
#include <QToolButton>
#include "Common/CommonPaths.h"
#include "Common/Config/Config.h"
#include "Common/FileUtil.h"
#include "Common/MsgHandler.h"
#include "Common/StringUtil.h"
#include "Common/VariantUtil.h"
#include "Core/Config/MainSettings.h"
#include "Core/HW/GCMemcard/GCMemcard.h"
#include "Core/HW/GCMemcard/GCMemcardUtils.h"
#include "DolphinQt/GCMemcardCreateNewDialog.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
constexpr float ROW_HEIGHT = 28;
constexpr int ROW_HEIGHT = 36;
constexpr int COLUMN_WIDTH_FILENAME = 100;
constexpr int COLUMN_WIDTH_BANNER = Memcard::MEMORY_CARD_BANNER_WIDTH + 6;
constexpr int COLUMN_WIDTH_TEXT = 160;
constexpr int COLUMN_WIDTH_ICON = Memcard::MEMORY_CARD_ICON_WIDTH + 6;
constexpr int COLUMN_WIDTH_BLOCKS = 40;
constexpr int COLUMN_INDEX_FILENAME = 0;
constexpr int COLUMN_INDEX_BANNER = 1;
constexpr int COLUMN_INDEX_TEXT = 2;
constexpr int COLUMN_INDEX_ICON = 3;
constexpr int COLUMN_INDEX_BLOCKS = 4;
constexpr int COLUMN_COUNT = 5;
struct GCMemcardManager::IconAnimationData
{
@ -78,11 +99,20 @@ void GCMemcardManager::CreateWidgets()
// Actions
m_select_button = new QPushButton;
m_copy_button = new QPushButton;
// Contents will be set by their appropriate functions
m_delete_button = new QPushButton(tr("&Delete"));
m_export_button = new QPushButton(tr("&Export..."));
m_export_all_button = new QPushButton(tr("Export &All..."));
m_export_button = new QToolButton(this);
m_export_menu = new QMenu(m_export_button);
m_export_gci_action = new QAction(tr("&Export as .gci..."), m_export_menu);
m_export_gcs_action = new QAction(tr("Export as .&gcs..."), m_export_menu);
m_export_sav_action = new QAction(tr("Export as .&sav..."), m_export_menu);
m_export_menu->addAction(m_export_gci_action);
m_export_menu->addAction(m_export_gcs_action);
m_export_menu->addAction(m_export_sav_action);
m_export_button->setDefaultAction(m_export_gci_action);
m_export_button->setPopupMode(QToolButton::MenuButtonPopup);
m_export_button->setMenu(m_export_menu);
m_import_button = new QPushButton(tr("&Import..."));
m_fix_checksums_button = new QPushButton(tr("Fix Checksums"));
@ -100,8 +130,25 @@ void GCMemcardManager::CreateWidgets()
m_slot_table[i]->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_slot_table[i]->setSelectionBehavior(QAbstractItemView::SelectRows);
m_slot_table[i]->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_slot_table[i]->setSortingEnabled(true);
m_slot_table[i]->horizontalHeader()->setHighlightSections(false);
m_slot_table[i]->horizontalHeader()->setMinimumSectionSize(0);
m_slot_table[i]->horizontalHeader()->setSortIndicatorShown(true);
m_slot_table[i]->setColumnCount(COLUMN_COUNT);
m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_FILENAME,
new QTableWidgetItem(tr("Filename")));
m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_BANNER,
new QTableWidgetItem(tr("Banner")));
m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_TEXT, new QTableWidgetItem(tr("Title")));
m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_ICON, new QTableWidgetItem(tr("Icon")));
m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_BLOCKS,
new QTableWidgetItem(tr("Blocks")));
m_slot_table[i]->setColumnWidth(COLUMN_INDEX_FILENAME, COLUMN_WIDTH_FILENAME);
m_slot_table[i]->setColumnWidth(COLUMN_INDEX_BANNER, COLUMN_WIDTH_BANNER);
m_slot_table[i]->setColumnWidth(COLUMN_INDEX_TEXT, COLUMN_WIDTH_TEXT);
m_slot_table[i]->setColumnWidth(COLUMN_INDEX_ICON, COLUMN_WIDTH_ICON);
m_slot_table[i]->setColumnWidth(COLUMN_INDEX_BLOCKS, COLUMN_WIDTH_BLOCKS);
m_slot_table[i]->verticalHeader()->setDefaultSectionSize(ROW_HEIGHT);
m_slot_table[i]->verticalHeader()->hide();
m_slot_table[i]->setShowGrid(false);
@ -114,7 +161,7 @@ void GCMemcardManager::CreateWidgets()
slot_layout->addWidget(m_slot_table[i], 1, 0, 1, 3);
slot_layout->addWidget(m_slot_stat_label[i], 2, 0);
layout->addWidget(m_slot_group[i], 0, i * 2, 9, 1);
layout->addWidget(m_slot_group[i], 0, i * 2, 8, 1);
UpdateSlotTable(i);
}
@ -123,10 +170,9 @@ void GCMemcardManager::CreateWidgets()
layout->addWidget(m_copy_button, 2, 1);
layout->addWidget(m_delete_button, 3, 1);
layout->addWidget(m_export_button, 4, 1);
layout->addWidget(m_export_all_button, 5, 1);
layout->addWidget(m_import_button, 6, 1);
layout->addWidget(m_fix_checksums_button, 7, 1);
layout->addWidget(m_button_box, 9, 2);
layout->addWidget(m_import_button, 5, 1);
layout->addWidget(m_fix_checksums_button, 6, 1);
layout->addWidget(m_button_box, 8, 2);
setLayout(layout);
}
@ -135,8 +181,12 @@ void GCMemcardManager::ConnectWidgets()
{
connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_select_button, &QPushButton::clicked, [this] { SetActiveSlot(!m_active_slot); });
connect(m_export_button, &QPushButton::clicked, [this] { ExportFiles(true); });
connect(m_export_all_button, &QPushButton::clicked, this, &GCMemcardManager::ExportAllFiles);
connect(m_export_gci_action, &QAction::triggered,
[this] { ExportFiles(Memcard::SavefileFormat::GCI); });
connect(m_export_gcs_action, &QAction::triggered,
[this] { ExportFiles(Memcard::SavefileFormat::GCS); });
connect(m_export_sav_action, &QAction::triggered,
[this] { ExportFiles(Memcard::SavefileFormat::SAV); });
connect(m_delete_button, &QPushButton::clicked, this, &GCMemcardManager::DeleteFiles);
connect(m_import_button, &QPushButton::clicked, this, &GCMemcardManager::ImportFile);
connect(m_copy_button, &QPushButton::clicked, this, &GCMemcardManager::CopyFiles);
@ -188,67 +238,69 @@ void GCMemcardManager::SetActiveSlot(int slot)
void GCMemcardManager::UpdateSlotTable(int slot)
{
m_slot_active_icons[slot].clear();
m_slot_table[slot]->clear();
m_slot_table[slot]->setColumnCount(6);
m_slot_table[slot]->verticalHeader()->setDefaultSectionSize(ROW_HEIGHT);
m_slot_table[slot]->verticalHeader()->setDefaultSectionSize(QHeaderView::Fixed);
m_slot_table[slot]->setHorizontalHeaderLabels(
{tr("Banner"), tr("Title"), tr("Comment"), tr("Icon"), tr("Blocks"), tr("First Block")});
if (m_slot_memcard[slot] == nullptr)
{
m_slot_table[slot]->setRowCount(0);
m_slot_stat_label[slot]->clear();
return;
}
auto& memcard = m_slot_memcard[slot];
auto* table = m_slot_table[slot];
const auto create_item = [](const QString& string = {}) {
QTableWidgetItem* item = new QTableWidgetItem(string);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
return item;
};
table->setSortingEnabled(false);
const u8 num_files = memcard->GetNumFiles();
m_slot_active_icons[slot].reserve(num_files);
const u8 free_files = Memcard::DIRLEN - num_files;
const u16 free_blocks = memcard->GetFreeBlocks();
table->setRowCount(num_files);
for (int i = 0; i < num_files; i++)
{
int file_index = memcard->GetFileIndex(i);
table->setRowCount(i + 1);
const u8 file_index = memcard->GetFileIndex(i);
const auto file_comments = memcard->GetSaveComments(file_index);
const u16 block_count = memcard->DEntry_BlockCount(file_index);
const auto entry = memcard->GetDEntry(file_index);
const std::string filename = entry ? Memcard::GenerateFilename(*entry) : "";
QString title;
QString comment;
if (file_comments)
const QString title =
file_comments ? QString::fromStdString(file_comments->first).trimmed() : QString();
const QString comment =
file_comments ? QString::fromStdString(file_comments->second).trimmed() : QString();
auto banner = GetBannerFromSaveFile(file_index, slot);
auto icon_data = GetIconFromSaveFile(file_index, slot);
auto* item_filename = new QTableWidgetItem(QString::fromStdString(filename));
auto* item_banner = new QTableWidgetItem();
auto* item_text = new QTableWidgetItem(QStringLiteral("%1\n%2").arg(title, comment));
auto* item_icon = new QTableWidgetItem();
auto* item_blocks = new QTableWidgetItem();
item_banner->setData(Qt::DecorationRole, banner);
item_icon->setData(Qt::DecorationRole, icon_data.m_frames[0]);
item_blocks->setData(Qt::DisplayRole, block_count);
for (auto* item : {item_filename, item_banner, item_text, item_icon, item_blocks})
{
title = QString::fromStdString(file_comments->first);
comment = QString::fromStdString(file_comments->second);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
item->setData(Qt::UserRole, static_cast<int>(file_index));
}
QString blocks = QStringLiteral("%1").arg(memcard->DEntry_BlockCount(file_index));
QString block_count = QStringLiteral("%1").arg(memcard->DEntry_FirstBlock(file_index));
m_slot_active_icons[slot].emplace(file_index, std::move(icon_data));
auto* banner = new QTableWidgetItem;
banner->setData(Qt::DecorationRole, GetBannerFromSaveFile(file_index, slot));
banner->setFlags(banner->flags() ^ Qt::ItemIsEditable);
auto icon_data = GetIconFromSaveFile(file_index, slot);
auto* icon = new QTableWidgetItem;
icon->setData(Qt::DecorationRole, icon_data.m_frames[0]);
m_slot_active_icons[slot].emplace_back(std::move(icon_data));
table->setItem(i, 0, banner);
table->setItem(i, 1, create_item(title));
table->setItem(i, 2, create_item(comment));
table->setItem(i, 3, icon);
table->setItem(i, 4, create_item(blocks));
table->setItem(i, 5, create_item(block_count));
table->resizeRowToContents(i);
table->setItem(i, COLUMN_INDEX_FILENAME, item_filename);
table->setItem(i, COLUMN_INDEX_BANNER, item_banner);
table->setItem(i, COLUMN_INDEX_TEXT, item_text);
table->setItem(i, COLUMN_INDEX_ICON, item_icon);
table->setItem(i, COLUMN_INDEX_BLOCKS, item_blocks);
}
m_slot_stat_label[slot]->setText(tr("%1 Free Blocks; %2 Free Dir Entries")
.arg(memcard->GetFreeBlocks())
.arg(Memcard::DIRLEN - memcard->GetNumFiles()));
const QString free_blocks_string = tr("Free Blocks: %1").arg(free_blocks);
const QString free_files_string = tr("Free Files: %1").arg(free_files);
m_slot_stat_label[slot]->setText(
QStringLiteral("%1 %2").arg(free_blocks_string, free_files_string));
table->setSortingEnabled(true);
}
void GCMemcardManager::UpdateActions()
@ -260,7 +312,6 @@ void GCMemcardManager::UpdateActions()
m_copy_button->setEnabled(have_selection && have_memcard_other);
m_export_button->setEnabled(have_selection);
m_export_all_button->setEnabled(have_memcard);
m_import_button->setEnabled(have_memcard);
m_delete_button->setEnabled(have_selection);
m_fix_checksums_button->setEnabled(have_memcard);
@ -278,7 +329,7 @@ void GCMemcardManager::SetSlotFile(int slot, QString path)
else
{
m_slot_memcard[slot] = nullptr;
ModalMessageBox::critical(
ModalMessageBox::warning(
this, tr("Error"),
tr("Failed opening memory card:\n%1").arg(GetErrorMessagesForErrorCode(error_code)));
}
@ -293,162 +344,322 @@ void GCMemcardManager::SetSlotFileInteractive(int slot)
this,
slot == 0 ? tr("Set memory card file for Slot A") : tr("Set memory card file for Slot B"),
QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
tr("GameCube Memory Cards (*.raw *.gcp)") + QStringLiteral(";;") + tr("All Files (*)")));
QStringLiteral("%1 (*.raw *.gcp);;%2 (*)")
.arg(tr("GameCube Memory Cards"), tr("All Files"))));
if (!path.isEmpty())
m_slot_file_edit[slot]->setText(path);
}
void GCMemcardManager::ExportFiles(bool prompt)
std::vector<u8> GCMemcardManager::GetSelectedFileIndices()
{
auto selection = m_slot_table[m_active_slot]->selectedItems();
auto& memcard = m_slot_memcard[m_active_slot];
auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
for (int i = 0; i < count; i++)
const auto selection = m_slot_table[m_active_slot]->selectedItems();
std::vector<bool> lookup(Memcard::DIRLEN);
for (const auto* item : selection)
{
auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
const int index = item->data(Qt::UserRole).toInt();
if (index < 0 || index >= static_cast<int>(Memcard::DIRLEN))
{
ModalMessageBox::warning(this, tr("Error"),
tr("Data inconsistency in GCMemcardManager, aborting action."));
return {};
}
lookup[index] = true;
}
std::string gci_filename;
if (!memcard->GCI_FileName(file_index, gci_filename))
std::vector<u8> selected_indices;
for (u8 i = 0; i < Memcard::DIRLEN; ++i)
{
if (lookup[i])
selected_indices.push_back(i);
}
return selected_indices;
}
static QString GetFormatDescription(Memcard::SavefileFormat format)
{
switch (format)
{
case Memcard::SavefileFormat::GCI:
return QObject::tr("Native GCI File");
case Memcard::SavefileFormat::GCS:
return QObject::tr("MadCatz Gameshark files");
case Memcard::SavefileFormat::SAV:
return QObject::tr("Datel MaxDrive/Pro files");
default:
assert(0);
return QObject::tr("Native GCI File");
}
}
void GCMemcardManager::ExportFiles(Memcard::SavefileFormat format)
{
const auto& memcard = m_slot_memcard[m_active_slot];
if (!memcard)
return;
const auto selected_indices = GetSelectedFileIndices();
if (selected_indices.empty())
return;
const auto savefiles = Memcard::GetSavefiles(*memcard, selected_indices);
if (savefiles.empty())
{
ModalMessageBox::warning(this, tr("Export Failed"),
tr("Failed to read selected savefile(s) from memory card."));
return;
}
std::string extension = Memcard::GetDefaultExtension(format);
if (savefiles.size() == 1)
{
// when exporting a single save file, let user specify exact path
const std::string basename = Memcard::GenerateFilename(savefiles[0].dir_entry);
const QString qformatdesc = GetFormatDescription(format);
const std::string default_path =
fmt::format("{}/{}{}", File::GetUserPath(D_GCUSER_IDX), basename, extension);
const QString qfilename = QFileDialog::getSaveFileName(
this, tr("Export Save File"), QString::fromStdString(default_path),
QStringLiteral("%1 (*%2);;%3 (*)")
.arg(qformatdesc, QString::fromStdString(extension), tr("All Files")));
if (qfilename.isEmpty())
return;
QString path;
if (prompt)
const std::string filename = qfilename.toStdString();
if (!Memcard::WriteSavefile(filename, savefiles[0], format))
{
path = QFileDialog::getSaveFileName(
this, tr("Export Save File"),
QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)) +
QStringLiteral("/%1").arg(QString::fromStdString(gci_filename)),
tr("Native GCI File (*.gci)") + QStringLiteral(";;") +
tr("MadCatz Gameshark files(*.gcs)") + QStringLiteral(";;") +
tr("Datel MaxDrive/Pro files(*.sav)"));
if (path.isEmpty())
return;
}
else
{
path = QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)) +
QStringLiteral("/%1").arg(QString::fromStdString(gci_filename));
File::Delete(filename);
ModalMessageBox::warning(this, tr("Export Failed"), tr("Failed to write savefile to disk."));
}
// TODO: This is obviously intended to check for success instead.
const auto exportRetval = memcard->ExportGci(file_index, path.toStdString(), "");
if (exportRetval == Memcard::GCMemcardExportFileRetVal::UNUSED)
return;
}
const QString qdirpath =
QFileDialog::getExistingDirectory(this, QObject::tr("Export Save Files"),
QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)));
if (qdirpath.isEmpty())
return;
const std::string dirpath = qdirpath.toStdString();
size_t failures = 0;
for (const auto& savefile : savefiles)
{
// find a free filename so we don't overwrite anything
const std::string basepath = dirpath + DIR_SEP + Memcard::GenerateFilename(savefile.dir_entry);
std::string filename = basepath + extension;
if (File::Exists(filename))
{
File::Delete(path.toStdString());
size_t tmp = 0;
std::string free_name;
do
{
free_name = fmt::format("{}_{}{}", basepath, tmp, extension);
++tmp;
} while (File::Exists(free_name));
filename = free_name;
}
if (!Memcard::WriteSavefile(filename, savefile, format))
{
File::Delete(filename);
++failures;
}
}
QString text = count == 1 ? tr("Successfully exported the save file.") :
tr("Successfully exported the %1 save files.").arg(count);
ModalMessageBox::information(this, tr("Success"), text);
if (failures > 0)
{
QString failure_string =
tr("Failed to export %n out of %1 save file(s).", "", static_cast<int>(failures))
.arg(savefiles.size());
if (failures == savefiles.size())
{
ModalMessageBox::warning(this, tr("Export Failed"), failure_string);
}
else
{
QString success_string = tr("Successfully exported %n out of %1 save file(s).", "",
static_cast<int>(savefiles.size() - failures))
.arg(savefiles.size());
ModalMessageBox::warning(this, tr("Export Failed"),
QStringLiteral("%1\n%2").arg(failure_string, success_string));
}
}
}
void GCMemcardManager::ExportAllFiles()
void GCMemcardManager::ImportFiles(int slot, const std::vector<Memcard::Savefile>& savefiles)
{
// This is nothing but a thin wrapper around ExportFiles()
m_slot_table[m_active_slot]->selectAll();
ExportFiles(false);
auto& card = m_slot_memcard[slot];
if (!card)
return;
const size_t number_of_files = savefiles.size();
const size_t number_of_blocks = Memcard::GetBlockCount(savefiles);
const size_t free_files = Memcard::DIRLEN - card->GetNumFiles();
const size_t free_blocks = card->GetFreeBlocks();
QStringList error_messages;
if (number_of_files > free_files)
{
error_messages.push_back(
tr("Not enough free files on the target memory card. At least %n free file(s) required.",
"", static_cast<int>(number_of_files)));
}
if (number_of_blocks > free_blocks)
{
error_messages.push_back(
tr("Not enough free blocks on the target memory card. At least %n free block(s) required.",
"", static_cast<int>(number_of_blocks)));
}
if (Memcard::HasDuplicateIdentity(savefiles))
{
error_messages.push_back(
tr("At least two of the selected save files have the same internal filename."));
}
for (const Memcard::Savefile& savefile : savefiles)
{
if (card->TitlePresent(savefile.dir_entry))
{
const std::string filename = Memcard::GenerateFilename(savefile.dir_entry);
error_messages.push_back(tr("The target memory card already contains a file \"%1\".")
.arg(QString::fromStdString(filename)));
}
}
if (!error_messages.empty())
{
ModalMessageBox::warning(this, tr("Import Failed"), error_messages.join(QLatin1Char('\n')));
return;
}
for (const Memcard::Savefile& savefile : savefiles)
{
const auto result = card->ImportFile(savefile);
// we've already checked everything that could realistically fail here, so this should only
// happen if the memory card data is corrupted in some way
if (result != Memcard::GCMemcardImportFileRetVal::SUCCESS)
{
const std::string filename = Memcard::GenerateFilename(savefile.dir_entry);
ModalMessageBox::warning(
this, tr("Import Failed"),
tr("Failed to import \"%1\".").arg(QString::fromStdString(filename)));
break;
}
}
if (!card->Save())
{
ModalMessageBox::warning(this, tr("Import Failed"),
tr("Failed to write modified memory card to disk."));
}
UpdateSlotTable(slot);
}
void GCMemcardManager::ImportFile()
{
QString path = QFileDialog::getOpenFileName(
this, tr("Import Save File"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
tr("Native GCI File (*.gci)") + QStringLiteral(";;") + tr("MadCatz Gameshark files(*.gcs)") +
QStringLiteral(";;") + tr("Datel MaxDrive/Pro files(*.sav)"));
if (path.isEmpty())
auto& card = m_slot_memcard[m_active_slot];
if (!card)
return;
const auto result = m_slot_memcard[m_active_slot]->ImportGci(path.toStdString());
const QStringList paths = QFileDialog::getOpenFileNames(
this, tr("Import Save File(s)"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
QStringLiteral("%1 (*.gci *.gcs *.sav);;%2 (*.gci);;%3 (*.gcs);;%4 (*.sav);;%5 (*)")
.arg(tr("Supported file formats"), GetFormatDescription(Memcard::SavefileFormat::GCI),
GetFormatDescription(Memcard::SavefileFormat::GCS),
GetFormatDescription(Memcard::SavefileFormat::SAV), tr("All Files")));
if (result != Memcard::GCMemcardImportFileRetVal::SUCCESS)
if (paths.isEmpty())
return;
std::vector<Memcard::Savefile> savefiles;
savefiles.reserve(paths.size());
QStringList errors;
for (const QString& path : paths)
{
ModalMessageBox::critical(this, tr("Import failed"), tr("Failed to import \"%1\".").arg(path));
auto read_result = Memcard::ReadSavefile(path.toStdString());
std::visit(overloaded{
[&](Memcard::Savefile savefile) { savefiles.emplace_back(std::move(savefile)); },
[&](Memcard::ReadSavefileErrorCode error_code) {
errors.push_back(
tr("%1: %2").arg(path, GetErrorMessageForErrorCode(error_code)));
},
},
std::move(read_result));
}
if (!errors.empty())
{
ModalMessageBox::warning(
this, tr("Import Failed"),
tr("Encountered the following errors while opening save files:\n%1\n\nAborting import.")
.arg(errors.join(QStringLiteral("\n"))));
return;
}
if (!m_slot_memcard[m_active_slot]->Save())
PanicAlertFmtT("File write failed");
UpdateSlotTable(m_active_slot);
ImportFiles(m_active_slot, savefiles);
}
void GCMemcardManager::CopyFiles()
{
auto selection = m_slot_table[m_active_slot]->selectedItems();
auto& memcard = m_slot_memcard[m_active_slot];
const auto& source_card = m_slot_memcard[m_active_slot];
if (!source_card)
return;
auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
auto& target_card = m_slot_memcard[!m_active_slot];
if (!target_card)
return;
for (int i = 0; i < count; i++)
const auto selected_indices = GetSelectedFileIndices();
if (selected_indices.empty())
return;
const auto savefiles = Memcard::GetSavefiles(*source_card, selected_indices);
if (savefiles.empty())
{
auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
const auto result = m_slot_memcard[!m_active_slot]->CopyFrom(*memcard, file_index);
if (result != Memcard::GCMemcardImportFileRetVal::SUCCESS)
{
ModalMessageBox::warning(this, tr("Copy failed"), tr("Failed to copy file"));
}
ModalMessageBox::warning(this, tr("Copy Failed"),
tr("Failed to read selected savefile(s) from memory card."));
return;
}
for (int i = 0; i < SLOT_COUNT; i++)
{
if (!m_slot_memcard[i]->Save())
PanicAlertFmtT("File write failed");
UpdateSlotTable(i);
}
ImportFiles(!m_active_slot, savefiles);
}
void GCMemcardManager::DeleteFiles()
{
auto selection = m_slot_table[m_active_slot]->selectedItems();
auto& memcard = m_slot_memcard[m_active_slot];
auto& card = m_slot_memcard[m_active_slot];
if (!card)
return;
auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
const auto selected_indices = GetSelectedFileIndices();
if (selected_indices.empty())
return;
// Ask for confirmation if we are to delete multiple files
if (count > 1)
const QString text = tr("Do you want to delete the %n selected save file(s)?", "",
static_cast<int>(selected_indices.size()));
const auto response = ModalMessageBox::question(this, tr("Question"), text);
if (response != QMessageBox::Yes)
return;
for (const u8 index : selected_indices)
{
QString text = count == 1 ? tr("Do you want to delete the selected save file?") :
tr("Do you want to delete the %1 selected save files?").arg(count);
auto response = ModalMessageBox::question(this, tr("Question"), text);
;
if (response == QMessageBox::Abort)
return;
}
std::vector<int> file_indices;
for (int i = 0; i < count; i++)
{
auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
file_indices.push_back(memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel)));
}
for (int file_index : file_indices)
{
if (memcard->RemoveFile(file_index) != Memcard::GCMemcardRemoveFileRetVal::SUCCESS)
if (card->RemoveFile(index) != Memcard::GCMemcardRemoveFileRetVal::SUCCESS)
{
ModalMessageBox::warning(this, tr("Remove failed"), tr("Failed to remove file"));
ModalMessageBox::warning(this, tr("Remove Failed"), tr("Failed to remove file."));
break;
}
}
if (!memcard->Save())
if (!card->Save())
{
PanicAlertFmtT("File write failed");
}
else
{
ModalMessageBox::information(this, tr("Success"), tr("Successfully deleted files."));
ModalMessageBox::warning(this, tr("Remove Failed"),
tr("Failed to write modified memory card to disk."));
}
UpdateSlotTable(m_active_slot);
@ -461,7 +672,10 @@ void GCMemcardManager::FixChecksums()
memcard->FixChecksums();
if (!memcard->Save())
PanicAlertFmtT("File write failed");
{
ModalMessageBox::warning(this, tr("Fix Checksums Failed"),
tr("Failed to write modified memory card to disk."));
}
}
void GCMemcardManager::CreateNewCard(int slot)
@ -473,22 +687,37 @@ void GCMemcardManager::CreateNewCard(int slot)
void GCMemcardManager::DrawIcons()
{
const auto column = 3;
const int column = COLUMN_INDEX_ICON;
for (int slot = 0; slot < SLOT_COUNT; slot++)
{
// skip loop if the table is empty
if (m_slot_table[slot]->rowCount() <= 0)
QTableWidget* table = m_slot_table[slot];
const int row_count = table->rowCount();
if (row_count <= 0)
continue;
const auto viewport = m_slot_table[slot]->viewport();
u32 row = m_slot_table[slot]->indexAt(viewport->rect().topLeft()).row();
const auto max_table_index = m_slot_table[slot]->indexAt(viewport->rect().bottomLeft());
const u32 max_row =
max_table_index.row() < 0 ? (m_slot_table[slot]->rowCount() - 1) : max_table_index.row();
const auto viewport = table->viewport();
const int viewport_first_row = table->indexAt(viewport->rect().topLeft()).row();
if (viewport_first_row >= row_count)
continue;
for (; row <= max_row; row++)
const int first_row = viewport_first_row < 0 ? 0 : viewport_first_row;
const int viewport_last_row = table->indexAt(viewport->rect().bottomLeft()).row();
const int last_row =
viewport_last_row < 0 ? (row_count - 1) : std::min(viewport_last_row, row_count - 1);
for (int row = first_row; row <= last_row; ++row)
{
const auto& icon = m_slot_active_icons[slot][row];
auto* item = table->item(row, column);
if (!item)
continue;
const u8 index = static_cast<u8>(item->data(Qt::UserRole).toInt());
auto it = m_slot_active_icons[slot].find(index);
if (it == m_slot_active_icons[slot].end())
continue;
const auto& icon = it->second;
// this icon doesn't have an animation
if (icon.m_frames.size() <= 1)
@ -502,11 +731,7 @@ void GCMemcardManager::DrawIcons()
if (prev_frame == current_frame)
continue;
auto* item = new QTableWidgetItem;
item->setData(Qt::DecorationRole, icon.m_frames[current_frame]);
item->setFlags(item->flags() ^ Qt::ItemIsEditable);
m_slot_table[slot]->setItem(row, column, item);
}
}
@ -613,3 +838,18 @@ QString GCMemcardManager::GetErrorMessagesForErrorCode(const Memcard::GCMemcardE
return sl.join(QLatin1Char{'\n'});
}
QString GCMemcardManager::GetErrorMessageForErrorCode(Memcard::ReadSavefileErrorCode code)
{
switch (code)
{
case Memcard::ReadSavefileErrorCode::OpenFileFail:
return tr("Failed to open file.");
case Memcard::ReadSavefileErrorCode::IOError:
return tr("Failed to read from file.");
case Memcard::ReadSavefileErrorCode::DataCorrupted:
return tr("Data in unrecognized format or corrupted.");
default:
return tr("Unknown error.");
}
}

View File

@ -5,6 +5,7 @@
#pragma once
#include <array>
#include <map>
#include <memory>
#include <utility>
#include <vector>
@ -17,17 +18,23 @@ namespace Memcard
{
class GCMemcard;
class GCMemcardErrorCode;
struct Savefile;
enum class ReadSavefileErrorCode;
enum class SavefileFormat;
} // namespace Memcard
class QAction;
class QDialogButtonBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QMenu;
class QPixmap;
class QPushButton;
class QString;
class QTableWidget;
class QTimer;
class QToolButton;
class GCMemcardManager : public QDialog
{
@ -37,6 +44,7 @@ public:
~GCMemcardManager();
static QString GetErrorMessagesForErrorCode(const Memcard::GCMemcardErrorCode& code);
static QString GetErrorMessageForErrorCode(Memcard::ReadSavefileErrorCode code);
private:
struct IconAnimationData;
@ -51,11 +59,14 @@ private:
void SetSlotFileInteractive(int slot);
void SetActiveSlot(int slot);
std::vector<u8> GetSelectedFileIndices();
void ImportFiles(int slot, const std::vector<Memcard::Savefile>& savefiles);
void CopyFiles();
void ImportFile();
void DeleteFiles();
void ExportFiles(bool prompt);
void ExportAllFiles();
void ExportFiles(Memcard::SavefileFormat format);
void FixChecksums();
void CreateNewCard(int slot);
void DrawIcons();
@ -67,15 +78,18 @@ private:
// Actions
QPushButton* m_select_button;
QPushButton* m_copy_button;
QPushButton* m_export_button;
QPushButton* m_export_all_button;
QToolButton* m_export_button;
QMenu* m_export_menu;
QAction* m_export_gci_action;
QAction* m_export_gcs_action;
QAction* m_export_sav_action;
QPushButton* m_import_button;
QPushButton* m_delete_button;
QPushButton* m_fix_checksums_button;
// Slots
static constexpr int SLOT_COUNT = 2;
std::array<std::vector<IconAnimationData>, SLOT_COUNT> m_slot_active_icons;
std::array<std::map<u8, IconAnimationData>, SLOT_COUNT> m_slot_active_icons;
std::array<std::unique_ptr<Memcard::GCMemcard>, SLOT_COUNT> m_slot_memcard;
std::array<QGroupBox*, SLOT_COUNT> m_slot_group;
std::array<QLineEdit*, SLOT_COUNT> m_slot_file_edit;