[WIP] Qt: Add config window controls (#655)

* Qt: Add config window controls

* Fix Windows build

* Fix audio slider

* Qt configs: Make thread-safe, properly update audio enable & renderdoc settings

* Qt configs: Add `connectCheckbox` function

* Qt configs: Add `connectCheckbox` function

* Rename spuLayout

* Add Discord RPC reloading

* Allow configuring the app icon

* Qt: Serialize icon & theme, properly set them

* Add rnap and rcow icons

* Qt: Fix forceShadergen config

---------

Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com>
This commit is contained in:
Jonian Guveli 2024-12-01 23:06:47 +02:00 committed by GitHub
parent c2b479889c
commit 156328fbfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 509 additions and 25 deletions

View file

@ -130,6 +130,16 @@ void EmulatorConfig::load() {
sdWriteProtected = toml::find_or<toml::boolean>(sd, "WriteProtectVirtualSD", false);
}
}
if (data.contains("UI")) {
auto uiResult = toml::expect<toml::value>(data.at("UI"));
if (uiResult.is_ok()) {
auto ui = uiResult.unwrap();
frontendSettings.theme = FrontendSettings::themeFromString(toml::find_or<std::string>(ui, "Theme", "dark"));
frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or<std::string>(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();

View file

@ -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
}

64
src/frontend_settings.cpp Normal file
View file

@ -0,0 +1,64 @@
#include "frontend_settings.hpp"
#include <algorithm>
#include <cctype>
#include <unordered_map>
// 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<std::string, Theme> 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<std::string, WindowIcon> 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";
}
}

View file

@ -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<int>(currentTheme));
themeSelect->setCurrentIndex(static_cast<int>(config.frontendSettings.theme));
connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) {
config.frontendSettings.theme = static_cast<Theme>(index);
setTheme(static_cast<Theme>(index));
themeSelect->setGeometry(40, 40, 100, 50);
themeSelect->show();
connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { setTheme(static_cast<Theme>(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<int>(config.frontendSettings.icon));
connect(iconSelect, &QComboBox::currentIndexChanged, this, [&](int index) {
config.frontendSettings.icon = static_cast<WindowIcon>(index);
setIcon(static_cast<WindowIcon>(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<int>(config.rendererType));
connect(rendererType, &QComboBox::currentIndexChanged, this, [&](int index) {
config.rendererType = static_cast<RendererType>(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);
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<int>(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<int>(config.dspType));
connect(dspType, &QComboBox::currentIndexChanged, this, [&](int index) {
config.dspType = static_cast<Audio::DSPCore::Type>(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<float>(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<int>(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;
}

View file

@ -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;
}
}

View file

@ -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