diff --git a/src/citron/configuration/configure_per_game.cpp b/src/citron/configuration/configure_per_game.cpp index 90db7e7c1..97dc30875 100644 --- a/src/citron/configuration/configure_per_game.cpp +++ b/src/citron/configuration/configure_per_game.cpp @@ -10,7 +10,6 @@ #include #include #include - #include #include @@ -28,7 +27,10 @@ #include "citron/configuration/style_animation_event_filter.h" #include #include +#include #include +#include +#include #include #include #include @@ -36,6 +38,9 @@ #include #include +#ifdef ARCHITECTURE_x86_64 +#include "common/x64/cpu_detect.h" +#endif #include "common/fs/fs_util.h" #include "common/hex_util.h" #include "common/settings_enums.h" @@ -69,8 +74,11 @@ #include "citron/util/util.h" #include "citron/vk_device_info.h" #include "citron/main.h" +#include "common/fs/path_util.h" #include "common/string_util.h" #include "common/xci_trimmer.h" +#include +#include // Helper function to detect if the application is using a dark theme static bool IsDarkMode() { @@ -137,6 +145,24 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st UpdateTheme(); + auto* share_button = new QPushButton(tr("Share Settings"), this); + auto* use_button = new QPushButton(tr("Use Settings"), this); + + share_button->setObjectName(QStringLiteral("share_settings_button")); + use_button->setObjectName(QStringLiteral("use_settings_button")); + + share_button->setToolTip(tr("Please choose your CPU/Graphics/Advanced settings manually. " + "This will capture your current UI selections exactly as they appear.")); + + share_button->setStyleSheet(ui->trim_xci_button->styleSheet()); + use_button->setStyleSheet(ui->trim_xci_button->styleSheet()); + + ui->gridLayout_2->addWidget(share_button, 11, 0, 1, 2); + ui->gridLayout_2->addWidget(use_button, 12, 0, 1, 2); + + connect(share_button, &QPushButton::clicked, this, &ConfigurePerGame::OnShareSettings); + connect(use_button, &QPushButton::clicked, this, &ConfigurePerGame::OnUseSettings); + auto* animation_filter = new StyleAnimationEventFilter(this); button_group = new QButtonGroup(this); @@ -893,4 +919,248 @@ void ConfigurePerGame::AnimateTabSwitch(int id) { button->setEnabled(false); } animation_group->start(QAbstractAnimation::DeleteWhenStopped); -} \ No newline at end of file +} + +void ConfigurePerGame::OnShareSettings() { + QFileInfo file_info(QString::fromStdString(file_name)); + QString base_name = file_info.baseName(); + auto config_path = Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "custom"; + QString default_path = QStringLiteral("%1/%2_shared.json").arg( + QString::fromStdString(config_path.string()), base_name); + + QString save_path = QFileDialog::getSaveFileName(this, tr("Share Settings Profile"), + default_path, tr("JSON Files (*.json)")); + if (save_path.isEmpty()) return; + + nlohmann::json profile; + profile["metadata"]["title_id"] = fmt::format("{:016X}", title_id); + + int count = 0; + + for (int i = 0; i < ui->stackedWidget->count(); ++i) { + QWidget* page = ui->stackedWidget->widget(i); + ConfigurationShared::Tab* tab = nullptr; + if (auto* scroll = qobject_cast(page)) { + tab = qobject_cast(scroll->widget()); + } + if (!tab) continue; + + auto* button = qobject_cast(button_group->button(i)); + if (!button) continue; + + QString tab_name = button->text(); + std::string section = (tab_name == tr("CPU")) ? "Cpu" : "Renderer"; + if (tab_name != tr("CPU") && tab_name != tr("Graphics") && tab_name != tr("Adv. Graphics")) continue; + + auto widgets = tab->findChildren(); + for (auto* w : widgets) { + std::string label = w->GetSetting().GetLabel(); + if (label == "renderer_force_max_clock") label = "force_max_clock"; + + QString final_value; + // Check for specific UI elements inside the wrapper + if (auto* dbox = w->findChild()) { + final_value = QString::number(dbox->value(), 'f', 6); + } else if (auto* sbox = w->findChild()) { + final_value = QString::number(sbox->value()); + } else if (auto* combo = w->findChild()) { + final_value = QString::number(combo->currentIndex()); + } else if (auto* slider = w->findChild()) { + final_value = QString::number(slider->value()); + } else { + auto all_checks = w->findChildren(); + for (auto* cb : all_checks) { + if (!cb->toolTip().contains(tr("global"), Qt::CaseInsensitive)) { + final_value = cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"); + break; + } + } + } + + if (!final_value.isEmpty()) { + profile["settings"][section][label] = final_value.toStdString(); + count++; + } + } + } + + #ifdef ARCHITECTURE_x86_64 + profile["notes"]["cpu"] = Common::GetCPUCaps().cpu_string; + #else + profile["notes"]["cpu"] = "Unknown CPU"; + #endif + + // Find the GPU name from the UI dropdown specifically + for (int i = 0; i < ui->stackedWidget->count(); ++i) { + if (auto* button = qobject_cast(button_group->button(i))) { + if (button->text() == tr("Graphics")) { + QWidget* page = ui->stackedWidget->widget(i); + if (auto* scroll = qobject_cast(page)) { + auto combos = scroll->widget()->findChildren(); + QComboBox* device_box = nullptr; + + // 1. Try object name first + for (auto* cb : combos) { + if (cb->objectName().toLower().contains(QStringLiteral("device"))) { + device_box = cb; + break; + } + } + + // 2. If object name failed, look for a box containing GPU keywords + if (!device_box) { + for (auto* cb : combos) { + QString txt = cb->currentText(); + // If the box contains a known GPU brand, it's definitely the device selector + if (txt.contains(QStringLiteral("NVIDIA"), Qt::CaseInsensitive) || + txt.contains(QStringLiteral("AMD"), Qt::CaseInsensitive) || + txt.contains(QStringLiteral("Intel"), Qt::CaseInsensitive) || + txt.contains(QStringLiteral("GeForce"), Qt::CaseInsensitive) || + txt.contains(QStringLiteral("Radeon"), Qt::CaseInsensitive) || + txt.contains(QStringLiteral("Graphics"), Qt::CaseInsensitive)) { + device_box = cb; + break; + } + } + } + + // 3. Final fallback: Avoid technical backend names + if (!device_box) { + for (auto* cb : combos) { + QString txt = cb->currentText(); + if (cb->count() > 0 && + txt != QStringLiteral("Vulkan") && + txt != QStringLiteral("OpenGL") && + txt != QStringLiteral("GLSL") && + txt != QStringLiteral("SPIR-V") && + txt != QStringLiteral("Null")) { + device_box = cb; + break; + } + } + } + + if (device_box) { + profile["notes"]["gpu"] = device_box->currentText().toStdString(); + } else { + profile["notes"]["gpu"] = "Unknown GPU"; + } + } + } + } + } + + std::ofstream o(save_path.toStdString()); + if (o.is_open()) { + o << profile.dump(4); + QMessageBox::information(this, tr("Success"), tr("Exported %1 settings.").arg(count)); + } +} + +void ConfigurePerGame::OnUseSettings() { + auto config_path = Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "custom"; + QString load_path = QFileDialog::getOpenFileName(this, tr("Use Settings Profile"), + QString::fromStdString(config_path.string()), + tr("JSON Files (*.json)")); + if (load_path.isEmpty()) return; + + std::ifstream config_file(load_path.toStdString()); + nlohmann::json profile; + try { config_file >> profile; } catch (...) { return; } + + // --- HARDWARE MISMATCH CHECK --- + if (profile.contains("notes")) { + QString creator_cpu = QString::fromStdString(profile["notes"].value("cpu", "Unknown")); + QString creator_gpu = QString::fromStdString(profile["notes"].value("gpu", "Unknown")); + +#ifdef ARCHITECTURE_x86_64 + QString current_cpu = QString::fromStdString(Common::GetCPUCaps().cpu_string); +#else + QString current_cpu = QStringLiteral("Unknown CPU"); +#endif + + QString gpu_vendor = QStringLiteral("Other"); + if (creator_gpu.contains(QStringLiteral("NVIDIA"), Qt::CaseInsensitive)) { + gpu_vendor = QStringLiteral("NVIDIA"); + } else if (creator_gpu.contains(QStringLiteral("AMD"), Qt::CaseInsensitive) || + creator_gpu.contains(QStringLiteral("Radeon"), Qt::CaseInsensitive)) { + gpu_vendor = QStringLiteral("AMD"); + } else if (creator_gpu.contains(QStringLiteral("Intel"), Qt::CaseInsensitive)) { + gpu_vendor = QStringLiteral("Intel"); + } + + QString msg = tr("This profile was created on:\n" + "CPU: %1\n" + "GPU: %2 (%3 Vendor)\n\n" + "Your current CPU: %4\n\n" + "Applying settings from a different GPU vendor (e.g., NVIDIA to AMD) " + "can cause crashes. Do you want to continue?") + .arg(creator_cpu, creator_gpu, gpu_vendor, current_cpu); + + auto result = QMessageBox::question(this, tr("Hardware Info"), msg, + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::No) return; + } + + int count = 0; + std::map incoming; + for (auto& [section, keys] : profile["settings"].items()) { + for (auto& [key, value] : keys.items()) { + incoming[key] = value.get(); + } + } + + for (int i = 0; i < ui->stackedWidget->count(); ++i) { + QWidget* page = ui->stackedWidget->widget(i); + ConfigurationShared::Tab* tab = nullptr; + if (auto* scroll = qobject_cast(page)) { + tab = qobject_cast(scroll->widget()); + } + if (!tab) continue; + + auto widgets = tab->findChildren(); + for (auto* w : widgets) { + std::string label = w->GetSetting().GetLabel(); + std::string val; + + if (incoming.count(label)) { + val = incoming[label]; + } else if (label == "renderer_force_max_clock" && incoming.count("force_max_clock")) { + val = incoming["force_max_clock"]; + } else { + continue; + } + + // UNCHECK THE GLOBAL BUTTON (Unlock the setting) + auto buttons = w->findChildren(); + for (auto* btn : buttons) { + QString tt = btn->toolTip().toLower(); + if (tt.contains(tr("global").toLower()) || tt.contains(QStringLiteral("restore"))) { + btn->setChecked(false); + } + } + + // INJECT VALUES INTO UI WIDGETS + if (auto* dbox = w->findChild()) { + dbox->setValue(std::stof(val)); + } else if (auto* sbox = w->findChild()) { + sbox->setValue(std::stoi(val)); + } else if (auto* combo = w->findChild()) { + combo->setCurrentIndex(std::stoi(val)); + } else if (auto* slider = w->findChild()) { + slider->setValue(std::stoi(val)); + } else { + auto all_checks = w->findChildren(); + for (auto* cb : all_checks) { + if (!cb->toolTip().contains(tr("global"), Qt::CaseInsensitive)) { + cb->setChecked(val == "true"); + } + } + } + count++; + } + } + + QMessageBox::information(this, tr("Import Successful"), + tr("Applied %1 settings to the UI. Click OK or Apply to save.").arg(count)); +} diff --git a/src/citron/configuration/configure_per_game.h b/src/citron/configuration/configure_per_game.h index 8edc6b1fc..1f6c2663a 100644 --- a/src/citron/configuration/configure_per_game.h +++ b/src/citron/configuration/configure_per_game.h @@ -62,6 +62,8 @@ public: public slots: void accept() override; void OnTrimXCI(); + void OnShareSettings(); + void OnUseSettings(); protected: void changeEvent(QEvent* event) override; diff --git a/src/citron/configuration/qt_config.h b/src/citron/configuration/qt_config.h index d82093581..583985780 100644 --- a/src/citron/configuration/qt_config.h +++ b/src/citron/configuration/qt_config.h @@ -44,12 +44,11 @@ protected: void SaveUIValues() override; void SaveUIGamelistValues() override; void SaveUILayoutValues() override; - void SaveMultiplayerValues() override; + void SaveMultiplayerValues(); void SaveNetworkValues(); - std::vector& FindRelevantList(Settings::Category category) override; - public: + std::vector& FindRelevantList(Settings::Category category) override; static const std::array default_buttons; static const std::array default_motions; static const std::array, Settings::NativeAnalog::NumAnalogs> default_analogs; diff --git a/src/citron/configuration/shared_widget.h b/src/citron/configuration/shared_widget.h index 2a5c44639..8f61bb2ec 100644 --- a/src/citron/configuration/shared_widget.h +++ b/src/citron/configuration/shared_widget.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -81,6 +82,9 @@ public: */ bool Valid() const; + // Settings sharing + Settings::BasicSetting& GetSetting() { return setting; } + /** * Creates a button to appear when a setting has been modified. This exists for custom * configurations and wasn't designed to work for the global configuration. It has public access