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