diff --git a/README.md b/README.md
index e2657f23a..e91a29e7e 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c
## Latest News
+- 2020/09/19: Memory card importer/editor added to Qt frontend.
- 2020/09/13: Support for chaining post processing shaders added.
- 2020/09/12: Additional texture filtering options added.
- 2020/09/09: Basic cheat support added. Not all instructions/commands are supported yet.
@@ -63,6 +64,7 @@ Other features include:
- Automatic content scanning - game titles/regions are provided by redump.org
- Optional automatic switching of memory cards for each game
- Supports loading cheats from libretro or PCSXR format lists
+ - Memory card editor and save importer
## System Requirements
- A CPU faster than a potato. But it needs to be 64-bit (either x86_64 or AArch64/ARMv8) otherwise you won't get a recompiler and it'll be slow. There are no plans to add any 32-bit recompilers.
diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index 776abe4de..38d9dfd1e 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -52,6 +52,9 @@ set(SRCS
mainwindow.cpp
mainwindow.h
mainwindow.ui
+ memorycardeditordialog.cpp
+ memorycardeditordialog.h
+ memorycardeditordialog.ui
memorycardsettingswidget.cpp
memorycardsettingswidget.h
postprocessingchainconfigwidget.cpp
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index 3dc78fe92..cfdf8fd73 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -56,6 +56,7 @@
+
@@ -70,6 +71,7 @@
+
@@ -155,6 +157,9 @@
Document
+
+ Document
+
@@ -181,6 +186,7 @@
+
diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp
index eaee65150..5ec59f656 100644
--- a/src/duckstation-qt/mainwindow.cpp
+++ b/src/duckstation-qt/mainwindow.cpp
@@ -9,6 +9,7 @@
#include "gamelistsettingswidget.h"
#include "gamelistwidget.h"
#include "gamepropertiesdialog.h"
+#include "memorycardeditordialog.h"
#include "qtdisplaywidget.h"
#include "qthostinterface.h"
#include "qtutils.h"
@@ -670,6 +671,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered);
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
+ connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered);
connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError,
Qt::BlockingQueuedConnection);
@@ -956,6 +958,15 @@ void MainWindow::onCheckForUpdatesActionTriggered()
checkForUpdates(true);
}
+void MainWindow::onToolsMemoryCardEditorTriggered()
+{
+ if (!m_memory_card_editor_dialog)
+ m_memory_card_editor_dialog = new MemoryCardEditorDialog(this);
+
+ m_memory_card_editor_dialog->setModal(false);
+ m_memory_card_editor_dialog->show();
+}
+
void MainWindow::checkForUpdates(bool display_message)
{
if (!AutoUpdaterDialog::isSupported())
diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h
index e00ad67c9..c0bd4a3f6 100644
--- a/src/duckstation-qt/mainwindow.h
+++ b/src/duckstation-qt/mainwindow.h
@@ -14,6 +14,7 @@ class GameListWidget;
class QtHostInterface;
class QtDisplayWidget;
class AutoUpdaterDialog;
+class MemoryCardEditorDialog;
class HostDisplay;
struct GameListEntry;
@@ -72,6 +73,7 @@ private Q_SLOTS:
void onDiscordServerActionTriggered();
void onAboutActionTriggered();
void onCheckForUpdatesActionTriggered();
+ void onToolsMemoryCardEditorTriggered();
void onGameListEntrySelected(const GameListEntry* entry);
void onGameListEntryDoubleClicked(const GameListEntry* entry);
@@ -116,6 +118,7 @@ private:
SettingsDialog* m_settings_dialog = nullptr;
AutoUpdaterDialog* m_auto_updater_dialog = nullptr;
+ MemoryCardEditorDialog* m_memory_card_editor_dialog = nullptr;
bool m_emulation_running = false;
};
diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui
index a229b8b73..2814523d8 100644
--- a/src/duckstation-qt/mainwindow.ui
+++ b/src/duckstation-qt/mainwindow.ui
@@ -14,7 +14,7 @@
DuckStation
-
+
:/icons/duck.png:/icons/duck.png
@@ -30,7 +30,7 @@
0
0
754
- 30
+ 21
@@ -78,7 +78,7 @@
Save State
-
+
:/icons/document-save.png:/icons/document-save.png
@@ -184,9 +184,16 @@
+
+
@@ -228,7 +235,7 @@
-
+
:/icons/drive-optical.png:/icons/drive-optical.png
@@ -237,7 +244,7 @@
-
+
:/icons/drive-removable-media.png:/icons/drive-removable-media.png
@@ -246,7 +253,7 @@
-
+
:/icons/folder-open.png:/icons/folder-open.png
@@ -255,7 +262,7 @@
-
+
:/icons/view-refresh.png:/icons/view-refresh.png
@@ -264,7 +271,7 @@
-
+
:/icons/system-shutdown.png:/icons/system-shutdown.png
@@ -273,7 +280,7 @@
-
+
:/icons/view-refresh.png:/icons/view-refresh.png
@@ -285,7 +292,7 @@
true
-
+
:/icons/media-playback-pause.png:/icons/media-playback-pause.png
@@ -294,7 +301,7 @@
-
+
:/icons/document-open.png:/icons/document-open.png
@@ -303,7 +310,7 @@
-
+
:/icons/document-save.png:/icons/document-save.png
@@ -317,7 +324,7 @@
-
+
:/icons/utilities-system-monitor.png:/icons/utilities-system-monitor.png
@@ -326,7 +333,7 @@
-
+
:/icons/input-gaming.png:/icons/input-gaming.png
@@ -335,7 +342,7 @@
-
+
:/icons/applications-other.png:/icons/applications-other.png
@@ -344,7 +351,7 @@
-
+
:/icons/video-display.png:/icons/video-display.png
@@ -353,7 +360,7 @@
-
+
:/icons/antialias-icon.png:/icons/antialias-icon.png
@@ -362,7 +369,7 @@
-
+
:/icons/applications-graphics.png:/icons/applications-graphics.png
@@ -371,7 +378,7 @@
-
+
:/icons/view-fullscreen.png:/icons/view-fullscreen.png
@@ -410,7 +417,7 @@
-
+
:/icons/media-optical.png:/icons/media-optical.png
@@ -419,7 +426,7 @@
-
+
:/icons/conical-flask-red.png:/icons/conical-flask-red.png
@@ -428,7 +435,7 @@
-
+
:/icons/audio-card.png:/icons/audio-card.png
@@ -437,7 +444,7 @@
-
+
:/icons/folder-open.png:/icons/folder-open.png
@@ -446,7 +453,7 @@
-
+
:/icons/applications-system.png:/icons/applications-system.png
@@ -455,7 +462,7 @@
-
+
:/icons/applications-development.png:/icons/applications-development.png
@@ -464,7 +471,7 @@
-
+
:/icons/edit-find.png:/icons/edit-find.png
@@ -473,7 +480,7 @@
-
+
:/icons/preferences-system.png:/icons/preferences-system.png
@@ -584,7 +591,7 @@
-
+
:/icons/camera-photo.png:/icons/camera-photo.png
@@ -593,7 +600,7 @@
-
+
:/icons/media-flash-24.png:/icons/media-flash-24.png
@@ -602,7 +609,7 @@
-
+
:/icons/media-playback-start.png:/icons/media-playback-start.png
@@ -647,6 +654,11 @@
System &Display
+
+
+ Memory &Card Editor
+
+
diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp
new file mode 100644
index 000000000..afc00717f
--- /dev/null
+++ b/src/duckstation-qt/memorycardeditordialog.cpp
@@ -0,0 +1,389 @@
+#include "memorycardeditordialog.h"
+#include "common/file_system.h"
+#include "common/string_util.h"
+#include "core/host_interface.h"
+#include "qtutils.h"
+#include
+#include
+#include
+
+static constexpr char MEMORY_CARD_IMAGE_FILTER[] =
+ QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Memory Card Types (*.mcd *.mcr *.mc)");
+static constexpr char MEMORY_CARD_IMPORT_FILTER[] =
+ QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme)");
+
+MemoryCardEditorDialog::MemoryCardEditorDialog(QWidget* parent) : QDialog(parent)
+{
+ m_ui.setupUi(this);
+ m_card_a.path_cb = m_ui.cardAPath;
+ m_card_a.table = m_ui.cardA;
+ m_card_a.blocks_free_label = m_ui.cardAUsage;
+ m_card_a.save_button = m_ui.saveCardA;
+ m_card_b.path_cb = m_ui.cardBPath;
+ m_card_b.table = m_ui.cardB;
+ m_card_b.blocks_free_label = m_ui.cardBUsage;
+ m_card_b.save_button = m_ui.saveCardB;
+
+ connectUi();
+ populateComboBox(m_ui.cardAPath);
+ populateComboBox(m_ui.cardBPath);
+}
+
+MemoryCardEditorDialog::~MemoryCardEditorDialog() = default;
+
+void MemoryCardEditorDialog::resizeEvent(QResizeEvent* ev)
+{
+ QtUtils::ResizeColumnsForTableView(m_card_a.table, {32, -1, 100, 45});
+ QtUtils::ResizeColumnsForTableView(m_card_b.table, {32, -1, 100, 45});
+}
+
+void MemoryCardEditorDialog::closeEvent(QCloseEvent* ev)
+{
+ promptForSave(&m_card_a);
+ promptForSave(&m_card_b);
+}
+
+void MemoryCardEditorDialog::connectUi()
+{
+ connect(m_ui.cardA, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorDialog::onCardASelectionChanged);
+ connect(m_ui.cardB, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorDialog::onCardBSelectionChanged);
+ connect(m_ui.moveLeft, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile);
+ connect(m_ui.moveRight, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile);
+ connect(m_ui.deleteFile, &QPushButton::clicked, this, &MemoryCardEditorDialog::doDeleteFile);
+
+ connect(m_ui.cardAPath, QOverload::of(&QComboBox::currentIndexChanged),
+ [this](int index) { loadCardFromComboBox(&m_card_a, index); });
+ connect(m_ui.cardBPath, QOverload::of(&QComboBox::currentIndexChanged),
+ [this](int index) { loadCardFromComboBox(&m_card_b, index); });
+ connect(m_ui.newCardA, &QPushButton::clicked, [this]() { newCard(&m_card_a); });
+ connect(m_ui.newCardB, &QPushButton::clicked, [this]() { newCard(&m_card_b); });
+ connect(m_ui.saveCardA, &QPushButton::clicked, [this]() { saveCard(&m_card_a); });
+ connect(m_ui.saveCardB, &QPushButton::clicked, [this]() { saveCard(&m_card_b); });
+ connect(m_ui.importCardA, &QPushButton::clicked, [this]() { importCard(&m_card_a); });
+ connect(m_ui.importCardB, &QPushButton::clicked, [this]() { importCard(&m_card_b); });
+}
+
+void MemoryCardEditorDialog::populateComboBox(QComboBox* cb)
+{
+ QSignalBlocker sb(cb);
+
+ cb->clear();
+
+ cb->addItem(QString());
+ cb->addItem(tr("Browse..."));
+
+ const std::string base_path(g_host_interface->GetUserDirectoryRelativePath("memcards"));
+ FileSystem::FindResultsArray results;
+ FileSystem::FindFiles(base_path.c_str(), "*.mcd", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &results);
+ for (FILESYSTEM_FIND_DATA& fd : results)
+ {
+ std::string real_filename(
+ StringUtil::StdStringFromFormat("%s%c%s", base_path.c_str(), FS_OSPATH_SEPERATOR_CHARACTER, fd.FileName.c_str()));
+ std::string::size_type pos = fd.FileName.rfind('.');
+ if (pos != std::string::npos)
+ fd.FileName.erase(pos);
+
+ cb->addItem(QString::fromStdString(fd.FileName), QVariant(QString::fromStdString(real_filename)));
+ }
+}
+
+void MemoryCardEditorDialog::loadCardFromComboBox(Card* card, int index)
+{
+ QString filename;
+ if (index == 1)
+ {
+ filename = QFileDialog::getOpenFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER));
+ if (!filename.isEmpty())
+ {
+ // add to combo box
+ QFileInfo file(filename);
+ QSignalBlocker sb(card->path_cb);
+ card->path_cb->addItem(file.baseName(), QVariant(filename));
+ card->path_cb->setCurrentIndex(card->path_cb->count() - 1);
+ }
+ }
+ else
+ {
+ filename = card->path_cb->itemData(index).toString();
+ }
+
+ if (filename.isEmpty())
+ return;
+
+ loadCard(filename, card);
+}
+
+void MemoryCardEditorDialog::onCardASelectionChanged()
+{
+ {
+ QSignalBlocker cb(m_card_b.table);
+ m_card_b.table->clearSelection();
+ }
+
+ updateButtonState();
+}
+
+void MemoryCardEditorDialog::onCardBSelectionChanged()
+{
+ {
+ QSignalBlocker cb(m_card_a.table);
+ m_card_a.table->clearSelection();
+ }
+
+ updateButtonState();
+}
+
+void MemoryCardEditorDialog::clearSelection()
+{
+ {
+ QSignalBlocker cb(m_card_a.table);
+ m_card_a.table->clearSelection();
+ }
+
+ {
+ QSignalBlocker cb(m_card_b.table);
+ m_card_b.table->clearSelection();
+ }
+
+ updateButtonState();
+}
+
+bool MemoryCardEditorDialog::loadCard(const QString& filename, Card* card)
+{
+ promptForSave(card);
+
+ card->table->setRowCount(0);
+ card->dirty = false;
+ card->blocks_free_label->clear();
+ card->save_button->setEnabled(false);
+
+ card->filename.clear();
+
+ std::string filename_str = filename.toStdString();
+ if (!MemoryCardImage::LoadFromFile(&card->data, filename_str.c_str()))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Failed to load memory card image."));
+ return false;
+ }
+
+ card->filename = std::move(filename_str);
+ updateCardTable(card);
+ updateCardBlocksFree(card);
+ updateButtonState();
+ return true;
+}
+
+void MemoryCardEditorDialog::updateCardTable(Card* card)
+{
+ card->table->setRowCount(0);
+
+ card->files = MemoryCardImage::EnumerateFiles(card->data);
+ for (const MemoryCardImage::FileInfo& fi : card->files)
+ {
+ const int row = card->table->rowCount();
+ card->table->insertRow(row);
+
+ if (!fi.icon_frames.empty())
+ {
+ const QImage image(reinterpret_cast(fi.icon_frames[0].pixels), MemoryCardImage::ICON_WIDTH,
+ MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888);
+
+ QTableWidgetItem* icon = new QTableWidgetItem();
+ icon->setIcon(QIcon(QPixmap::fromImage(image)));
+ card->table->setItem(row, 0, icon);
+ }
+
+ card->table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(fi.title)));
+ card->table->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(fi.filename)));
+ card->table->setItem(row, 3, new QTableWidgetItem(QStringLiteral("%1").arg(fi.num_blocks)));
+ }
+}
+
+void MemoryCardEditorDialog::updateCardBlocksFree(Card* card)
+{
+ card->blocks_free = MemoryCardImage::GetFreeBlockCount(card->data);
+ card->blocks_free_label->setText(
+ tr("%1 blocks free%2").arg(card->blocks_free).arg(card->dirty ? QStringLiteral(" (*)") : QString()));
+}
+
+void MemoryCardEditorDialog::setCardDirty(Card* card)
+{
+ card->dirty = true;
+ card->save_button->setEnabled(true);
+}
+
+void MemoryCardEditorDialog::newCard(Card* card)
+{
+ promptForSave(card);
+
+ QString filename =
+ QFileDialog::getSaveFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER));
+ if (filename.isEmpty())
+ return;
+
+ {
+ // add to combo box
+ QFileInfo file(filename);
+ QSignalBlocker sb(card->path_cb);
+ card->path_cb->addItem(file.baseName(), QVariant(filename));
+ card->path_cb->setCurrentIndex(card->path_cb->count() - 1);
+ }
+
+ card->filename = filename.toStdString();
+
+ MemoryCardImage::Format(&card->data);
+ updateCardTable(card);
+ updateCardBlocksFree(card);
+ updateButtonState();
+ saveCard(card);
+}
+
+void MemoryCardEditorDialog::saveCard(Card* card)
+{
+ if (card->filename.empty())
+ return;
+
+ if (!MemoryCardImage::SaveToFile(card->data, card->filename.c_str()))
+ {
+ QMessageBox::critical(this, tr("Error"),
+ tr("Failed to write card to '%1'").arg(QString::fromStdString(card->filename)));
+ return;
+ }
+
+ card->dirty = false;
+ card->save_button->setEnabled(false);
+ updateCardBlocksFree(card);
+}
+
+void MemoryCardEditorDialog::promptForSave(Card* card)
+{
+ if (card->filename.empty() || !card->dirty)
+ return;
+
+ if (QMessageBox::question(this, tr("Save memory card?"),
+ tr("Memory card '%1' is not saved, do you want to save before closing?")
+ .arg(QString::fromStdString(card->filename)),
+ QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
+ {
+ return;
+ }
+
+ saveCard(card);
+}
+
+void MemoryCardEditorDialog::doCopyFile()
+{
+ const auto [src, fi] = getSelectedFile();
+ if (!fi)
+ return;
+
+ Card* dst = (src == &m_card_a) ? &m_card_b : &m_card_a;
+
+ if (dst->blocks_free < fi->num_blocks)
+ {
+ QMessageBox::critical(this, tr("Error"),
+ tr("Insufficient blocks, this file needs %1 but only %2 are available.")
+ .arg(fi->num_blocks)
+ .arg(dst->blocks_free));
+ return;
+ }
+
+ std::vector buffer;
+ if (!MemoryCardImage::ReadFile(src->data, *fi, &buffer))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Failed to read file %1").arg(QString::fromStdString(fi->filename)));
+ return;
+ }
+
+ if (!MemoryCardImage::WriteFile(&dst->data, fi->filename, buffer))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Failed to write file %1").arg(QString::fromStdString(fi->filename)));
+ return;
+ }
+
+ clearSelection();
+ updateCardTable(dst);
+ updateCardBlocksFree(dst);
+ setCardDirty(dst);
+ updateButtonState();
+}
+
+void MemoryCardEditorDialog::doDeleteFile()
+{
+ const auto [card, fi] = getSelectedFile();
+ if (!fi)
+ return;
+
+ if (!MemoryCardImage::DeleteFile(&card->data, *fi))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Failed to delete file %1").arg(QString::fromStdString(fi->filename)));
+ return;
+ }
+
+ clearSelection();
+ updateCardTable(card);
+ updateCardBlocksFree(card);
+ setCardDirty(card);
+ updateButtonState();
+}
+
+void MemoryCardEditorDialog::importCard(Card* card)
+{
+ promptForSave(card);
+
+ QString filename =
+ QFileDialog::getOpenFileName(this, tr("Select Import File"), QString(), tr(MEMORY_CARD_IMPORT_FILTER));
+ if (filename.isEmpty())
+ return;
+
+ std::unique_ptr temp = std::make_unique();
+ if (!MemoryCardImage::ImportCard(temp.get(), filename.toStdString().c_str()))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Failed to import memory card. The log may contain more information."));
+ return;
+ }
+
+ clearSelection();
+
+ card->data = *temp;
+ updateCardTable(card);
+ updateCardBlocksFree(card);
+ setCardDirty(card);
+ updateButtonState();
+}
+
+std::tuple MemoryCardEditorDialog::getSelectedFile()
+{
+ QList sel = m_card_a.table->selectedRanges();
+ Card* card = &m_card_a;
+
+ if (sel.isEmpty())
+ {
+ sel = m_card_b.table->selectedRanges();
+ card = &m_card_b;
+ }
+
+ if (sel.isEmpty())
+ return std::tuple(nullptr, nullptr);
+
+ const int index = sel.front().topRow();
+ Assert(index >= 0 && static_cast(index) < card->files.size());
+
+ return std::tuple(card, &card->files[index]);
+}
+
+void MemoryCardEditorDialog::updateButtonState()
+{
+ const auto [selected_card, selected_file] = getSelectedFile();
+ const bool is_card_b = (selected_card == &m_card_b);
+ const bool has_selection = (selected_file != nullptr);
+ const bool card_a_present = !m_card_a.filename.empty();
+ const bool card_b_present = !m_card_b.filename.empty();
+ const bool both_cards_present = card_a_present && card_b_present;
+ m_ui.deleteFile->setEnabled(has_selection);
+ m_ui.exportFile->setEnabled(has_selection);
+ m_ui.moveLeft->setEnabled(both_cards_present && has_selection && is_card_b);
+ m_ui.moveRight->setEnabled(both_cards_present && has_selection && !is_card_b);
+ m_ui.importCardA->setEnabled(card_a_present);
+ m_ui.importCardB->setEnabled(card_b_present);
+}
diff --git a/src/duckstation-qt/memorycardeditordialog.h b/src/duckstation-qt/memorycardeditordialog.h
new file mode 100644
index 000000000..58ef95691
--- /dev/null
+++ b/src/duckstation-qt/memorycardeditordialog.h
@@ -0,0 +1,63 @@
+#pragma once
+#include "core/memory_card_image.h"
+#include "ui_memorycardeditordialog.h"
+#include
+#include
+#include
+#include
+#include
+
+class MemoryCardEditorDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ MemoryCardEditorDialog(QWidget* parent);
+ ~MemoryCardEditorDialog();
+
+protected:
+ void resizeEvent(QResizeEvent* ev);
+ void closeEvent(QCloseEvent* ev);
+
+private Q_SLOTS:
+ void onCardASelectionChanged();
+ void onCardBSelectionChanged();
+ void doCopyFile();
+ void doDeleteFile();
+
+private:
+ struct Card
+ {
+ std::string filename;
+ MemoryCardImage::DataArray data;
+ std::vector files;
+ u32 blocks_free = 0;
+ bool dirty = false;
+
+ QComboBox* path_cb = nullptr;
+ QTableWidget* table = nullptr;
+ QLabel* blocks_free_label = nullptr;
+ QPushButton* save_button = nullptr;
+ };
+
+ void connectUi();
+ void populateComboBox(QComboBox* cb);
+ void clearSelection();
+ void loadCardFromComboBox(Card* card, int index);
+ bool loadCard(const QString& filename, Card* card);
+ void updateCardTable(Card* card);
+ void updateCardBlocksFree(Card* card);
+ void setCardDirty(Card* card);
+ void newCard(Card* card);
+ void saveCard(Card* card);
+ void promptForSave(Card* card);
+ void importCard(Card* card);
+
+ std::tuple getSelectedFile();
+ void updateButtonState();
+
+ Ui::MemoryCardEditorDialog m_ui;
+
+ Card m_card_a;
+ Card m_card_b;
+};
diff --git a/src/duckstation-qt/memorycardeditordialog.ui b/src/duckstation-qt/memorycardeditordialog.ui
new file mode 100644
index 000000000..d09e2e945
--- /dev/null
+++ b/src/duckstation-qt/memorycardeditordialog.ui
@@ -0,0 +1,290 @@
+
+
+ MemoryCardEditorDialog
+
+
+
+ 0
+ 0
+ 846
+ 515
+
+
+
+ Memory Card Editor
+
+
+ -
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ 16
+ 16
+
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+ File Name
+
+
+
+
+ Blocks
+
+
+
+
+ -
+
+
-
+
+
+ Memory Card:
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ New...
+
+
+
+
+
+ -
+
+
-
+
+
+ 0 blocks used
+
+
+
+ -
+
+
+ false
+
+
+ Import File...
+
+
+
+ -
+
+
+ false
+
+
+ Import Card...
+
+
+
+ -
+
+
+ false
+
+
+ Save
+
+
+
+
+
+ -
+
+
-
+
+
+ 0 blocks used
+
+
+
+ -
+
+
+ false
+
+
+ Import File...
+
+
+
+ -
+
+
+ false
+
+
+ Import Card...
+
+
+
+ -
+
+
+ false
+
+
+ Save
+
+
+
+
+
+ -
+
+
-
+
+
+ Memory Card:
+
+
+
+ -
+
+
+ -
+
+
+ New...
+
+
+
+
+
+ -
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ 16
+ 16
+
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+ File Name
+
+
+
+
+ Blocks
+
+
+
+
+ -
+
+
-
+
+
+ false
+
+
+ Delete File
+
+
+
+ -
+
+
+ false
+
+
+ Export File
+
+
+
+ -
+
+
+ false
+
+
+ <<
+
+
+
+ -
+
+
+ false
+
+
+ >>
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+