diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a718fbb3..c202f80cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,8 +185,8 @@ set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files if(ENABLE_QT_GUI) - set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp) - set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp) + set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp) + set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) @@ -429,7 +429,7 @@ if(ENABLE_QT_GUI) qt_add_resources(Alber "app_images" PREFIX "/" FILES - docs/img/rsob_icon.png + docs/img/rsob_icon.png docs/img/rstarstruck_icon.png ) else() target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_SDL=1") diff --git a/docs/img/rstarstruck_icon.png b/docs/img/rstarstruck_icon.png new file mode 100644 index 000000000..2b4aab062 Binary files /dev/null and b/docs/img/rstarstruck_icon.png differ diff --git a/include/emulator.hpp b/include/emulator.hpp index df9303730..2da768475 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -52,12 +52,14 @@ class Emulator { // We bind gyro to right click + mouse movement bool holdingRightClick = false; + public: static constexpr u32 width = 400; static constexpr u32 height = 240 * 2; // * 2 because 2 screens ROMType romType = ROMType::None; bool running = false; // Is the emulator running a game? bool programRunning = false; // Is the emulator program itself running? + private: #ifdef PANDA3DS_ENABLE_HTTP_SERVER HttpServer httpServer; friend struct HttpServer; diff --git a/include/panda_qt/about_window.hpp b/include/panda_qt/about_window.hpp new file mode 100644 index 000000000..78812d145 --- /dev/null +++ b/include/panda_qt/about_window.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +class AboutWindow : public QDialog { + Q_OBJECT + + public: + AboutWindow(QWidget* parent = nullptr); +}; \ No newline at end of file diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index b5b93d56a..a50ee9a1e 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -9,9 +9,12 @@ #include #include #include +#include #include "emulator.hpp" +#include "panda_qt/about_window.hpp" #include "panda_qt/screen.hpp" +#include "services/hid.hpp" class MainWindow : public QMainWindow { Q_OBJECT @@ -21,6 +24,27 @@ class MainWindow : public QMainWindow { System = 0, Light = 1, Dark = 2, + GreetingsCat = 3, + }; + + // Types of messages we might send from the GUI thread to the emulator thread + enum class MessageType { + LoadROM, Reset, Pause, Resume, TogglePause, DumpRomFS, PressKey, ReleaseKey + }; + + // Tagged union representing our message queue messages + struct EmulatorMessage { + MessageType type; + + union { + struct { + std::filesystem::path* p; + } path; + + struct { + u32 key; + } key; + }; }; // This would normally be an std::unique_ptr but it's shared between threads so definitely not @@ -28,12 +52,12 @@ class MainWindow : public QMainWindow { std::thread emuThread; std::atomic appRunning = true; // Is the application itself running? - std::mutex messageQueueMutex; // Used for synchronizing messages between the emulator and UI - std::filesystem::path romToLoad = ""; - - bool needToLoadROM = false; + // Used for synchronizing messages between the emulator and UI + std::mutex messageQueueMutex; + std::vector messageQueue; ScreenWidget screen; + AboutWindow* aboutWindow; QComboBox* themeSelect = nullptr; QMenuBar* menuBar = nullptr; @@ -43,6 +67,9 @@ class MainWindow : public QMainWindow { void emuThreadMainLoop(); void selectROM(); void dumpRomFS(); + void showAboutMenu(); + void sendMessage(const EmulatorMessage& message); + void dispatchMessage(const EmulatorMessage& message); // Tracks whether we are using an OpenGL-backed renderer or a Vulkan-backed renderer bool usingGL = false; @@ -51,4 +78,7 @@ class MainWindow : public QMainWindow { public: MainWindow(QApplication* app, QWidget* parent = nullptr); ~MainWindow(); + + void keyPressEvent(QKeyEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; }; \ No newline at end of file diff --git a/src/panda_qt/about_window.cpp b/src/panda_qt/about_window.cpp new file mode 100644 index 000000000..6f799c23c --- /dev/null +++ b/src/panda_qt/about_window.cpp @@ -0,0 +1,62 @@ +#include "panda_qt/about_window.hpp" + +#include +#include +#include +#include + +// Based on https://github.com/dolphin-emu/dolphin/blob/master/Source/Core/DolphinQt/AboutDialog.cpp + +AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) { + resize(200, 200); + + setWindowTitle(tr("About Panda3DS")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + const QString text = + QStringLiteral(R"( +

Panda3DS

+ +

+%ABOUT_PANDA3DS%
+%SUPPORT%
+

+ +

+%AUTHORS% +

+)") + .replace(QStringLiteral("%ABOUT_PANDA3DS%"), tr("Panda3DS is a free and open source Nintendo 3DS emulator, for Windows, MacOS and Linux")) + .replace(QStringLiteral("%SUPPORT%"), tr("Visit panda3ds.com for help with Panda3DS and links to our official support sites.")) + .replace( + QStringLiteral("%AUTHORS%"), tr("Panda3DS is developed by volunteers in their spare time. Below is a list of some of these" + " volunteers who've agreed to be listed here, in no particular order.
If you think you should be " + "listed here too, please inform us

