duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

memorycardeditorwindow.cpp (20385B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "memorycardeditorwindow.h"
      5 #include "qtutils.h"
      6 
      7 #include "core/host.h"
      8 #include "core/settings.h"
      9 
     10 #include "common/assert.h"
     11 #include "common/error.h"
     12 #include "common/file_system.h"
     13 #include "common/path.h"
     14 #include "common/string_util.h"
     15 
     16 #include <QtCore/QFileInfo>
     17 #include <QtWidgets/QFileDialog>
     18 #include <QtWidgets/QMessageBox>
     19 
     20 static constexpr char MEMORY_CARD_IMAGE_FILTER[] = QT_TRANSLATE_NOOP(
     21   "MemoryCardEditorWindow", "All Memory Card Types (*.mcd *.mcr *.mc *.srm *.psm *.ps *.ddf *.mem *.vgs *.psx)");
     22 static constexpr char MEMORY_CARD_IMPORT_FILTER[] =
     23   QT_TRANSLATE_NOOP("MemoryCardEditorWindow", "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme)");
     24 static constexpr char SINGLE_SAVEFILE_FILTER[] =
     25   TRANSLATE_NOOP("MemoryCardEditorWindow", "Single Save Files (*.mcs);;All Files (*.*)");
     26 
     27 MemoryCardEditorWindow::MemoryCardEditorWindow() : QWidget()
     28 {
     29   m_ui.setupUi(this);
     30   setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
     31 
     32   m_deleteFile = m_ui.centerButtonBox->addButton(tr("Delete File"), QDialogButtonBox::ActionRole);
     33   m_undeleteFile = m_ui.centerButtonBox->addButton(tr("Undelete File"), QDialogButtonBox::ActionRole);
     34   m_exportFile = m_ui.centerButtonBox->addButton(tr("Export File"), QDialogButtonBox::ActionRole);
     35   m_moveLeft = m_ui.centerButtonBox->addButton(tr("<<"), QDialogButtonBox::ActionRole);
     36   m_moveRight = m_ui.centerButtonBox->addButton(tr(">>"), QDialogButtonBox::ActionRole);
     37 
     38   m_card_a.path_cb = m_ui.cardAPath;
     39   m_card_a.table = m_ui.cardA;
     40   m_card_a.blocks_free_label = m_ui.cardAUsage;
     41   m_card_b.path_cb = m_ui.cardBPath;
     42   m_card_b.table = m_ui.cardB;
     43   m_card_b.blocks_free_label = m_ui.cardBUsage;
     44 
     45   createCardButtons(&m_card_a, m_ui.buttonBoxA);
     46   createCardButtons(&m_card_b, m_ui.buttonBoxB);
     47   connectUi();
     48   connectCardUi(&m_card_a, m_ui.buttonBoxA);
     49   connectCardUi(&m_card_b, m_ui.buttonBoxB);
     50   populateComboBox(m_ui.cardAPath);
     51   populateComboBox(m_ui.cardBPath);
     52 
     53   const QString new_card_hover_text(tr("New Card..."));
     54   const QString open_card_hover_text(tr("Open Card..."));
     55   m_ui.newCardA->setToolTip(new_card_hover_text);
     56   m_ui.newCardB->setToolTip(new_card_hover_text);
     57   m_ui.openCardA->setToolTip(open_card_hover_text);
     58   m_ui.openCardB->setToolTip(open_card_hover_text);
     59 }
     60 
     61 MemoryCardEditorWindow::~MemoryCardEditorWindow() = default;
     62 
     63 bool MemoryCardEditorWindow::setCardA(const QString& path)
     64 {
     65   int index = m_ui.cardAPath->findData(QVariant(QDir::toNativeSeparators(path)));
     66   if (index < 0)
     67   {
     68     QFileInfo file(path);
     69     if (!file.exists())
     70       return false;
     71 
     72     QSignalBlocker sb(m_card_a.path_cb);
     73     m_card_a.path_cb->addItem(file.baseName(), QVariant(path));
     74     index = m_card_a.path_cb->count() - 1;
     75   }
     76 
     77   m_ui.cardAPath->setCurrentIndex(index);
     78   return true;
     79 }
     80 
     81 bool MemoryCardEditorWindow::setCardB(const QString& path)
     82 {
     83   int index = m_ui.cardBPath->findData(QVariant(QDir::toNativeSeparators(path)));
     84   if (index < 0)
     85   {
     86     QFileInfo file(path);
     87     if (!file.exists())
     88       return false;
     89 
     90     QSignalBlocker sb(m_card_b.path_cb);
     91     m_card_b.path_cb->addItem(file.baseName(), QVariant(path));
     92     index = m_card_b.path_cb->count() - 1;
     93   }
     94 
     95   m_ui.cardBPath->setCurrentIndex(index);
     96   return true;
     97 }
     98 
     99 bool MemoryCardEditorWindow::createMemoryCard(const QString& path, Error* error)
    100 {
    101   MemoryCardImage::DataArray data;
    102   MemoryCardImage::Format(&data);
    103 
    104   return MemoryCardImage::SaveToFile(data, path.toUtf8().constData(), error);
    105 }
    106 
    107 void MemoryCardEditorWindow::resizeEvent(QResizeEvent* ev)
    108 {
    109   QtUtils::ResizeColumnsForTableView(m_card_a.table, {32, -1, 155, 45});
    110   QtUtils::ResizeColumnsForTableView(m_card_b.table, {32, -1, 155, 45});
    111 }
    112 
    113 void MemoryCardEditorWindow::closeEvent(QCloseEvent* ev)
    114 {
    115   m_card_a.path_cb->setCurrentIndex(0);
    116   m_card_b.path_cb->setCurrentIndex(0);
    117 }
    118 
    119 void MemoryCardEditorWindow::createCardButtons(Card* card, QDialogButtonBox* buttonBox)
    120 {
    121   card->format_button = buttonBox->addButton(tr("Format Card"), QDialogButtonBox::ActionRole);
    122   card->import_file_button = buttonBox->addButton(tr("Import File..."), QDialogButtonBox::ActionRole);
    123   card->import_button = buttonBox->addButton(tr("Import Card..."), QDialogButtonBox::ActionRole);
    124   card->save_button = buttonBox->addButton(tr("Save"), QDialogButtonBox::ActionRole);
    125 }
    126 
    127 void MemoryCardEditorWindow::connectCardUi(Card* card, QDialogButtonBox* buttonBox)
    128 {
    129   connect(card->save_button, &QPushButton::clicked, [this, card] { saveCard(card); });
    130   connect(card->format_button, &QPushButton::clicked, [this, card] { formatCard(card); });
    131   connect(card->import_file_button, &QPushButton::clicked, [this, card] { importSaveFile(card); });
    132   connect(card->import_button, &QPushButton::clicked, [this, card] { importCard(card); });
    133 }
    134 
    135 void MemoryCardEditorWindow::connectUi()
    136 {
    137   connect(m_ui.cardA, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorWindow::onCardASelectionChanged);
    138   connect(m_ui.cardB, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorWindow::onCardBSelectionChanged);
    139   connect(m_moveLeft, &QPushButton::clicked, this, &MemoryCardEditorWindow::doCopyFile);
    140   connect(m_moveRight, &QPushButton::clicked, this, &MemoryCardEditorWindow::doCopyFile);
    141   connect(m_deleteFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doDeleteFile);
    142   connect(m_undeleteFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doUndeleteFile);
    143 
    144   connect(m_ui.cardAPath, QOverload<int>::of(&QComboBox::currentIndexChanged),
    145           [this](int index) { loadCardFromComboBox(&m_card_a, index); });
    146   connect(m_ui.cardBPath, QOverload<int>::of(&QComboBox::currentIndexChanged),
    147           [this](int index) { loadCardFromComboBox(&m_card_b, index); });
    148   connect(m_ui.newCardA, &QPushButton::clicked, [this]() { newCard(&m_card_a); });
    149   connect(m_ui.newCardB, &QPushButton::clicked, [this]() { newCard(&m_card_b); });
    150   connect(m_ui.openCardA, &QPushButton::clicked, [this]() { openCard(&m_card_a); });
    151   connect(m_ui.openCardB, &QPushButton::clicked, [this]() { openCard(&m_card_b); });
    152   connect(m_exportFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doExportSaveFile);
    153 }
    154 
    155 void MemoryCardEditorWindow::populateComboBox(QComboBox* cb)
    156 {
    157   QSignalBlocker sb(cb);
    158 
    159   cb->clear();
    160 
    161   cb->addItem(QString());
    162 
    163   FileSystem::FindResultsArray results;
    164   FileSystem::FindFiles(EmuFolders::MemoryCards.c_str(), "*.mcd",
    165                         FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_SORT_BY_NAME,
    166                         &results);
    167   for (FILESYSTEM_FIND_DATA& fd : results)
    168   {
    169     std::string real_filename(Path::Combine(EmuFolders::MemoryCards, fd.FileName));
    170     std::string::size_type pos = fd.FileName.rfind('.');
    171     if (pos != std::string::npos)
    172       fd.FileName.erase(pos);
    173 
    174     cb->addItem(QString::fromStdString(fd.FileName), QVariant(QString::fromStdString(real_filename)));
    175   }
    176 }
    177 
    178 void MemoryCardEditorWindow::loadCardFromComboBox(Card* card, int index)
    179 {
    180   loadCard(card->path_cb->itemData(index).toString(), card);
    181 }
    182 
    183 void MemoryCardEditorWindow::onCardASelectionChanged()
    184 {
    185   {
    186     QSignalBlocker cb(m_card_b.table);
    187     m_card_b.table->clearSelection();
    188   }
    189 
    190   updateButtonState();
    191 }
    192 
    193 void MemoryCardEditorWindow::onCardBSelectionChanged()
    194 {
    195   {
    196     QSignalBlocker cb(m_card_a.table);
    197     m_card_a.table->clearSelection();
    198   }
    199 
    200   updateButtonState();
    201 }
    202 
    203 void MemoryCardEditorWindow::clearSelection()
    204 {
    205   {
    206     QSignalBlocker cb(m_card_a.table);
    207     m_card_a.table->clearSelection();
    208   }
    209 
    210   {
    211     QSignalBlocker cb(m_card_b.table);
    212     m_card_b.table->clearSelection();
    213   }
    214 
    215   updateButtonState();
    216 }
    217 
    218 bool MemoryCardEditorWindow::loadCard(const QString& filename, Card* card)
    219 {
    220   promptForSave(card);
    221 
    222   card->table->setRowCount(0);
    223   card->dirty = false;
    224   card->blocks_free_label->clear();
    225   card->save_button->setEnabled(false);
    226 
    227   card->filename.clear();
    228 
    229   if (filename.isEmpty())
    230   {
    231     updateButtonState();
    232     return false;
    233   }
    234 
    235   Error error;
    236   std::string filename_str = filename.toStdString();
    237   if (!MemoryCardImage::LoadFromFile(&card->data, filename_str.c_str(), &error))
    238   {
    239     QMessageBox::critical(this, tr("Error"),
    240                           tr("Failed to load memory card: %1").arg(QString::fromStdString(error.GetDescription())));
    241     return false;
    242   }
    243 
    244   card->filename = std::move(filename_str);
    245   updateCardTable(card);
    246   updateCardBlocksFree(card);
    247   updateButtonState();
    248   return true;
    249 }
    250 
    251 static void setCardTableItemProperties(QTableWidgetItem* item, const MemoryCardImage::FileInfo& fi)
    252 {
    253   item->setFlags(item->flags() & ~(Qt::ItemIsEditable));
    254   if (fi.deleted)
    255   {
    256     item->setBackground(Qt::darkRed);
    257     item->setForeground(Qt::white);
    258   }
    259 }
    260 
    261 void MemoryCardEditorWindow::updateCardTable(Card* card)
    262 {
    263   card->table->setRowCount(0);
    264 
    265   card->files = MemoryCardImage::EnumerateFiles(card->data, true);
    266   for (const MemoryCardImage::FileInfo& fi : card->files)
    267   {
    268     const int row = card->table->rowCount();
    269     card->table->insertRow(row);
    270 
    271     if (!fi.icon_frames.empty())
    272     {
    273       const QImage image(reinterpret_cast<const u8*>(fi.icon_frames[0].pixels), MemoryCardImage::ICON_WIDTH,
    274                          MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888);
    275 
    276       QTableWidgetItem* icon = new QTableWidgetItem();
    277       setCardTableItemProperties(icon, fi);
    278       icon->setIcon(QIcon(QPixmap::fromImage(image)));
    279       card->table->setItem(row, 0, icon);
    280     }
    281 
    282     QString title_str(QString::fromStdString(fi.title));
    283     if (fi.deleted)
    284       title_str += tr(" (Deleted)");
    285 
    286     QTableWidgetItem* item = new QTableWidgetItem(title_str);
    287     setCardTableItemProperties(item, fi);
    288     card->table->setItem(row, 1, item);
    289 
    290     item = new QTableWidgetItem(QString::fromStdString(fi.filename));
    291     setCardTableItemProperties(item, fi);
    292     card->table->setItem(row, 2, item);
    293 
    294     item = new QTableWidgetItem(QString::number(fi.num_blocks));
    295     setCardTableItemProperties(item, fi);
    296     card->table->setItem(row, 3, item);
    297   }
    298 }
    299 
    300 void MemoryCardEditorWindow::updateCardBlocksFree(Card* card)
    301 {
    302   card->blocks_free = MemoryCardImage::GetFreeBlockCount(card->data);
    303   card->blocks_free_label->setText(
    304     tr("%n block(s) free%1", "", card->blocks_free).arg(card->dirty ? QStringLiteral(" (*)") : QString()));
    305 }
    306 
    307 void MemoryCardEditorWindow::setCardDirty(Card* card)
    308 {
    309   card->dirty = true;
    310   card->save_button->setEnabled(true);
    311 }
    312 
    313 void MemoryCardEditorWindow::newCard(Card* card)
    314 {
    315   promptForSave(card);
    316 
    317   QString filename = QDir::toNativeSeparators(
    318     QFileDialog::getSaveFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER)));
    319   if (filename.isEmpty())
    320     return;
    321 
    322   {
    323     // add to combo box
    324     QFileInfo file(filename);
    325     QSignalBlocker sb(card->path_cb);
    326     card->path_cb->addItem(file.baseName(), QVariant(filename));
    327     card->path_cb->setCurrentIndex(card->path_cb->count() - 1);
    328   }
    329 
    330   card->filename = filename.toStdString();
    331 
    332   MemoryCardImage::Format(&card->data);
    333   updateCardTable(card);
    334   updateCardBlocksFree(card);
    335   updateButtonState();
    336   saveCard(card);
    337 }
    338 
    339 void MemoryCardEditorWindow::openCard(Card* card)
    340 {
    341   promptForSave(card);
    342 
    343   QString filename = QDir::toNativeSeparators(
    344     QFileDialog::getOpenFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER)));
    345   if (filename.isEmpty())
    346     return;
    347 
    348   Error error;
    349   if (!MemoryCardImage::LoadFromFile(&card->data, filename.toUtf8().constData(), &error))
    350   {
    351     QMessageBox::critical(this, tr("Error"),
    352                           tr("Failed to load memory card: %1").arg(QString::fromStdString(error.GetDescription())));
    353     return;
    354   }
    355 
    356   {
    357     // add to combo box
    358     QFileInfo file(filename);
    359     QSignalBlocker sb(card->path_cb);
    360     card->path_cb->addItem(file.baseName(), QVariant(filename));
    361     card->path_cb->setCurrentIndex(card->path_cb->count() - 1);
    362   }
    363 
    364   card->filename = filename.toStdString();
    365   updateCardTable(card);
    366   updateCardBlocksFree(card);
    367   updateButtonState();
    368 }
    369 
    370 void MemoryCardEditorWindow::saveCard(Card* card)
    371 {
    372   if (card->filename.empty())
    373     return;
    374 
    375   Error error;
    376   if (!MemoryCardImage::SaveToFile(card->data, card->filename.c_str(), &error))
    377   {
    378     QMessageBox::critical(this, tr("Error"),
    379                           tr("Failed to save memory card: %1").arg(QString::fromStdString(error.GetDescription())));
    380     return;
    381   }
    382 
    383   card->dirty = false;
    384   card->save_button->setEnabled(false);
    385   updateCardBlocksFree(card);
    386 }
    387 
    388 void MemoryCardEditorWindow::promptForSave(Card* card)
    389 {
    390   if (card->filename.empty() || !card->dirty)
    391     return;
    392 
    393   if (QMessageBox::question(this, tr("Save memory card?"),
    394                             tr("Memory card '%1' is not saved, do you want to save before closing?")
    395                               .arg(QString::fromStdString(card->filename)),
    396                             QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
    397   {
    398     return;
    399   }
    400 
    401   saveCard(card);
    402 }
    403 
    404 void MemoryCardEditorWindow::doCopyFile()
    405 {
    406   const auto [src, fi] = getSelectedFile();
    407   if (!fi)
    408     return;
    409 
    410   Card* dst = (src == &m_card_a) ? &m_card_b : &m_card_a;
    411 
    412   for (const MemoryCardImage::FileInfo& dst_fi : dst->files)
    413   {
    414     if (dst_fi.filename == fi->filename)
    415     {
    416       QMessageBox::critical(
    417         this, tr("Error"),
    418         tr("Destination memory card already contains a save file with the same name (%1) as the one you are attempting "
    419            "to copy. Please delete this file from the destination memory card before copying.")
    420           .arg(QString(fi->filename.c_str())));
    421       return;
    422     }
    423   }
    424 
    425   if (dst->blocks_free < fi->num_blocks)
    426   {
    427     QMessageBox::critical(this, tr("Error"),
    428                           tr("Insufficient blocks, this file needs %1 but only %2 are available.")
    429                             .arg(fi->num_blocks)
    430                             .arg(dst->blocks_free));
    431     return;
    432   }
    433 
    434   Error error;
    435   std::vector<u8> buffer;
    436   if (!MemoryCardImage::ReadFile(src->data, *fi, &buffer, &error))
    437   {
    438     QMessageBox::critical(this, tr("Error"),
    439                           tr("Failed to read file %1:\n%2")
    440                             .arg(QString::fromStdString(fi->filename))
    441                             .arg(QString::fromStdString(error.GetDescription())));
    442     return;
    443   }
    444 
    445   if (!MemoryCardImage::WriteFile(&dst->data, fi->filename, buffer, &error))
    446   {
    447     QMessageBox::critical(this, tr("Error"),
    448                           tr("Failed to write file %1:\n%2")
    449                             .arg(QString::fromStdString(fi->filename))
    450                             .arg(QString::fromStdString(error.GetDescription())));
    451     return;
    452   }
    453 
    454   clearSelection();
    455   setCardDirty(dst);
    456   updateCardTable(dst);
    457   updateCardBlocksFree(dst);
    458   updateButtonState();
    459 }
    460 
    461 void MemoryCardEditorWindow::doDeleteFile()
    462 {
    463   const auto [card, fi] = getSelectedFile();
    464   if (!fi)
    465     return;
    466 
    467   if (!MemoryCardImage::DeleteFile(&card->data, *fi, fi->deleted))
    468   {
    469     QMessageBox::critical(this, tr("Error"), tr("Failed to delete file %1").arg(QString::fromStdString(fi->filename)));
    470     return;
    471   }
    472 
    473   clearSelection();
    474   setCardDirty(card);
    475   updateCardTable(card);
    476   updateCardBlocksFree(card);
    477   updateButtonState();
    478 }
    479 
    480 void MemoryCardEditorWindow::doUndeleteFile()
    481 {
    482   const auto [card, fi] = getSelectedFile();
    483   if (!fi)
    484     return;
    485 
    486   if (!MemoryCardImage::UndeleteFile(&card->data, *fi))
    487   {
    488     QMessageBox::critical(
    489       this, tr("Error"),
    490       tr("Failed to undelete file %1. The file may have been partially overwritten by another save.")
    491         .arg(QString::fromStdString(fi->filename)));
    492     return;
    493   }
    494 
    495   clearSelection();
    496   setCardDirty(card);
    497   updateCardTable(card);
    498   updateCardBlocksFree(card);
    499   updateButtonState();
    500 }
    501 
    502 void MemoryCardEditorWindow::doExportSaveFile()
    503 {
    504   QString filename = QDir::toNativeSeparators(
    505     QFileDialog::getSaveFileName(this, tr("Select Single Savefile"), QString(), tr(SINGLE_SAVEFILE_FILTER)));
    506 
    507   if (filename.isEmpty())
    508     return;
    509 
    510   const auto [card, fi] = getSelectedFile();
    511   if (!fi)
    512     return;
    513 
    514   Error error;
    515   if (!MemoryCardImage::ExportSave(&card->data, *fi, filename.toStdString().c_str(), &error))
    516   {
    517     QMessageBox::critical(this, tr("Error"),
    518                           tr("Failed to export save file %1:\n%2")
    519                             .arg(QString::fromStdString(fi->filename))
    520                             .arg(QString::fromStdString(error.GetDescription())));
    521     return;
    522   }
    523 }
    524 
    525 void MemoryCardEditorWindow::importCard(Card* card)
    526 {
    527   promptForSave(card);
    528 
    529   QString filename = QDir::toNativeSeparators(
    530     QFileDialog::getOpenFileName(this, tr("Select Import File"), QString(), tr(MEMORY_CARD_IMPORT_FILTER)));
    531   if (filename.isEmpty())
    532     return;
    533 
    534   Error error;
    535   std::unique_ptr<MemoryCardImage::DataArray> temp = std::make_unique<MemoryCardImage::DataArray>();
    536   if (!MemoryCardImage::ImportCard(temp.get(), filename.toStdString().c_str(), &error))
    537   {
    538     QMessageBox::critical(this, tr("Error"),
    539                           tr("Failed to import memory card from %1:\n%2")
    540                             .arg(QFileInfo(filename).fileName())
    541                             .arg(QString::fromStdString(error.GetDescription())));
    542     return;
    543   }
    544 
    545   clearSelection();
    546 
    547   card->data = *temp;
    548   setCardDirty(card);
    549   updateCardTable(card);
    550   updateCardBlocksFree(card);
    551   updateButtonState();
    552 }
    553 
    554 void MemoryCardEditorWindow::formatCard(Card* card)
    555 {
    556   promptForSave(card);
    557 
    558   if (QMessageBox::question(this, tr("Format memory card?"),
    559                             tr("Formatting the memory card will destroy all saves, and they will not be recoverable. "
    560                                "The memory card which will be formatted is located at '%1'.")
    561                               .arg(QString::fromStdString(card->filename)),
    562                             QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
    563   {
    564     return;
    565   }
    566 
    567   clearSelection();
    568 
    569   MemoryCardImage::Format(&card->data);
    570 
    571   setCardDirty(card);
    572   updateCardTable(card);
    573   updateCardBlocksFree(card);
    574   updateButtonState();
    575 }
    576 
    577 void MemoryCardEditorWindow::importSaveFile(Card* card)
    578 {
    579   QString filename = QDir::toNativeSeparators(
    580     QFileDialog::getOpenFileName(this, tr("Select Save File"), QString(), tr(SINGLE_SAVEFILE_FILTER)));
    581 
    582   if (filename.isEmpty())
    583     return;
    584 
    585   Error error;
    586   if (!MemoryCardImage::ImportSave(&card->data, filename.toStdString().c_str(), &error))
    587   {
    588     QMessageBox::critical(this, tr("Error"),
    589                           tr("Failed to import save from %1:\n%2")
    590                             .arg(QFileInfo(filename).fileName())
    591                             .arg(QString::fromStdString(error.GetDescription())));
    592     return;
    593   }
    594 
    595   setCardDirty(card);
    596   updateCardTable(card);
    597   updateCardBlocksFree(card);
    598 }
    599 
    600 std::tuple<MemoryCardEditorWindow::Card*, const MemoryCardImage::FileInfo*> MemoryCardEditorWindow::getSelectedFile()
    601 {
    602   QList<QTableWidgetSelectionRange> sel = m_card_a.table->selectedRanges();
    603   Card* card = &m_card_a;
    604 
    605   if (sel.isEmpty())
    606   {
    607     sel = m_card_b.table->selectedRanges();
    608     card = &m_card_b;
    609   }
    610 
    611   if (sel.isEmpty())
    612     return std::tuple<Card*, const MemoryCardImage::FileInfo*>(nullptr, nullptr);
    613 
    614   const int index = sel.front().topRow();
    615   Assert(index >= 0 && static_cast<u32>(index) < card->files.size());
    616 
    617   return std::tuple<Card*, const MemoryCardImage::FileInfo*>(card, &card->files[index]);
    618 }
    619 
    620 void MemoryCardEditorWindow::updateButtonState()
    621 {
    622   const auto [selected_card, selected_file] = getSelectedFile();
    623   const bool is_card_b = (selected_card == &m_card_b);
    624   const bool has_selection = (selected_file != nullptr);
    625   const bool is_deleted = (selected_file != nullptr && selected_file->deleted);
    626   const bool card_a_present = !m_card_a.filename.empty();
    627   const bool card_b_present = !m_card_b.filename.empty();
    628   const bool both_cards_present = card_a_present && card_b_present;
    629   m_deleteFile->setEnabled(has_selection);
    630   m_undeleteFile->setEnabled(is_deleted);
    631   m_exportFile->setEnabled(has_selection);
    632   m_moveLeft->setEnabled(both_cards_present && has_selection && is_card_b);
    633   m_moveRight->setEnabled(both_cards_present && has_selection && !is_card_b);
    634   m_ui.buttonBoxA->setEnabled(card_a_present);
    635   m_ui.buttonBoxB->setEnabled(card_b_present);
    636 }