diff --git a/Source/Core/DolphinQt2/CMakeLists.txt b/Source/Core/DolphinQt2/CMakeLists.txt
index 9295ea8c65..7c8841784d 100644
--- a/Source/Core/DolphinQt2/CMakeLists.txt
+++ b/Source/Core/DolphinQt2/CMakeLists.txt
@@ -85,6 +85,7 @@ set(SRCS
GameList/GameTracker.cpp
GameList/GridProxyModel.cpp
GameList/ListProxyModel.cpp
+ GCMemcardManager.cpp
QtUtils/BlockUserInputFilter.cpp
NetPlay/GameListDialog.cpp
NetPlay/MD5Dialog.cpp
diff --git a/Source/Core/DolphinQt2/DolphinQt2.vcxproj b/Source/Core/DolphinQt2/DolphinQt2.vcxproj
index 86b08a7f71..f708808611 100644
--- a/Source/Core/DolphinQt2/DolphinQt2.vcxproj
+++ b/Source/Core/DolphinQt2/DolphinQt2.vcxproj
@@ -221,6 +221,7 @@
+
diff --git a/Source/Core/DolphinQt2/GCMemcardManager.cpp b/Source/Core/DolphinQt2/GCMemcardManager.cpp
new file mode 100644
index 0000000000..64ea448b94
--- /dev/null
+++ b/Source/Core/DolphinQt2/GCMemcardManager.cpp
@@ -0,0 +1,476 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "DolphinQt2/GCMemcardManager.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "Common/FileUtil.h"
+#include "Common/MsgHandler.h"
+#include "Common/StringUtil.h"
+
+#include "Core/HW/GCMemcard/GCMemcard.h"
+
+constexpr u32 BANNER_WIDTH = 96;
+constexpr u32 ANIM_FRAME_WIDTH = 32;
+constexpr u32 IMAGE_HEIGHT = 32;
+constexpr u32 ANIM_MAX_FRAMES = 8;
+constexpr float ROW_HEIGHT = 28;
+
+GCMemcardManager::GCMemcardManager(QWidget* parent) : QDialog(parent)
+{
+ CreateWidgets();
+ ConnectWidgets();
+
+ SetActiveSlot(0);
+ UpdateActions();
+
+ m_timer = new QTimer(this);
+ connect(m_timer, &QTimer::timeout, this, &GCMemcardManager::DrawIcons);
+
+ m_timer->start(1000 / 8);
+
+ // Make the dimensions more reasonable on startup
+ resize(650, 500);
+}
+
+GCMemcardManager::~GCMemcardManager() = default;
+
+void GCMemcardManager::CreateWidgets()
+{
+ m_button_box = new QDialogButtonBox(QDialogButtonBox::Ok);
+
+ // Actions
+ m_select_button = new QPushButton;
+ m_copy_button = new QPushButton;
+
+ // Contents will be set by their appropriate functions
+ m_delete_button = new QPushButton(tr("&Delete"));
+ m_export_button = new QPushButton(tr("&Export..."));
+ m_export_all_button = new QPushButton(tr("Export &All..."));
+ m_import_button = new QPushButton(tr("&Import..."));
+ m_fix_checksums_button = new QPushButton(tr("Fix Checksums"));
+
+ auto* layout = new QGridLayout;
+
+ for (int i = 0; i < SLOT_COUNT; i++)
+ {
+ m_slot_group[i] = new QGroupBox(i == 0 ? tr("Slot A") : tr("Slot B"));
+ m_slot_file_edit[i] = new QLineEdit;
+ m_slot_file_button[i] = new QPushButton(tr("&Browse..."));
+ m_slot_table[i] = new QTableWidget;
+ m_slot_stat_label[i] = new QLabel;
+
+ m_slot_table[i]->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_slot_table[i]->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_slot_table[i]->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ m_slot_table[i]->verticalHeader()->hide();
+
+ auto* slot_layout = new QGridLayout;
+ m_slot_group[i]->setLayout(slot_layout);
+
+ slot_layout->addWidget(m_slot_file_edit[i], 0, 0);
+ slot_layout->addWidget(m_slot_file_button[i], 0, 1);
+ slot_layout->addWidget(m_slot_table[i], 1, 0, 1, 2);
+ slot_layout->addWidget(m_slot_stat_label[i], 2, 0);
+
+ layout->addWidget(m_slot_group[i], 0, i * 2, 7, 1);
+ }
+
+ layout->addWidget(m_select_button, 0, 1);
+ layout->addWidget(m_copy_button, 1, 1);
+ layout->addWidget(m_delete_button, 2, 1);
+ layout->addWidget(m_export_button, 3, 1);
+ layout->addWidget(m_export_all_button, 4, 1);
+ layout->addWidget(m_import_button, 5, 1);
+ layout->addWidget(m_fix_checksums_button, 6, 1);
+ layout->addWidget(m_button_box, 7, 2);
+
+ setLayout(layout);
+}
+
+void GCMemcardManager::ConnectWidgets()
+{
+ connect(m_button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(m_select_button, &QPushButton::pressed, this, [this] { SetActiveSlot(!m_active_slot); });
+ connect(m_export_button, &QPushButton::pressed, this, [this] { ExportFiles(true); });
+ connect(m_export_all_button, &QPushButton::pressed, this, &GCMemcardManager::ExportAllFiles);
+ connect(m_delete_button, &QPushButton::pressed, this, &GCMemcardManager::DeleteFiles);
+ connect(m_import_button, &QPushButton::pressed, this, &GCMemcardManager::ImportFile);
+ connect(m_copy_button, &QPushButton::pressed, this, &GCMemcardManager::CopyFiles);
+ connect(m_fix_checksums_button, &QPushButton::pressed, this, &GCMemcardManager::FixChecksums);
+
+ for (int slot = 0; slot < SLOT_COUNT; slot++)
+ {
+ connect(m_slot_file_edit[slot], &QLineEdit::textChanged, this,
+ [this, slot](const QString& path) { SetSlotFile(slot, path); });
+ connect(m_slot_file_button[slot], &QPushButton::pressed, this,
+ [this, slot] { SetSlotFileInteractive(slot); });
+ connect(m_slot_table[slot], &QTableWidget::itemSelectionChanged, this,
+ &GCMemcardManager::UpdateActions);
+ }
+}
+
+void GCMemcardManager::SetActiveSlot(int slot)
+{
+ for (int i = 0; i < SLOT_COUNT; i++)
+ m_slot_group[i]->setEnabled(i == slot);
+
+ m_select_button->setText(slot == 0 ? QStringLiteral("<") : QStringLiteral(">"));
+ m_copy_button->setText(slot == 0 ? QStringLiteral("Copy to B") : QStringLiteral("Copy to A"));
+
+ m_active_slot = slot;
+
+ UpdateSlotTable(slot);
+ UpdateActions();
+}
+
+void GCMemcardManager::UpdateSlotTable(int slot)
+{
+ m_slot_active_icons[m_active_slot].clear();
+ m_slot_table[slot]->clear();
+ m_slot_table[slot]->setColumnCount(6);
+ m_slot_table[slot]->verticalHeader()->setDefaultSectionSize(ROW_HEIGHT);
+ m_slot_table[slot]->verticalHeader()->setDefaultSectionSize(QHeaderView::Fixed);
+ m_slot_table[slot]->setHorizontalHeaderLabels(
+ {tr("Banner"), tr("Title"), tr("Comment"), tr("Icon"), tr("Blocks"), tr("First Block")});
+
+ if (m_slot_memcard[slot] == nullptr)
+ return;
+
+ auto& memcard = m_slot_memcard[slot];
+ auto* table = m_slot_table[slot];
+
+ auto create_item = [this](const QString string = QStringLiteral("")) {
+ QTableWidgetItem* item = new QTableWidgetItem(string);
+ item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+ return item;
+ };
+
+ for (int i = 0; i < memcard->GetNumFiles(); i++)
+ {
+ int file_index = memcard->GetFileIndex(i);
+ table->setRowCount(i + 1);
+
+ auto const string_decoder = memcard->IsShiftJIS() ? SHIFTJISToUTF8 : CP1252ToUTF8;
+
+ QString title = QString::fromStdString(string_decoder(memcard->GetSaveComment1(file_index)));
+ QString comment = QString::fromStdString(string_decoder(memcard->GetSaveComment2(file_index)));
+ QString blocks = QStringLiteral("%1").arg(memcard->DEntry_BlockCount(file_index));
+ QString block_count = QStringLiteral("%1").arg(memcard->DEntry_FirstBlock(file_index));
+
+ auto* banner = new QTableWidgetItem;
+ banner->setData(Qt::DecorationRole, GetBannerFromSaveFile(file_index));
+ banner->setFlags(banner->flags() ^ Qt::ItemIsEditable);
+
+ auto frames = GetIconFromSaveFile(file_index);
+ auto* icon = new QTableWidgetItem;
+ icon->setData(Qt::DecorationRole, frames[0]);
+
+ DEntry d;
+ memcard->GetDEntry(file_index, d);
+
+ const auto speed = ((d.AnimSpeed[0] & 1) << 2) + (d.AnimSpeed[1] & 1);
+
+ m_slot_active_icons[m_active_slot].push_back({speed, frames});
+
+ table->setItem(i, 0, banner);
+ table->setItem(i, 1, create_item(title));
+ table->setItem(i, 2, create_item(comment));
+ table->setItem(i, 3, icon);
+ table->setItem(i, 4, create_item(blocks));
+ table->setItem(i, 5, create_item(block_count));
+ table->resizeRowToContents(i);
+ }
+}
+
+void GCMemcardManager::UpdateActions()
+{
+ auto selection = m_slot_table[m_active_slot]->selectedItems();
+ bool have_selection = selection.count();
+ bool have_memcard = m_slot_memcard[m_active_slot] != nullptr;
+ bool have_memcard_other = m_slot_memcard[!m_active_slot] != nullptr;
+
+ m_copy_button->setEnabled(have_selection && have_memcard_other);
+ m_export_button->setEnabled(have_selection);
+ m_export_all_button->setEnabled(have_memcard);
+ m_import_button->setEnabled(have_memcard);
+ m_delete_button->setEnabled(have_selection);
+ m_fix_checksums_button->setEnabled(have_memcard);
+}
+
+void GCMemcardManager::SetSlotFile(int slot, QString path)
+{
+ auto memcard = std::make_unique(path.toStdString());
+
+ if (!memcard->IsValid())
+ return;
+
+ m_slot_stat_label[slot]->setText(tr("%1 Free Blocks; %2 Free Dir Entries")
+ .arg(memcard->GetFreeBlocks())
+ .arg(DIRLEN - memcard->GetNumFiles()));
+
+ m_slot_file_edit[slot]->setText(path);
+ m_slot_memcard[slot] = std::move(memcard);
+
+ UpdateSlotTable(slot);
+ UpdateActions();
+}
+
+void GCMemcardManager::SetSlotFileInteractive(int slot)
+{
+ QString path =
+ QFileDialog::getOpenFileName(this, slot == 0 ? tr("Set memory card file for Slot A") :
+ tr("Set memory card file for Slot B"),
+ QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
+ tr("GameCube Memory Cards (*.raw *.gcp)"));
+
+ if (!path.isEmpty())
+ m_slot_file_edit[slot]->setText(path);
+}
+
+void GCMemcardManager::ExportFiles(bool prompt)
+{
+ auto selection = m_slot_table[m_active_slot]->selectedItems();
+ auto& memcard = m_slot_memcard[m_active_slot];
+
+ auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
+
+ for (int i = 0; i < count; i++)
+ {
+ auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
+ int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
+
+ std::string gci_filename;
+ if (!memcard->GCI_FileName(file_index, gci_filename))
+ return;
+
+ QString path;
+ if (prompt)
+ {
+ path = QFileDialog::getSaveFileName(
+ this, tr("Export Save File"),
+ QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)) +
+ QStringLiteral("/%1").arg(QString::fromStdString(gci_filename)),
+ tr("Native GCI File (*.gci)") + QStringLiteral(";;") +
+ tr("MadCatz Gameshark files(*.gcs)") + QStringLiteral(";;") +
+ tr("Datel MaxDrive/Pro files(*.sav)"));
+
+ if (path.isEmpty())
+ return;
+ }
+ else
+ {
+ path = QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)) +
+ QStringLiteral("/%1").arg(QString::fromStdString(gci_filename));
+ }
+
+ if (!memcard->ExportGci(file_index, path.toStdString(), ""))
+ {
+ File::Delete(path.toStdString());
+ }
+ }
+
+ QMessageBox::information(this, tr("Success"),
+ tr("Successfully exported %1 %2")
+ .arg(count)
+ .arg(count == 1 ? tr("save file") : tr("save files")));
+}
+
+void GCMemcardManager::ExportAllFiles()
+{
+ // This is nothing but a thin wrapper around ExportFiles()
+ m_slot_table[m_active_slot]->selectAll();
+ ExportFiles(false);
+}
+
+void GCMemcardManager::ImportFile()
+{
+ QString path = QFileDialog::getOpenFileName(
+ this, tr("Export Save File"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
+ tr("Native GCI File (*.gci)") + QStringLiteral(";;") + tr("MadCatz Gameshark files(*.gcs)") +
+ QStringLiteral(";;") + tr("Datel MaxDrive/Pro files(*.sav)"));
+
+ if (path.isEmpty())
+ return;
+
+ m_slot_memcard[m_active_slot]->ImportGci(path.toStdString(), "");
+
+ if (!m_slot_memcard[m_active_slot]->Save())
+ PanicAlertT("File write failed");
+
+ UpdateSlotTable(m_active_slot);
+}
+
+void GCMemcardManager::CopyFiles()
+{
+ auto selection = m_slot_table[m_active_slot]->selectedItems();
+ auto& memcard = m_slot_memcard[m_active_slot];
+
+ auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
+
+ for (int i = 0; i < count; i++)
+ {
+ auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
+ int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
+
+ m_slot_memcard[!m_active_slot]->CopyFrom(*memcard, file_index);
+ }
+
+ if (!m_slot_memcard[!m_active_slot]->Save())
+ PanicAlertT("File write failed");
+
+ for (int i = 0; i < SLOT_COUNT; i++)
+ UpdateSlotTable(i);
+}
+
+void GCMemcardManager::DeleteFiles()
+{
+ auto selection = m_slot_table[m_active_slot]->selectedItems();
+ auto& memcard = m_slot_memcard[m_active_slot];
+
+ auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
+
+ // Ask for confirmation if we are to delete multiple files
+ if (count > 1)
+ {
+ auto response = QMessageBox::warning(this, tr("Question"),
+ tr("Do you want to delete the %1 selected %2?")
+ .arg(count)
+ .arg(count == 1 ? tr("save file") : tr("save files")),
+ QMessageBox::Yes | QMessageBox::Abort);
+
+ if (response == QMessageBox::Abort)
+ return;
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
+ int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
+ memcard->RemoveFile(file_index);
+ }
+
+ QMessageBox::information(this, tr("Success"), tr("Successfully deleted files."));
+
+ if (!memcard->Save())
+ PanicAlertT("File write failed");
+
+ UpdateSlotTable(m_active_slot);
+ UpdateActions();
+}
+
+void GCMemcardManager::FixChecksums()
+{
+ auto& memcard = m_slot_memcard[m_active_slot];
+ memcard->FixChecksums();
+
+ if (!memcard->Save())
+ PanicAlertT("File write failed");
+}
+
+void GCMemcardManager::DrawIcons()
+{
+ m_current_frame++;
+ m_current_frame %= 15;
+
+ int row = 0;
+ const auto column = 3;
+ for (auto& icon : m_slot_active_icons[m_active_slot])
+ {
+ int frame = (m_current_frame / 3 - icon.first) % icon.second.size();
+
+ auto* item = new QTableWidgetItem;
+ item->setData(Qt::DecorationRole, icon.second[frame]);
+ item->setFlags(item->flags() ^ Qt::ItemIsEditable);
+
+ m_slot_table[m_active_slot]->setItem(row, column, item);
+ row++;
+ }
+}
+
+QPixmap GCMemcardManager::GetBannerFromSaveFile(int file_index)
+{
+ auto& memcard = m_slot_memcard[m_active_slot];
+
+ std::vector pxdata(BANNER_WIDTH * IMAGE_HEIGHT);
+
+ QImage image;
+ if (memcard->ReadBannerRGBA8(file_index, pxdata.data()))
+ {
+ image = QImage(reinterpret_cast(pxdata.data()), BANNER_WIDTH, IMAGE_HEIGHT,
+ QImage::Format_ARGB32);
+ }
+
+ return QPixmap::fromImage(image);
+}
+
+std::vector GCMemcardManager::GetIconFromSaveFile(int file_index)
+{
+ auto& memcard = m_slot_memcard[m_active_slot];
+
+ std::vector pxdata(BANNER_WIDTH * IMAGE_HEIGHT);
+ std::vector anim_delay(ANIM_MAX_FRAMES);
+ std::vector anim_data(ANIM_FRAME_WIDTH * IMAGE_HEIGHT * ANIM_MAX_FRAMES);
+
+ std::vector frame_pixmaps;
+
+ u32 num_frames = memcard->ReadAnimRGBA8(file_index, anim_data.data(), anim_delay.data());
+
+ // Decode Save File Animation
+ if (num_frames > 0)
+ {
+ u32 frames = BANNER_WIDTH / ANIM_FRAME_WIDTH;
+
+ if (num_frames < frames)
+ {
+ frames = num_frames;
+
+ // Clear unused frame's pixels from the buffer.
+ std::fill(pxdata.begin(), pxdata.end(), 0);
+ }
+
+ for (u32 f = 0; f < frames; ++f)
+ {
+ for (u32 y = 0; y < IMAGE_HEIGHT; ++y)
+ {
+ for (u32 x = 0; x < ANIM_FRAME_WIDTH; ++x)
+ {
+ // NOTE: pxdata is stacked horizontal, anim_data is stacked vertical
+ pxdata[y * BANNER_WIDTH + f * ANIM_FRAME_WIDTH + x] =
+ anim_data[f * ANIM_FRAME_WIDTH * IMAGE_HEIGHT + y * IMAGE_HEIGHT + x];
+ }
+ }
+ }
+ QImage anims(reinterpret_cast(pxdata.data()), BANNER_WIDTH, IMAGE_HEIGHT,
+ QImage::Format_ARGB32);
+
+ for (u32 f = 0; f < frames; f++)
+ {
+ frame_pixmaps.push_back(
+ QPixmap::fromImage(anims.copy(ANIM_FRAME_WIDTH * f, 0, ANIM_FRAME_WIDTH, IMAGE_HEIGHT)));
+ }
+ }
+ else
+ {
+ // No Animation found, use an empty placeholder instead.
+ frame_pixmaps.push_back(QPixmap());
+ }
+
+ return frame_pixmaps;
+}
diff --git a/Source/Core/DolphinQt2/GCMemcardManager.h b/Source/Core/DolphinQt2/GCMemcardManager.h
new file mode 100644
index 0000000000..b6b71ea12f
--- /dev/null
+++ b/Source/Core/DolphinQt2/GCMemcardManager.h
@@ -0,0 +1,75 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include
+
+class GCMemcard;
+
+class QDialogButtonBox;
+class QGroupBox;
+class QLabel;
+class QLineEdit;
+class QPushButton;
+class QTableWidget;
+class QTimer;
+
+class GCMemcardManager : public QDialog
+{
+public:
+ explicit GCMemcardManager(QWidget* parent = nullptr);
+ ~GCMemcardManager();
+
+private:
+ void CreateWidgets();
+ void ConnectWidgets();
+
+ void UpdateActions();
+ void UpdateSlotTable(int slot);
+ void SetSlotFile(int slot, QString path);
+ void SetSlotFileInteractive(int slot);
+ void SetActiveSlot(int slot);
+
+ void CopyFiles();
+ void ImportFile();
+ void DeleteFiles();
+ void ExportFiles(bool prompt);
+ void ExportAllFiles();
+ void FixChecksums();
+ void DrawIcons();
+
+ QPixmap GetBannerFromSaveFile(int file_index);
+ std::vector GetIconFromSaveFile(int file_index);
+
+ // Actions
+ QPushButton* m_select_button;
+ QPushButton* m_copy_button;
+ QPushButton* m_export_button;
+ QPushButton* m_export_all_button;
+ QPushButton* m_import_button;
+ QPushButton* m_delete_button;
+ QPushButton* m_fix_checksums_button;
+
+ // Slots
+ static constexpr int SLOT_COUNT = 2;
+ std::array>>, SLOT_COUNT> m_slot_active_icons;
+ std::array, SLOT_COUNT> m_slot_memcard;
+ std::array m_slot_group;
+ std::array m_slot_file_edit;
+ std::array m_slot_file_button;
+ std::array m_slot_table;
+ std::array m_slot_stat_label;
+
+ int m_active_slot;
+ int m_current_frame;
+
+ QDialogButtonBox* m_button_box;
+ QTimer* m_timer;
+};
diff --git a/Source/Core/DolphinQt2/MainWindow.cpp b/Source/Core/DolphinQt2/MainWindow.cpp
index f64daf44ff..1253aa3ec1 100644
--- a/Source/Core/DolphinQt2/MainWindow.cpp
+++ b/Source/Core/DolphinQt2/MainWindow.cpp
@@ -53,6 +53,7 @@
#include "DolphinQt2/Debugger/RegisterWidget.h"
#include "DolphinQt2/Debugger/WatchWidget.h"
#include "DolphinQt2/FIFOPlayerWindow.h"
+#include "DolphinQt2/GCMemcardManager.h"
#include "DolphinQt2/Host.h"
#include "DolphinQt2/HotkeyScheduler.h"
#include "DolphinQt2/MainWindow.h"
@@ -234,6 +235,7 @@ void MainWindow::ConnectMenuBar()
connect(m_menu_bar, &MenuBar::ConfigureHotkeys, this, &MainWindow::ShowHotkeyDialog);
// Tools
+ connect(m_menu_bar, &MenuBar::ShowMemcardManager, this, &MainWindow::ShowMemcardManager);
connect(m_menu_bar, &MenuBar::BootGameCubeIPL, this, &MainWindow::OnBootGameCubeIPL);
connect(m_menu_bar, &MenuBar::ImportNANDBackup, this, &MainWindow::OnImportNANDBackup);
connect(m_menu_bar, &MenuBar::PerformOnlineUpdate, this, &MainWindow::PerformOnlineUpdate);
@@ -1065,3 +1067,10 @@ void MainWindow::OnConnectWiiRemote(int id)
Wiimote::Connect(id, !is_connected);
});
}
+
+void MainWindow::ShowMemcardManager()
+{
+ GCMemcardManager manager(this);
+
+ manager.exec();
+}
diff --git a/Source/Core/DolphinQt2/MainWindow.h b/Source/Core/DolphinQt2/MainWindow.h
index 9759558c7f..92aac61a17 100644
--- a/Source/Core/DolphinQt2/MainWindow.h
+++ b/Source/Core/DolphinQt2/MainWindow.h
@@ -104,6 +104,7 @@ private:
void ShowHotkeyDialog();
void ShowNetPlaySetupDialog();
void ShowFIFOPlayer();
+ void ShowMemcardManager();
void NetPlayInit();
bool NetPlayJoin();
diff --git a/Source/Core/DolphinQt2/MenuBar.cpp b/Source/Core/DolphinQt2/MenuBar.cpp
index bd580713ca..f03f81c911 100644
--- a/Source/Core/DolphinQt2/MenuBar.cpp
+++ b/Source/Core/DolphinQt2/MenuBar.cpp
@@ -109,6 +109,11 @@ void MenuBar::AddToolsMenu()
{
QMenu* tools_menu = addMenu(tr("&Tools"));
+ AddAction(tools_menu, tr("&Memory Card Manager (GC)"), this,
+ [this] { emit ShowMemcardManager(); });
+
+ tools_menu->addSeparator();
+
AddAction(tools_menu, tr("Import Wii Save..."), this, &MenuBar::ImportWiiSave);
AddAction(tools_menu, tr("Export All Wii Saves"), this, &MenuBar::ExportWiiSaves);
diff --git a/Source/Core/DolphinQt2/MenuBar.h b/Source/Core/DolphinQt2/MenuBar.h
index a8fb661701..136d518798 100644
--- a/Source/Core/DolphinQt2/MenuBar.h
+++ b/Source/Core/DolphinQt2/MenuBar.h
@@ -62,6 +62,7 @@ signals:
void PerformOnlineUpdate(const std::string& region);
// Tools
+ void ShowMemcardManager();
void BootGameCubeIPL(DiscIO::Region region);
void ShowFIFOPlayer();
void ShowAboutDialog();