mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-04-24 09:02:47 +00:00
Creates a layer outside the game config layer system and passes it to the created gfx widows, so as to not interfere with the global config system. Supports multiple game properties being open at once. Supports editing while a game is playing, but the options only save and update the active game when the window is closed. Right-clicking will remove a property from the game ini.
367 lines
13 KiB
C++
367 lines
13 KiB
C++
// Copyright 2018 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "DolphinQt/Config/GameConfigWidget.h"
|
|
|
|
#include <QFont>
|
|
#include <QGroupBox>
|
|
#include <QHBoxLayout>
|
|
#include <QIcon>
|
|
#include <QLabel>
|
|
#include <QTabWidget>
|
|
#include <QTimer>
|
|
#include <QToolTip>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "Common/CommonPaths.h"
|
|
#include "Common/Config/Config.h"
|
|
#include "Common/Config/Layer.h"
|
|
#include "Common/FileUtil.h"
|
|
|
|
#include "Core/Config/GraphicsSettings.h"
|
|
#include "Core/Config/MainSettings.h"
|
|
#include "Core/ConfigLoaders/GameConfigLoader.h"
|
|
#include "Core/ConfigManager.h"
|
|
#include "DolphinQt/Config/ConfigControls/ConfigBool.h"
|
|
#include "DolphinQt/Config/ConfigControls/ConfigChoice.h"
|
|
#include "DolphinQt/Config/ConfigControls/ConfigInteger.h"
|
|
#include "DolphinQt/Config/ConfigControls/ConfigRadio.h"
|
|
#include "DolphinQt/Config/ConfigControls/ConfigSlider.h"
|
|
#include "DolphinQt/Config/GameConfigEdit.h"
|
|
#include "DolphinQt/Config/Graphics/AdvancedWidget.h"
|
|
#include "DolphinQt/Config/Graphics/EnhancementsWidget.h"
|
|
#include "DolphinQt/Config/Graphics/GeneralWidget.h"
|
|
#include "DolphinQt/Config/Graphics/HacksWidget.h"
|
|
#include "DolphinQt/QtUtils/WrapInScrollArea.h"
|
|
|
|
#include "UICommon/GameFile.h"
|
|
|
|
static void PopulateTab(QTabWidget* tab, const std::string& path, std::string& game_id,
|
|
u16 revision, bool read_only)
|
|
{
|
|
for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision))
|
|
{
|
|
const std::string ini_path = path + filename;
|
|
if (File::Exists(ini_path))
|
|
{
|
|
auto* edit = new GameConfigEdit(nullptr, QString::fromStdString(ini_path), read_only);
|
|
tab->addTab(edit, QString::fromStdString(filename));
|
|
}
|
|
}
|
|
}
|
|
|
|
GameConfigWidget::GameConfigWidget(const UICommon::GameFile& game) : m_game(game)
|
|
{
|
|
m_game_id = m_game.GetGameID();
|
|
|
|
m_gameini_local_path =
|
|
QString::fromStdString(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini");
|
|
|
|
m_layer = std::make_unique<Config::Layer>(
|
|
ConfigLoaders::GenerateLocalGameConfigLoader(m_game_id, m_game.GetRevision()));
|
|
m_global_layer = std::make_unique<Config::Layer>(
|
|
ConfigLoaders::GenerateGlobalGameConfigLoader(m_game_id, m_game.GetRevision()));
|
|
|
|
CreateWidgets();
|
|
connect(&Settings::Instance(), &Settings::ConfigChanged, this, &GameConfigWidget::LoadSettings);
|
|
|
|
PopulateTab(m_default_tab, File::GetSysDirectory() + GAMESETTINGS_DIR DIR_SEP, m_game_id,
|
|
m_game.GetRevision(), true);
|
|
PopulateTab(m_local_tab, File::GetUserPath(D_GAMESETTINGS_IDX), m_game_id, m_game.GetRevision(),
|
|
false);
|
|
|
|
bool game_id_tab = false;
|
|
for (int i = 0; i < m_local_tab->count(); i++)
|
|
{
|
|
if (m_local_tab->tabText(i).toStdString() == m_game_id + ".ini")
|
|
game_id_tab = true;
|
|
}
|
|
|
|
if (game_id_tab == false)
|
|
{
|
|
// Create new local game ini tab if none exists.
|
|
auto* edit = new GameConfigEdit(
|
|
nullptr, QString::fromStdString(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini"),
|
|
false);
|
|
m_local_tab->addTab(edit, QString::fromStdString(m_game_id + ".ini"));
|
|
}
|
|
|
|
// Fails to change font if it's directly called at this time. Is there a better workaround?
|
|
QTimer::singleShot(100, this, [this]() {
|
|
SetItalics();
|
|
Config::OnConfigChanged();
|
|
});
|
|
}
|
|
|
|
void GameConfigWidget::CreateWidgets()
|
|
{
|
|
Config::Layer* layer = m_layer.get();
|
|
// Core
|
|
auto* core_box = new QGroupBox(tr("Core"));
|
|
auto* core_layout = new QGridLayout;
|
|
core_box->setLayout(core_layout);
|
|
|
|
m_enable_dual_core = new ConfigBool(tr("Enable Dual Core"), Config::MAIN_CPU_THREAD, layer);
|
|
m_enable_mmu = new ConfigBool(tr("Enable MMU"), Config::MAIN_MMU, layer);
|
|
m_enable_fprf = new ConfigBool(tr("Enable FPRF"), Config::MAIN_FPRF, layer);
|
|
m_sync_gpu = new ConfigBool(tr("Synchronize GPU thread"), Config::MAIN_SYNC_GPU, layer);
|
|
m_emulate_disc_speed =
|
|
new ConfigBool(tr("Emulate Disc Speed"), Config::MAIN_FAST_DISC_SPEED, layer, true);
|
|
m_use_dsp_hle = new ConfigBool(tr("DSP HLE (fast)"), Config::MAIN_DSP_HLE, layer);
|
|
|
|
const std::vector<std::string> choice{tr("auto").toStdString(), tr("none").toStdString(),
|
|
tr("fake-completion").toStdString()};
|
|
m_deterministic_dual_core =
|
|
new ConfigStringChoice(choice, Config::MAIN_GPU_DETERMINISM_MODE, layer);
|
|
|
|
m_enable_mmu->setToolTip(tr(
|
|
"Enables the Memory Management Unit, needed for some games. (ON = Compatible, OFF = Fast)"));
|
|
|
|
m_enable_fprf->setToolTip(tr("Enables Floating Point Result Flag calculation, needed for a few "
|
|
"games. (ON = Compatible, OFF = Fast)"));
|
|
m_sync_gpu->setToolTip(tr("Synchronizes the GPU and CPU threads to help prevent random freezes "
|
|
"in Dual core mode. (ON = Compatible, OFF = Fast)"));
|
|
m_emulate_disc_speed->setToolTip(
|
|
tr("Enable emulated disc speed. Disabling this can cause crashes "
|
|
"and other problems in some games. "
|
|
"(ON = Compatible, OFF = Unlocked)"));
|
|
|
|
core_layout->addWidget(m_enable_dual_core, 0, 0);
|
|
core_layout->addWidget(m_enable_mmu, 1, 0);
|
|
core_layout->addWidget(m_enable_fprf, 2, 0);
|
|
core_layout->addWidget(m_sync_gpu, 3, 0);
|
|
core_layout->addWidget(m_emulate_disc_speed, 4, 0);
|
|
core_layout->addWidget(m_use_dsp_hle, 5, 0);
|
|
core_layout->addWidget(new QLabel(tr("Deterministic dual core:")), 6, 0);
|
|
core_layout->addWidget(m_deterministic_dual_core, 6, 1);
|
|
|
|
// Stereoscopy
|
|
auto* stereoscopy_box = new QGroupBox(tr("Stereoscopy"));
|
|
auto* stereoscopy_layout = new QGridLayout;
|
|
stereoscopy_box->setLayout(stereoscopy_layout);
|
|
|
|
m_depth_slider = new ConfigSlider(100, 200, Config::GFX_STEREO_DEPTH_PERCENTAGE, layer);
|
|
m_convergence_spin = new ConfigInteger(0, INT32_MAX, Config::GFX_STEREO_CONVERGENCE, layer);
|
|
m_use_monoscopic_shadows =
|
|
new ConfigBool(tr("Monoscopic Shadows"), Config::GFX_STEREO_EFB_MONO_DEPTH, layer);
|
|
|
|
m_depth_slider->setToolTip(
|
|
tr("This value is multiplied with the depth set in the graphics configuration."));
|
|
m_convergence_spin->setToolTip(
|
|
tr("This value is added to the convergence value set in the graphics configuration."));
|
|
m_use_monoscopic_shadows->setToolTip(
|
|
tr("Use a single depth buffer for both eyes. Needed for a few games."));
|
|
|
|
stereoscopy_layout->addWidget(new QLabel(tr("Depth Percentage:")), 0, 0);
|
|
stereoscopy_layout->addWidget(m_depth_slider, 0, 1);
|
|
stereoscopy_layout->addWidget(new QLabel(tr("Convergence:")), 1, 0);
|
|
stereoscopy_layout->addWidget(m_convergence_spin, 1, 1);
|
|
stereoscopy_layout->addWidget(m_use_monoscopic_shadows, 2, 0);
|
|
|
|
auto* general_layout = new QVBoxLayout;
|
|
general_layout->addWidget(core_box);
|
|
general_layout->addWidget(stereoscopy_box);
|
|
general_layout->addStretch();
|
|
|
|
auto* general_widget = new QWidget;
|
|
general_widget->setLayout(general_layout);
|
|
|
|
// Editor tab
|
|
auto* advanced_layout = new QVBoxLayout;
|
|
|
|
auto* default_group = new QGroupBox(tr("Default Config (Read Only)"));
|
|
auto* default_layout = new QVBoxLayout;
|
|
m_default_tab = new QTabWidget;
|
|
|
|
default_group->setLayout(default_layout);
|
|
default_layout->addWidget(m_default_tab);
|
|
|
|
auto* local_group = new QGroupBox(tr("User Config"));
|
|
auto* local_layout = new QVBoxLayout;
|
|
m_local_tab = new QTabWidget;
|
|
|
|
local_group->setLayout(local_layout);
|
|
local_layout->addWidget(m_local_tab);
|
|
|
|
advanced_layout->addWidget(default_group);
|
|
advanced_layout->addWidget(local_group);
|
|
|
|
auto* advanced_widget = new QWidget;
|
|
|
|
advanced_widget->setLayout(advanced_layout);
|
|
|
|
auto* layout = new QVBoxLayout;
|
|
auto* tab_widget = new QTabWidget;
|
|
tab_widget->addTab(general_widget, tr("General"));
|
|
|
|
// GFX settings tabs. Placed in a QWidget for consistent margins.
|
|
auto* gfx_tab_holder = new QWidget;
|
|
auto* gfx_layout = new QVBoxLayout;
|
|
gfx_tab_holder->setLayout(gfx_layout);
|
|
tab_widget->addTab(gfx_tab_holder, tr("Graphics"));
|
|
|
|
auto* gfx_tabs = new QTabWidget;
|
|
|
|
gfx_tabs->addTab(GetWrappedWidget(new GeneralWidget(this, m_layer.get()), this, 125, 100),
|
|
tr("General"));
|
|
gfx_tabs->addTab(GetWrappedWidget(new EnhancementsWidget(this, m_layer.get()), this, 125, 100),
|
|
tr("Enhancements"));
|
|
gfx_tabs->addTab(GetWrappedWidget(new HacksWidget(this, m_layer.get()), this, 125, 100),
|
|
tr("Hacks"));
|
|
gfx_tabs->addTab(GetWrappedWidget(new AdvancedWidget(this, m_layer.get()), this, 125, 100),
|
|
tr("Advanced"));
|
|
const int editor_index = tab_widget->addTab(advanced_widget, tr("Editor"));
|
|
gfx_layout->addWidget(gfx_tabs);
|
|
|
|
connect(tab_widget, &QTabWidget::currentChanged, this, [this, editor_index](int index) {
|
|
// Update the ini editor after editing other tabs.
|
|
if (index == editor_index)
|
|
{
|
|
// Layer only auto-saves when it is destroyed.
|
|
m_layer->Save();
|
|
|
|
// There can be multiple ini loaded for a game, only replace the one related to the game
|
|
// ini being edited.
|
|
for (int i = 0; i < m_local_tab->count(); i++)
|
|
{
|
|
if (m_local_tab->tabText(i).toStdString() == m_game_id + ".ini")
|
|
{
|
|
m_local_tab->removeTab(i);
|
|
|
|
auto* edit = new GameConfigEdit(
|
|
nullptr,
|
|
QString::fromStdString(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini"),
|
|
false);
|
|
|
|
m_local_tab->insertTab(i, edit, QString::fromStdString(m_game_id + ".ini"));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update other tabs after using ini editor.
|
|
if (m_prev_tab_index == editor_index)
|
|
{
|
|
// Load won't clear deleted keys, so everything is wiped before loading.
|
|
m_layer->DeleteAllKeys();
|
|
m_layer->Load();
|
|
Config::OnConfigChanged();
|
|
}
|
|
|
|
m_prev_tab_index = index;
|
|
});
|
|
|
|
const QString help_msg = tr(
|
|
"Italics mark default game settings, bold marks user settings.\nRight-click to remove user "
|
|
"settings.\nGraphics tabs don't display the value of a default game setting.\nAnti-Aliasing "
|
|
"settings are disabled when the global graphics backend doesn't "
|
|
"match the game setting.");
|
|
|
|
auto help_icon = style()->standardIcon(QStyle::SP_MessageBoxQuestion);
|
|
auto* help_label = new QLabel(tr("These settings override core Dolphin settings."));
|
|
help_label->setToolTip(help_msg);
|
|
auto help_label_icon = new QLabel();
|
|
help_label_icon->setPixmap(help_icon.pixmap(12, 12));
|
|
help_label_icon->setToolTip(help_msg);
|
|
auto* help_layout = new QHBoxLayout();
|
|
help_layout->addWidget(help_label);
|
|
help_layout->addWidget(help_label_icon);
|
|
help_layout->addStretch();
|
|
|
|
layout->addLayout(help_layout);
|
|
layout->addWidget(tab_widget);
|
|
setLayout(layout);
|
|
}
|
|
|
|
GameConfigWidget::~GameConfigWidget()
|
|
{
|
|
// Destructor saves the layer to file.
|
|
m_layer.reset();
|
|
|
|
// If a game is running and the game properties window is closed, update local game layer with
|
|
// any new changes. Not sure if doing it more frequently is safe.
|
|
auto local_layer = Config::GetLayer(Config::LayerType::LocalGame);
|
|
if (local_layer && SConfig::GetInstance().GetGameID() == m_game_id)
|
|
{
|
|
local_layer->DeleteAllKeys();
|
|
local_layer->Load();
|
|
Config::OnConfigChanged();
|
|
}
|
|
|
|
// Delete empty configs
|
|
if (File::GetSize(m_gameini_local_path.toStdString()) == 0)
|
|
File::Delete(m_gameini_local_path.toStdString());
|
|
}
|
|
|
|
void GameConfigWidget::LoadSettings()
|
|
{
|
|
// Load globals
|
|
auto update_bool = [this](auto config, bool reverse = false) {
|
|
const Config::Location& setting = config->GetLocation();
|
|
|
|
// Don't overwrite local with global
|
|
if (m_layer->Exists(setting) || !m_global_layer->Exists(setting))
|
|
return;
|
|
|
|
std::optional<bool> value = m_global_layer->Get<bool>(config->GetLocation());
|
|
|
|
if (value.has_value())
|
|
{
|
|
const QSignalBlocker blocker(config);
|
|
config->setChecked(value.value() ^ reverse);
|
|
}
|
|
};
|
|
|
|
auto update_int = [this](auto config) {
|
|
const Config::Location& setting = config->GetLocation();
|
|
|
|
if (m_layer->Exists(setting) || !m_global_layer->Exists(setting))
|
|
return;
|
|
|
|
std::optional<int> value = m_global_layer->Get<int>(setting);
|
|
|
|
if (value.has_value())
|
|
{
|
|
const QSignalBlocker blocker(config);
|
|
config->setValue(value.value());
|
|
}
|
|
};
|
|
|
|
for (ConfigBool* config : {m_enable_dual_core, m_enable_mmu, m_enable_fprf, m_sync_gpu,
|
|
m_use_dsp_hle, m_use_monoscopic_shadows})
|
|
{
|
|
update_bool(config);
|
|
}
|
|
|
|
update_bool(m_emulate_disc_speed, true);
|
|
|
|
update_int(m_depth_slider);
|
|
update_int(m_convergence_spin);
|
|
}
|
|
|
|
void GameConfigWidget::SetItalics()
|
|
{
|
|
// Mark system game settings with italics. Called once because it should never change.
|
|
auto italics = [this](auto config) {
|
|
if (!m_global_layer->Exists(config->GetLocation()))
|
|
return;
|
|
|
|
QFont ifont = config->font();
|
|
ifont.setItalic(true);
|
|
config->setFont(ifont);
|
|
};
|
|
|
|
for (auto* config : findChildren<ConfigBool*>())
|
|
italics(config);
|
|
for (auto* config : findChildren<ConfigSlider*>())
|
|
italics(config);
|
|
for (auto* config : findChildren<ConfigInteger*>())
|
|
italics(config);
|
|
for (auto* config : findChildren<ConfigRadioInt*>())
|
|
italics(config);
|
|
for (auto* config : findChildren<ConfigChoice*>())
|
|
italics(config);
|
|
for (auto* config : findChildren<ConfigStringChoice*>())
|
|
italics(config);
|
|
}
|