diff --git a/README.md b/README.md index e2657f23a..e91a29e7e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c ## Latest News +- 2020/09/19: Memory card importer/editor added to Qt frontend. - 2020/09/13: Support for chaining post processing shaders added. - 2020/09/12: Additional texture filtering options added. - 2020/09/09: Basic cheat support added. Not all instructions/commands are supported yet. @@ -63,6 +64,7 @@ Other features include: - Automatic content scanning - game titles/regions are provided by redump.org - Optional automatic switching of memory cards for each game - Supports loading cheats from libretro or PCSXR format lists + - Memory card editor and save importer ## System Requirements - A CPU faster than a potato. But it needs to be 64-bit (either x86_64 or AArch64/ARMv8) otherwise you won't get a recompiler and it'll be slow. There are no plans to add any 32-bit recompilers. diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 776abe4de..38d9dfd1e 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -52,6 +52,9 @@ set(SRCS mainwindow.cpp mainwindow.h mainwindow.ui + memorycardeditordialog.cpp + memorycardeditordialog.h + memorycardeditordialog.ui memorycardsettingswidget.cpp memorycardsettingswidget.h postprocessingchainconfigwidget.cpp diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index 3dc78fe92..cfdf8fd73 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -56,6 +56,7 @@ + @@ -70,6 +71,7 @@ + @@ -155,6 +157,9 @@ Document + + Document + @@ -181,6 +186,7 @@ + diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index eaee65150..5ec59f656 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -9,6 +9,7 @@ #include "gamelistsettingswidget.h" #include "gamelistwidget.h" #include "gamepropertiesdialog.h" +#include "memorycardeditordialog.h" #include "qtdisplaywidget.h" #include "qthostinterface.h" #include "qtutils.h" @@ -670,6 +671,7 @@ void MainWindow::connectSignals() connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered); connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered); + connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered); connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError, Qt::BlockingQueuedConnection); @@ -956,6 +958,15 @@ void MainWindow::onCheckForUpdatesActionTriggered() checkForUpdates(true); } +void MainWindow::onToolsMemoryCardEditorTriggered() +{ + if (!m_memory_card_editor_dialog) + m_memory_card_editor_dialog = new MemoryCardEditorDialog(this); + + m_memory_card_editor_dialog->setModal(false); + m_memory_card_editor_dialog->show(); +} + void MainWindow::checkForUpdates(bool display_message) { if (!AutoUpdaterDialog::isSupported()) diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index e00ad67c9..c0bd4a3f6 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -14,6 +14,7 @@ class GameListWidget; class QtHostInterface; class QtDisplayWidget; class AutoUpdaterDialog; +class MemoryCardEditorDialog; class HostDisplay; struct GameListEntry; @@ -72,6 +73,7 @@ private Q_SLOTS: void onDiscordServerActionTriggered(); void onAboutActionTriggered(); void onCheckForUpdatesActionTriggered(); + void onToolsMemoryCardEditorTriggered(); void onGameListEntrySelected(const GameListEntry* entry); void onGameListEntryDoubleClicked(const GameListEntry* entry); @@ -116,6 +118,7 @@ private: SettingsDialog* m_settings_dialog = nullptr; AutoUpdaterDialog* m_auto_updater_dialog = nullptr; + MemoryCardEditorDialog* m_memory_card_editor_dialog = nullptr; bool m_emulation_running = false; }; diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index a229b8b73..2814523d8 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -14,7 +14,7 @@ DuckStation - + :/icons/duck.png:/icons/duck.png @@ -30,7 +30,7 @@ 0 0 754 - 30 + 21 @@ -42,7 +42,7 @@ Change Disc - + :/icons/media-optical.png:/icons/media-optical.png @@ -60,7 +60,7 @@ Cheats - + :/icons/conical-flask-red.png:/icons/conical-flask-red.png @@ -69,7 +69,7 @@ Load State - + :/icons/document-open.png:/icons/document-open.png @@ -78,7 +78,7 @@ Save State - + :/icons/document-save.png:/icons/document-save.png @@ -184,9 +184,16 @@ + + + &Tools + + + + @@ -228,7 +235,7 @@ - + :/icons/drive-optical.png:/icons/drive-optical.png @@ -237,7 +244,7 @@ - + :/icons/drive-removable-media.png:/icons/drive-removable-media.png @@ -246,7 +253,7 @@ - + :/icons/folder-open.png:/icons/folder-open.png @@ -255,7 +262,7 @@ - + :/icons/view-refresh.png:/icons/view-refresh.png @@ -264,7 +271,7 @@ - + :/icons/system-shutdown.png:/icons/system-shutdown.png @@ -273,7 +280,7 @@ - + :/icons/view-refresh.png:/icons/view-refresh.png @@ -285,7 +292,7 @@ true - + :/icons/media-playback-pause.png:/icons/media-playback-pause.png @@ -294,7 +301,7 @@ - + :/icons/document-open.png:/icons/document-open.png @@ -303,7 +310,7 @@ - + :/icons/document-save.png:/icons/document-save.png @@ -317,7 +324,7 @@ - + :/icons/utilities-system-monitor.png:/icons/utilities-system-monitor.png @@ -326,7 +333,7 @@ - + :/icons/input-gaming.png:/icons/input-gaming.png @@ -335,7 +342,7 @@ - + :/icons/applications-other.png:/icons/applications-other.png @@ -344,7 +351,7 @@ - + :/icons/video-display.png:/icons/video-display.png @@ -353,7 +360,7 @@ - + :/icons/antialias-icon.png:/icons/antialias-icon.png @@ -362,7 +369,7 @@ - + :/icons/applications-graphics.png:/icons/applications-graphics.png @@ -371,7 +378,7 @@ - + :/icons/view-fullscreen.png:/icons/view-fullscreen.png @@ -410,7 +417,7 @@ - + :/icons/media-optical.png:/icons/media-optical.png @@ -419,7 +426,7 @@ - + :/icons/conical-flask-red.png:/icons/conical-flask-red.png @@ -428,7 +435,7 @@ - + :/icons/audio-card.png:/icons/audio-card.png @@ -437,7 +444,7 @@ - + :/icons/folder-open.png:/icons/folder-open.png @@ -446,7 +453,7 @@ - + :/icons/applications-system.png:/icons/applications-system.png @@ -455,7 +462,7 @@ - + :/icons/applications-development.png:/icons/applications-development.png @@ -464,7 +471,7 @@ - + :/icons/edit-find.png:/icons/edit-find.png @@ -473,7 +480,7 @@ - + :/icons/preferences-system.png:/icons/preferences-system.png @@ -584,7 +591,7 @@ - + :/icons/camera-photo.png:/icons/camera-photo.png @@ -593,7 +600,7 @@ - + :/icons/media-flash-24.png:/icons/media-flash-24.png @@ -602,7 +609,7 @@ - + :/icons/media-playback-start.png:/icons/media-playback-start.png @@ -647,6 +654,11 @@ System &Display + + + Memory &Card Editor + + diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp new file mode 100644 index 000000000..afc00717f --- /dev/null +++ b/src/duckstation-qt/memorycardeditordialog.cpp @@ -0,0 +1,389 @@ +#include "memorycardeditordialog.h" +#include "common/file_system.h" +#include "common/string_util.h" +#include "core/host_interface.h" +#include "qtutils.h" +#include +#include +#include + +static constexpr char MEMORY_CARD_IMAGE_FILTER[] = + QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Memory Card Types (*.mcd *.mcr *.mc)"); +static constexpr char MEMORY_CARD_IMPORT_FILTER[] = + QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme)"); + +MemoryCardEditorDialog::MemoryCardEditorDialog(QWidget* parent) : QDialog(parent) +{ + m_ui.setupUi(this); + m_card_a.path_cb = m_ui.cardAPath; + m_card_a.table = m_ui.cardA; + m_card_a.blocks_free_label = m_ui.cardAUsage; + m_card_a.save_button = m_ui.saveCardA; + m_card_b.path_cb = m_ui.cardBPath; + m_card_b.table = m_ui.cardB; + m_card_b.blocks_free_label = m_ui.cardBUsage; + m_card_b.save_button = m_ui.saveCardB; + + connectUi(); + populateComboBox(m_ui.cardAPath); + populateComboBox(m_ui.cardBPath); +} + +MemoryCardEditorDialog::~MemoryCardEditorDialog() = default; + +void MemoryCardEditorDialog::resizeEvent(QResizeEvent* ev) +{ + QtUtils::ResizeColumnsForTableView(m_card_a.table, {32, -1, 100, 45}); + QtUtils::ResizeColumnsForTableView(m_card_b.table, {32, -1, 100, 45}); +} + +void MemoryCardEditorDialog::closeEvent(QCloseEvent* ev) +{ + promptForSave(&m_card_a); + promptForSave(&m_card_b); +} + +void MemoryCardEditorDialog::connectUi() +{ + connect(m_ui.cardA, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorDialog::onCardASelectionChanged); + connect(m_ui.cardB, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorDialog::onCardBSelectionChanged); + connect(m_ui.moveLeft, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile); + connect(m_ui.moveRight, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile); + connect(m_ui.deleteFile, &QPushButton::clicked, this, &MemoryCardEditorDialog::doDeleteFile); + + connect(m_ui.cardAPath, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { loadCardFromComboBox(&m_card_a, index); }); + connect(m_ui.cardBPath, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { loadCardFromComboBox(&m_card_b, index); }); + connect(m_ui.newCardA, &QPushButton::clicked, [this]() { newCard(&m_card_a); }); + connect(m_ui.newCardB, &QPushButton::clicked, [this]() { newCard(&m_card_b); }); + connect(m_ui.saveCardA, &QPushButton::clicked, [this]() { saveCard(&m_card_a); }); + connect(m_ui.saveCardB, &QPushButton::clicked, [this]() { saveCard(&m_card_b); }); + connect(m_ui.importCardA, &QPushButton::clicked, [this]() { importCard(&m_card_a); }); + connect(m_ui.importCardB, &QPushButton::clicked, [this]() { importCard(&m_card_b); }); +} + +void MemoryCardEditorDialog::populateComboBox(QComboBox* cb) +{ + QSignalBlocker sb(cb); + + cb->clear(); + + cb->addItem(QString()); + cb->addItem(tr("Browse...")); + + const std::string base_path(g_host_interface->GetUserDirectoryRelativePath("memcards")); + FileSystem::FindResultsArray results; + FileSystem::FindFiles(base_path.c_str(), "*.mcd", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &results); + for (FILESYSTEM_FIND_DATA& fd : results) + { + std::string real_filename( + StringUtil::StdStringFromFormat("%s%c%s", base_path.c_str(), FS_OSPATH_SEPERATOR_CHARACTER, fd.FileName.c_str())); + std::string::size_type pos = fd.FileName.rfind('.'); + if (pos != std::string::npos) + fd.FileName.erase(pos); + + cb->addItem(QString::fromStdString(fd.FileName), QVariant(QString::fromStdString(real_filename))); + } +} + +void MemoryCardEditorDialog::loadCardFromComboBox(Card* card, int index) +{ + QString filename; + if (index == 1) + { + filename = QFileDialog::getOpenFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER)); + if (!filename.isEmpty()) + { + // add to combo box + QFileInfo file(filename); + QSignalBlocker sb(card->path_cb); + card->path_cb->addItem(file.baseName(), QVariant(filename)); + card->path_cb->setCurrentIndex(card->path_cb->count() - 1); + } + } + else + { + filename = card->path_cb->itemData(index).toString(); + } + + if (filename.isEmpty()) + return; + + loadCard(filename, card); +} + +void MemoryCardEditorDialog::onCardASelectionChanged() +{ + { + QSignalBlocker cb(m_card_b.table); + m_card_b.table->clearSelection(); + } + + updateButtonState(); +} + +void MemoryCardEditorDialog::onCardBSelectionChanged() +{ + { + QSignalBlocker cb(m_card_a.table); + m_card_a.table->clearSelection(); + } + + updateButtonState(); +} + +void MemoryCardEditorDialog::clearSelection() +{ + { + QSignalBlocker cb(m_card_a.table); + m_card_a.table->clearSelection(); + } + + { + QSignalBlocker cb(m_card_b.table); + m_card_b.table->clearSelection(); + } + + updateButtonState(); +} + +bool MemoryCardEditorDialog::loadCard(const QString& filename, Card* card) +{ + promptForSave(card); + + card->table->setRowCount(0); + card->dirty = false; + card->blocks_free_label->clear(); + card->save_button->setEnabled(false); + + card->filename.clear(); + + std::string filename_str = filename.toStdString(); + if (!MemoryCardImage::LoadFromFile(&card->data, filename_str.c_str())) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to load memory card image.")); + return false; + } + + card->filename = std::move(filename_str); + updateCardTable(card); + updateCardBlocksFree(card); + updateButtonState(); + return true; +} + +void MemoryCardEditorDialog::updateCardTable(Card* card) +{ + card->table->setRowCount(0); + + card->files = MemoryCardImage::EnumerateFiles(card->data); + for (const MemoryCardImage::FileInfo& fi : card->files) + { + const int row = card->table->rowCount(); + card->table->insertRow(row); + + if (!fi.icon_frames.empty()) + { + const QImage image(reinterpret_cast(fi.icon_frames[0].pixels), MemoryCardImage::ICON_WIDTH, + MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888); + + QTableWidgetItem* icon = new QTableWidgetItem(); + icon->setIcon(QIcon(QPixmap::fromImage(image))); + card->table->setItem(row, 0, icon); + } + + card->table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(fi.title))); + card->table->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(fi.filename))); + card->table->setItem(row, 3, new QTableWidgetItem(QStringLiteral("%1").arg(fi.num_blocks))); + } +} + +void MemoryCardEditorDialog::updateCardBlocksFree(Card* card) +{ + card->blocks_free = MemoryCardImage::GetFreeBlockCount(card->data); + card->blocks_free_label->setText( + tr("%1 blocks free%2").arg(card->blocks_free).arg(card->dirty ? QStringLiteral(" (*)") : QString())); +} + +void MemoryCardEditorDialog::setCardDirty(Card* card) +{ + card->dirty = true; + card->save_button->setEnabled(true); +} + +void MemoryCardEditorDialog::newCard(Card* card) +{ + promptForSave(card); + + QString filename = + QFileDialog::getSaveFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER)); + if (filename.isEmpty()) + return; + + { + // add to combo box + QFileInfo file(filename); + QSignalBlocker sb(card->path_cb); + card->path_cb->addItem(file.baseName(), QVariant(filename)); + card->path_cb->setCurrentIndex(card->path_cb->count() - 1); + } + + card->filename = filename.toStdString(); + + MemoryCardImage::Format(&card->data); + updateCardTable(card); + updateCardBlocksFree(card); + updateButtonState(); + saveCard(card); +} + +void MemoryCardEditorDialog::saveCard(Card* card) +{ + if (card->filename.empty()) + return; + + if (!MemoryCardImage::SaveToFile(card->data, card->filename.c_str())) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to write card to '%1'").arg(QString::fromStdString(card->filename))); + return; + } + + card->dirty = false; + card->save_button->setEnabled(false); + updateCardBlocksFree(card); +} + +void MemoryCardEditorDialog::promptForSave(Card* card) +{ + if (card->filename.empty() || !card->dirty) + return; + + if (QMessageBox::question(this, tr("Save memory card?"), + tr("Memory card '%1' is not saved, do you want to save before closing?") + .arg(QString::fromStdString(card->filename)), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) + { + return; + } + + saveCard(card); +} + +void MemoryCardEditorDialog::doCopyFile() +{ + const auto [src, fi] = getSelectedFile(); + if (!fi) + return; + + Card* dst = (src == &m_card_a) ? &m_card_b : &m_card_a; + + if (dst->blocks_free < fi->num_blocks) + { + QMessageBox::critical(this, tr("Error"), + tr("Insufficient blocks, this file needs %1 but only %2 are available.") + .arg(fi->num_blocks) + .arg(dst->blocks_free)); + return; + } + + std::vector buffer; + if (!MemoryCardImage::ReadFile(src->data, *fi, &buffer)) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to read file %1").arg(QString::fromStdString(fi->filename))); + return; + } + + if (!MemoryCardImage::WriteFile(&dst->data, fi->filename, buffer)) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to write file %1").arg(QString::fromStdString(fi->filename))); + return; + } + + clearSelection(); + updateCardTable(dst); + updateCardBlocksFree(dst); + setCardDirty(dst); + updateButtonState(); +} + +void MemoryCardEditorDialog::doDeleteFile() +{ + const auto [card, fi] = getSelectedFile(); + if (!fi) + return; + + if (!MemoryCardImage::DeleteFile(&card->data, *fi)) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to delete file %1").arg(QString::fromStdString(fi->filename))); + return; + } + + clearSelection(); + updateCardTable(card); + updateCardBlocksFree(card); + setCardDirty(card); + updateButtonState(); +} + +void MemoryCardEditorDialog::importCard(Card* card) +{ + promptForSave(card); + + QString filename = + QFileDialog::getOpenFileName(this, tr("Select Import File"), QString(), tr(MEMORY_CARD_IMPORT_FILTER)); + if (filename.isEmpty()) + return; + + std::unique_ptr temp = std::make_unique(); + if (!MemoryCardImage::ImportCard(temp.get(), filename.toStdString().c_str())) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to import memory card. The log may contain more information.")); + return; + } + + clearSelection(); + + card->data = *temp; + updateCardTable(card); + updateCardBlocksFree(card); + setCardDirty(card); + updateButtonState(); +} + +std::tuple MemoryCardEditorDialog::getSelectedFile() +{ + QList sel = m_card_a.table->selectedRanges(); + Card* card = &m_card_a; + + if (sel.isEmpty()) + { + sel = m_card_b.table->selectedRanges(); + card = &m_card_b; + } + + if (sel.isEmpty()) + return std::tuple(nullptr, nullptr); + + const int index = sel.front().topRow(); + Assert(index >= 0 && static_cast(index) < card->files.size()); + + return std::tuple(card, &card->files[index]); +} + +void MemoryCardEditorDialog::updateButtonState() +{ + 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 card_a_present = !m_card_a.filename.empty(); + const bool card_b_present = !m_card_b.filename.empty(); + const bool both_cards_present = card_a_present && card_b_present; + m_ui.deleteFile->setEnabled(has_selection); + m_ui.exportFile->setEnabled(has_selection); + m_ui.moveLeft->setEnabled(both_cards_present && has_selection && is_card_b); + m_ui.moveRight->setEnabled(both_cards_present && has_selection && !is_card_b); + m_ui.importCardA->setEnabled(card_a_present); + m_ui.importCardB->setEnabled(card_b_present); +} diff --git a/src/duckstation-qt/memorycardeditordialog.h b/src/duckstation-qt/memorycardeditordialog.h new file mode 100644 index 000000000..58ef95691 --- /dev/null +++ b/src/duckstation-qt/memorycardeditordialog.h @@ -0,0 +1,63 @@ +#pragma once +#include "core/memory_card_image.h" +#include "ui_memorycardeditordialog.h" +#include +#include +#include +#include +#include + +class MemoryCardEditorDialog : public QDialog +{ + Q_OBJECT + +public: + MemoryCardEditorDialog(QWidget* parent); + ~MemoryCardEditorDialog(); + +protected: + void resizeEvent(QResizeEvent* ev); + void closeEvent(QCloseEvent* ev); + +private Q_SLOTS: + void onCardASelectionChanged(); + void onCardBSelectionChanged(); + void doCopyFile(); + void doDeleteFile(); + +private: + struct Card + { + std::string filename; + MemoryCardImage::DataArray data; + std::vector files; + u32 blocks_free = 0; + bool dirty = false; + + QComboBox* path_cb = nullptr; + QTableWidget* table = nullptr; + QLabel* blocks_free_label = nullptr; + QPushButton* save_button = nullptr; + }; + + void connectUi(); + void populateComboBox(QComboBox* cb); + void clearSelection(); + void loadCardFromComboBox(Card* card, int index); + bool loadCard(const QString& filename, Card* card); + void updateCardTable(Card* card); + void updateCardBlocksFree(Card* card); + void setCardDirty(Card* card); + void newCard(Card* card); + void saveCard(Card* card); + void promptForSave(Card* card); + void importCard(Card* card); + + std::tuple getSelectedFile(); + void updateButtonState(); + + Ui::MemoryCardEditorDialog m_ui; + + Card m_card_a; + Card m_card_b; +}; diff --git a/src/duckstation-qt/memorycardeditordialog.ui b/src/duckstation-qt/memorycardeditordialog.ui new file mode 100644 index 000000000..d09e2e945 --- /dev/null +++ b/src/duckstation-qt/memorycardeditordialog.ui @@ -0,0 +1,290 @@ + + + MemoryCardEditorDialog + + + + 0 + 0 + 846 + 515 + + + + Memory Card Editor + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + 16 + 16 + + + + true + + + false + + + + + + + + + Title + + + + + File Name + + + + + Blocks + + + + + + + + + + Memory Card: + + + + + + + + + + + + + + New... + + + + + + + + + + + 0 blocks used + + + + + + + false + + + Import File... + + + + + + + false + + + Import Card... + + + + + + + false + + + Save + + + + + + + + + + + 0 blocks used + + + + + + + false + + + Import File... + + + + + + + false + + + Import Card... + + + + + + + false + + + Save + + + + + + + + + + + Memory Card: + + + + + + + + + + New... + + + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + 16 + 16 + + + + true + + + false + + + + + + + + + Title + + + + + File Name + + + + + Blocks + + + + + + + + + + false + + + Delete File + + + + + + + false + + + Export File + + + + + + + false + + + << + + + + + + + false + + + >> + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + +