From 09467d50e1c8bcae53e11792ad062a05a6b577a1 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 19 Jul 2026 20:31:36 +1000 Subject: [PATCH] Qt: Add icon extraction to memory card editor --- src/duckstation-qt/memorycardeditorwindow.cpp | 93 +++++++++++++++++-- src/duckstation-qt/memorycardeditorwindow.h | 4 + 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/duckstation-qt/memorycardeditorwindow.cpp b/src/duckstation-qt/memorycardeditorwindow.cpp index d1df5d402..7e4e55cbd 100644 --- a/src/duckstation-qt/memorycardeditorwindow.cpp +++ b/src/duckstation-qt/memorycardeditorwindow.cpp @@ -9,6 +9,9 @@ #include "core/host.h" #include "core/settings.h" +#include "util/animated_image.h" +#include "util/image.h" + #include "common/assert.h" #include "common/error.h" #include "common/file_system.h" @@ -35,6 +38,10 @@ static constexpr char MEMORY_CARD_IMPORT_FILTER[] = QT_TRANSLATE_NOOP( "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme *.srm *.psm *.ps *.ddf *.mem *.vgs *.psx)"); static constexpr char SINGLE_SAVEFILE_FILTER[] = QT_TRANSLATE_NOOP("MemoryCardEditorWindow", "Single Save Files (*.mcs);;All Files (*.*)"); +static constexpr char ICON_IMAGE_FILTER[] = + QT_TRANSLATE_NOOP("MemoryCardEditorWindow", "PNG Images (*.png);;JPEG Images (*.jpg *.jpeg);;WebP Images (*.webp)"); +static constexpr char ANIMATED_ICON_IMAGE_FILTER[] = + QT_TRANSLATE_NOOP("MemoryCardEditorWindow", "Animated PNG Images (*.png)"); static constexpr std::array, 3> MEMORY_CARD_FILE_REGION_PREFIXES = {{ {ConsoleRegion::NTSC_U, "BA"}, {ConsoleRegion::NTSC_J, "BI"}, @@ -136,6 +143,7 @@ MemoryCardEditorWindow::MemoryCardEditorWindow() : QWidget() 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_extractIcon = m_ui.centerButtonBox->addButton(tr("Extract Icon"), QDialogButtonBox::ActionRole); m_moveLeft = m_ui.centerButtonBox->addButton(tr("<<"), QDialogButtonBox::ActionRole); m_moveRight = m_ui.centerButtonBox->addButton(tr(">>"), QDialogButtonBox::ActionRole); @@ -290,6 +298,7 @@ void MemoryCardEditorWindow::connectUi() 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); + connect(m_extractIcon, &QPushButton::clicked, this, &MemoryCardEditorWindow::onExtractIconClicked); } void MemoryCardEditorWindow::populateComboBox(QComboBox* cb) @@ -597,7 +606,7 @@ void MemoryCardEditorWindow::promptForSave(Card* card) void MemoryCardEditorWindow::doCopyFile() { - const auto [src, fi] = getSelectedFile(); + const auto& [src, fi] = getSelectedFile(); if (!fi) return; @@ -654,7 +663,7 @@ void MemoryCardEditorWindow::doCopyFile() void MemoryCardEditorWindow::doDeleteFile() { - const auto [card, fi] = getSelectedFile(); + const auto& [card, fi] = getSelectedFile(); if (!fi) return; @@ -674,7 +683,7 @@ void MemoryCardEditorWindow::doDeleteFile() void MemoryCardEditorWindow::doUndeleteFile() { - const auto [card, fi] = getSelectedFile(); + const auto& [card, fi] = getSelectedFile(); if (!fi) return; @@ -702,7 +711,7 @@ void MemoryCardEditorWindow::doExportSaveFile() if (filename.isEmpty()) return; - const auto [card, fi] = getSelectedFile(); + const auto& [card, fi] = getSelectedFile(); if (!fi) return; @@ -717,9 +726,74 @@ void MemoryCardEditorWindow::doExportSaveFile() } } +void MemoryCardEditorWindow::onExtractIconClicked() +{ + QMenu* const extract_icon_menu = QtUtils::NewPopupMenu(m_extractIcon); + extract_icon_menu->addAction(tr("Extract Icon"), this, &MemoryCardEditorWindow::doExtractIcon); + extract_icon_menu->addAction(tr("Extract Animated Icon"), this, &MemoryCardEditorWindow::doExtractAnimatedIcon); + extract_icon_menu->popup(QCursor::pos()); +} + +void MemoryCardEditorWindow::doExtractIcon() +{ + const auto& [card, fi] = getSelectedFile(); + if (!fi || fi->icon_frames.empty()) + return; + + const QString default_filename = QString::fromStdString(fi->filename) + QStringLiteral(".png"); + const QString filename = QDir::toNativeSeparators( + QFileDialog::getSaveFileName(this, tr("Extract Icon"), default_filename, tr(ICON_IMAGE_FILTER))); + if (filename.isEmpty()) + return; + + const MemoryCardImage::IconFrame& frame = fi->icon_frames.front(); + const Image image(MemoryCardImage::ICON_WIDTH, MemoryCardImage::ICON_HEIGHT, ImageFormat::RGBA8, frame.pixels, + MemoryCardImage::ICON_WIDTH * sizeof(frame.pixels[0])); + + Error error; + if (!image.SaveToFile(filename.toStdString().c_str(), Image::DEFAULT_SAVE_QUALITY, &error)) + { + QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Error"), + tr("Failed to extract icon from save file %1:\n%2") + .arg(QString::fromStdString(fi->filename)) + .arg(QString::fromStdString(error.GetDescription()))); + } +} + +void MemoryCardEditorWindow::doExtractAnimatedIcon() +{ + const auto& [card, fi] = getSelectedFile(); + if (!fi || fi->icon_frames.empty()) + return; + + const QString default_filename = QString::fromStdString(fi->filename) + QStringLiteral(".png"); + const QString filename = QDir::toNativeSeparators( + QFileDialog::getSaveFileName(this, tr("Extract Animated Icon"), default_filename, tr(ANIMATED_ICON_IMAGE_FILTER))); + if (filename.isEmpty()) + return; + + const AnimatedImage::FrameDelay frame_delay = {MEMORY_CARD_ICON_FRAME_DURATION_MS, 1000}; + AnimatedImage image(MemoryCardImage::ICON_WIDTH, MemoryCardImage::ICON_HEIGHT, + static_cast(fi->icon_frames.size()), frame_delay); + for (u32 frame_index = 0; frame_index < static_cast(fi->icon_frames.size()); frame_index++) + { + image.SetPixels(frame_index, fi->icon_frames[frame_index].pixels, + MemoryCardImage::ICON_WIDTH * sizeof(fi->icon_frames[frame_index].pixels[0])); + } + + Error error; + if (!image.SaveToFile(filename.toStdString().c_str(), AnimatedImage::DEFAULT_SAVE_QUALITY, &error)) + { + QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Error"), + tr("Failed to extract animated icon from save file %1:\n%2") + .arg(QString::fromStdString(fi->filename)) + .arg(QString::fromStdString(error.GetDescription()))); + } +} + void MemoryCardEditorWindow::doRenameSaveFile() { - const auto [card, fi] = getSelectedFile(); + const auto& [card, fi] = getSelectedFile(); if (!fi) return; @@ -727,7 +801,7 @@ void MemoryCardEditorWindow::doRenameSaveFile() dlg->setAttribute(Qt::WA_DeleteOnClose); connect(dlg, &QDialog::accepted, this, [this, dlg] { - const auto [card, fi] = getSelectedFile(); + const auto& [card, fi] = getSelectedFile(); if (!fi) return; @@ -852,6 +926,10 @@ void MemoryCardEditorWindow::onCardContextMenuRequested(const QPoint& pos) action->setEnabled(fi != nullptr); action = menu->addAction(tr("Export File"), this, &MemoryCardEditorWindow::doExportSaveFile); action->setEnabled(fi != nullptr); + action = menu->addAction(tr("Extract Icon"), this, &MemoryCardEditorWindow::doExtractIcon); + action->setEnabled(fi && !fi->icon_frames.empty()); + action = menu->addAction(tr("Extract Animated Icon"), this, &MemoryCardEditorWindow::doExtractAnimatedIcon); + action->setEnabled(fi && !fi->icon_frames.empty()); action = menu->addAction(tr("Copy File"), this, &MemoryCardEditorWindow::doCopyFile); action->setEnabled(fi && !m_card_a.filename.empty() && !m_card_b.filename.empty()); @@ -880,7 +958,7 @@ std::tuple Memo void MemoryCardEditorWindow::updateButtonState() { - const auto [selected_card, selected_file] = getSelectedFile(); + const auto& [selected_card, selected_file] = getSelectedFile(); const bool is_card_b = (selected_card == &m_card_b); const bool has_selection = (selected_file != nullptr); const bool is_deleted = (selected_file != nullptr && selected_file->deleted); @@ -890,6 +968,7 @@ void MemoryCardEditorWindow::updateButtonState() m_deleteFile->setEnabled(has_selection); m_undeleteFile->setEnabled(is_deleted); m_exportFile->setEnabled(has_selection); + m_extractIcon->setEnabled(has_selection && !selected_file->icon_frames.empty()); 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); diff --git a/src/duckstation-qt/memorycardeditorwindow.h b/src/duckstation-qt/memorycardeditorwindow.h index 97183b868..cf2e7b033 100644 --- a/src/duckstation-qt/memorycardeditorwindow.h +++ b/src/duckstation-qt/memorycardeditorwindow.h @@ -75,6 +75,9 @@ private: void doRenameSaveFile(); void doExportSaveFile(); + void onExtractIconClicked(); + void doExtractIcon(); + void doExtractAnimatedIcon(); void importSaveFile(Card* card); std::tuple getSelectedFile(); @@ -95,6 +98,7 @@ private: QPushButton* m_undeleteFile; QPushButton* m_renameFile; QPushButton* m_exportFile; + QPushButton* m_extractIcon; QPushButton* m_moveLeft; QPushButton* m_moveRight;