diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index 291942f50..ef0de47d8 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -116,6 +116,7 @@ set(SRCS
memorycardeditorwindow.cpp
memorycardeditorwindow.h
memorycardeditorwindow.ui
+ memorycardrenamefiledialog.ui
memorycardsettingswidget.cpp
memorycardsettingswidget.h
memoryscannerwindow.cpp
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index 30a86b6af..10a75a986 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -351,6 +351,9 @@
Document
+
+ Document
+
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
index d6cf8760a..b6afd53bf 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj.filters
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -288,6 +288,7 @@
+
diff --git a/src/duckstation-qt/memorycardeditorwindow.cpp b/src/duckstation-qt/memorycardeditorwindow.cpp
index 6e99d787c..d06e74acb 100644
--- a/src/duckstation-qt/memorycardeditorwindow.cpp
+++ b/src/duckstation-qt/memorycardeditorwindow.cpp
@@ -15,6 +15,7 @@
#include
#include
+#include
#include
static constexpr char MEMORY_CARD_IMAGE_FILTER[] = QT_TRANSLATE_NOOP(
@@ -23,6 +24,11 @@ static constexpr char MEMORY_CARD_IMPORT_FILTER[] =
QT_TRANSLATE_NOOP("MemoryCardEditorWindow", "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme)");
static constexpr char SINGLE_SAVEFILE_FILTER[] =
TRANSLATE_NOOP("MemoryCardEditorWindow", "Single Save Files (*.mcs);;All Files (*.*)");
+static constexpr std::array, 3> MEMORY_CARD_FILE_REGION_PREFIXES = {{
+ {ConsoleRegion::NTSC_U, "BA"},
+ {ConsoleRegion::NTSC_J, "BI"},
+ {ConsoleRegion::PAL, "BE"},
+}};
MemoryCardEditorWindow::MemoryCardEditorWindow() : QWidget()
{
@@ -31,6 +37,7 @@ MemoryCardEditorWindow::MemoryCardEditorWindow() : QWidget()
m_deleteFile = m_ui.centerButtonBox->addButton(tr("Delete File"), QDialogButtonBox::ActionRole);
m_undeleteFile = m_ui.centerButtonBox->addButton(tr("Undelete File"), QDialogButtonBox::ActionRole);
+ m_renameFile = m_ui.centerButtonBox->addButton(tr("Rename File"), QDialogButtonBox::ActionRole);
m_exportFile = m_ui.centerButtonBox->addButton(tr("Export File"), QDialogButtonBox::ActionRole);
m_moveLeft = m_ui.centerButtonBox->addButton(tr("<<"), QDialogButtonBox::ActionRole);
m_moveRight = m_ui.centerButtonBox->addButton(tr(">>"), QDialogButtonBox::ActionRole);
@@ -135,7 +142,11 @@ void MemoryCardEditorWindow::connectCardUi(Card* card, QDialogButtonBox* buttonB
void MemoryCardEditorWindow::connectUi()
{
connect(m_ui.cardA, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorWindow::onCardASelectionChanged);
+ connect(m_ui.cardA, &QTableWidget::customContextMenuRequested, this,
+ &MemoryCardEditorWindow::onCardContextMenuRequested);
connect(m_ui.cardB, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorWindow::onCardBSelectionChanged);
+ connect(m_ui.cardB, &QTableWidget::customContextMenuRequested, this,
+ &MemoryCardEditorWindow::onCardContextMenuRequested);
connect(m_moveLeft, &QPushButton::clicked, this, &MemoryCardEditorWindow::doCopyFile);
connect(m_moveRight, &QPushButton::clicked, this, &MemoryCardEditorWindow::doCopyFile);
connect(m_deleteFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doDeleteFile);
@@ -149,6 +160,7 @@ void MemoryCardEditorWindow::connectUi()
connect(m_ui.newCardB, &QPushButton::clicked, [this]() { newCard(&m_card_b); });
connect(m_ui.openCardA, &QPushButton::clicked, [this]() { openCard(&m_card_a); });
connect(m_ui.openCardB, &QPushButton::clicked, [this]() { openCard(&m_card_b); });
+ connect(m_renameFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doRenameSaveFile);
connect(m_exportFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doExportSaveFile);
}
@@ -522,6 +534,33 @@ void MemoryCardEditorWindow::doExportSaveFile()
}
}
+void MemoryCardEditorWindow::doRenameSaveFile()
+{
+ const auto [card, fi] = getSelectedFile();
+ if (!fi)
+ return;
+
+ const std::string new_name = MemoryCardRenameFileDialog::promptForNewName(this, fi->filename);
+ if (new_name.empty())
+ return;
+
+ Error error;
+ if (!MemoryCardImage::RenameFile(&card->data, *fi, new_name, &error))
+ {
+ QMessageBox::critical(this, tr("Error"),
+ tr("Failed to rename save file %1:\n%2")
+ .arg(QString::fromStdString(fi->filename))
+ .arg(QString::fromStdString(error.GetDescription())));
+ return;
+ }
+
+ clearSelection();
+ setCardDirty(card);
+ updateCardTable(card);
+ updateCardBlocksFree(card);
+ updateButtonState();
+}
+
void MemoryCardEditorWindow::importCard(Card* card)
{
promptForSave(card);
@@ -597,6 +636,35 @@ void MemoryCardEditorWindow::importSaveFile(Card* card)
updateCardBlocksFree(card);
}
+void MemoryCardEditorWindow::onCardContextMenuRequested(const QPoint& pos)
+{
+ QTableWidget* table = qobject_cast(sender());
+ if (!table)
+ return;
+
+ const auto& [card, fi] = getSelectedFile();
+ if (!card)
+ return;
+
+ QMenu menu(table);
+ QAction* action = menu.addAction(tr("Delete File"));
+ action->setEnabled(fi && !fi->deleted);
+ connect(action, &QAction::triggered, this, &MemoryCardEditorWindow::doDeleteFile);
+ action = menu.addAction(tr("Undelete File"));
+ action->setEnabled(fi && fi->deleted);
+ connect(action, &QAction::triggered, this, &MemoryCardEditorWindow::doUndeleteFile);
+ action = menu.addAction(tr("Rename File"));
+ action->setEnabled(fi != nullptr);
+ connect(action, &QAction::triggered, this, &MemoryCardEditorWindow::doRenameSaveFile);
+ action = menu.addAction(tr("Export File"));
+ connect(action, &QAction::triggered, this, &MemoryCardEditorWindow::doExportSaveFile);
+ action = menu.addAction(tr("Copy File"));
+ action->setEnabled(fi && !m_card_a.filename.empty() && !m_card_b.filename.empty());
+ connect(action, &QAction::triggered, this, &MemoryCardEditorWindow::doCopyFile);
+
+ menu.exec(table->mapToGlobal(pos));
+}
+
std::tuple MemoryCardEditorWindow::getSelectedFile()
{
QList sel = m_card_a.table->selectedRanges();
@@ -629,8 +697,115 @@ void MemoryCardEditorWindow::updateButtonState()
m_deleteFile->setEnabled(has_selection);
m_undeleteFile->setEnabled(is_deleted);
m_exportFile->setEnabled(has_selection);
+ m_renameFile->setEnabled(has_selection);
m_moveLeft->setEnabled(both_cards_present && has_selection && is_card_b);
m_moveRight->setEnabled(both_cards_present && has_selection && !is_card_b);
m_ui.buttonBoxA->setEnabled(card_a_present);
m_ui.buttonBoxB->setEnabled(card_b_present);
}
+
+MemoryCardRenameFileDialog::MemoryCardRenameFileDialog(QWidget* parent, std::string_view old_name) : QDialog(parent)
+{
+ m_ui.setupUi(this);
+ setupAdditionalUi();
+
+ const QString original_name = QtUtils::StringViewToQString(old_name);
+ m_ui.originalName->setText(original_name);
+ m_ui.fullFilename->setText(original_name);
+ updateSimplifiedFieldsFromFullName();
+}
+
+MemoryCardRenameFileDialog::~MemoryCardRenameFileDialog() = default;
+
+std::string MemoryCardRenameFileDialog::promptForNewName(QWidget* parent, std::string_view old_name)
+{
+ MemoryCardRenameFileDialog dlg(parent, old_name);
+
+ std::string ret;
+ if (dlg.exec() != 1)
+ return ret;
+
+ ret = dlg.m_ui.fullFilename->text().toStdString();
+ return ret;
+}
+
+void MemoryCardRenameFileDialog::setupAdditionalUi()
+{
+ m_ui.icon->setPixmap(QIcon::fromTheme(QStringLiteral("memcard-line")).pixmap(32, 32));
+
+ for (const auto& [region, prefix] : MEMORY_CARD_FILE_REGION_PREFIXES)
+ {
+ m_ui.region->addItem(QtUtils::GetIconForRegion(region), Settings::GetConsoleRegionDisplayName(region),
+ QVariant(QString::fromUtf8(prefix)));
+ }
+
+ connect(m_ui.region, &QComboBox::currentIndexChanged, this,
+ &MemoryCardRenameFileDialog::updateFullNameFromSimplifiedFields);
+ connect(m_ui.serial, &QLineEdit::textChanged, this, &MemoryCardRenameFileDialog::updateFullNameFromSimplifiedFields);
+ connect(m_ui.filename, &QLineEdit::textChanged, this,
+ &MemoryCardRenameFileDialog::updateFullNameFromSimplifiedFields);
+
+ connect(m_ui.fullFilename, &QLineEdit::textChanged, this,
+ &MemoryCardRenameFileDialog::updateSimplifiedFieldsFromFullName);
+
+ connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &MemoryCardRenameFileDialog::accept);
+ connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &MemoryCardRenameFileDialog::reject);
+
+ m_ui.fullFilename->setFocus();
+}
+
+void MemoryCardRenameFileDialog::updateSimplifiedFieldsFromFullName()
+{
+ const QString full_name = m_ui.fullFilename->text();
+
+ const QString region = full_name.mid(0, MemoryCardImage::FILE_REGION_LENGTH);
+ const QString serial = full_name.mid(MemoryCardImage::FILE_REGION_LENGTH, MemoryCardImage::FILE_SERIAL_LENGTH);
+ const QString filename = full_name.mid(MemoryCardImage::FILE_REGION_LENGTH + MemoryCardImage::FILE_SERIAL_LENGTH);
+
+ {
+ QSignalBlocker sb(m_ui.region);
+
+ while (m_ui.region->count() > static_cast(MEMORY_CARD_FILE_REGION_PREFIXES.size()))
+ m_ui.region->removeItem(m_ui.region->count() - 1);
+
+ const std::string regionStr = region.toStdString();
+ size_t i;
+ for (i = 0; i < MEMORY_CARD_FILE_REGION_PREFIXES.size(); i++)
+ {
+ if (regionStr == MEMORY_CARD_FILE_REGION_PREFIXES[i].second)
+ {
+ m_ui.region->setCurrentIndex(static_cast(i));
+ break;
+ }
+ }
+ if (i == MEMORY_CARD_FILE_REGION_PREFIXES.size())
+ {
+ m_ui.region->addItem(tr("Unknown (%1)").arg(region), region);
+ m_ui.region->setCurrentIndex(m_ui.region->count() - 1);
+ }
+ }
+
+ {
+ QSignalBlocker sb(m_ui.serial);
+ m_ui.serial->setText(serial);
+ }
+
+ {
+ QSignalBlocker sb(m_ui.filename);
+ m_ui.filename->setText(filename);
+ }
+}
+
+void MemoryCardRenameFileDialog::updateFullNameFromSimplifiedFields()
+{
+ const QString region = m_ui.region->currentData().toString();
+ const QString serial = m_ui.serial->text()
+ .left(MemoryCardImage::FILE_SERIAL_LENGTH)
+ .leftJustified(MemoryCardImage::FILE_SERIAL_LENGTH, QChar(' '));
+ const QString filename = m_ui.filename->text()
+ .left(MemoryCardImage::FILE_FILENAME_LENGTH)
+ .leftJustified(MemoryCardImage::FILE_FILENAME_LENGTH, QChar(' '));
+
+ const QSignalBlocker sb(m_ui.fullFilename);
+ m_ui.fullFilename->setText(QStringLiteral("%1%2%3").arg(region).arg(serial).arg(filename));
+}
diff --git a/src/duckstation-qt/memorycardeditorwindow.h b/src/duckstation-qt/memorycardeditorwindow.h
index 1964bb97b..1a5aeb47f 100644
--- a/src/duckstation-qt/memorycardeditorwindow.h
+++ b/src/duckstation-qt/memorycardeditorwindow.h
@@ -4,6 +4,7 @@
#pragma once
#include "ui_memorycardeditorwindow.h"
+#include "ui_memorycardrenamefiledialog.h"
#include "core/memory_card_image.h"
@@ -36,6 +37,7 @@ protected:
private Q_SLOTS:
void onCardASelectionChanged();
void onCardBSelectionChanged();
+ void onCardContextMenuRequested(const QPoint& pos);
void doCopyFile();
void doDeleteFile();
void doUndeleteFile();
@@ -76,6 +78,7 @@ private:
void importCard(Card* card);
void formatCard(Card* card);
+ void doRenameSaveFile();
void doExportSaveFile();
void importSaveFile(Card* card);
@@ -85,6 +88,7 @@ private:
Ui::MemoryCardEditorDialog m_ui;
QPushButton* m_deleteFile;
QPushButton* m_undeleteFile;
+ QPushButton* m_renameFile;
QPushButton* m_exportFile;
QPushButton* m_moveLeft;
QPushButton* m_moveRight;
@@ -92,3 +96,22 @@ private:
Card m_card_a;
Card m_card_b;
};
+
+class MemoryCardRenameFileDialog final : public QDialog
+{
+ Q_OBJECT
+public:
+ MemoryCardRenameFileDialog(QWidget* parent, std::string_view old_name);
+ ~MemoryCardRenameFileDialog() override;
+
+ static std::string promptForNewName(QWidget* parent, std::string_view old_name);
+
+private Q_SLOTS:
+ void updateSimplifiedFieldsFromFullName();
+ void updateFullNameFromSimplifiedFields();
+
+private:
+ void setupAdditionalUi();
+
+ Ui::MemoryCardRenameFileDialog m_ui;
+};
diff --git a/src/duckstation-qt/memorycardeditorwindow.ui b/src/duckstation-qt/memorycardeditorwindow.ui
index c87a8cc2c..1c97b45d0 100644
--- a/src/duckstation-qt/memorycardeditorwindow.ui
+++ b/src/duckstation-qt/memorycardeditorwindow.ui
@@ -14,17 +14,20 @@
Memory Card Editor
-
+
:/icons/duck.png:/icons/duck.png
-
+
+ Qt::ContextMenuPolicy::CustomContextMenu
+
- QAbstractItemView::SingleSelection
+ QAbstractItemView::SelectionMode::SingleSelection
- QAbstractItemView::SelectRows
+ QAbstractItemView::SelectionBehavior::SelectRows
@@ -82,7 +85,7 @@
New...
-
+
:/icons/document-new.png:/icons/document-new.png
@@ -93,7 +96,7 @@
Open...
-
+
:/icons/document-open.png:/icons/document-open.png
@@ -112,7 +115,7 @@
-
- QDialogButtonBox::NoButton
+ QDialogButtonBox::StandardButton::NoButton
@@ -130,7 +133,7 @@
-
- QDialogButtonBox::NoButton
+ QDialogButtonBox::StandardButton::NoButton
@@ -154,7 +157,7 @@
New...
-
+
:/icons/document-new.png:/icons/document-new.png
@@ -165,7 +168,7 @@
Open...
-
+
:/icons/document-open.png:/icons/document-open.png
@@ -174,11 +177,14 @@
-
+
+ Qt::ContextMenuPolicy::CustomContextMenu
+
- QAbstractItemView::SingleSelection
+ QAbstractItemView::SelectionMode::SingleSelection
- QAbstractItemView::SelectRows
+ QAbstractItemView::SelectionBehavior::SelectRows
@@ -217,17 +223,17 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
- QDialogButtonBox::NoButton
+ QDialogButtonBox::StandardButton::NoButton
-
+
diff --git a/src/duckstation-qt/memorycardrenamefiledialog.ui b/src/duckstation-qt/memorycardrenamefiledialog.ui
new file mode 100644
index 000000000..0d232d07f
--- /dev/null
+++ b/src/duckstation-qt/memorycardrenamefiledialog.ui
@@ -0,0 +1,137 @@
+
+
+ MemoryCardRenameFileDialog
+
+
+
+ 0
+ 0
+ 438
+ 232
+
+
+
+ Rename Memory Card File
+
+
+ -
+
+
+ 10
+
+
-
+
+
+
+ 32
+ 32
+
+
+
+
+ 32
+ 32
+
+
+
+ Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-weight:700;">WARNING: </span>Renaming memory card files may result in saves becoming inaccessible or corrupted. Be sure to make backups first.</p></body></html>
+
+
+ Qt::TextFormat::RichText
+
+
+ Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop
+
+
+ true
+
+
+
+
+
+ -
+
+
+ Original Name:
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Region:
+
+
+
+ -
+
+
+ -
+
+
+ Serial:
+
+
+
+ -
+
+
+ -
+
+
+ File Name:
+
+
+
+ -
+
+
+ -
+
+
+ Full File Name:
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 0
+
+
+
+
+ -
+
+
+ QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
+
+
+
+
+
+
+
+