diff --git a/CMakeLists.txt b/CMakeLists.txt index d3ca834fd..33faba5f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -280,6 +280,7 @@ set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp src/miniaudio.cpp src/renderdoc.cpp + src/frontend_settings.cpp ) set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp) set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp @@ -361,7 +362,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/PICA/pica_vert_config.hpp include/sdl_sensors.hpp include/PICA/draw_acceleration.hpp include/renderdoc.hpp include/align.hpp include/audio/aac_decoder.hpp include/PICA/pica_simd.hpp include/services/fonts.hpp include/audio/audio_interpolation.hpp include/audio/hle_mixer.hpp include/audio/dsp_simd.hpp - include/services/dsp_firmware_db.hpp + include/services/dsp_firmware_db.hpp include/frontend_settings.hpp ) cmrc_add_resource_library( @@ -695,6 +696,9 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) PREFIX "/" FILES docs/img/rsob_icon.png docs/img/rstarstruck_icon.png docs/img/rpog_icon.png docs/img/rsyn_icon.png + docs/img/settings_icon.png docs/img/display_icon.png docs/img/speaker_icon.png + docs/img/sparkling_icon.png docs/img/battery_icon.png docs/img/sdcard_icon.png + docs/img/rnap_icon.png docs/img/rcow_icon.png ) else() set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp) diff --git a/docs/img/battery_icon.png b/docs/img/battery_icon.png new file mode 100644 index 000000000..5768a9280 Binary files /dev/null and b/docs/img/battery_icon.png differ diff --git a/docs/img/display_icon.png b/docs/img/display_icon.png new file mode 100644 index 000000000..cf6a68beb Binary files /dev/null and b/docs/img/display_icon.png differ diff --git a/docs/img/rcow_icon.png b/docs/img/rcow_icon.png new file mode 100644 index 000000000..5facb301d Binary files /dev/null and b/docs/img/rcow_icon.png differ diff --git a/docs/img/rnap_icon.png b/docs/img/rnap_icon.png new file mode 100644 index 000000000..7967102b2 Binary files /dev/null and b/docs/img/rnap_icon.png differ diff --git a/docs/img/sdcard_icon.png b/docs/img/sdcard_icon.png new file mode 100644 index 000000000..07ed3fceb Binary files /dev/null and b/docs/img/sdcard_icon.png differ diff --git a/docs/img/settings_icon.png b/docs/img/settings_icon.png new file mode 100644 index 000000000..bf21c417c Binary files /dev/null and b/docs/img/settings_icon.png differ diff --git a/docs/img/sparkling_icon.png b/docs/img/sparkling_icon.png new file mode 100644 index 000000000..4a46d8d8d Binary files /dev/null and b/docs/img/sparkling_icon.png differ diff --git a/docs/img/speaker_icon.png b/docs/img/speaker_icon.png new file mode 100644 index 000000000..06adcfb32 Binary files /dev/null and b/docs/img/speaker_icon.png differ diff --git a/include/config.hpp b/include/config.hpp index 5a7c7fff9..7361a40d1 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -3,6 +3,7 @@ #include "audio/dsp_core.hpp" #include "renderer.hpp" +#include "frontend_settings.hpp" struct AudioDeviceConfig { float volumeRaw = 1.0f; @@ -86,8 +87,9 @@ struct EmulatorConfig { WindowSettings windowSettings; AudioDeviceConfig audioDeviceConfig; + FrontendSettings frontendSettings; EmulatorConfig(const std::filesystem::path& path); void load(); void save(); -}; +}; \ No newline at end of file diff --git a/include/discord_rpc.hpp b/include/discord_rpc.hpp index 9b244fafc..62bd0c6b9 100644 --- a/include/discord_rpc.hpp +++ b/include/discord_rpc.hpp @@ -17,6 +17,8 @@ namespace Discord { void init(); void update(RPCStatus status, const std::string& title); void stop(); + + bool running() const { return enabled; } }; } // namespace Discord diff --git a/include/emulator.hpp b/include/emulator.hpp index abb74089b..cf231328f 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -118,6 +118,9 @@ class Emulator { void setOutputSize(u32 width, u32 height) { gpu.setOutputSize(width, height); } void deinitGraphicsContext() { gpu.deinitGraphicsContext(); } + // Reloads some settings that require special handling, such as audio enable + void reloadSettings(); + EmulatorConfig& getConfig() { return config; } Cheats& getCheats() { return cheats; } ServiceManager& getServiceManager() { return kernel.getServiceManager(); } diff --git a/include/frontend_settings.hpp b/include/frontend_settings.hpp new file mode 100644 index 000000000..aaf9eaf0e --- /dev/null +++ b/include/frontend_settings.hpp @@ -0,0 +1,32 @@ +#pragma once +#include + +// Some UI settings that aren't fully frontend-dependent. Note: Not all frontends will support the same settings. +// Note: Any enums should ideally be ordered in the same order we want to show them in UI dropdown menus, so that we can cast indices to enums +// directly. +struct FrontendSettings { + enum class Theme : int { + System = 0, + Light = 1, + Dark = 2, + GreetingsCat = 3, + Cream = 4, + }; + + // Different panda-themed window icons + enum class WindowIcon : int { + Rpog = 0, + Rsyn = 1, + Rnap = 2, + Rcow = 3, + }; + + Theme theme = Theme::Dark; + WindowIcon icon = WindowIcon::Rpog; + + static Theme themeFromString(std::string inString); + static const char* themeToString(Theme theme); + + static WindowIcon iconFromString(std::string inString); + static const char* iconToString(WindowIcon icon); +}; diff --git a/include/panda_qt/config_window.hpp b/include/panda_qt/config_window.hpp index 4a5238794..3cf4a1c82 100644 --- a/include/panda_qt/config_window.hpp +++ b/include/panda_qt/config_window.hpp @@ -1,30 +1,56 @@ #pragma once #include +#include #include #include +#include #include +#include +#include #include #include +#include +#include +#include + +#include "emulator.hpp" +#include "frontend_settings.hpp" class ConfigWindow : public QDialog { Q_OBJECT private: - enum class Theme : int { - System = 0, - Light = 1, - Dark = 2, - GreetingsCat = 3, - Cream = 4, - }; + using ConfigCallback = std::function; + using IconCallback = std::function; + + using Theme = FrontendSettings::Theme; + using WindowIcon = FrontendSettings::WindowIcon; + + QTextEdit* helpText = nullptr; + QListWidget* widgetList = nullptr; + QStackedWidget* widgetContainer = nullptr; + + static constexpr size_t settingWidgetCount = 6; + std::array helpTexts; - Theme currentTheme; - QComboBox* themeSelect = nullptr; + // The config class holds a copy of the emulator config which it edits and sends + // over to the emulator in a thread-safe manner + EmulatorConfig config; - void setTheme(Theme theme); + ConfigCallback updateConfig; + IconCallback updateIcon; + + void addWidget(QWidget* widget, QString title, QString icon, QString helpText); + void setTheme(FrontendSettings::Theme theme); + void setIcon(FrontendSettings::WindowIcon icon); public: - ConfigWindow(QWidget* parent = nullptr); + ConfigWindow(ConfigCallback configCallback, IconCallback iconCallback, const EmulatorConfig& config, QWidget* parent = nullptr); ~ConfigWindow(); + + EmulatorConfig& getConfig() { return config; } + + private: + Emulator* emu; }; diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index eb6b30e03..eb1cfb16f 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -51,6 +51,7 @@ class MainWindow : public QMainWindow { ReleaseTouchscreen, ReloadUbershader, SetScreenSize, + UpdateConfig, }; // Tagged union representing our message queue messages diff --git a/include/renderdoc.hpp b/include/renderdoc.hpp index 02f2ade43..9c7de1e35 100644 --- a/include/renderdoc.hpp +++ b/include/renderdoc.hpp @@ -23,6 +23,9 @@ namespace Renderdoc { // Sets output directory for captures void setOutputDir(const std::string& path, const std::string& prefix); + // Returns whether Renderdoc has been loaded + bool isLoaded(); + // Returns whether we've compiled with Renderdoc support static constexpr bool isSupported() { return true; } } // namespace Renderdoc @@ -34,6 +37,7 @@ namespace Renderdoc { static void triggerCapture() { Helpers::panic("Tried to trigger a Renderdoc capture while support for renderdoc is disabled"); } static void setOutputDir(const std::string& path, const std::string& prefix) {} static constexpr bool isSupported() { return false; } + static constexpr bool isLoaded() { return false; } } // namespace Renderdoc #endif diff --git a/src/config.cpp b/src/config.cpp index 93aed1060..a8c88a68f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -130,6 +130,16 @@ void EmulatorConfig::load() { sdWriteProtected = toml::find_or(sd, "WriteProtectVirtualSD", false); } } + + if (data.contains("UI")) { + auto uiResult = toml::expect(data.at("UI")); + if (uiResult.is_ok()) { + auto ui = uiResult.unwrap(); + + frontendSettings.theme = FrontendSettings::themeFromString(toml::find_or(ui, "Theme", "dark")); + frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or(ui, "WindowIcon", "rpog")); + } + } } void EmulatorConfig::save() { @@ -186,6 +196,9 @@ void EmulatorConfig::save() { data["SD"]["UseVirtualSD"] = sdCardInserted; data["SD"]["WriteProtectVirtualSD"] = sdWriteProtected; + data["UI"]["Theme"] = std::string(FrontendSettings::themeToString(frontendSettings.theme)); + data["UI"]["WindowIcon"] = std::string(FrontendSettings::iconToString(frontendSettings.icon)); + std::ofstream file(path, std::ios::out); file << data; file.close(); diff --git a/src/emulator.cpp b/src/emulator.cpp index fc25eacb0..1bb117b5d 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -444,3 +444,24 @@ void Emulator::loadRenderdoc() { Renderdoc::loadRenderdoc(); Renderdoc::setOutputDir(capturePath, ""); } + +void Emulator::reloadSettings() { + setAudioEnabled(config.audioEnabled); + + if (Renderdoc::isSupported() && config.enableRenderdoc && !Renderdoc::isLoaded()) { + loadRenderdoc(); + } + +#ifdef PANDA3DS_ENABLE_DISCORD_RPC + // Reload RPC setting if we're compiling with RPC support + + if (discordRpc.running() != config.discordRpcEnabled) { + if (config.discordRpcEnabled) { + discordRpc.init(); + updateDiscord(); + } else { + discordRpc.stop(); + } + } +#endif +} \ No newline at end of file diff --git a/src/frontend_settings.cpp b/src/frontend_settings.cpp new file mode 100644 index 000000000..16bae3615 --- /dev/null +++ b/src/frontend_settings.cpp @@ -0,0 +1,64 @@ +#include "frontend_settings.hpp" + +#include +#include +#include + +// Frontend setting serialization/deserialization functions + +FrontendSettings::Theme FrontendSettings::themeFromString(std::string inString) { + // Transform to lower-case to make the setting case-insensitive + std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); }); + + static const std::unordered_map map = { + {"system", Theme::System}, {"light", Theme::Light}, {"dark", Theme::Dark}, {"greetingscat", Theme::GreetingsCat}, {"cream", Theme::Cream}, + }; + + if (auto search = map.find(inString); search != map.end()) { + return search->second; + } + + // Default to dark theme + return Theme::Dark; +} + +const char* FrontendSettings::themeToString(Theme theme) { + switch (theme) { + case Theme::System: return "system"; + case Theme::Light: return "light"; + case Theme::GreetingsCat: return "greetingscat"; + case Theme::Cream: return "cream"; + + case Theme::Dark: + default: return "dark"; + } +} + +FrontendSettings::WindowIcon FrontendSettings::iconFromString(std::string inString) { // Transform to lower-case to make the setting case-insensitive + std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); }); + + static const std::unordered_map map = { + {"rpog", WindowIcon::Rpog}, + {"rsyn", WindowIcon::Rsyn}, + {"rcow", WindowIcon::Rcow}, + {"rnap", WindowIcon::Rnap}, + }; + + if (auto search = map.find(inString); search != map.end()) { + return search->second; + } + + // Default to the icon rpog icon + return WindowIcon::Rpog; +} + +const char* FrontendSettings::iconToString(WindowIcon icon) { + switch (icon) { + case WindowIcon::Rsyn: return "rsyn"; + case WindowIcon::Rcow: return "rcow"; + case WindowIcon::Rnap: return "rnap"; + + case WindowIcon::Rpog: + default: return "rpog"; + } +} \ No newline at end of file diff --git a/src/panda_qt/config_window.cpp b/src/panda_qt/config_window.cpp index 75293742e..64cade49b 100644 --- a/src/panda_qt/config_window.cpp +++ b/src/panda_qt/config_window.cpp @@ -1,26 +1,286 @@ #include "panda_qt/config_window.hpp" -ConfigWindow::ConfigWindow(QWidget* parent) : QDialog(parent) { +ConfigWindow::ConfigWindow(ConfigCallback configCallback, IconCallback iconCallback, const EmulatorConfig& emuConfig, QWidget* parent) + : QDialog(parent), config(emuConfig) { setWindowTitle(tr("Configuration")); + updateConfig = std::move(configCallback); + updateIcon = std::move(iconCallback); + // Set up theme selection - setTheme(Theme::Dark); - themeSelect = new QComboBox(this); + setTheme(config.frontendSettings.theme); + setIcon(config.frontendSettings.icon); + + // Initialize the widget list and the widget container widgets + widgetList = new QListWidget(this); + widgetContainer = new QStackedWidget(this); + + helpText = new QTextEdit(this); + helpText->setReadOnly(true); + + helpText->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + helpText->setFixedHeight(50); + + widgetList->setMinimumWidth(100); + widgetList->setMaximumWidth(100); + widgetList->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + widgetList->setPalette(QPalette(QColor(25, 25, 25))); + + widgetList->setCurrentRow(0); + widgetContainer->setCurrentIndex(0); + + connect(widgetList, &QListWidget::currentRowChanged, this, [&](int row) { + widgetContainer->setCurrentIndex(row); + helpText->setText(helpTexts[row]); + }); + + auto connectCheckbox = [&](QCheckBox* checkbox, bool& setting) { + checkbox->setChecked(setting); + + connect(checkbox, &QCheckBox::toggled, this, [&](bool checked) { + setting = checked; + updateConfig(); + }); + }; + + QVBoxLayout* mainLayout = new QVBoxLayout(); + QHBoxLayout* hLayout = new QHBoxLayout(); + + // Set up widget layouts + setLayout(mainLayout); + mainLayout->addLayout(hLayout); + mainLayout->addWidget(helpText); + + hLayout->setAlignment(Qt::AlignLeft); + hLayout->addWidget(widgetList); + hLayout->addWidget(widgetContainer); + + // Interface settings + QGroupBox* guiGroupBox = new QGroupBox(tr("Interface Settings"), this); + QFormLayout* guiLayout = new QFormLayout(guiGroupBox); + guiLayout->setHorizontalSpacing(20); + guiLayout->setVerticalSpacing(10); + + QComboBox* themeSelect = new QComboBox(); themeSelect->addItem(tr("System")); themeSelect->addItem(tr("Light")); themeSelect->addItem(tr("Dark")); themeSelect->addItem(tr("Greetings Cat")); themeSelect->addItem(tr("Cream")); - themeSelect->setCurrentIndex(static_cast(currentTheme)); + themeSelect->setCurrentIndex(static_cast(config.frontendSettings.theme)); + connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { + config.frontendSettings.theme = static_cast(index); + setTheme(static_cast(index)); + + updateConfig(); + }); + guiLayout->addRow(tr("Color theme"), themeSelect); + + QComboBox* iconSelect = new QComboBox(); + iconSelect->addItem(tr("Happy panda")); + iconSelect->addItem(tr("Happy panda (colourful)")); + iconSelect->addItem(tr("Sleepy panda")); + iconSelect->addItem(tr("Cow panda")); + iconSelect->setCurrentIndex(static_cast(config.frontendSettings.icon)); + + connect(iconSelect, &QComboBox::currentIndexChanged, this, [&](int index) { + config.frontendSettings.icon = static_cast(index); + setIcon(static_cast(index)); + + updateConfig(); + }); + guiLayout->addRow(tr("Window icon"), iconSelect); + + QCheckBox* showAppVersion = new QCheckBox(tr("Show version on window title")); + connectCheckbox(showAppVersion, config.windowSettings.showAppVersion); + guiLayout->addRow(showAppVersion); + + QCheckBox* rememberPosition = new QCheckBox(tr("Remember window position")); + connectCheckbox(rememberPosition, config.windowSettings.rememberPosition); + guiLayout->addRow(rememberPosition); + + // General settings + QGroupBox* genGroupBox = new QGroupBox(tr("General Settings"), this); + QFormLayout* genLayout = new QFormLayout(genGroupBox); + genLayout->setHorizontalSpacing(20); + genLayout->setVerticalSpacing(10); + + QLineEdit* defaultRomPath = new QLineEdit; + defaultRomPath->setText(QString::fromStdU16String(config.defaultRomPath.u16string())); + connect(defaultRomPath, &QLineEdit::textChanged, this, [&](const QString& text) { + config.defaultRomPath = text.toStdString(); + updateConfig(); + }); + QPushButton* browseRomPath = new QPushButton(tr("Browse...")); + browseRomPath->setAutoDefault(false); + connect(browseRomPath, &QPushButton::pressed, this, [&, defaultRomPath]() { + QString newPath = QFileDialog::getExistingDirectory( + this, tr("Select Directory"), QString::fromStdU16String(config.defaultRomPath.u16string()), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks + ); + if (!newPath.isEmpty()) { + defaultRomPath->setText(newPath); + } + }); + QHBoxLayout* romLayout = new QHBoxLayout; + romLayout->setSpacing(4); + romLayout->addWidget(defaultRomPath); + romLayout->addWidget(browseRomPath); + genLayout->addRow(tr("Default ROMs path"), romLayout); + + QCheckBox* discordRpcEnabled = new QCheckBox(tr("Enable Discord RPC")); + connectCheckbox(discordRpcEnabled, config.discordRpcEnabled); + genLayout->addRow(discordRpcEnabled); + + QCheckBox* usePortableBuild = new QCheckBox(tr("Use portable build")); + connectCheckbox(usePortableBuild, config.usePortableBuild); + genLayout->addRow(usePortableBuild); + + QCheckBox* printAppVersion = new QCheckBox(tr("Print version in console output")); + connectCheckbox(printAppVersion, config.printAppVersion); + genLayout->addRow(printAppVersion); + + // Graphics settings + QGroupBox* gpuGroupBox = new QGroupBox(tr("Graphics Settings"), this); + QFormLayout* gpuLayout = new QFormLayout(gpuGroupBox); + gpuLayout->setHorizontalSpacing(20); + gpuLayout->setVerticalSpacing(10); + + QComboBox* rendererType = new QComboBox; + rendererType->addItem(tr("Null")); + rendererType->addItem(tr("OpenGL")); + rendererType->addItem(tr("Vulkan")); + rendererType->setCurrentIndex(static_cast(config.rendererType)); + connect(rendererType, &QComboBox::currentIndexChanged, this, [&](int index) { + config.rendererType = static_cast(index); + updateConfig(); + }); + gpuLayout->addRow(tr("GPU renderer"), rendererType); + + QCheckBox* enableRenderdoc = new QCheckBox(tr("Enable Renderdoc")); + connectCheckbox(enableRenderdoc, config.enableRenderdoc); + gpuLayout->addRow(enableRenderdoc); + + QCheckBox* shaderJitEnabled = new QCheckBox(tr("Enable shader JIT")); + connectCheckbox(shaderJitEnabled, config.shaderJitEnabled); + gpuLayout->addRow(shaderJitEnabled); + + QCheckBox* vsyncEnabled = new QCheckBox(tr("Enable VSync")); + connectCheckbox(vsyncEnabled, config.vsyncEnabled); + gpuLayout->addRow(vsyncEnabled); + + QCheckBox* useUbershaders = new QCheckBox(tr("Use ubershaders (No stutter, maybe slower)")); + connectCheckbox(useUbershaders, config.useUbershaders); + gpuLayout->addRow(useUbershaders); - themeSelect->setGeometry(40, 40, 100, 50); - themeSelect->show(); - connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { setTheme(static_cast(index)); }); + QCheckBox* accurateShaderMul = new QCheckBox(tr("Accurate shader multiplication")); + connectCheckbox(accurateShaderMul, config.accurateShaderMul); + gpuLayout->addRow(accurateShaderMul); + + QCheckBox* accelerateShaders = new QCheckBox(tr("Accelerate shaders")); + connectCheckbox(accelerateShaders, config.accelerateShaders); + gpuLayout->addRow(accelerateShaders); + + QCheckBox* forceShadergenForLights = new QCheckBox(tr("Force shadergen when rendering lights")); + connectCheckbox(forceShadergenForLights, config.forceShadergenForLights); + gpuLayout->addRow(forceShadergenForLights); + + QSpinBox* lightShadergenThreshold = new QSpinBox; + lightShadergenThreshold->setRange(1, 8); + lightShadergenThreshold->setValue(config.lightShadergenThreshold); + connect(lightShadergenThreshold, &QSpinBox::valueChanged, this, [&](int value) { + config.lightShadergenThreshold = static_cast(value); + updateConfig(); + }); + gpuLayout->addRow(tr("Light threshold for forcing shadergen"), lightShadergenThreshold); + + // Audio settings + QGroupBox* spuGroupBox = new QGroupBox(tr("Audio Settings"), this); + QFormLayout* audioLayout = new QFormLayout(spuGroupBox); + audioLayout->setHorizontalSpacing(20); + audioLayout->setVerticalSpacing(10); + + QComboBox* dspType = new QComboBox; + dspType->addItem(tr("Null")); + dspType->addItem(tr("LLE")); + dspType->addItem(tr("HLE")); + dspType->setCurrentIndex(static_cast(config.dspType)); + connect(dspType, &QComboBox::currentIndexChanged, this, [&](int index) { + config.dspType = static_cast(index); + updateConfig(); + }); + audioLayout->addRow(tr("DSP emulation"), dspType); + + QCheckBox* audioEnabled = new QCheckBox(tr("Enable audio")); + connectCheckbox(audioEnabled, config.audioEnabled); + audioLayout->addRow(audioEnabled); + + QCheckBox* aacEnabled = new QCheckBox(tr("Enable AAC audio")); + connectCheckbox(aacEnabled, config.aacEnabled); + audioLayout->addRow(aacEnabled); + + QCheckBox* printDSPFirmware = new QCheckBox(tr("Print DSP firmware")); + connectCheckbox(printDSPFirmware, config.printDSPFirmware); + audioLayout->addRow(printDSPFirmware); + + QCheckBox* muteAudio = new QCheckBox(tr("Mute audio device")); + connectCheckbox(muteAudio, config.audioDeviceConfig.muteAudio); + audioLayout->addRow(muteAudio); + + QSpinBox* volumeRaw = new QSpinBox(); + volumeRaw->setRange(0, 200); + volumeRaw->setValue(config.audioDeviceConfig.volumeRaw* 100); + connect(volumeRaw, &QSpinBox::valueChanged, this, [&](int value) { + config.audioDeviceConfig.volumeRaw = static_cast(value) / 100.0f; + updateConfig(); + }); + audioLayout->addRow(tr("Audio device volume"), volumeRaw); + + // Battery settings + QGroupBox* batGroupBox = new QGroupBox(tr("Battery Settings"), this); + QFormLayout* batLayout = new QFormLayout(batGroupBox); + batLayout->setHorizontalSpacing(20); + batLayout->setVerticalSpacing(10); + + QSpinBox* batteryPercentage = new QSpinBox; + batteryPercentage->setRange(1, 100); + batteryPercentage->setValue(config.batteryPercentage); + connect(batteryPercentage, &QSpinBox::valueChanged, this, [&](int value) { + config.batteryPercentage = static_cast(value); + updateConfig(); + }); + batLayout->addRow(tr("Battery percentage"), batteryPercentage); + + QCheckBox* chargerPlugged = new QCheckBox(tr("Charger plugged")); + connectCheckbox(chargerPlugged, config.chargerPlugged); + batLayout->addRow(chargerPlugged); + + // SD Card settings + QGroupBox* sdcGroupBox = new QGroupBox(tr("SD Card Settings"), this); + QFormLayout* sdcLayout = new QFormLayout(sdcGroupBox); + sdcLayout->setHorizontalSpacing(20); + sdcLayout->setVerticalSpacing(10); + + QCheckBox* sdCardInserted = new QCheckBox(tr("Enable virtual SD card")); + connectCheckbox(sdCardInserted, config.sdCardInserted); + sdcLayout->addRow(sdCardInserted); + + QCheckBox* sdWriteProtected = new QCheckBox(tr("Write protect virtual SD card")); + connectCheckbox(sdWriteProtected, config.sdWriteProtected); + sdcLayout->addRow(sdWriteProtected); + + // Add all our settings widgets to our widget list + addWidget(guiGroupBox, tr("Interface"), ":/docs/img/sparkling_icon.png", tr("User Interface settings")); + addWidget(genGroupBox, tr("General"), ":/docs/img/settings_icon.png", tr("General emulator settings")); + addWidget(gpuGroupBox, tr("Graphics"), ":/docs/img/display_icon.png", tr("Graphics emulation and output settings")); + addWidget(spuGroupBox, tr("Audio"), ":/docs/img/speaker_icon.png", tr("Audio emulation and output settings")); + addWidget(batGroupBox, tr("Battery"), ":/docs/img/battery_icon.png", tr("Battery emulation settings")); + addWidget(sdcGroupBox, tr("SD Card"), ":/docs/img/sdcard_icon.png", tr("SD Card emulation settings")); + + widgetList->setCurrentRow(0); } void ConfigWindow::setTheme(Theme theme) { - currentTheme = theme; - switch (theme) { case Theme::Dark: { QApplication::setStyle(QStyleFactory::create("Fusion")); @@ -119,4 +379,36 @@ void ConfigWindow::setTheme(Theme theme) { } } -ConfigWindow::~ConfigWindow() { delete themeSelect; } +void ConfigWindow::setIcon(WindowIcon icon) { + switch (icon) { + case WindowIcon::Rsyn: updateIcon(":/docs/img/rsyn_icon.png"); break; + case WindowIcon::Rnap: updateIcon(":/docs/img/rnap_icon.png"); break; + case WindowIcon::Rcow: updateIcon(":/docs/img/rcow_icon.png"); break; + + case WindowIcon::Rpog: + default: updateIcon(":/docs/img/rpog_icon.png"); break; + } +} + +void ConfigWindow::addWidget(QWidget* widget, QString title, QString icon, QString helpText) { + const int index = widgetList->count(); + + QListWidgetItem* item = new QListWidgetItem(widgetList); + item->setText(title); + if (!icon.isEmpty()) { + item->setIcon(QIcon::fromTheme(icon)); + } + + widgetContainer->addWidget(widget); + + if (index >= settingWidgetCount) { + Helpers::panic("Qt: ConfigWindow::settingWidgetCount has not been updated correctly!"); + } + helpTexts[index] = std::move(helpText); +} + +ConfigWindow::~ConfigWindow() { + delete helpText; + delete widgetList; + delete widgetContainer; +} diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 93ce26133..fa3efae7f 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -15,7 +15,6 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) { setWindowTitle("Alber"); - setWindowIcon(QIcon(":/docs/img/rpog_icon.png")); // Enable drop events for loading ROMs setAcceptDrops(true); @@ -81,7 +80,6 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) // Set up misc objects aboutWindow = new AboutWindow(nullptr); - configWindow = new ConfigWindow(this); cheatsEditor = new CheatsWindow(emu, {}, this); patchWindow = new PatchWindow(this); luaEditor = new TextEditorWindow(this, "script.lua", ""); @@ -92,6 +90,14 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) shaderEditor->setText(emu->getRenderer()->getUbershader()); } + configWindow = new ConfigWindow( + [&]() { + EmulatorMessage message{.type = MessageType::UpdateConfig}; + sendMessage(message); + }, + [&](const QString& icon) { setWindowIcon(QIcon(icon)); }, emu->getConfig(), this + ); + auto args = QCoreApplication::arguments(); if (args.size() > 1) { auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String(); @@ -410,6 +416,14 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { screen->resizeSurface(width, height); break; } + + case MessageType::UpdateConfig: + emu->getConfig() = configWindow->getConfig(); + emu->reloadSettings(); + + // Save new settings to disk + emu->getConfig().save(); + break; } } diff --git a/src/renderdoc.cpp b/src/renderdoc.cpp index 1de9c451c..43627b666 100644 --- a/src/renderdoc.cpp +++ b/src/renderdoc.cpp @@ -23,6 +23,8 @@ namespace Renderdoc { }; static CaptureState captureState{CaptureState::Idle}; + static bool renderdocLoaded{false}; + RENDERDOC_API_1_6_0* rdocAPI{}; void loadRenderdoc() { @@ -73,6 +75,8 @@ namespace Renderdoc { } #endif if (rdocAPI) { + renderdocLoaded = true; + // Disable default capture keys as they suppose to trigger present-to-present capturing // and it is not what we want rdocAPI->SetCaptureKeys(nullptr, 0); @@ -115,5 +119,7 @@ namespace Renderdoc { rdocAPI->SetCaptureFilePathTemplate((path + '\\' + prefix).c_str()); } } + + bool isLoaded() { return renderdocLoaded; } } // namespace Renderdoc #endif \ No newline at end of file