From dc2985f81970e126901eb817d331894c3130bb02 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:15:12 +0900 Subject: [PATCH] [Config/UI] Add simple emulator settings dialog Also rework a bunch of cvar handling --- src/xenia/app/emulator_window.cc | 13 +- src/xenia/app/emulator_window.h | 1 + src/xenia/app/xenia_main.cc | 55 +- src/xenia/ui/config_dialog_qt.cc | 4 +- src/xenia/ui/config_helpers.h | 31 +- src/xenia/ui/moc_simple_config_dialog_qt.cc | 124 +++++ src/xenia/ui/simple_config_dialog_qt.cc | 589 ++++++++++++++++++++ src/xenia/ui/simple_config_dialog_qt.h | 70 +++ 8 files changed, 836 insertions(+), 51 deletions(-) create mode 100644 src/xenia/ui/moc_simple_config_dialog_qt.cc create mode 100644 src/xenia/ui/simple_config_dialog_qt.cc create mode 100644 src/xenia/ui/simple_config_dialog_qt.h diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 04d87b798..8de81a5c4 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -40,6 +40,7 @@ #include "xenia/ui/postprocessing_dialog_qt.h" #include "xenia/ui/profile_dialog_qt.h" #include "xenia/ui/qt_util.h" +#include "xenia/ui/simple_config_dialog_qt.h" #include "xenia/ui/xmp_dialog_qt.h" #if XE_PLATFORM_WIN32 @@ -1526,13 +1527,15 @@ void EmulatorWindow::ToggleProfilesConfigDialog() { } void EmulatorWindow::ToggleConfigDialog() { - if (!config_dialog_qt_) { - config_dialog_qt_ = new ConfigDialogQt(nullptr, this); + if (!simple_config_dialog_qt_) { + simple_config_dialog_qt_ = new SimpleConfigDialogQt(nullptr, this); + } else { + simple_config_dialog_qt_->ReloadConfigValues(); } - config_dialog_qt_->show(); - config_dialog_qt_->raise(); - config_dialog_qt_->activateWindow(); + simple_config_dialog_qt_->show(); + simple_config_dialog_qt_->raise(); + simple_config_dialog_qt_->activateWindow(); } void EmulatorWindow::OpenConfigDialog(const std::string& category) { diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index 30d5ecc6a..52dc1ac40 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -277,6 +277,7 @@ class EmulatorWindow { QPointer postprocessing_dialog_qt_; QPointer game_list_dialog_qt_; QPointer profile_dialog_qt_; + QPointer simple_config_dialog_qt_; QPointer config_dialog_qt_; QPointer context_menu_widget_qt_; QPointer xmp_dialog_qt_; diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index c1d65d16e..dd0c950c2 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -64,23 +64,28 @@ #endif // XE_PLATFORM_WIN32 #if XE_PLATFORM_WIN32 -#define APU_OPTIONS "[any, nop, sdl, xaudio2]" -#define GPU_OPTIONS "[any, d3d12, vulkan, null]" -#define HID_OPTIONS "[any, nop, sdl, winkey, xinput]" +#define APU_OPTIONS "[xaudio2, sdl, nop]" +#define GPU_OPTIONS "[d3d12, vulkan, null]" +#define HID_OPTIONS "[sdl, winkey, xinput, nop]" +DEFINE_string(apu, "xaudio2", "Audio system. Use: " APU_OPTIONS, "APU"); +DEFINE_string(gpu, "d3d12", "Graphics system. Use: " GPU_OPTIONS, "GPU"); +DEFINE_string(hid, "sdl", "Input system. Use: " HID_OPTIONS, "HID"); #elif XE_PLATFORM_LINUX -#define APU_OPTIONS "[any, alsa, nop, sdl]" -#define GPU_OPTIONS "[any, vulkan, null]" -#define HID_OPTIONS "[any, nop, sdl]" +#define APU_OPTIONS "[alsa, sdl, nop]" +#define GPU_OPTIONS "[vulkan, null]" +#define HID_OPTIONS "[sdl, nop]" +DEFINE_string(apu, "alsa", "Audio system. Use: " APU_OPTIONS, "APU"); +DEFINE_string(gpu, "vulkan", "Graphics system. Use: " GPU_OPTIONS, "GPU"); +DEFINE_string(hid, "sdl", "Input system. Use: " HID_OPTIONS, "HID"); #else -#define APU_OPTIONS "[any, nop, sdl]" -#define GPU_OPTIONS "[any, vulkan, null]" -#define HID_OPTIONS "[any, nop, sdl]" +#define APU_OPTIONS "[sdl, nop]" +#define GPU_OPTIONS "[vulkan, null]" +#define HID_OPTIONS "[sdl, nop]" +DEFINE_string(apu, "sdl", "Audio system. Use: " APU_OPTIONS, "APU"); +DEFINE_string(gpu, "vulkan", "Graphics system. Use: " GPU_OPTIONS, "GPU"); +DEFINE_string(hid, "sdl", "Input system. Use: " HID_OPTIONS, "HID"); #endif -DEFINE_string(apu, "any", "Audio system. Use: " APU_OPTIONS, "APU"); -DEFINE_string(gpu, "any", "Graphics system. Use: " GPU_OPTIONS, "GPU"); -DEFINE_string(hid, "any", "Input system. Use: " HID_OPTIONS, "HID"); - DEFINE_path( storage_root, "", "Root path for persistent internal data storage (config, etc.), or empty " @@ -247,7 +252,7 @@ class EmulatorApp final : public xe::ui::WindowedApp { } std::unique_ptr Create(const std::string_view name, Args... args) { - if (!name.empty() && name != "any") { + if (!name.empty()) { auto it = std::find_if( creators_.cbegin(), creators_.cend(), [&name](const auto& f) { return name.compare(f.name) == 0; }); @@ -270,27 +275,6 @@ class EmulatorApp final : public xe::ui::WindowedApp { Args... args) { std::vector> instances; - // "Any" path - if (name.empty() || name == "any") { - for (const auto& creator : creators_) { - if (!creator.is_available()) { - continue; - } - - // Skip xinput for "any" and use SDL - if (creator.name.compare("xinput") == 0) { - continue; - } - - auto instance = creator.instantiate(std::forward(args)...); - if (instance) { - instances.emplace_back(std::move(instance)); - } - } - return instances; - } - - // "Specified" path. Winkey is always added on windows. if (name != "winkey") { auto it = std::find_if( creators_.cbegin(), creators_.cend(), @@ -304,7 +288,6 @@ class EmulatorApp final : public xe::ui::WindowedApp { } } - // Always add winkey for passthrough. auto it = std::find_if( creators_.cbegin(), creators_.cend(), [&name](const auto& f) { return f.name.compare("winkey") == 0; }); diff --git a/src/xenia/ui/config_dialog_qt.cc b/src/xenia/ui/config_dialog_qt.cc index 6b515ee82..5bddbfe7c 100644 --- a/src/xenia/ui/config_dialog_qt.cc +++ b/src/xenia/ui/config_dialog_qt.cc @@ -525,7 +525,9 @@ void ConfigDialogQt::ResetToDefaults() { XELOGI("Resetting config to defaults (UI only)"); for (auto& var_info : config_vars_) { - // Get the default value + if (var_info.name.find("logged_profile_slot_") != std::string::npos) { + continue; + } var_info.var->ResetConfigValueToDefault(); std::string default_value; diff --git a/src/xenia/ui/config_helpers.h b/src/xenia/ui/config_helpers.h index 0fd9a4943..454a82b98 100644 --- a/src/xenia/ui/config_helpers.h +++ b/src/xenia/ui/config_helpers.h @@ -36,6 +36,19 @@ inline const std::vector& GetCvarAliases() { {"render_target_path_d3d12", "rov", "render_target_path", "accuracy"}, {"render_target_path_vulkan", "fbo", "render_target_path", "performance"}, {"render_target_path_vulkan", "fsi", "render_target_path", "accuracy"}, +#if XE_PLATFORM_WIN32 + {"gpu", "any", "gpu", "d3d12"}, + {"apu", "any", "apu", "xaudio2"}, + {"hid", "any", "hid", "sdl"}, +#elif XE_PLATFORM_LINUX + {"gpu", "any", "gpu", "vulkan"}, + {"apu", "any", "apu", "alsa"}, + {"hid", "any", "hid", "sdl"}, +#else + {"gpu", "any", "gpu", "vulkan"}, + {"apu", "any", "apu", "sdl"}, + {"hid", "any", "hid", "sdl"}, +#endif }; return aliases; } @@ -45,17 +58,17 @@ inline const std::map>& GetKnownEnumOptions() { static const std::map> options = { #if XE_PLATFORM_WIN32 - {"gpu", {"any", "d3d12", "vulkan", "null"}}, - {"apu", {"any", "nop", "sdl", "xaudio2"}}, - {"hid", {"any", "nop", "sdl", "winkey", "xinput"}}, + {"gpu", {"d3d12", "vulkan", "null"}}, + {"apu", {"xaudio2", "sdl", "nop"}}, + {"hid", {"sdl", "winkey", "xinput", "nop"}}, #elif XE_PLATFORM_LINUX - {"gpu", {"any", "vulkan", "null"}}, - {"apu", {"any", "alsa", "nop", "sdl"}}, - {"hid", {"any", "nop", "sdl"}}, + {"gpu", {"vulkan", "null"}}, + {"apu", {"alsa", "sdl", "nop"}}, + {"hid", {"sdl", "nop"}}, #else - {"gpu", {"any", "vulkan", "null"}}, - {"apu", {"any", "nop", "sdl"}}, - {"hid", {"any", "nop", "sdl"}}, + {"gpu", {"vulkan", "null"}}, + {"apu", {"nop", "sdl"}}, + {"hid", {"sdl", "nop"}}, #endif {"d3d12_readback_resolve", {"kCopy", "kComputeLuminance", "kComputeRGBA16"}}, diff --git a/src/xenia/ui/moc_simple_config_dialog_qt.cc b/src/xenia/ui/moc_simple_config_dialog_qt.cc new file mode 100644 index 000000000..ffbc42f2d --- /dev/null +++ b/src/xenia/ui/moc_simple_config_dialog_qt.cc @@ -0,0 +1,124 @@ +/**************************************************************************** +** Meta object code from reading C++ file 'simple_config_dialog_qt.h' +** +** Created by: The Qt Meta Object Compiler version 69 (Qt 6.9.2) +** +** WARNING! All changes made in this file will be lost! +*****************************************************************************/ + +#include +#include "simple_config_dialog_qt.h" + +#include + +#include + +#include +#if !defined(Q_MOC_OUTPUT_REVISION) +#error "The header file 'simple_config_dialog_qt.h' doesn't include ." +#elif Q_MOC_OUTPUT_REVISION != 69 +#error "This file was generated using the moc from 6.9.2. It" +#error "cannot be used with the include files from this version of Qt." +#error "(The moc has changed too much.)" +#endif + +#ifndef Q_CONSTINIT +#define Q_CONSTINIT +#endif + +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED +QT_WARNING_DISABLE_GCC("-Wuseless-cast") +namespace { +struct qt_meta_tag_ZN2xe3app20SimpleConfigDialogQtE_t {}; +} // unnamed namespace + +template <> +constexpr inline auto xe::app::SimpleConfigDialogQt::qt_create_metaobjectdata< + qt_meta_tag_ZN2xe3app20SimpleConfigDialogQtE_t>() { + namespace QMC = QtMocConstants; + QtMocHelpers::StringRefStorage qt_stringData{"xe::app::SimpleConfigDialogQt", + "OnSaveClicked", + "", + "OnDiscardClicked", + "OnAdvancedClicked", + "OnValueChanged"}; + + QtMocHelpers::UintData qt_methods{ + // Slot 'OnSaveClicked' + QtMocHelpers::SlotData(1, 2, QMC::AccessPrivate, QMetaType::Void), + // Slot 'OnDiscardClicked' + QtMocHelpers::SlotData(3, 2, QMC::AccessPrivate, QMetaType::Void), + // Slot 'OnAdvancedClicked' + QtMocHelpers::SlotData(4, 2, QMC::AccessPrivate, QMetaType::Void), + // Slot 'OnValueChanged' + QtMocHelpers::SlotData(5, 2, QMC::AccessPrivate, QMetaType::Void), + }; + QtMocHelpers::UintData qt_properties{}; + QtMocHelpers::UintData qt_enums{}; + return QtMocHelpers::metaObjectData< + SimpleConfigDialogQt, qt_meta_tag_ZN2xe3app20SimpleConfigDialogQtE_t>( + QMC::MetaObjectFlag{}, qt_stringData, qt_methods, qt_properties, + qt_enums); +} +Q_CONSTINIT const QMetaObject xe::app::SimpleConfigDialogQt::staticMetaObject = { { + QMetaObject::SuperData::link(), + qt_staticMetaObjectStaticContent.stringdata, + qt_staticMetaObjectStaticContent.data, + qt_static_metacall, + nullptr, + qt_staticMetaObjectRelocatingContent.metaTypes, + nullptr +} }; + +void xe::app::SimpleConfigDialogQt::qt_static_metacall(QObject* _o, + QMetaObject::Call _c, + int _id, void** _a) { + auto* _t = static_cast(_o); + if (_c == QMetaObject::InvokeMetaMethod) { + switch (_id) { + case 0: + _t->OnSaveClicked(); + break; + case 1: + _t->OnDiscardClicked(); + break; + case 2: + _t->OnAdvancedClicked(); + break; + case 3: + _t->OnValueChanged(); + break; + default:; + } + } + (void)_a; +} + +const QMetaObject* xe::app::SimpleConfigDialogQt::metaObject() const { + return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() + : &staticMetaObject; +} + +void* xe::app::SimpleConfigDialogQt::qt_metacast(const char* _clname) { + if (!_clname) return nullptr; + if (!strcmp(_clname, qt_staticMetaObjectStaticContent.strings)) + return static_cast(this); + return QDialog::qt_metacast(_clname); +} + +int xe::app::SimpleConfigDialogQt::qt_metacall(QMetaObject::Call _c, int _id, + void** _a) { + _id = QDialog::qt_metacall(_c, _id, _a); + if (_id < 0) return _id; + if (_c == QMetaObject::InvokeMetaMethod) { + if (_id < 4) qt_static_metacall(this, _c, _id, _a); + _id -= 4; + } + if (_c == QMetaObject::RegisterMethodArgumentMetaType) { + if (_id < 4) *reinterpret_cast(_a[0]) = QMetaType(); + _id -= 4; + } + return _id; +} +QT_WARNING_POP diff --git a/src/xenia/ui/simple_config_dialog_qt.cc b/src/xenia/ui/simple_config_dialog_qt.cc new file mode 100644 index 000000000..a1da8da85 --- /dev/null +++ b/src/xenia/ui/simple_config_dialog_qt.cc @@ -0,0 +1,589 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/simple_config_dialog_qt.h" + +#include +#include +#include +#include +#include +#include + +#include "xenia/app/emulator_window.h" +#include "xenia/base/cvar.h" +#include "xenia/config.h" +#include "xenia/ui/config_dialog_qt.h" +#include "xenia/ui/config_helpers.h" +#include "xenia/ui/qt_util.h" + +namespace xe { +namespace app { + +using xe::ui::SafeQString; +using xe::ui::SafeStdString; + +SimpleConfigDialogQt::SimpleConfigDialogQt(QWidget* parent, + EmulatorWindow* emulator_window) + : QDialog(parent), emulator_window_(emulator_window) { + setWindowTitle("Emulator Settings"); + setMinimumWidth(500); + SetupUI(); + LoadConfigValues(); +} + +SimpleConfigDialogQt::~SimpleConfigDialogQt() = default; + +void SimpleConfigDialogQt::ReloadConfigValues() { LoadConfigValues(); } + +void SimpleConfigDialogQt::SetupUI() { + auto* main_layout = new QVBoxLayout(this); + + // Audio section + auto* audio_group = new QGroupBox("Audio", this); + auto* audio_layout = new QFormLayout(audio_group); + + auto* apu_combo = new QComboBox(this); + const auto& enum_options = ui::GetKnownEnumOptions(); + auto apu_it = enum_options.find("apu"); + if (apu_it != enum_options.end()) { + for (const auto& opt : apu_it->second) { + apu_combo->addItem(SafeQString(opt)); + } + } + options_["apu"].cvar_name = "apu"; + options_["apu"].editor_widget = apu_combo; + options_["apu"].label_widget = new QLabel("Audio Backend:", this); + connect(apu_combo, QOverload::of(&QComboBox::currentIndexChanged), this, + &SimpleConfigDialogQt::OnValueChanged); + audio_layout->addRow(options_["apu"].label_widget, apu_combo); + + auto* xma_combo = new QComboBox(this); + auto xma_it = enum_options.find("xma_decoder"); + if (xma_it != enum_options.end()) { + for (const auto& opt : xma_it->second) { + xma_combo->addItem(SafeQString(opt)); + } + } + options_["xma_decoder"].cvar_name = "xma_decoder"; + options_["xma_decoder"].editor_widget = xma_combo; + options_["xma_decoder"].label_widget = new QLabel("Audio Decoder:", this); + connect(xma_combo, QOverload::of(&QComboBox::currentIndexChanged), this, + &SimpleConfigDialogQt::OnValueChanged); + audio_layout->addRow(options_["xma_decoder"].label_widget, xma_combo); + + auto* xma_thread_check = new QCheckBox(this); + options_["use_dedicated_xma_thread"].cvar_name = "use_dedicated_xma_thread"; + options_["use_dedicated_xma_thread"].editor_widget = xma_thread_check; + options_["use_dedicated_xma_thread"].label_widget = + new QLabel("Dedicated Thread:", this); + connect(xma_thread_check, &QCheckBox::checkStateChanged, this, + &SimpleConfigDialogQt::OnValueChanged); + audio_layout->addRow(options_["use_dedicated_xma_thread"].label_widget, + xma_thread_check); + + main_layout->addWidget(audio_group); + + // Graphics section + auto* graphics_group = new QGroupBox("Graphics", this); + auto* graphics_layout = new QFormLayout(graphics_group); + + auto* gpu_combo = new QComboBox(this); + auto gpu_it = enum_options.find("gpu"); + if (gpu_it != enum_options.end()) { + for (const auto& opt : gpu_it->second) { + gpu_combo->addItem(SafeQString(opt)); + } + } + options_["gpu"].cvar_name = "gpu"; + options_["gpu"].editor_widget = gpu_combo; + options_["gpu"].label_widget = new QLabel("Graphics Backend:", this); + connect(gpu_combo, QOverload::of(&QComboBox::currentIndexChanged), this, + &SimpleConfigDialogQt::OnValueChanged); + graphics_layout->addRow(options_["gpu"].label_widget, gpu_combo); + + auto* render_path_combo = new QComboBox(this); + auto render_path_it = enum_options.find("render_target_path"); + if (render_path_it != enum_options.end()) { + for (const auto& opt : render_path_it->second) { + render_path_combo->addItem(SafeQString(opt)); + } + } + options_["render_target_path"].cvar_name = "render_target_path"; + options_["render_target_path"].editor_widget = render_path_combo; + options_["render_target_path"].label_widget = new QLabel("Rendering:", this); + connect(render_path_combo, + QOverload::of(&QComboBox::currentIndexChanged), this, + &SimpleConfigDialogQt::OnValueChanged); + graphics_layout->addRow(options_["render_target_path"].label_widget, + render_path_combo); + + auto* scale_combo = new QComboBox(this); + scale_combo->addItem("Native", 1); + scale_combo->addItem("2x", 2); + scale_combo->addItem("3x", 3); + scale_combo->addItem("4x", 4); + scale_combo->addItem("5x", 5); + scale_combo->addItem("6x", 6); + scale_combo->addItem("7x", 7); + scale_combo->addItem("8x", 8); + options_["draw_resolution_scale"].cvar_name = "draw_resolution_scale"; + options_["draw_resolution_scale"].editor_widget = scale_combo; + options_["draw_resolution_scale"].label_widget = + new QLabel("Resolution Scale:", this); + connect(scale_combo, QOverload::of(&QComboBox::currentIndexChanged), + this, &SimpleConfigDialogQt::OnValueChanged); + graphics_layout->addRow(options_["draw_resolution_scale"].label_widget, + scale_combo); + + connect(gpu_combo, QOverload::of(&QComboBox::currentIndexChanged), + [scale_combo, gpu_combo]() { + int current_value = scale_combo->currentData().toInt(); + if (SafeStdString(gpu_combo->currentText()) == "vulkan") { + while (scale_combo->count() > 3) { + scale_combo->removeItem(scale_combo->count() - 1); + } + } else { + if (scale_combo->count() < 8) { + scale_combo->addItem("4x", 4); + scale_combo->addItem("5x", 5); + scale_combo->addItem("6x", 6); + scale_combo->addItem("7x", 7); + scale_combo->addItem("8x", 8); + } + } + int index = scale_combo->findData(current_value); + if (index >= 0) { + scale_combo->setCurrentIndex(index); + } else { + scale_combo->setCurrentIndex(0); + } + }); + + auto* fps_spin = new QSpinBox(this); + fps_spin->setMinimum(0); + fps_spin->setMaximum(1000); + fps_spin->setSpecialValueText("Unlimited"); + options_["framerate_limit"].cvar_name = "framerate_limit"; + options_["framerate_limit"].editor_widget = fps_spin; + options_["framerate_limit"].label_widget = new QLabel("FPS Limit:", this); + connect(fps_spin, QOverload::of(&QSpinBox::valueChanged), this, + &SimpleConfigDialogQt::OnValueChanged); + graphics_layout->addRow(options_["framerate_limit"].label_widget, fps_spin); + + auto* vsync_check = new QCheckBox(this); + options_["vsync"].cvar_name = "vsync"; + options_["vsync"].editor_widget = vsync_check; + options_["vsync"].label_widget = new QLabel("VSync:", this); + connect(vsync_check, &QCheckBox::checkStateChanged, this, + &SimpleConfigDialogQt::OnValueChanged); + graphics_layout->addRow(options_["vsync"].label_widget, vsync_check); + + auto* fullscreen_check = new QCheckBox(this); + options_["fullscreen"].cvar_name = "fullscreen"; + options_["fullscreen"].editor_widget = fullscreen_check; + options_["fullscreen"].label_widget = new QLabel("Full Screen:", this); + connect(fullscreen_check, &QCheckBox::checkStateChanged, this, + &SimpleConfigDialogQt::OnValueChanged); + graphics_layout->addRow(options_["fullscreen"].label_widget, + fullscreen_check); + + auto* letterbox_check = new QCheckBox(this); + options_["present_letterbox"].cvar_name = "present_letterbox"; + options_["present_letterbox"].editor_widget = letterbox_check; + options_["present_letterbox"].label_widget = new QLabel("Letterbox:", this); + connect(letterbox_check, &QCheckBox::checkStateChanged, this, + &SimpleConfigDialogQt::OnValueChanged); + graphics_layout->addRow(options_["present_letterbox"].label_widget, + letterbox_check); + + auto* vrr_check = new QCheckBox(this); + options_["variable_refresh_rate"].cvar_name = "variable_refresh_rate"; + options_["variable_refresh_rate"].editor_widget = vrr_check; + options_["variable_refresh_rate"].label_widget = + new QLabel("Variable Refresh Rate:", this); + connect(vrr_check, &QCheckBox::checkStateChanged, this, + &SimpleConfigDialogQt::OnValueChanged); + graphics_layout->addRow(options_["variable_refresh_rate"].label_widget, + vrr_check); + + main_layout->addWidget(graphics_group); + + // Other section + auto* other_group = new QGroupBox("Other", this); + auto* other_layout = new QFormLayout(other_group); + + auto* license_check = new QCheckBox(this); + options_["license_mask"].cvar_name = "license_mask"; + options_["license_mask"].editor_widget = license_check; + options_["license_mask"].label_widget = new QLabel("Enable License:", this); + connect(license_check, &QCheckBox::checkStateChanged, this, + &SimpleConfigDialogQt::OnValueChanged); + other_layout->addRow(options_["license_mask"].label_widget, license_check); + + auto* discord_check = new QCheckBox(this); + options_["discord"].cvar_name = "discord"; + options_["discord"].editor_widget = discord_check; + options_["discord"].label_widget = new QLabel("Discord Rich Presence:", this); + connect(discord_check, &QCheckBox::checkStateChanged, this, + &SimpleConfigDialogQt::OnValueChanged); + other_layout->addRow(options_["discord"].label_widget, discord_check); + + auto* language_combo = new QComboBox(this); + auto language_it = enum_options.find("user_language"); + if (language_it != enum_options.end()) { + for (const auto& opt : language_it->second) { + language_combo->addItem(SafeQString(opt)); + } + } + options_["user_language"].cvar_name = "user_language"; + options_["user_language"].editor_widget = language_combo; + options_["user_language"].label_widget = new QLabel("Language:", this); + connect(language_combo, QOverload::of(&QComboBox::currentIndexChanged), + this, &SimpleConfigDialogQt::OnValueChanged); + other_layout->addRow(options_["user_language"].label_widget, language_combo); + + auto* country_combo = new QComboBox(this); + auto country_it = enum_options.find("user_country"); + if (country_it != enum_options.end()) { + for (const auto& opt : country_it->second) { + country_combo->addItem(SafeQString(opt)); + } + } + options_["user_country"].cvar_name = "user_country"; + options_["user_country"].editor_widget = country_combo; + options_["user_country"].label_widget = new QLabel("Country:", this); + connect(country_combo, QOverload::of(&QComboBox::currentIndexChanged), + this, &SimpleConfigDialogQt::OnValueChanged); + other_layout->addRow(options_["user_country"].label_widget, country_combo); + + main_layout->addWidget(other_group); + + // Buttons + auto* button_layout = new QHBoxLayout(); + + auto* advanced_button = new QPushButton("Advanced...", this); + connect(advanced_button, &QPushButton::clicked, this, + &SimpleConfigDialogQt::OnAdvancedClicked); + button_layout->addWidget(advanced_button); + + auto* reset_button = new QPushButton("Reset to Default", this); + connect(reset_button, &QPushButton::clicked, this, + &SimpleConfigDialogQt::OnResetClicked); + button_layout->addWidget(reset_button); + + button_layout->addStretch(); + + auto* save_button = new QPushButton("Save", this); + connect(save_button, &QPushButton::clicked, this, + &SimpleConfigDialogQt::OnSaveClicked); + button_layout->addWidget(save_button); + + auto* discard_button = new QPushButton("Cancel", this); + connect(discard_button, &QPushButton::clicked, this, + &SimpleConfigDialogQt::OnDiscardClicked); + button_layout->addWidget(discard_button); + + main_layout->addLayout(button_layout); +} + +void SimpleConfigDialogQt::LoadConfigValues() { + if (!cvar::ConfigVars) { + return; + } + + for (auto& [cvar_name, option] : options_) { + if (cvar_name == "draw_resolution_scale") { + auto it_x = (*cvar::ConfigVars).find("draw_resolution_scale_x"); + if (it_x != (*cvar::ConfigVars).end()) { + auto config_var_x = static_cast(it_x->second); + std::string value = config_var_x->config_value(); + + if (value.length() >= 2 && value.front() == '"' && + value.back() == '"') { + value = value.substr(1, value.length() - 2); + } + + option.current_value = value; + option.pending_value = value; + + if (auto* combo = qobject_cast(option.editor_widget)) { + int scale_value = std::stoi(value); + int index = combo->findData(scale_value); + if (index >= 0) { + combo->setCurrentIndex(index); + } + } + } + continue; + } + + if (cvar_name == "variable_refresh_rate") { + auto it_gpu = (*cvar::ConfigVars).find("gpu"); + std::string gpu_value = "d3d12"; + if (it_gpu != (*cvar::ConfigVars).end()) { + auto gpu_var = static_cast(it_gpu->second); + gpu_value = gpu_var->config_value(); + if (gpu_value.length() >= 2 && gpu_value.front() == '"' && + gpu_value.back() == '"') { + gpu_value = gpu_value.substr(1, gpu_value.length() - 2); + } + } + + std::string actual_cvar_name = + (gpu_value == "vulkan") + ? "vulkan_allow_present_mode_immediate" + : "d3d12_allow_variable_refresh_rate_and_tearing"; + + auto it_vrr = (*cvar::ConfigVars).find(actual_cvar_name); + if (it_vrr != (*cvar::ConfigVars).end()) { + auto config_var_vrr = static_cast(it_vrr->second); + std::string value = config_var_vrr->config_value(); + + if (value.length() >= 2 && value.front() == '"' && + value.back() == '"') { + value = value.substr(1, value.length() - 2); + } + + option.current_value = value; + option.pending_value = value; + + if (auto* check = qobject_cast(option.editor_widget)) { + check->setChecked(value == "true"); + } + } + continue; + } + + auto it = (*cvar::ConfigVars).find(cvar_name); + if (it != (*cvar::ConfigVars).end()) { + auto config_var = static_cast(it->second); + std::string value = config_var->config_value(); + + if (value.length() >= 2 && value.front() == '"' && value.back() == '"') { + value = value.substr(1, value.length() - 2); + } + + option.current_value = value; + option.pending_value = value; + + if (auto* combo = qobject_cast(option.editor_widget)) { + int index = combo->findText(SafeQString(value)); + if (index >= 0) { + combo->setCurrentIndex(index); + } + } else if (auto* check = qobject_cast(option.editor_widget)) { + if (cvar_name == "license_mask") { + check->setChecked(value == "1"); + } else { + check->setChecked(value == "true"); + } + } else if (auto* spin = qobject_cast(option.editor_widget)) { + spin->setValue(std::stoi(value)); + } + } + } + + has_unsaved_changes_ = false; +} + +void SimpleConfigDialogQt::SaveConfigChanges() { + if (!cvar::ConfigVars) { + return; + } + + for (auto& [cvar_name, option] : options_) { + UpdatePendingValueFromEditor(&option); + + if (cvar_name == "draw_resolution_scale") { + auto it_x = (*cvar::ConfigVars).find("draw_resolution_scale_x"); + auto it_y = (*cvar::ConfigVars).find("draw_resolution_scale_y"); + if (it_x != (*cvar::ConfigVars).end() && + it_y != (*cvar::ConfigVars).end()) { + auto config_var_x = static_cast(it_x->second); + auto config_var_y = static_cast(it_y->second); + toml::value new_value(std::stoi(option.pending_value)); + config_var_x->LoadConfigValue(&new_value); + config_var_y->LoadConfigValue(&new_value); + } + continue; + } + + if (cvar_name == "variable_refresh_rate") { + auto it_gpu = (*cvar::ConfigVars).find("gpu"); + std::string gpu_value = "d3d12"; + if (it_gpu != (*cvar::ConfigVars).end()) { + auto gpu_var = static_cast(it_gpu->second); + gpu_value = gpu_var->config_value(); + if (gpu_value.length() >= 2 && gpu_value.front() == '"' && + gpu_value.back() == '"') { + gpu_value = gpu_value.substr(1, gpu_value.length() - 2); + } + } + + std::string actual_cvar_name = + (gpu_value == "vulkan") + ? "vulkan_allow_present_mode_immediate" + : "d3d12_allow_variable_refresh_rate_and_tearing"; + + auto it_vrr = (*cvar::ConfigVars).find(actual_cvar_name); + if (it_vrr != (*cvar::ConfigVars).end()) { + auto config_var_vrr = static_cast(it_vrr->second); + toml::value new_value(option.pending_value == "true"); + config_var_vrr->LoadConfigValue(&new_value); + } + continue; + } + + auto it = (*cvar::ConfigVars).find(cvar_name); + if (it != (*cvar::ConfigVars).end()) { + auto config_var = static_cast(it->second); + + if (cvar_name == "framerate_limit") { + toml::value new_value(std::stoull(option.pending_value)); + config_var->LoadConfigValue(&new_value); + } else if (cvar_name == "license_mask") { + toml::value new_value(std::stoi(option.pending_value)); + config_var->LoadConfigValue(&new_value); + } else if (cvar_name == "vsync" || cvar_name == "fullscreen" || + cvar_name == "present_letterbox" || cvar_name == "discord" || + cvar_name == "use_dedicated_xma_thread") { + toml::value new_value(option.pending_value == "true"); + config_var->LoadConfigValue(&new_value); + } else { + toml::value new_value(option.pending_value); + config_var->LoadConfigValue(&new_value); + } + } + } + + config::SaveConfig(); + has_unsaved_changes_ = false; +} + +void SimpleConfigDialogQt::OnSaveClicked() { + SaveConfigChanges(); + accept(); +} + +void SimpleConfigDialogQt::OnDiscardClicked() { + if (has_unsaved_changes_) { + auto reply = QMessageBox::question( + this, "Unsaved Changes", + "You have unsaved changes. Are you sure you want to discard them?", + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::No) { + return; + } + } + + reject(); +} + +void SimpleConfigDialogQt::OnAdvancedClicked() { + if (has_unsaved_changes_) { + auto reply = QMessageBox::question( + this, "Unsaved Changes", + "You have unsaved changes. Do you want to save them before opening " + "the advanced settings?", + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + + if (reply == QMessageBox::Cancel) { + return; + } else if (reply == QMessageBox::Save) { + SaveConfigChanges(); + } + } + + accept(); + + config::ReloadConfig(); + + auto* advanced_dialog = new ConfigDialogQt(nullptr, emulator_window_); + advanced_dialog->exec(); + delete advanced_dialog; +} + +void SimpleConfigDialogQt::OnResetClicked() { + auto reply = QMessageBox::question( + this, "Reset to Defaults", + "Are you sure you want to reset all settings to their default values?", + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + if (!cvar::ConfigVars) { + return; + } + + for (auto& [name, var] : *cvar::ConfigVars) { + if (name.find("logged_profile_slot_") != std::string::npos) { + continue; + } + auto config_var = static_cast(var); + config_var->ResetConfigValueToDefault(); + } + + LoadConfigValues(); + has_unsaved_changes_ = true; + } +} + +void SimpleConfigDialogQt::OnValueChanged() { + for (auto& [cvar_name, option] : options_) { + UpdatePendingValueFromEditor(&option); + bool is_modified = (option.pending_value != option.current_value); + + if (option.label_widget) { + QFont font = option.label_widget->font(); + font.setBold(is_modified); + option.label_widget->setFont(font); + } + } + + has_unsaved_changes_ = true; +} + +void SimpleConfigDialogQt::UpdatePendingValueFromEditor(ConfigOption* option) { + option->pending_value = + GetEditorValue(option->editor_widget, option->cvar_name); +} + +void SimpleConfigDialogQt::UpdateLabelModifiedState(ConfigOption* option) { + if (!option || !option->label_widget) { + return; + } + + bool is_modified = (option->pending_value != option->current_value); + QFont font = option->label_widget->font(); + font.setBold(is_modified); + option->label_widget->setFont(font); +} + +std::string SimpleConfigDialogQt::GetEditorValue(QWidget* editor, + const std::string& cvar_name) { + if (auto* combo = qobject_cast(editor)) { + if (cvar_name == "draw_resolution_scale") { + return std::to_string(combo->currentData().toInt()); + } + return SafeStdString(combo->currentText()); + } else if (auto* check = qobject_cast(editor)) { + if (cvar_name == "license_mask") { + return check->isChecked() ? "1" : "0"; + } else { + return check->isChecked() ? "true" : "false"; + } + } else if (auto* spin = qobject_cast(editor)) { + return std::to_string(spin->value()); + } + return ""; +} + +} // namespace app +} // namespace xe diff --git a/src/xenia/ui/simple_config_dialog_qt.h b/src/xenia/ui/simple_config_dialog_qt.h new file mode 100644 index 000000000..fe01145b2 --- /dev/null +++ b/src/xenia/ui/simple_config_dialog_qt.h @@ -0,0 +1,70 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_SIMPLE_CONFIG_DIALOG_QT_H_ +#define XENIA_UI_SIMPLE_CONFIG_DIALOG_QT_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xenia/base/cvar.h" + +namespace xe { +namespace app { + +class EmulatorWindow; + +class SimpleConfigDialogQt : public QDialog { + Q_OBJECT + + public: + SimpleConfigDialogQt(QWidget* parent, EmulatorWindow* emulator_window); + ~SimpleConfigDialogQt() override; + + void ReloadConfigValues(); + + private slots: + void OnSaveClicked(); + void OnDiscardClicked(); + void OnAdvancedClicked(); + void OnResetClicked(); + void OnValueChanged(); + + private: + struct ConfigOption { + std::string cvar_name; + std::string current_value; + std::string pending_value; + QWidget* editor_widget = nullptr; + QLabel* label_widget = nullptr; + }; + + void LoadConfigValues(); + void SaveConfigChanges(); + void SetupUI(); + void UpdatePendingValueFromEditor(ConfigOption* option); + void UpdateLabelModifiedState(ConfigOption* option); + std::string GetEditorValue(QWidget* editor, const std::string& cvar_name); + + EmulatorWindow* emulator_window_; + std::map options_; + + bool has_unsaved_changes_ = false; +}; + +} // namespace app +} // namespace xe + +#endif // XENIA_UI_SIMPLE_CONFIG_DIALOG_QT_H_