diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt
index 757a62294e..6e99bee910 100644
--- a/pcsx2-qt/CMakeLists.txt
+++ b/pcsx2-qt/CMakeLists.txt
@@ -55,6 +55,9 @@ target_sources(pcsx2-qt PRIVATE
Settings/ControllerSettingsDialog.cpp
Settings/ControllerSettingsDialog.h
Settings/ControllerSettingsDialog.ui
+ Settings/CreateMemoryCardDialog.cpp
+ Settings/CreateMemoryCardDialog.h
+ Settings/CreateMemoryCardDialog.ui
Settings/EmulationSettingsWidget.cpp
Settings/EmulationSettingsWidget.h
Settings/EmulationSettingsWidget.ui
diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp
index 8181d0c696..f987d6c4f2 100644
--- a/pcsx2-qt/MainWindow.cpp
+++ b/pcsx2-qt/MainWindow.cpp
@@ -145,7 +145,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionSystemSettings, &QAction::triggered, [this]() { doSettings("System"); });
connect(m_ui.actionGraphicsSettings, &QAction::triggered, [this]() { doSettings("Graphics"); });
connect(m_ui.actionAudioSettings, &QAction::triggered, [this]() { doSettings("Audio"); });
- connect(m_ui.actionMemoryCardSettings, &QAction::triggered, [this]() { doSettings("Memory Card"); });
+ connect(m_ui.actionMemoryCardSettings, &QAction::triggered, [this]() { doSettings("Memory Cards"); });
connect(
m_ui.actionControllerSettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsDialog::Category::GlobalSettings); });
connect(m_ui.actionHotkeySettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsDialog::Category::HotkeySettings); });
diff --git a/pcsx2-qt/Settings/CreateMemoryCardDialog.cpp b/pcsx2-qt/Settings/CreateMemoryCardDialog.cpp
new file mode 100644
index 0000000000..b4b36bb475
--- /dev/null
+++ b/pcsx2-qt/Settings/CreateMemoryCardDialog.cpp
@@ -0,0 +1,128 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#include "PrecompiledHeader.h"
+
+#include "common/StringUtil.h"
+
+#include
+#include
+
+#include "Settings/CreateMemoryCardDialog.h"
+
+#include "pcsx2/MemoryCardFile.h"
+#include "pcsx2/System.h"
+
+CreateMemoryCardDialog::CreateMemoryCardDialog(QWidget* parent /* = nullptr */)
+ : QDialog(parent)
+{
+ m_ui.setupUi(this);
+
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+ connect(m_ui.name, &QLineEdit::textChanged, this, &CreateMemoryCardDialog::nameTextChanged);
+
+ connect(m_ui.size8MB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS2_8MB); });
+ connect(m_ui.size16MB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS2_16MB); });
+ connect(m_ui.size32MB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS2_32MB); });
+ connect(m_ui.size64MB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS2_64MB); });
+ connect(m_ui.size128KB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS1); });
+ connect(m_ui.sizeFolder, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::Folder, MemoryCardFileType::Unknown); });
+
+ disconnect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, nullptr);
+
+ connect(m_ui.buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &CreateMemoryCardDialog::createCard);
+ connect(m_ui.buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &CreateMemoryCardDialog::close);
+ connect(m_ui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, &CreateMemoryCardDialog::restoreDefaults);
+
+ updateState();
+}
+
+CreateMemoryCardDialog::~CreateMemoryCardDialog() = default;
+
+void CreateMemoryCardDialog::nameTextChanged()
+{
+ const QString controlName(m_ui.name->text());
+ const int cursorPos = m_ui.name->cursorPosition();
+
+ QString nameWithoutExtension;
+ if (controlName.endsWith(QStringLiteral(".ps2")))
+ nameWithoutExtension = controlName.left(controlName.size() - 4);
+ else
+ nameWithoutExtension = controlName;
+
+ QSignalBlocker sb(m_ui.name);
+ if (nameWithoutExtension.isEmpty())
+ m_ui.name->setText(QString());
+ else
+ m_ui.name->setText(nameWithoutExtension + QStringLiteral(".ps2"));
+
+ m_ui.name->setCursorPosition(cursorPos);
+ updateState();
+}
+
+void CreateMemoryCardDialog::setType(MemoryCardType type, MemoryCardFileType fileType)
+{
+ m_type = type;
+ m_fileType = fileType;
+ updateState();
+}
+
+void CreateMemoryCardDialog::restoreDefaults()
+{
+ setType(MemoryCardType::File, MemoryCardFileType::PS2_8MB);
+ m_ui.size8MB->setChecked(true);
+ m_ui.size16MB->setChecked(false);
+ m_ui.size32MB->setChecked(false);
+ m_ui.size64MB->setChecked(false);
+ m_ui.size128KB->setChecked(false);
+ m_ui.sizeFolder->setChecked(false);
+}
+
+void CreateMemoryCardDialog::updateState()
+{
+ const bool okay = (m_ui.name->text().length() > 4);
+
+ m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(okay);
+ m_ui.ntfsCompression->setEnabled(m_type == MemoryCardType::File);
+}
+
+void CreateMemoryCardDialog::createCard()
+{
+ const QString name(m_ui.name->text());
+ const std::string nameStr(name.toStdString());
+ if (FileMcd_GetCardInfo(nameStr).has_value())
+ {
+ QMessageBox::critical(this, tr("Create Memory Card"),
+ tr("Failed to create the memory card, because another card with the name '%1' already exists.").arg(name));
+ return;
+ }
+
+ if (!FileMcd_CreateNewCard(nameStr, m_type, m_fileType))
+ {
+ QMessageBox::critical(this, tr("Create Memory Card"),
+ tr("Failed to create the memory card, the log may contain more information."));
+ return;
+ }
+
+ if (m_ui.ntfsCompression->isChecked() && m_type == MemoryCardType::File)
+ {
+ const std::string fullPath(Path::CombineStdString(EmuFolders::MemoryCards, nameStr));
+ NTFS_CompressFile(StringUtil::UTF8StringToWxString(fullPath), true);
+ }
+
+ QMessageBox::information(this, tr("Create Memory Card"), tr("Memory card '%1' created.").arg(name));
+ accept();
+}
diff --git a/pcsx2-qt/Settings/CreateMemoryCardDialog.h b/pcsx2-qt/Settings/CreateMemoryCardDialog.h
new file mode 100644
index 0000000000..d7900e3c2d
--- /dev/null
+++ b/pcsx2-qt/Settings/CreateMemoryCardDialog.h
@@ -0,0 +1,45 @@
+/* PCSX2 - PS2 Emulator for PCs
+ * Copyright (C) 2002-2022 PCSX2 Dev Team
+ *
+ * PCSX2 is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU Lesser General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with PCSX2.
+ * If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "ui_CreateMemoryCardDialog.h"
+
+#include "pcsx2/Config.h"
+
+class CreateMemoryCardDialog final : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit CreateMemoryCardDialog(QWidget* parent = nullptr);
+ ~CreateMemoryCardDialog();
+
+private Q_SLOTS:
+ void nameTextChanged();
+ void createCard();
+
+private:
+ void setType(MemoryCardType type, MemoryCardFileType fileType);
+ void restoreDefaults();
+ void updateState();
+
+ Ui::CreateMemoryCardDialog m_ui;
+
+ MemoryCardType m_type = MemoryCardType::File;
+ MemoryCardFileType m_fileType = MemoryCardFileType::PS2_8MB;
+};
diff --git a/pcsx2-qt/Settings/CreateMemoryCardDialog.ui b/pcsx2-qt/Settings/CreateMemoryCardDialog.ui
new file mode 100644
index 0000000000..c42b072838
--- /dev/null
+++ b/pcsx2-qt/Settings/CreateMemoryCardDialog.ui
@@ -0,0 +1,312 @@
+
+
+ CreateMemoryCardDialog
+
+
+
+ 0
+ 0
+ 593
+ 545
+
+
+
+ Create Memory Card
+
+
+ true
+
+
+ -
+
+
+ 10
+
+
-
+
+
+
+ 48
+ 48
+
+
+
+
+ 48
+ 48
+
+
+
+ :/icons/black/48/sd-card-line.png
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-weight:700;">Create Memory Card</span><br />Enter the name of the memory card you wish to create, and choose a size. We recommend either using 8MB memory cards, or folder memory cards for best compatibility.</p></body></html>
+
+
+ Qt::RichText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+ Memory Card Name:
+
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+ 8 MB [Most Compatible]
+
+
+ true
+
+
+
+ -
+
+
+ This is the standard Sony-provisioned size, and is supported by all games and BIOS versions.
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+ 16 MB
+
+
+
+ -
+
+
+ A typical size for third-party memory cards which should work with most games.
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+ 32 MB
+
+
+
+ -
+
+
+ A typical size for third-party memory cards which should work with most games.
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+ 64 MB
+
+
+
+ -
+
+
+ Low compatiblity warning: yes, it's very big, but may not work with many games.
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+ Folder [Recommended]
+
+
+
+ -
+
+
+ Store memory card contents in the host filesystem instead of a file.
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+ 128 KB (PS1)
+
+
+
+ -
+
+
+ This is the standard Sony-provisioned size PS1 memory card, and only compatible with PS1 games.
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+ Use NTFS Compression
+
+
+ true
+
+
+
+ -
+
+
+ NTFS compression is built-in, fast, and completely reliable. Typically compresses memory cards (highly recommended).
+
+
+ Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft
+
+
+ true
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults
+
+
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ CreateMemoryCardDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ CreateMemoryCardDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/pcsx2-qt/Settings/GameListSettingsWidget.ui b/pcsx2-qt/Settings/GameListSettingsWidget.ui
index 6b61d81330..9789e66165 100644
--- a/pcsx2-qt/Settings/GameListSettingsWidget.ui
+++ b/pcsx2-qt/Settings/GameListSettingsWidget.ui
@@ -60,7 +60,8 @@
Add
-
+
+ ..
@@ -76,7 +77,8 @@
Remove
-
+
+ ..
@@ -130,7 +132,8 @@
Add
-
+
+ ..
@@ -146,7 +149,8 @@
Remove
-
+
+ ..
@@ -182,7 +186,8 @@
Scan For New Games
-
+
+ ..
@@ -198,7 +203,8 @@
Rescan All Games
-
+
+ ..
@@ -207,7 +213,7 @@
-
+
diff --git a/pcsx2-qt/Settings/MemoryCardSettingsWidget.cpp b/pcsx2-qt/Settings/MemoryCardSettingsWidget.cpp
index e2803b8869..4004d8c991 100644
--- a/pcsx2-qt/Settings/MemoryCardSettingsWidget.cpp
+++ b/pcsx2-qt/Settings/MemoryCardSettingsWidget.cpp
@@ -15,21 +15,450 @@
#include "PrecompiledHeader.h"
+#include
+#include
+#include
#include
#include
+#include "common/StringUtil.h"
+
#include "MemoryCardSettingsWidget.h"
+#include "CreateMemoryCardDialog.h"
#include "EmuThread.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsDialog.h"
+#include "pcsx2/MemoryCardFile.h"
+
+static std::string getSlotFilenameKey(u32 slot)
+{
+ return StringUtil::StdStringFromFormat("Slot%u_Filename", slot + 1);
+}
+
MemoryCardSettingsWidget::MemoryCardSettingsWidget(SettingsDialog* dialog, QWidget* parent)
: QWidget(parent)
+ , m_dialog(dialog)
{
- // SettingsInterface* sif = dialog->getSettingsInterface();
+ SettingsInterface* sif = m_dialog->getSettingsInterface();
m_ui.setupUi(this);
+
+ // this is a bit lame, but resizeEvent() isn't good enough to autosize our columns,
+ // since the group box hasn't been resized at that point.
+ m_ui.cardGroupBox->installEventFilter(this);
+
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.autoEject, "EmuCore", "McdEnableEjection", true);
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.automaticManagement, "EmuCore", "McdFolderAutoManage", true);
+
+ for (u32 i = 0; i < static_cast(m_slots.size()); i++)
+ createSlotWidgets(&m_slots[i], i);
+
+ m_ui.cardList->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(m_ui.cardList, &MemoryCardListWidget::itemSelectionChanged, this, &MemoryCardSettingsWidget::updateCardActions);
+ connect(m_ui.cardList, &MemoryCardListWidget::customContextMenuRequested, this, &MemoryCardSettingsWidget::listContextMenuRequested);
+
+ connect(m_ui.refreshCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::refresh);
+ connect(m_ui.createCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::createCard);
+ connect(m_ui.duplicateCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::duplicateCard);
+ connect(m_ui.renameCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::renameCard);
+ connect(m_ui.convertCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::convertCard);
+ connect(m_ui.deleteCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::deleteCard);
+
+ refresh();
}
MemoryCardSettingsWidget::~MemoryCardSettingsWidget() = default;
+
+void MemoryCardSettingsWidget::resizeEvent(QResizeEvent* event)
+{
+ QWidget::resizeEvent(event);
+ autoSizeUI();
+}
+
+bool MemoryCardSettingsWidget::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == m_ui.cardGroupBox && event->type() == QEvent::Resize)
+ autoSizeUI();
+
+ return QWidget::eventFilter(watched, event);
+}
+
+void MemoryCardSettingsWidget::createSlotWidgets(SlotGroup* port, u32 slot)
+{
+ port->root = new QWidget(m_ui.portGroupBox);
+
+ SettingsInterface* sif = m_dialog->getSettingsInterface();
+ port->enable = new QCheckBox(tr("Port %1").arg(slot + 1), port->root);
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, port->enable, "MemoryCards", StringUtil::StdStringFromFormat("Slot%u_Enable", slot + 1), true);
+ connect(port->enable, &QCheckBox::stateChanged, this, &MemoryCardSettingsWidget::refresh);
+
+ port->eject = new QToolButton(port->root);
+ port->eject->setIcon(QIcon::fromTheme("eject-line"));
+ port->eject->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
+ connect(port->eject, &QToolButton::clicked, this, [this, slot]() { ejectSlot(slot); });
+
+ port->slot = new MemoryCardSlotWidget(port->root);
+ connect(port->slot, &MemoryCardSlotWidget::cardDropped, this, [this, slot](const QString& card) { tryInsertCard(slot, card); });
+
+ QHBoxLayout* bottom_layout = new QHBoxLayout();
+ bottom_layout->setContentsMargins(0, 0, 0, 0);
+ bottom_layout->addWidget(port->slot, 1);
+ bottom_layout->addWidget(port->eject, 0);
+
+ QVBoxLayout* vert_layout = new QVBoxLayout(port->root);
+ vert_layout->setContentsMargins(0, 0, 0, 0);
+ vert_layout->addWidget(port->enable, 0);
+ vert_layout->addLayout(bottom_layout, 1);
+
+ static_cast(m_ui.portGroupBox->layout())->addWidget(port->root, 0, slot);
+}
+
+void MemoryCardSettingsWidget::autoSizeUI()
+{
+ QtUtils::ResizeColumnsForTreeView(m_ui.cardList, {-1, 100, 80, 150});
+}
+
+void MemoryCardSettingsWidget::tryInsertCard(u32 slot, const QString& newCard)
+{
+ // handle where the card is dragged in from explorer or something
+ const int lastSlashPos = std::max(newCard.lastIndexOf('/'), newCard.lastIndexOf('\\'));
+ const std::string newCardStr((lastSlashPos >= 0) ? newCard.mid(0, lastSlashPos).toStdString() : newCard.toStdString());
+ if (newCardStr.empty())
+ return;
+
+ // make sure it's a card in the directory
+ const std::vector mcds(FileMcd_GetAvailableCards(true));
+ if (std::none_of(mcds.begin(), mcds.end(), [&newCardStr](const AvailableMcdInfo& mcd) { return mcd.name == newCardStr; }))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("This memory card is unknown."));
+ return;
+ }
+
+ m_dialog->setStringSettingValue("MemoryCards", getSlotFilenameKey(slot).c_str(), newCardStr.c_str());
+ refresh();
+}
+
+void MemoryCardSettingsWidget::ejectSlot(u32 slot)
+{
+ m_dialog->setStringSettingValue("MemoryCards", getSlotFilenameKey(slot).c_str(), m_dialog->isPerGameSettings() ? nullptr : "");
+ refresh();
+}
+
+void MemoryCardSettingsWidget::createCard()
+{
+ CreateMemoryCardDialog dialog(QtUtils::GetRootWidget(this));
+ if (dialog.exec() == QDialog::Accepted)
+ refresh();
+}
+
+QString MemoryCardSettingsWidget::getSelectedCard() const
+{
+ QString ret;
+
+ const QList selection(m_ui.cardList->selectedItems());
+ if (!selection.empty())
+ ret = selection[0]->text(0);
+
+ return ret;
+}
+
+void MemoryCardSettingsWidget::updateCardActions()
+{
+ const bool hasSelection = !getSelectedCard().isEmpty();
+
+ m_ui.convertCard->setEnabled(hasSelection);
+ m_ui.duplicateCard->setEnabled(hasSelection);
+ m_ui.renameCard->setEnabled(hasSelection);
+ m_ui.deleteCard->setEnabled(hasSelection);
+}
+
+void MemoryCardSettingsWidget::duplicateCard()
+{
+ const QString selectedCard(getSelectedCard());
+ if (selectedCard.isEmpty())
+ return;
+
+ QMessageBox::critical(this, tr("Error"), tr("Not yet implemented."));
+}
+
+void MemoryCardSettingsWidget::deleteCard()
+{
+ const QString selectedCard(getSelectedCard());
+ if (selectedCard.isEmpty())
+ return;
+
+ if (QMessageBox::question(QtUtils::GetRootWidget(this), tr("Delete Memory Card"),
+ tr("Are you sure you wish to delete the memory card '%1'?\n\n"
+ "This action cannot be reversed, and you will lose any saves on the card.")
+ .arg(selectedCard)) != QMessageBox::Yes)
+ {
+ return;
+ }
+
+ if (!FileMcd_DeleteCard(selectedCard.toStdString()))
+ {
+ QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Delete Memory Card"),
+ tr("Failed to delete the memory card. The log may have more information."));
+ return;
+ }
+
+ refresh();
+}
+
+void MemoryCardSettingsWidget::renameCard()
+{
+ const QString selectedCard(getSelectedCard());
+ if (selectedCard.isEmpty())
+ return;
+
+ const QString newName(QInputDialog::getText(QtUtils::GetRootWidget(this),
+ tr("Rename Memory Card"), tr("New Card Name"), QLineEdit::Normal, selectedCard));
+ if (newName.isEmpty() || newName == selectedCard)
+ return;
+
+ if (!newName.endsWith(QStringLiteral(".ps2")) || newName.length() <= 4)
+ {
+ QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Rename Memory Card"),
+ tr("New name is invalid, it must end with .ps2"));
+ return;
+ }
+
+ const std::string newNameStr(newName.toStdString());
+ if (FileMcd_GetCardInfo(newNameStr).has_value())
+ {
+ QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Rename Memory Card"),
+ tr("New name is invalid, a card with this name already exists."));
+ return;
+ }
+
+ if (!FileMcd_RenameCard(selectedCard.toStdString(), newNameStr))
+ {
+ QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Rename Memory Card"),
+ tr("Failed to rename memory card. The log may contain more information."));
+ return;
+ }
+
+ refresh();
+}
+
+void MemoryCardSettingsWidget::convertCard()
+{
+ const QString selectedCard(getSelectedCard());
+ if (selectedCard.isEmpty())
+ return;
+
+ QMessageBox::critical(this, tr("Error"), tr("Not yet implemented."));
+}
+
+void MemoryCardSettingsWidget::listContextMenuRequested(const QPoint& pos)
+{
+ QMenu menu(this);
+
+ const QString selectedCard(getSelectedCard());
+ if (!selectedCard.isEmpty())
+ {
+ for (u32 slot = 0; slot < MAX_SLOTS; slot++)
+ {
+ connect(menu.addAction(tr("Use for Port %1").arg(slot + 1)), &QAction::triggered,
+ this, [this, &selectedCard, slot]() { tryInsertCard(slot, selectedCard); });
+ }
+ menu.addSeparator();
+
+ connect(menu.addAction(tr("Duplicate")), &QAction::triggered, this, &MemoryCardSettingsWidget::duplicateCard);
+ connect(menu.addAction(tr("Rename")), &QAction::triggered, this, &MemoryCardSettingsWidget::renameCard);
+ connect(menu.addAction(tr("Convert")), &QAction::triggered, this, &MemoryCardSettingsWidget::convertCard);
+ connect(menu.addAction(tr("Delete")), &QAction::triggered, this, &MemoryCardSettingsWidget::deleteCard);
+ menu.addSeparator();
+ }
+
+ connect(menu.addAction("Create"), &QAction::triggered, this, &MemoryCardSettingsWidget::createCard);
+
+ menu.exec(m_ui.cardList->mapToGlobal(pos));
+}
+
+void MemoryCardSettingsWidget::refresh()
+{
+ for (u32 slot = 0; slot < static_cast(m_slots.size()); slot++)
+ {
+ const bool enabled = m_slots[slot].enable->isChecked();
+ const std::optional name(
+ m_dialog->getStringValue("MemoryCards", getSlotFilenameKey(slot).c_str(),
+ FileMcd_GetDefaultName(slot).c_str()));
+
+ m_slots[slot].slot->setCard(name);
+ m_slots[slot].slot->setEnabled(enabled);
+ m_slots[slot].eject->setEnabled(enabled);
+ }
+
+ m_ui.cardList->refresh(m_dialog);
+ updateCardActions();
+}
+
+static QString getSizeSummary(const AvailableMcdInfo& mcd)
+{
+ if (mcd.type == MemoryCardType::File)
+ {
+ switch (mcd.file_type)
+ {
+ case MemoryCardFileType::PS2_8MB:
+ return qApp->translate("MemoryCardSettingsWidget", "PS2 (8MB)");
+
+ case MemoryCardFileType::PS2_16MB:
+ return qApp->translate("MemoryCardSettingsWidget", "PS2 (16MB)");
+
+ case MemoryCardFileType::PS2_32MB:
+ return qApp->translate("MemoryCardSettingsWidget", "PS2 (32MB)");
+
+ case MemoryCardFileType::PS2_64MB:
+ return qApp->translate("MemoryCardSettingsWidget", "PS2 (64MB)");
+
+ case MemoryCardFileType::PS1:
+ return qApp->translate("MemoryCardSettingsWidget", "PS1 (128KB)");
+
+ case MemoryCardFileType::Unknown:
+ default:
+ return qApp->translate("MemoryCardSettingsWidget", "Unknown");
+ }
+ }
+ else if (mcd.type == MemoryCardType::Folder)
+ {
+ return qApp->translate("MemoryCardSettingsWidget", "PS2 (Folder)");
+ }
+ else
+ {
+ return qApp->translate("MemoryCardSettingsWidget", "Unknown");
+ }
+}
+
+static QIcon getCardIcon(const AvailableMcdInfo& mcd)
+{
+ if (mcd.type == MemoryCardType::File)
+ return QIcon::fromTheme(QStringLiteral("sd-card-line"));
+ else
+ return QIcon::fromTheme(QStringLiteral("folder-open-line"));
+}
+
+MemoryCardListWidget::MemoryCardListWidget(QWidget* parent)
+ : QTreeWidget(parent)
+{
+}
+
+MemoryCardListWidget::~MemoryCardListWidget() = default;
+
+void MemoryCardListWidget::mousePressEvent(QMouseEvent* event)
+{
+ if (event->button() == Qt::LeftButton)
+ m_dragStartPos = event->pos();
+
+ QTreeWidget::mousePressEvent(event);
+}
+
+void MemoryCardListWidget::mouseMoveEvent(QMouseEvent* event)
+{
+ if (!(event->buttons() & Qt::LeftButton) ||
+ (event->pos() - m_dragStartPos).manhattanLength() < QApplication::startDragDistance())
+ {
+ QTreeWidget::mouseMoveEvent(event);
+ return;
+ }
+
+ const QList selection(selectedItems());
+ if (selection.empty())
+ return;
+
+ QDrag* drag = new QDrag(this);
+ QMimeData* mimeData = new QMimeData();
+ mimeData->setText(selection[0]->text(0));
+ drag->setMimeData(mimeData);
+ drag->exec(Qt::CopyAction);
+}
+
+void MemoryCardListWidget::refresh(SettingsDialog* dialog)
+{
+ clear();
+
+ // we can't use the in use flag here anyway, because the config may not be in line with per game settings.
+ const std::vector mcds(FileMcd_GetAvailableCards(true));
+ if (mcds.empty())
+ return;
+
+ std::array currentCards;
+ for (u32 i = 0; i < static_cast(currentCards.size()); i++)
+ {
+ const std::optional filename = dialog->getStringValue("MemoryCards",
+ getSlotFilenameKey(i).c_str(), FileMcd_GetDefaultName(i).c_str());
+ if (filename.has_value())
+ currentCards[i] = std::move(filename.value());
+ }
+
+ for (const AvailableMcdInfo& mcd : mcds)
+ {
+ QTreeWidgetItem* item = new QTreeWidgetItem();
+ const QDateTime mtime(QDateTime::fromSecsSinceEpoch(static_cast(mcd.modified_time)));
+ const bool inUse = (std::find(currentCards.begin(), currentCards.end(), mcd.name) != currentCards.end());
+
+ item->setDisabled(inUse);
+ item->setIcon(0, getCardIcon(mcd));
+ item->setText(0, QString::fromStdString(mcd.name));
+ item->setText(1, getSizeSummary(mcd));
+ item->setText(2, mcd.formatted ? tr("Yes") : tr("No"));
+ item->setText(3, mtime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)));
+ addTopLevelItem(item);
+ }
+}
+
+MemoryCardSlotWidget::MemoryCardSlotWidget(QWidget* parent)
+ : QListWidget(parent)
+{
+ setAcceptDrops(true);
+ setSelectionMode(NoSelection);
+}
+
+MemoryCardSlotWidget::~MemoryCardSlotWidget() = default;
+
+void MemoryCardSlotWidget::dragEnterEvent(QDragEnterEvent* event)
+{
+ if (event->mimeData()->hasFormat("text/plain"))
+ event->acceptProposedAction();
+}
+
+void MemoryCardSlotWidget::dragMoveEvent(QDragMoveEvent* event)
+{
+}
+
+void MemoryCardSlotWidget::dropEvent(QDropEvent* event)
+{
+ const QMimeData* data = event->mimeData();
+ const QString text(data ? data->text() : QString());
+ if (text.isEmpty())
+ {
+ event->ignore();
+ return;
+ }
+
+ event->acceptProposedAction();
+ emit cardDropped(text);
+}
+
+void MemoryCardSlotWidget::setCard(const std::optional& name)
+{
+ clear();
+ if (!name.has_value() || name->empty())
+ return;
+
+ const std::optional mcd(FileMcd_GetCardInfo(name.value()));
+ QListWidgetItem* item = new QListWidgetItem(this);
+
+ if (mcd.has_value())
+ {
+ item->setIcon(getCardIcon(mcd.value()));
+ item->setText(tr("%1 [%2]").arg(QString::fromStdString(mcd->name)).arg(getSizeSummary(mcd.value())));
+ }
+ else
+ {
+ item->setIcon(QIcon::fromTheme("close-line"));
+ item->setText(tr("%1 [Missing]").arg(QString::fromStdString(name.value())));
+ }
+}
diff --git a/pcsx2-qt/Settings/MemoryCardSettingsWidget.h b/pcsx2-qt/Settings/MemoryCardSettingsWidget.h
index ea1efd4f68..8976f86ffc 100644
--- a/pcsx2-qt/Settings/MemoryCardSettingsWidget.h
+++ b/pcsx2-qt/Settings/MemoryCardSettingsWidget.h
@@ -15,20 +15,106 @@
#pragma once
-#include
+#include
+#include
+#include
-#include "ui_MemoryCardSettingsWidget.h"
+#include
+#include
+#include
+#include
+#include
class SettingsDialog;
+struct AvailableMcdInfo;
+
+class MemoryCardListWidget final : public QTreeWidget
+{
+ Q_OBJECT
+public:
+ explicit MemoryCardListWidget(QWidget* parent);
+ ~MemoryCardListWidget() override;
+
+ void refresh(SettingsDialog* dialog);
+
+protected:
+ void mousePressEvent(QMouseEvent* event);
+ void mouseMoveEvent(QMouseEvent* event);
+
+private:
+ QPoint m_dragStartPos = {};
+};
+
+class MemoryCardSlotWidget final : public QListWidget
+{
+ Q_OBJECT
+public:
+ explicit MemoryCardSlotWidget(QWidget* parent);
+ ~MemoryCardSlotWidget() override;
+
+Q_SIGNALS:
+ void cardDropped(const QString& newCard);
+
+public:
+ void setCard(const std::optional& name);
+
+protected:
+ void dragEnterEvent(QDragEnterEvent* event);
+ void dragMoveEvent(QDragMoveEvent* event);
+ void dropEvent(QDropEvent* event);
+};
+
+// Must be included *after* the custom widgets.
+#include "ui_MemoryCardSettingsWidget.h"
+
class MemoryCardSettingsWidget : public QWidget
{
Q_OBJECT
public:
+ enum : u32
+ {
+ MAX_SLOTS = 2
+ };
+
MemoryCardSettingsWidget(SettingsDialog* dialog, QWidget* parent);
~MemoryCardSettingsWidget();
+protected:
+ void resizeEvent(QResizeEvent* event);
+ bool eventFilter(QObject* watched, QEvent* event);
+
+private Q_SLOTS:
+ void listContextMenuRequested(const QPoint& pos);
+ void refresh();
+
private:
+ struct SlotGroup
+ {
+ QWidget* root;
+ QCheckBox* enable;
+ QToolButton* eject;
+ MemoryCardSlotWidget* slot;
+ };
+
+ void createSlotWidgets(SlotGroup* port, u32 slot);
+ void autoSizeUI();
+
+ void tryInsertCard(u32 slot, const QString& newCard);
+ void ejectSlot(u32 slot);
+
+ void createCard();
+
+ QString getSelectedCard() const;
+ void updateCardActions();
+ void duplicateCard();
+ void deleteCard();
+ void renameCard();
+ void convertCard();
+
+ SettingsDialog* m_dialog;
Ui::MemoryCardSettingsWidget m_ui;
+
+ std::array m_slots;
};
diff --git a/pcsx2-qt/Settings/MemoryCardSettingsWidget.ui b/pcsx2-qt/Settings/MemoryCardSettingsWidget.ui
index cc3ea4d760..d291186c2c 100644
--- a/pcsx2-qt/Settings/MemoryCardSettingsWidget.ui
+++ b/pcsx2-qt/Settings/MemoryCardSettingsWidget.ui
@@ -6,14 +6,179 @@
0
0
- 400
- 300
+ 796
+ 443
Form
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 100
+
+
+
+ Console Ports
+
+
+
+
+ -
+
+
+ Memory Cards
+
+
+
-
+
+
+ false
+
+
+
+ Name
+
+
+
+
+ Type
+
+
+
+
+ Formatted
+
+
+
+
+ Last Modified
+
+
+
+
+ -
+
+
-
+
+
+ Refresh
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Create
+
+
+
+ -
+
+
+ Duplicate
+
+
+
+ -
+
+
+ Rename
+
+
+
+ -
+
+
+ Convert
+
+
+
+ -
+
+
+ Delete
+
+
+
+
+
+
+
+
+ -
+
+
+ Settings
+
+
+
-
+
+
+ Automatically manage saves based on running game
+
+
+
+ -
+
+
+ Auto-eject memory cards when loading save states
+
+
+
+
+
+
+
+
+
+ MemoryCardListWidget
+ QTreeWidget
+ MemoryCardSettingsWidget.h
+
+
diff --git a/pcsx2-qt/Settings/SettingsDialog.cpp b/pcsx2-qt/Settings/SettingsDialog.cpp
index d20d767e5c..bd72f410dc 100644
--- a/pcsx2-qt/Settings/SettingsDialog.cpp
+++ b/pcsx2-qt/Settings/SettingsDialog.cpp
@@ -35,6 +35,7 @@
#include "GraphicsSettingsWidget.h"
#include "HotkeySettingsWidget.h"
#include "InterfaceSettingsWidget.h"
+#include "MemoryCardSettingsWidget.h"
#include "SystemSettingsWidget.h"
#include
@@ -118,8 +119,13 @@ void SettingsDialog::setupUi(const GameList::Entry* game)
addWidget(m_audio_settings = new AudioSettingsWidget(this, m_ui.settingsContainer), tr("Audio"), QStringLiteral("volume-up-line"),
tr("Audio Settings
These options control the audio output of the console. Mouse over an option for additional "
"information."));
- addWidget(
- new QWidget(m_ui.settingsContainer), tr("Memory Cards"), QStringLiteral("sd-card-line"), tr("Memory Card Settings
"));
+
+ // for now, memory cards aren't settable per-game
+ if (!isPerGameSettings())
+ {
+ addWidget(m_memory_card_settings = new MemoryCardSettingsWidget(this, m_ui.settingsContainer), tr("Memory Cards"),
+ QStringLiteral("sd-card-line"), tr("Memory Card Settings
"));
+ }
m_ui.settingsCategory->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_ui.settingsCategory->setCurrentRow(0);
diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj
index 822a289be8..eb099fb42b 100644
--- a/pcsx2-qt/pcsx2-qt.vcxproj
+++ b/pcsx2-qt/pcsx2-qt.vcxproj
@@ -152,6 +152,7 @@
+
@@ -199,6 +200,7 @@
+
@@ -223,6 +225,7 @@
+
@@ -294,6 +297,9 @@
Document
+
+ Document
+
Document
diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters
index b539830e13..3d9270a21c 100644
--- a/pcsx2-qt/pcsx2-qt.vcxproj.filters
+++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters
@@ -194,6 +194,12 @@
Settings
+
+ moc
+
+
+ Settings
+
@@ -274,6 +280,9 @@
Settings
+
+ Settings
+
@@ -333,5 +342,8 @@
Settings
+
+ Settings
+
\ No newline at end of file
diff --git a/pcsx2-qt/resources/icons/black/16/eject-line.png b/pcsx2-qt/resources/icons/black/16/eject-line.png
new file mode 100644
index 0000000000..7f12c4ec72
Binary files /dev/null and b/pcsx2-qt/resources/icons/black/16/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/black/24/eject-line.png b/pcsx2-qt/resources/icons/black/24/eject-line.png
new file mode 100644
index 0000000000..b7cec7dcb4
Binary files /dev/null and b/pcsx2-qt/resources/icons/black/24/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/black/32/eject-line.png b/pcsx2-qt/resources/icons/black/32/eject-line.png
new file mode 100644
index 0000000000..26b3f78232
Binary files /dev/null and b/pcsx2-qt/resources/icons/black/32/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/black/48/eject-line.png b/pcsx2-qt/resources/icons/black/48/eject-line.png
new file mode 100644
index 0000000000..51cd907242
Binary files /dev/null and b/pcsx2-qt/resources/icons/black/48/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/black/64/eject-line.png b/pcsx2-qt/resources/icons/black/64/eject-line.png
new file mode 100644
index 0000000000..18c1321b4e
Binary files /dev/null and b/pcsx2-qt/resources/icons/black/64/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/white/16/eject-line.png b/pcsx2-qt/resources/icons/white/16/eject-line.png
new file mode 100644
index 0000000000..249130c90a
Binary files /dev/null and b/pcsx2-qt/resources/icons/white/16/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/white/24/eject-line.png b/pcsx2-qt/resources/icons/white/24/eject-line.png
new file mode 100644
index 0000000000..f9233f0caf
Binary files /dev/null and b/pcsx2-qt/resources/icons/white/24/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/white/32/eject-line.png b/pcsx2-qt/resources/icons/white/32/eject-line.png
new file mode 100644
index 0000000000..0aab8be4b3
Binary files /dev/null and b/pcsx2-qt/resources/icons/white/32/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/white/48/eject-line.png b/pcsx2-qt/resources/icons/white/48/eject-line.png
new file mode 100644
index 0000000000..5298118125
Binary files /dev/null and b/pcsx2-qt/resources/icons/white/48/eject-line.png differ
diff --git a/pcsx2-qt/resources/icons/white/64/eject-line.png b/pcsx2-qt/resources/icons/white/64/eject-line.png
new file mode 100644
index 0000000000..9465116154
Binary files /dev/null and b/pcsx2-qt/resources/icons/white/64/eject-line.png differ
diff --git a/pcsx2-qt/resources/resources.qrc b/pcsx2-qt/resources/resources.qrc
index 8397b06cdd..7d159c2504 100644
--- a/pcsx2-qt/resources/resources.qrc
+++ b/pcsx2-qt/resources/resources.qrc
@@ -17,6 +17,7 @@
icons/black/16/door-open-line.png
icons/black/16/download-2-line.png
icons/black/16/dvd-line.png
+ icons/black/16/eject-line.png
icons/black/16/file-add-line.png
icons/black/16/file-line.png
icons/black/16/file-list-line.png
@@ -56,6 +57,7 @@
icons/black/24/door-open-line.png
icons/black/24/download-2-line.png
icons/black/24/dvd-line.png
+ icons/black/24/eject-line.png
icons/black/24/file-add-line.png
icons/black/24/file-line.png
icons/black/24/file-list-line.png
@@ -94,6 +96,7 @@
icons/black/32/door-open-line.png
icons/black/32/download-2-line.png
icons/black/32/dvd-line.png
+ icons/black/32/eject-line.png
icons/black/32/file-add-line.png
icons/black/32/file-line.png
icons/black/32/file-list-line.png
@@ -133,6 +136,7 @@
icons/black/48/door-open-line.png
icons/black/48/download-2-line.png
icons/black/48/dvd-line.png
+ icons/black/48/eject-line.png
icons/black/48/file-add-line.png
icons/black/48/file-line.png
icons/black/48/file-list-line.png
@@ -171,6 +175,7 @@
icons/black/64/door-open-line.png
icons/black/64/download-2-line.png
icons/black/64/dvd-line.png
+ icons/black/64/eject-line.png
icons/black/64/file-add-line.png
icons/black/64/file-line.png
icons/black/64/file-list-line.png
@@ -239,6 +244,7 @@
icons/white/16/door-open-line.png
icons/white/16/download-2-line.png
icons/white/16/dvd-line.png
+ icons/white/16/eject-line.png
icons/white/16/file-add-line.png
icons/white/16/file-line.png
icons/white/16/file-list-line.png
@@ -278,6 +284,7 @@
icons/white/24/door-open-line.png
icons/white/24/download-2-line.png
icons/white/24/dvd-line.png
+ icons/white/24/eject-line.png
icons/white/24/file-add-line.png
icons/white/24/file-line.png
icons/white/24/file-list-line.png
@@ -317,6 +324,7 @@
icons/white/32/door-open-line.png
icons/white/32/download-2-line.png
icons/white/32/dvd-line.png
+ icons/white/32/eject-line.png
icons/white/32/file-add-line.png
icons/white/32/file-line.png
icons/white/32/file-list-line.png
@@ -356,6 +364,7 @@
icons/white/48/door-open-line.png
icons/white/48/download-2-line.png
icons/white/48/dvd-line.png
+ icons/white/48/eject-line.png
icons/white/48/file-add-line.png
icons/white/48/file-line.png
icons/white/48/file-list-line.png
@@ -395,6 +404,7 @@
icons/white/64/door-open-line.png
icons/white/64/download-2-line.png
icons/white/64/dvd-line.png
+ icons/white/64/eject-line.png
icons/white/64/file-add-line.png
icons/white/64/file-line.png
icons/white/64/file-list-line.png