From 21db1e95045835efa129062ec4ac21bf1b84a9e5 Mon Sep 17 00:00:00 2001 From: spycrab Date: Thu, 25 Jan 2018 19:54:50 +0100 Subject: [PATCH] Qt: Implement GameCube memory manager --- Source/Core/DolphinQt2/CMakeLists.txt | 1 + Source/Core/DolphinQt2/DolphinQt2.vcxproj | 1 + Source/Core/DolphinQt2/GCMemcardManager.cpp | 476 ++++++++++++++++++++ Source/Core/DolphinQt2/GCMemcardManager.h | 75 +++ Source/Core/DolphinQt2/MainWindow.cpp | 9 + Source/Core/DolphinQt2/MainWindow.h | 1 + Source/Core/DolphinQt2/MenuBar.cpp | 5 + Source/Core/DolphinQt2/MenuBar.h | 1 + 8 files changed, 569 insertions(+) create mode 100644 Source/Core/DolphinQt2/GCMemcardManager.cpp create mode 100644 Source/Core/DolphinQt2/GCMemcardManager.h 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();