Qt: Add 'Rename File' to memory card editor

And context menu for ease of use.
This commit is contained in:
Stenzek 2024-10-18 23:44:20 +10:00
parent 50d8bb091f
commit b99ee59224
No known key found for this signature in database
7 changed files with 360 additions and 14 deletions

View File

@ -116,6 +116,7 @@ set(SRCS
memorycardeditorwindow.cpp
memorycardeditorwindow.h
memorycardeditorwindow.ui
memorycardrenamefiledialog.ui
memorycardsettingswidget.cpp
memorycardsettingswidget.h
memoryscannerwindow.cpp

View File

@ -351,6 +351,9 @@
<QtUi Include="gamecheatcodechoiceeditordialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="memorycardrenamefiledialog.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>

View File

@ -288,6 +288,7 @@
<QtUi Include="gamecheatsettingswidget.ui" />
<QtUi Include="gamepatchdetailswidget.ui" />
<QtUi Include="gamecheatcodechoiceeditordialog.ui" />
<QtUi Include="memorycardrenamefiledialog.ui" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="duckstation-qt.rc" />

View File

@ -15,6 +15,7 @@
#include <QtCore/QFileInfo>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
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<std::pair<ConsoleRegion, const char*>, 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<QTableWidget*>(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::Card*, const MemoryCardImage::FileInfo*> MemoryCardEditorWindow::getSelectedFile()
{
QList<QTableWidgetSelectionRange> 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<int>(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<int>(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));
}

View File

@ -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;
};

View File

@ -14,17 +14,20 @@
<string>Memory Card Editor</string>
</property>
<property name="windowIcon">
<iconset resource="resources/resources.qrc">
<iconset resource="resources/duckstation-qt.qrc">
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QTableWidget" name="cardA">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="iconSize">
<size>
@ -82,7 +85,7 @@
<string>New...</string>
</property>
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset resource="resources/duckstation-qt.qrc">
<normaloff>:/icons/document-new.png</normaloff>:/icons/document-new.png</iconset>
</property>
</widget>
@ -93,7 +96,7 @@
<string>Open...</string>
</property>
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset resource="resources/duckstation-qt.qrc">
<normaloff>:/icons/document-open.png</normaloff>:/icons/document-open.png</iconset>
</property>
</widget>
@ -112,7 +115,7 @@
<item>
<widget class="QDialogButtonBox" name="buttonBoxB">
<property name="standardButtons">
<set>QDialogButtonBox::NoButton</set>
<set>QDialogButtonBox::StandardButton::NoButton</set>
</property>
</widget>
</item>
@ -130,7 +133,7 @@
<item>
<widget class="QDialogButtonBox" name="buttonBoxA">
<property name="standardButtons">
<set>QDialogButtonBox::NoButton</set>
<set>QDialogButtonBox::StandardButton::NoButton</set>
</property>
</widget>
</item>
@ -154,7 +157,7 @@
<string>New...</string>
</property>
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset resource="resources/duckstation-qt.qrc">
<normaloff>:/icons/document-new.png</normaloff>:/icons/document-new.png</iconset>
</property>
</widget>
@ -165,7 +168,7 @@
<string>Open...</string>
</property>
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset resource="resources/duckstation-qt.qrc">
<normaloff>:/icons/document-open.png</normaloff>:/icons/document-open.png</iconset>
</property>
</widget>
@ -174,11 +177,14 @@
</item>
<item row="1" column="3">
<widget class="QTableWidget" name="cardB">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="iconSize">
<size>
@ -217,17 +223,17 @@
<item row="1" column="1">
<widget class="QDialogButtonBox" name="centerButtonBox">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::NoButton</set>
<set>QDialogButtonBox::StandardButton::NoButton</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources/resources.qrc"/>
<include location="resources/duckstation-qt.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MemoryCardRenameFileDialog</class>
<widget class="QDialog" name="MemoryCardRenameFileDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>438</width>
<height>232</height>
</rect>
</property>
<property name="windowTitle">
<string>Rename Memory Card File</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="icon">
<property name="minimumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;WARNING: &lt;/span&gt;Renaming memory card files may result in saves becoming inaccessible or corrupted. Be sure to make backups first.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="originalNameLabel">
<property name="text">
<string>Original Name:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="originalName">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="regionLabel">
<property name="text">
<string>Region:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="region"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="serialLabel">
<property name="text">
<string>Serial:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="serial"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="filenameLabel">
<property name="text">
<string>File Name:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="filename"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="fullFilenameLabel">
<property name="text">
<string>Full File Name:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="fullFilename"/>
</item>
<item row="6" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>