diff --git a/include/config.hpp b/include/config.hpp index fbdd8b0c..ba53dcdc 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -108,6 +108,9 @@ struct EmulatorConfig { std::filesystem::path defaultRomPath = ""; std::filesystem::path filePath; + static constexpr size_t maxRecentGames = 8; + std::vector recentlyPlayed; + // Frontend window settings struct WindowSettings { static constexpr int defaultX = 200; @@ -132,6 +135,8 @@ struct EmulatorConfig { void load(); void save(); + void addToRecentGames(const std::filesystem::path& path); + static LanguageCodes languageCodeFromString(std::string inString); static const char* languageCodeToString(LanguageCodes code); }; diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index b259a1bc..c1e9ff10 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -103,6 +103,7 @@ class MainWindow : public QMainWindow { std::vector messageQueue; QMenuBar* menuBar = nullptr; + QMenu* recentsMenu = nullptr; InputMappings keyboardMappings; ScreenWidget* screen; AboutWindow* aboutWindow; @@ -123,6 +124,8 @@ class MainWindow : public QMainWindow { void emuThreadMainLoop(); void selectLuaFile(); void selectROM(); + void loadROMFromPath(const std::filesystem::path& path); + void updateRecentsMenu(); void dumpDspFirmware(); void dumpRomFS(); void showAboutMenu(); diff --git a/src/config.cpp b/src/config.cpp index 4d489c95..f9625f69 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -50,6 +50,23 @@ void EmulatorConfig::load() { circlePadProEnabled = toml::find_or(general, "EnableCirclePadPro", true); fastmemEnabled = toml::find_or(general, "EnableFastmem", enableFastmemDefault); systemLanguage = languageCodeFromString(toml::find_or(general, "SystemLanguage", "en")); + + // Load recent games list + if (general.contains("RecentGames") && general.at("RecentGames").is_array()) { + const auto& recentsArray = general.at("RecentGames").as_array(); + recentlyPlayed.clear(); + + for (const auto& item : recentsArray) { + if (item.is_string()) { + std::filesystem::path gamePath = toml::get(item); + + recentlyPlayed.push_back(gamePath); + if (recentlyPlayed.size() >= maxRecentGames) { + break; + } + } + } + } } } @@ -183,6 +200,12 @@ void EmulatorConfig::save() { data["General"]["EnableCirclePadPro"] = circlePadProEnabled; data["General"]["EnableFastmem"] = fastmemEnabled; + toml::array recentsArray; + for (const auto& gamePath : recentlyPlayed) { + recentsArray.push_back(gamePath.string()); + } + data["General"]["RecentGames"] = recentsArray; + data["Window"]["AppVersionOnWindow"] = windowSettings.showAppVersion; data["Window"]["RememberWindowPosition"] = windowSettings.rememberPosition; data["Window"]["WindowPosX"] = windowSettings.x; @@ -277,4 +300,18 @@ const char* EmulatorConfig::languageCodeToString(LanguageCodes code) { } else { return codes[static_cast(code)]; } -} \ No newline at end of file +} +void EmulatorConfig::addToRecentGames(const std::filesystem::path& path) { + // Remove path if it's already in the list + auto it = std::find(recentlyPlayed.begin(), recentlyPlayed.end(), path); + if (it != recentlyPlayed.end()) { + recentlyPlayed.erase(it); + } + + recentlyPlayed.insert(recentlyPlayed.begin(), path); + + // Limit how many games can be saved + if (recentlyPlayed.size() > maxRecentGames) { + recentlyPlayed.resize(maxRecentGames); + } +} diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index f74f2061..bc751a53 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -53,6 +53,11 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) // Create and bind actions for them auto loadGameAction = fileMenu->addAction(tr("Load game")); + + recentsMenu = fileMenu->addMenu(tr("Recents")); + updateRecentsMenu(); + + fileMenu->addSeparator(); auto loadLuaAction = fileMenu->addAction(tr("Load Lua script")); auto openAppFolderAction = fileMenu->addAction(tr("Open Panda3DS folder")); @@ -140,6 +145,10 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) if (!emu->loadROM(romPath)) { // For some reason just .c_str() doesn't show the proper path Helpers::warn("Failed to load ROM file: %s", romPath.string().c_str()); + } else { + emu->getConfig().addToRecentGames(romPath); + emu->getConfig().save(); + updateRecentsMenu(); } } @@ -240,11 +249,48 @@ void MainWindow::selectROM() { ); if (!path.isEmpty()) { - std::filesystem::path* p = new std::filesystem::path(path.toStdU16String()); + loadROMFromPath(std::filesystem::path(path.toStdU16String())); + } +} - EmulatorMessage message{.type = MessageType::LoadROM}; - message.path.p = p; - sendMessage(message); +void MainWindow::loadROMFromPath(const std::filesystem::path& path) { + std::filesystem::path* p = new std::filesystem::path(path); + + EmulatorMessage message{.type = MessageType::LoadROM}; + message.path.p = p; + sendMessage(message); + + emu->getConfig().addToRecentGames(path); + emu->getConfig().save(); + updateRecentsMenu(); +} + +void MainWindow::updateRecentsMenu() { + recentsMenu->clear(); + const auto& recentGames = emu->getConfig().recentlyPlayed; + + if (recentGames.empty()) { + // Add a disabled "No recent games" item + QAction* noRecentsAction = recentsMenu->addAction(tr("No recent games")); + noRecentsAction->setEnabled(false); + } else { + for (const auto& gamePath : recentGames) { + QString displayName = QString::fromStdU16String(gamePath.filename().u16string()); + QAction* action = recentsMenu->addAction(displayName); + + // Store the full path in the action's data, set tooltip to show full path + action->setData(QString::fromStdU16String(gamePath.u16string())); + action->setToolTip(QString::fromStdU16String(gamePath.u16string())); + connect(action, &QAction::triggered, this, [this, gamePath]() { loadROMFromPath(gamePath); }); + } + + recentsMenu->addSeparator(); + QAction* clearAction = recentsMenu->addAction(tr("Clear recent games")); + connect(clearAction, &QAction::triggered, this, [this]() { + emu->getConfig().recentlyPlayed.clear(); + emu->getConfig().save(); + updateRecentsMenu(); + }); } }