mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-03-30 04:21:16 +00:00
GCMemcardManager: Rewrite file exporting logic to provide a better user experience.
This commit is contained in:
parent
7bb7aa16c2
commit
87ae7ccd75
@ -1,6 +1,7 @@
|
|||||||
#include "Core/HW/GCMemcard/GCMemcardUtils.h"
|
#include "Core/HW/GCMemcard/GCMemcardUtils.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cassert>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@ -304,4 +305,34 @@ std::string GenerateFilename(const DEntry& entry)
|
|||||||
|
|
||||||
return Common::EscapeFileName(maker + '-' + gamecode + '-' + filename);
|
return Common::EscapeFileName(maker + '-' + gamecode + '-' + filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string GetDefaultExtension(SavefileFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case SavefileFormat::GCI:
|
||||||
|
return ".gci";
|
||||||
|
case SavefileFormat::GCS:
|
||||||
|
return ".gcs";
|
||||||
|
case SavefileFormat::SAV:
|
||||||
|
return ".sav";
|
||||||
|
default:
|
||||||
|
assert(0);
|
||||||
|
return ".gci";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Savefile> GetSavefiles(const GCMemcard& card, const std::vector<u8>& file_indices)
|
||||||
|
{
|
||||||
|
std::vector<Savefile> files;
|
||||||
|
files.reserve(file_indices.size());
|
||||||
|
for (const u8 index : file_indices)
|
||||||
|
{
|
||||||
|
std::optional<Savefile> file = card.ExportFile(index);
|
||||||
|
if (!file)
|
||||||
|
return {};
|
||||||
|
files.emplace_back(std::move(*file));
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
} // namespace Memcard
|
} // namespace Memcard
|
||||||
|
@ -36,4 +36,10 @@ bool WriteSavefile(const std::string& filename, const Savefile& savefile, Savefi
|
|||||||
|
|
||||||
// Generates a filename (without extension) for the given directory entry.
|
// Generates a filename (without extension) for the given directory entry.
|
||||||
std::string GenerateFilename(const DEntry& entry);
|
std::string GenerateFilename(const DEntry& entry);
|
||||||
|
|
||||||
|
// Returns the expected extension for a filename in the given format. Includes the leading dot.
|
||||||
|
std::string GetDefaultExtension(SavefileFormat format);
|
||||||
|
|
||||||
|
// Reads multiple savefiles from a card. Returns empty vector if even a single file can't be read.
|
||||||
|
std::vector<Savefile> GetSavefiles(const GCMemcard& card, const std::vector<u8>& file_indices);
|
||||||
} // namespace Memcard
|
} // namespace Memcard
|
||||||
|
@ -5,6 +5,11 @@
|
|||||||
#include "DolphinQt/GCMemcardManager.h"
|
#include "DolphinQt/GCMemcardManager.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@ -15,13 +20,16 @@
|
|||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
#include <QMenu>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QTableWidget>
|
#include <QTableWidget>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
#include "Common/CommonPaths.h"
|
||||||
#include "Common/Config/Config.h"
|
#include "Common/Config/Config.h"
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
#include "Common/MsgHandler.h"
|
#include "Common/MsgHandler.h"
|
||||||
@ -84,11 +92,20 @@ void GCMemcardManager::CreateWidgets()
|
|||||||
// Actions
|
// Actions
|
||||||
m_select_button = new QPushButton;
|
m_select_button = new QPushButton;
|
||||||
m_copy_button = new QPushButton;
|
m_copy_button = new QPushButton;
|
||||||
|
|
||||||
// Contents will be set by their appropriate functions
|
|
||||||
m_delete_button = new QPushButton(tr("&Delete"));
|
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_import_button = new QPushButton(tr("&Import..."));
|
||||||
m_fix_checksums_button = new QPushButton(tr("Fix Checksums"));
|
m_fix_checksums_button = new QPushButton(tr("Fix Checksums"));
|
||||||
|
|
||||||
@ -120,7 +137,7 @@ void GCMemcardManager::CreateWidgets()
|
|||||||
slot_layout->addWidget(m_slot_table[i], 1, 0, 1, 3);
|
slot_layout->addWidget(m_slot_table[i], 1, 0, 1, 3);
|
||||||
slot_layout->addWidget(m_slot_stat_label[i], 2, 0);
|
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);
|
UpdateSlotTable(i);
|
||||||
}
|
}
|
||||||
@ -129,10 +146,9 @@ void GCMemcardManager::CreateWidgets()
|
|||||||
layout->addWidget(m_copy_button, 2, 1);
|
layout->addWidget(m_copy_button, 2, 1);
|
||||||
layout->addWidget(m_delete_button, 3, 1);
|
layout->addWidget(m_delete_button, 3, 1);
|
||||||
layout->addWidget(m_export_button, 4, 1);
|
layout->addWidget(m_export_button, 4, 1);
|
||||||
layout->addWidget(m_export_all_button, 5, 1);
|
layout->addWidget(m_import_button, 5, 1);
|
||||||
layout->addWidget(m_import_button, 6, 1);
|
layout->addWidget(m_fix_checksums_button, 6, 1);
|
||||||
layout->addWidget(m_fix_checksums_button, 7, 1);
|
layout->addWidget(m_button_box, 8, 2);
|
||||||
layout->addWidget(m_button_box, 9, 2);
|
|
||||||
|
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
}
|
}
|
||||||
@ -141,8 +157,12 @@ void GCMemcardManager::ConnectWidgets()
|
|||||||
{
|
{
|
||||||
connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
connect(m_select_button, &QPushButton::clicked, [this] { SetActiveSlot(!m_active_slot); });
|
connect(m_select_button, &QPushButton::clicked, [this] { SetActiveSlot(!m_active_slot); });
|
||||||
connect(m_export_button, &QPushButton::clicked, [this] { ExportFiles(true); });
|
connect(m_export_gci_action, &QAction::triggered,
|
||||||
connect(m_export_all_button, &QPushButton::clicked, this, &GCMemcardManager::ExportAllFiles);
|
[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_delete_button, &QPushButton::clicked, this, &GCMemcardManager::DeleteFiles);
|
||||||
connect(m_import_button, &QPushButton::clicked, this, &GCMemcardManager::ImportFile);
|
connect(m_import_button, &QPushButton::clicked, this, &GCMemcardManager::ImportFile);
|
||||||
connect(m_copy_button, &QPushButton::clicked, this, &GCMemcardManager::CopyFiles);
|
connect(m_copy_button, &QPushButton::clicked, this, &GCMemcardManager::CopyFiles);
|
||||||
@ -262,7 +282,6 @@ void GCMemcardManager::UpdateActions()
|
|||||||
|
|
||||||
m_copy_button->setEnabled(have_selection && have_memcard_other);
|
m_copy_button->setEnabled(have_selection && have_memcard_other);
|
||||||
m_export_button->setEnabled(have_selection);
|
m_export_button->setEnabled(have_selection);
|
||||||
m_export_all_button->setEnabled(have_memcard);
|
|
||||||
m_import_button->setEnabled(have_memcard);
|
m_import_button->setEnabled(have_memcard);
|
||||||
m_delete_button->setEnabled(have_selection);
|
m_delete_button->setEnabled(have_selection);
|
||||||
m_fix_checksums_button->setEnabled(have_memcard);
|
m_fix_checksums_button->setEnabled(have_memcard);
|
||||||
@ -300,60 +319,142 @@ void GCMemcardManager::SetSlotFileInteractive(int slot)
|
|||||||
m_slot_file_edit[slot]->setText(path);
|
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();
|
const auto selection = m_slot_table[m_active_slot]->selectedItems();
|
||||||
auto& memcard = m_slot_memcard[m_active_slot];
|
std::vector<bool> lookup(Memcard::DIRLEN);
|
||||||
|
for (const auto* item : selection)
|
||||||
auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
{
|
||||||
auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
|
const int index = item->data(Qt::UserRole).toInt();
|
||||||
int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
|
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;
|
std::vector<u8> selected_indices;
|
||||||
if (!memcard->GCI_FileName(file_index, gci_filename))
|
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;
|
return;
|
||||||
|
|
||||||
QString path;
|
const std::string filename = qfilename.toStdString();
|
||||||
if (prompt)
|
if (!Memcard::WriteSavefile(filename, savefiles[0], format))
|
||||||
{
|
{
|
||||||
path = QFileDialog::getSaveFileName(
|
File::Delete(filename);
|
||||||
this, tr("Export Save File"),
|
ModalMessageBox::warning(this, tr("Export Failed"), tr("Failed to write savefile to disk."));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is obviously intended to check for success instead.
|
return;
|
||||||
const auto exportRetval = memcard->ExportGci(file_index, path.toStdString(), "");
|
}
|
||||||
if (exportRetval == Memcard::GCMemcardExportFileRetVal::UNUSED)
|
|
||||||
|
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.") :
|
if (failures > 0)
|
||||||
tr("Successfully exported the %1 save files.").arg(count);
|
{
|
||||||
ModalMessageBox::information(this, tr("Success"), text);
|
QString failure_string =
|
||||||
}
|
tr("Failed to export %n out of %1 save file(s).", "", static_cast<int>(failures))
|
||||||
|
.arg(savefiles.size());
|
||||||
void GCMemcardManager::ExportAllFiles()
|
if (failures == savefiles.size())
|
||||||
{
|
{
|
||||||
// This is nothing but a thin wrapper around ExportFiles()
|
ModalMessageBox::warning(this, tr("Export Failed"), failure_string);
|
||||||
m_slot_table[m_active_slot]->selectAll();
|
}
|
||||||
ExportFiles(false);
|
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::ImportFile()
|
void GCMemcardManager::ImportFile()
|
||||||
|
@ -17,17 +17,21 @@ namespace Memcard
|
|||||||
{
|
{
|
||||||
class GCMemcard;
|
class GCMemcard;
|
||||||
class GCMemcardErrorCode;
|
class GCMemcardErrorCode;
|
||||||
|
enum class SavefileFormat;
|
||||||
} // namespace Memcard
|
} // namespace Memcard
|
||||||
|
|
||||||
|
class QAction;
|
||||||
class QDialogButtonBox;
|
class QDialogButtonBox;
|
||||||
class QGroupBox;
|
class QGroupBox;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
|
class QMenu;
|
||||||
class QPixmap;
|
class QPixmap;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QString;
|
class QString;
|
||||||
class QTableWidget;
|
class QTableWidget;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
|
class QToolButton;
|
||||||
|
|
||||||
class GCMemcardManager : public QDialog
|
class GCMemcardManager : public QDialog
|
||||||
{
|
{
|
||||||
@ -51,11 +55,12 @@ private:
|
|||||||
void SetSlotFileInteractive(int slot);
|
void SetSlotFileInteractive(int slot);
|
||||||
void SetActiveSlot(int slot);
|
void SetActiveSlot(int slot);
|
||||||
|
|
||||||
|
std::vector<u8> GetSelectedFileIndices();
|
||||||
|
|
||||||
void CopyFiles();
|
void CopyFiles();
|
||||||
void ImportFile();
|
void ImportFile();
|
||||||
void DeleteFiles();
|
void DeleteFiles();
|
||||||
void ExportFiles(bool prompt);
|
void ExportFiles(Memcard::SavefileFormat format);
|
||||||
void ExportAllFiles();
|
|
||||||
void FixChecksums();
|
void FixChecksums();
|
||||||
void CreateNewCard(int slot);
|
void CreateNewCard(int slot);
|
||||||
void DrawIcons();
|
void DrawIcons();
|
||||||
@ -67,8 +72,11 @@ private:
|
|||||||
// Actions
|
// Actions
|
||||||
QPushButton* m_select_button;
|
QPushButton* m_select_button;
|
||||||
QPushButton* m_copy_button;
|
QPushButton* m_copy_button;
|
||||||
QPushButton* m_export_button;
|
QToolButton* m_export_button;
|
||||||
QPushButton* m_export_all_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_import_button;
|
||||||
QPushButton* m_delete_button;
|
QPushButton* m_delete_button;
|
||||||
QPushButton* m_fix_checksums_button;
|
QPushButton* m_fix_checksums_button;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user