" + "- Peach (wheremyfoodat)
" + "- noumidev
" + "- liuk707
" + "- Wunk
" + "- marysaka
" + "- Sky
" + "- merryhime
" + "- TGP17
") + ); + + QLabel* textLabel = new QLabel(text); + textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + textLabel->setOpenExternalLinks(true); + + QLabel* logo = new QLabel(); + logo->setPixmap(QPixmap(":/docs/img/rstarstruck_icon.png")); + logo->setContentsMargins(30, 0, 30, 0); + + QVBoxLayout* mainLayout = new QVBoxLayout; + QHBoxLayout* hLayout = new QHBoxLayout; + + setLayout(mainLayout); + mainLayout->addLayout(hLayout); + + hLayout->setAlignment(Qt::AlignLeft); + hLayout->addWidget(logo); + hLayout->addWidget(textLabel); +} \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 2c2cc64f9..804dc63aa 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -20,28 +20,41 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto fileMenu = menuBar->addMenu(tr("File")); auto emulationMenu = menuBar->addMenu(tr("Emulation")); auto toolsMenu = menuBar->addMenu(tr("Tools")); - auto helpMenu = menuBar->addMenu(tr("Help")); auto aboutMenu = menuBar->addMenu(tr("About")); // Create and bind actions for them auto pandaAction = fileMenu->addAction(tr("panda...")); connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM); + auto pauseAction = emulationMenu->addAction(tr("Pause")); + auto resumeAction = emulationMenu->addAction(tr("Resume")); + auto resetAction = emulationMenu->addAction(tr("Reset")); + connect(pauseAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Pause}); }); + connect(resumeAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Resume}); }); + connect(resetAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Reset}); }); + auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); + auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); + connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu); + // Set up theme selection setTheme(Theme::Dark); themeSelect = new QComboBox(this); themeSelect->addItem(tr("System")); themeSelect->addItem(tr("Light")); themeSelect->addItem(tr("Dark")); + themeSelect->addItem(tr("Greetings Cat")); themeSelect->setCurrentIndex(static_cast(currentTheme)); themeSelect->setGeometry(40, 40, 100, 50); themeSelect->show(); connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { setTheme(static_cast(index)); }); + // Set up misc objects + aboutWindow = new AboutWindow(nullptr); + emu = new Emulator(); emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight); @@ -73,17 +86,21 @@ void MainWindow::emuThreadMainLoop() { { std::unique_lock lock(messageQueueMutex); - if (needToLoadROM) { - needToLoadROM = false; - - bool success = emu->loadROM(romToLoad); - if (!success) { - printf("Failed to load ROM"); + // Dispatch all messages in the message queue + if (!messageQueue.empty()) { + for (const auto& msg : messageQueue) { + dispatchMessage(msg); } + + messageQueue.clear(); } } emu->runFrame(); + if (emu->romType != ROMType::None) { + emu->getServiceManager().getHID().updateInputs(emu->getTicks()); + } + swapEmuBuffer(); } @@ -102,23 +119,15 @@ void MainWindow::swapEmuBuffer() { } void MainWindow::selectROM() { - // Are we already waiting for a ROM to be loaded? Then complain about it! - { - std::unique_lock lock(messageQueueMutex); - if (needToLoadROM) { - QMessageBox::warning(this, tr("Already loading ROM"), tr("Panda3DS is already busy loading a ROM, please wait")); - return; - } - } - auto path = QFileDialog::getOpenFileName(this, tr("Select 3DS ROM to load"), "", tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)")); if (!path.isEmpty()) { - std::unique_lock lock(messageQueueMutex); + std::filesystem::path* p = new std::filesystem::path(path.toStdU16String()); - romToLoad = path.toStdU16String(); - needToLoadROM = true; + EmulatorMessage message{.type = MessageType::LoadROM}; + message.path.p = p; + sendMessage(message); } } @@ -132,9 +141,16 @@ MainWindow::~MainWindow() { delete emu; delete menuBar; + delete aboutWindow; delete themeSelect; } +// Send a message to the emulator thread. Lock the mutex and just push back to the vector. +void MainWindow::sendMessage(const EmulatorMessage& message) { + std::unique_lock lock(messageQueueMutex); + messageQueue.push_back(message); +} + void MainWindow::setTheme(Theme theme) { currentTheme = theme; @@ -183,6 +199,28 @@ void MainWindow::setTheme(Theme theme) { break; } + case Theme::GreetingsCat: { + QApplication::setStyle(QStyleFactory::create("Fusion")); + + QPalette p; + p.setColor(QPalette::Window, QColor(250, 207, 228)); + p.setColor(QPalette::WindowText, QColor(225, 22, 137)); + p.setColor(QPalette::Base, QColor(250, 207, 228)); + p.setColor(QPalette::AlternateBase, QColor(250, 207, 228)); + p.setColor(QPalette::ToolTipBase, QColor(225, 22, 137)); + p.setColor(QPalette::ToolTipText, QColor(225, 22, 137)); + p.setColor(QPalette::Text, QColor(225, 22, 137)); + p.setColor(QPalette::Button, QColor(250, 207, 228)); + p.setColor(QPalette::ButtonText, QColor(225, 22, 137)); + p.setColor(QPalette::BrightText, Qt::black); + p.setColor(QPalette::Link, QColor(42, 130, 218)); + + p.setColor(QPalette::Highlight, QColor(42, 130, 218)); + p.setColor(QPalette::HighlightedText, Qt::black); + qApp->setPalette(p); + break; + } + case Theme::System: { qApp->setPalette(this->style()->standardPalette()); qApp->setStyle(QStyleFactory::create("WindowsVista")); @@ -225,4 +263,82 @@ void MainWindow::dumpRomFS() { QMessageBox::warning(this, tr("No RomFS found"), tr("No RomFS partition was found in the loaded app")); break; } +} + +void MainWindow::showAboutMenu() { + AboutWindow about(this); + about.exec(); +} + +void MainWindow::dispatchMessage(const EmulatorMessage& message) { + switch (message.type) { + case MessageType::LoadROM: + emu->loadROM(*message.path.p); + // Clean up the allocated path + delete message.path.p; + break; + + case MessageType::Pause: emu->pause(); break; + case MessageType::Resume: emu->resume(); break; + case MessageType::TogglePause: emu->togglePause(); break; + case MessageType::Reset: emu->reset(Emulator::ReloadOption::Reload); break; + case MessageType::PressKey: emu->getServiceManager().getHID().pressKey(message.key.key); break; + case MessageType::ReleaseKey: emu->getServiceManager().getHID().releaseKey(message.key.key); break; + } +} + +void MainWindow::keyPressEvent(QKeyEvent* event) { + auto pressKey = [this](u32 key) { + EmulatorMessage message{.type = MessageType::PressKey}; + message.key.key = key; + + sendMessage(message); + }; + + switch (event->key()) { + case Qt::Key_L: pressKey(HID::Keys::A); break; + case Qt::Key_K: pressKey(HID::Keys::B); break; + case Qt::Key_O: pressKey(HID::Keys::X); break; + case Qt::Key_I: pressKey(HID::Keys::Y); break; + + case Qt::Key_Q: pressKey(HID::Keys::L); break; + case Qt::Key_P: pressKey(HID::Keys::R); break; + + case Qt::Key_Right: pressKey(HID::Keys::Right); break; + case Qt::Key_Left: pressKey(HID::Keys::Left); break; + case Qt::Key_Up: pressKey(HID::Keys::Up); break; + case Qt::Key_Down: pressKey(HID::Keys::Down); break; + + case Qt::Key_Return: pressKey(HID::Keys::Start); break; + case Qt::Key_Backspace: pressKey(HID::Keys::Select); break; + case Qt::Key_F4: sendMessage(EmulatorMessage{.type = MessageType::TogglePause}); break; + case Qt::Key_F5: sendMessage(EmulatorMessage{.type = MessageType::Reset}); break; + } +} + +void MainWindow::keyReleaseEvent(QKeyEvent* event) { + auto releaseKey = [this](u32 key) { + EmulatorMessage message{.type = MessageType::ReleaseKey}; + message.key.key = key; + + sendMessage(message); + }; + + switch (event->key()) { + case Qt::Key_L: releaseKey(HID::Keys::A); break; + case Qt::Key_K: releaseKey(HID::Keys::B); break; + case Qt::Key_O: releaseKey(HID::Keys::X); break; + case Qt::Key_I: releaseKey(HID::Keys::Y); break; + + case Qt::Key_Q: releaseKey(HID::Keys::L); break; + case Qt::Key_P: releaseKey(HID::Keys::R); break; + + case Qt::Key_Right: releaseKey(HID::Keys::Right); break; + case Qt::Key_Left: releaseKey(HID::Keys::Left); break; + case Qt::Key_Up: releaseKey(HID::Keys::Up); break; + case Qt::Key_Down: releaseKey(HID::Keys::Down); break; + + case Qt::Key_Return: releaseKey(HID::Keys::Start); break; + case Qt::Key_Backspace: releaseKey(HID::Keys::Select); break; + } } \ No newline at end of file