// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "yuzu/configuration/shared_widget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/assert.h" #include "common/common_types.h" #include "common/logging/log.h" #include "common/settings.h" #include "common/settings_common.h" #include "yuzu/configuration/shared_translation.h" namespace ConfigurationShared { static int restore_button_count = 0; static std::string RelevantDefault(const Settings::BasicSetting& setting) { return Settings::IsConfiguringGlobal() ? setting.DefaultToString() : setting.ToStringGlobal(); } static QString DefaultSuffix(QWidget* parent, Settings::BasicSetting& setting) { const auto tr = [parent](const char* text, const char* context) { return parent->tr(text, context); }; if ((setting.Specialization() & Settings::SpecializationAttributeMask) == Settings::Specialization::Percentage) { std::string context{fmt::format("{} percentage (e.g. 50%)", setting.GetLabel())}; return tr("%", context.c_str()); } return QStringLiteral(""); } QPushButton* Widget::CreateRestoreGlobalButton(bool using_global, QWidget* parent) { restore_button_count++; QStyle* style = parent->style(); QIcon* icon = new QIcon(style->standardIcon(QStyle::SP_LineEditClearButton)); QPushButton* restore_button = new QPushButton(*icon, QStringLiteral(""), parent); restore_button->setObjectName(QStringLiteral("RestoreButton%1").arg(restore_button_count)); restore_button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); // Workaround for dark theme causing min-width to be much larger than 0 restore_button->setStyleSheet( QStringLiteral("QAbstractButton#%1 { min-width: 0px }").arg(restore_button->objectName())); QSizePolicy sp_retain = restore_button->sizePolicy(); sp_retain.setRetainSizeWhenHidden(true); restore_button->setSizePolicy(sp_retain); restore_button->setEnabled(!using_global); restore_button->setVisible(!using_global); return restore_button; } QLabel* Widget::CreateLabel(const QString& text) { QLabel* qt_label = new QLabel(text, this->parent); qt_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); return qt_label; } QWidget* Widget::CreateCheckBox(Settings::BasicSetting* bool_setting, const QString& label, std::function& serializer, std::function& restore_func, const std::function& touch) { checkbox = new QCheckBox(label, this); checkbox->setCheckState(bool_setting->ToString() == "true" ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); checkbox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); if (!bool_setting->Save() && !Settings::IsConfiguringGlobal() && runtime_lock) { checkbox->setEnabled(false); } serializer = [this]() { return checkbox->checkState() == Qt::CheckState::Checked ? "true" : "false"; }; restore_func = [this, bool_setting]() { checkbox->setCheckState(RelevantDefault(*bool_setting) == "true" ? Qt::Checked : Qt::Unchecked); }; if (!Settings::IsConfiguringGlobal()) { QObject::connect(checkbox, &QCheckBox::clicked, [touch]() { touch(); }); } return checkbox; } QWidget* Widget::CreateCombobox(std::function& serializer, std::function& restore_func, const std::function& touch) { const auto type = setting.EnumIndex(); combobox = new QComboBox(this); combobox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); const ComboboxTranslations* enumeration{nullptr}; if (combobox_enumerations.contains(type)) { enumeration = &combobox_enumerations.at(type); for (const auto& [id, name] : *enumeration) { combobox->addItem(name); } } else { return combobox; } const auto find_index = [=](u32 value) -> int { for (u32 i = 0; i < enumeration->size(); i++) { if (enumeration->at(i).first == value) { return i; } } return -1; }; const u32 setting_value = std::stoi(setting.ToString()); combobox->setCurrentIndex(find_index(setting_value)); serializer = [this, enumeration]() { int current = combobox->currentIndex(); return std::to_string(enumeration->at(current).first); }; restore_func = [this, find_index]() { const u32 global_value = std::stoi(RelevantDefault(setting)); combobox->setCurrentIndex(find_index(global_value)); }; if (!Settings::IsConfiguringGlobal()) { QObject::connect(combobox, QOverload::of(&QComboBox::activated), [touch]() { touch(); }); } return combobox; } QWidget* Widget::CreateRadioGroup(std::function& serializer, std::function& restore_func, const std::function& touch) { const auto type = setting.EnumIndex(); QWidget* group = new QWidget(this); QHBoxLayout* layout = new QHBoxLayout(group); layout->setContentsMargins(0, 0, 0, 0); group->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); const ComboboxTranslations* enumeration{nullptr}; if (combobox_enumerations.contains(type)) { enumeration = &combobox_enumerations.at(type); for (const auto& [id, name] : *enumeration) { QRadioButton* radio_button = new QRadioButton(name, group); layout->addWidget(radio_button); radio_buttons.push_back({id, radio_button}); } } else { return group; } const auto get_selected = [=]() -> int { for (const auto& [id, button] : radio_buttons) { if (button->isChecked()) { return id; } } return -1; }; const auto set_index = [=](u32 value) { for (const auto& [id, button] : radio_buttons) { button->setChecked(id == value); } }; const u32 setting_value = std::stoi(setting.ToString()); set_index(setting_value); serializer = [get_selected]() { int current = get_selected(); return std::to_string(current); }; restore_func = [this, set_index]() { const u32 global_value = std::stoi(RelevantDefault(setting)); set_index(global_value); }; if (!Settings::IsConfiguringGlobal()) { for (const auto& [id, button] : radio_buttons) { QObject::connect(button, &QAbstractButton::clicked, [touch]() { touch(); }); } } return group; } QWidget* Widget::CreateLineEdit(std::function& serializer, std::function& restore_func, const std::function& touch, bool managed) { const QString text = QString::fromStdString(setting.ToString()); line_edit = new QLineEdit(this); line_edit->setText(text); serializer = [this]() { return line_edit->text().toStdString(); }; if (!managed) { return line_edit; } restore_func = [this]() { line_edit->setText(QString::fromStdString(RelevantDefault(setting))); }; if (!Settings::IsConfiguringGlobal()) { QObject::connect(line_edit, &QLineEdit::textChanged, [touch]() { touch(); }); } return line_edit; } QWidget* Widget::CreateSlider(bool reversed, float multiplier, const QString& given_suffix, std::function& serializer, std::function& restore_func, const std::function& touch) { if (!setting.Ranged()) { LOG_ERROR(Frontend, "\"{}\" is not a ranged setting, but a slider was requested.", setting.GetLabel()); return nullptr; } QWidget* container = new QWidget(this); QHBoxLayout* layout = new QHBoxLayout(container); slider = new QSlider(Qt::Horizontal, this); QLabel* feedback = new QLabel(this); layout->addWidget(slider); layout->addWidget(feedback); container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); layout->setContentsMargins(0, 0, 0, 0); int max_val = std::stoi(setting.MaxVal()); QString suffix = given_suffix == QStringLiteral("") ? DefaultSuffix(this, setting) : given_suffix; const QString use_format = QStringLiteral("%1").append(suffix); QObject::connect(slider, &QAbstractSlider::valueChanged, [=](int value) { int present = (reversed ? max_val - value : value) * multiplier + 0.5f; feedback->setText(use_format.arg(QVariant::fromValue(present).value())); }); slider->setMinimum(std::stoi(setting.MinVal())); slider->setMaximum(max_val); slider->setValue(std::stoi(setting.ToString())); slider->setInvertedAppearance(reversed); slider->setInvertedControls(reversed); serializer = [this]() { return std::to_string(slider->value()); }; restore_func = [this]() { slider->setValue(std::stoi(RelevantDefault(setting))); }; if (!Settings::IsConfiguringGlobal()) { QObject::connect(slider, &QAbstractSlider::actionTriggered, [touch]() { touch(); }); } return container; } QWidget* Widget::CreateSpinBox(const QString& given_suffix, std::function& serializer, std::function& restore_func, const std::function& touch) { const int min_val = setting.Ranged() ? std::stoi(setting.MinVal()) : std::numeric_limits::min(); const int max_val = setting.Ranged() ? std::stoi(setting.MaxVal()) : std::numeric_limits::max(); const int default_val = std::stoi(setting.ToString()); QString suffix = given_suffix == QStringLiteral("") ? DefaultSuffix(this, setting) : given_suffix; spinbox = new QSpinBox(this); spinbox->setRange(min_val, max_val); spinbox->setValue(default_val); spinbox->setSuffix(suffix); spinbox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); serializer = [this]() { return std::to_string(spinbox->value()); }; restore_func = [this]() { auto value{std::stol(RelevantDefault(setting))}; spinbox->setValue(value); }; if (!Settings::IsConfiguringGlobal()) { QObject::connect(spinbox, QOverload::of(&QSpinBox::valueChanged), [this, touch]() { if (spinbox->value() != std::stoi(setting.ToStringGlobal())) { touch(); } }); } return spinbox; } QWidget* Widget::CreateHexEdit(std::function& serializer, std::function& restore_func, const std::function& touch) { auto* data_component = CreateLineEdit(serializer, restore_func, touch, false); if (data_component == nullptr) { return nullptr; } auto to_hex = [=](const std::string& input) { return QString::fromStdString(fmt::format("{:08x}", std::stoul(input))); }; QRegularExpressionValidator* regex = new QRegularExpressionValidator( QRegularExpression{QStringLiteral("^[0-9a-fA-F]{0,8}$")}, line_edit); const QString default_val = to_hex(setting.ToString()); line_edit->setText(default_val); line_edit->setMaxLength(8); line_edit->setValidator(regex); auto hex_to_dec = [this]() -> std::string { return std::to_string(std::stoul(line_edit->text().toStdString(), nullptr, 16)); }; serializer = [hex_to_dec]() { return hex_to_dec(); }; restore_func = [this, to_hex]() { line_edit->setText(to_hex(RelevantDefault(setting))); }; if (!Settings::IsConfiguringGlobal()) { QObject::connect(line_edit, &QLineEdit::textChanged, [touch]() { touch(); }); } return line_edit; } QWidget* Widget::CreateDateTimeEdit(bool disabled, bool restrict, std::function& serializer, std::function& restore_func, const std::function& touch) { const long long current_time = QDateTime::currentSecsSinceEpoch(); const s64 the_time = disabled ? current_time : std::stoll(setting.ToString()); const auto default_val = QDateTime::fromSecsSinceEpoch(the_time); date_time_edit = new QDateTimeEdit(this); date_time_edit->setDateTime(default_val); date_time_edit->setMinimumDateTime(QDateTime::fromSecsSinceEpoch(0)); date_time_edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); serializer = [this]() { return std::to_string(date_time_edit->dateTime().toSecsSinceEpoch()); }; auto get_clear_val = [this, restrict, current_time]() { return QDateTime::fromSecsSinceEpoch([this, restrict, current_time]() { if (restrict && checkbox->checkState() == Qt::Checked) { return std::stoll(RelevantDefault(setting)); } return current_time; }()); }; restore_func = [this, get_clear_val]() { date_time_edit->setDateTime(get_clear_val()); }; if (!Settings::IsConfiguringGlobal()) { QObject::connect(date_time_edit, &QDateTimeEdit::editingFinished, [this, get_clear_val, touch]() { if (date_time_edit->dateTime() != get_clear_val()) { touch(); } }); } return date_time_edit; } void Widget::SetupComponent(const QString& label, std::function& load_func, bool managed, RequestType request, float multiplier, Settings::BasicSetting* other_setting, const QString& suffix) { created = true; const auto type = setting.TypeId(); QLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); if (other_setting == nullptr) { other_setting = setting.PairedSetting(); } const bool require_checkbox = other_setting != nullptr && other_setting->TypeId() == typeid(bool); if (other_setting != nullptr && other_setting->TypeId() != typeid(bool)) { LOG_WARNING( Frontend, "Extra setting \"{}\" specified but is not bool, refusing to create checkbox for it.", other_setting->GetLabel()); } std::function checkbox_serializer = []() -> std::string { return {}; }; std::function checkbox_restore_func = []() {}; std::function touch = []() {}; std::function serializer = []() -> std::string { return {}; }; std::function restore_func = []() {}; QWidget* data_component{nullptr}; request = [&]() { if (request != RequestType::Default) { return request; } switch (setting.Specialization() & Settings::SpecializationTypeMask) { case Settings::Specialization::Default: return RequestType::Default; case Settings::Specialization::Time: return RequestType::DateTimeEdit; case Settings::Specialization::Hex: return RequestType::HexEdit; case Settings::Specialization::RuntimeList: managed = false; [[fallthrough]]; case Settings::Specialization::List: return RequestType::ComboBox; case Settings::Specialization::Scalar: return RequestType::Slider; case Settings::Specialization::Countable: return RequestType::SpinBox; case Settings::Specialization::Radio: return RequestType::RadioGroup; default: break; } return request; }(); if (!Settings::IsConfiguringGlobal() && managed) { restore_button = CreateRestoreGlobalButton(setting.UsingGlobal(), this); touch = [this]() { LOG_DEBUG(Frontend, "Enabling custom setting for \"{}\"", setting.GetLabel()); restore_button->setEnabled(true); restore_button->setVisible(true); }; } if (require_checkbox) { QWidget* lhs = CreateCheckBox(other_setting, label, checkbox_serializer, checkbox_restore_func, touch); layout->addWidget(lhs); } else if (setting.TypeId() != typeid(bool)) { QLabel* qt_label = CreateLabel(label); layout->addWidget(qt_label); } if (setting.TypeId() == typeid(bool)) { data_component = CreateCheckBox(&setting, label, serializer, restore_func, touch); } else if (setting.IsEnum()) { if (request == RequestType::RadioGroup) { data_component = CreateRadioGroup(serializer, restore_func, touch); } else { data_component = CreateCombobox(serializer, restore_func, touch); } } else if (type == typeid(u32) || type == typeid(int) || type == typeid(u16) || type == typeid(s64) || type == typeid(u8)) { switch (request) { case RequestType::Slider: case RequestType::ReverseSlider: data_component = CreateSlider(request == RequestType::ReverseSlider, multiplier, suffix, serializer, restore_func, touch); break; case RequestType::Default: case RequestType::LineEdit: data_component = CreateLineEdit(serializer, restore_func, touch); break; case RequestType::DateTimeEdit: data_component = CreateDateTimeEdit(other_setting->ToString() != "true", true, serializer, restore_func, touch); break; case RequestType::SpinBox: data_component = CreateSpinBox(suffix, serializer, restore_func, touch); break; case RequestType::HexEdit: data_component = CreateHexEdit(serializer, restore_func, touch); break; case RequestType::ComboBox: data_component = CreateCombobox(serializer, restore_func, touch); break; default: UNIMPLEMENTED(); } } else if (type == typeid(std::string)) { switch (request) { case RequestType::Default: case RequestType::LineEdit: data_component = CreateLineEdit(serializer, restore_func, touch); break; case RequestType::ComboBox: data_component = CreateCombobox(serializer, restore_func, touch); break; default: UNIMPLEMENTED(); } } if (data_component == nullptr) { LOG_ERROR(Frontend, "Failed to create widget for \"{}\"", setting.GetLabel()); created = false; return; } layout->addWidget(data_component); if (!managed) { return; } if (Settings::IsConfiguringGlobal()) { load_func = [this, serializer, checkbox_serializer, require_checkbox, other_setting]() { if (require_checkbox && other_setting->UsingGlobal()) { other_setting->LoadString(checkbox_serializer()); } if (setting.UsingGlobal()) { setting.LoadString(serializer()); } }; } else { layout->addWidget(restore_button); QObject::connect(restore_button, &QAbstractButton::clicked, [this, restore_func, checkbox_restore_func](bool) { LOG_DEBUG(Frontend, "Restore global state for \"{}\"", setting.GetLabel()); restore_button->setEnabled(false); restore_button->setVisible(false); checkbox_restore_func(); restore_func(); }); load_func = [this, serializer, require_checkbox, checkbox_serializer, other_setting]() { bool using_global = !restore_button->isEnabled(); setting.SetGlobal(using_global); if (!using_global) { setting.LoadString(serializer()); } if (require_checkbox) { other_setting->SetGlobal(using_global); if (!using_global) { other_setting->LoadString(checkbox_serializer()); } } }; } if (other_setting != nullptr) { const auto reset = [restore_func, data_component](int state) { data_component->setEnabled(state == Qt::Checked); if (state != Qt::Checked) { restore_func(); } }; connect(checkbox, &QCheckBox::stateChanged, reset); reset(checkbox->checkState()); } } bool Widget::Valid() const { return created; } Widget::~Widget() = default; Widget::Widget(Settings::BasicSetting* setting_, const TranslationMap& translations_, const ComboboxTranslationMap& combobox_translations_, QWidget* parent_, bool runtime_lock_, std::vector>& apply_funcs_, RequestType request, bool managed, float multiplier, Settings::BasicSetting* other_setting, const QString& suffix) : QWidget(parent_), parent{parent_}, translations{translations_}, combobox_enumerations{combobox_translations_}, setting{*setting_}, apply_funcs{apply_funcs_}, runtime_lock{runtime_lock_} { if (!Settings::IsConfiguringGlobal() && !setting.Switchable()) { LOG_DEBUG(Frontend, "\"{}\" is not switchable, skipping...", setting.GetLabel()); return; } const int id = setting.Id(); const auto [label, tooltip] = [&]() { const auto& setting_label = setting.GetLabel(); if (translations.contains(id)) { return std::pair{translations.at(id).first, translations.at(id).second}; } LOG_WARNING(Frontend, "Translation table lacks entry for \"{}\"", setting_label); return std::pair{QString::fromStdString(setting_label), QStringLiteral("")}; }(); if (label == QStringLiteral("")) { LOG_DEBUG(Frontend, "Translation table has empty entry for \"{}\", skipping...", setting.GetLabel()); return; } std::function load_func = []() {}; SetupComponent(label, load_func, managed, request, multiplier, other_setting, suffix); if (!created) { LOG_WARNING(Frontend, "No widget was created for \"{}\"", setting.GetLabel()); return; } apply_funcs.push_back([load_func, setting_](bool powered_on) { if (setting_->RuntimeModfiable() || !powered_on) { load_func(); } }); bool enable = runtime_lock || setting.RuntimeModfiable(); if (setting.Switchable() && Settings::IsConfiguringGlobal() && !runtime_lock) { enable &= setting.UsingGlobal(); } this->setEnabled(enable); this->setToolTip(tooltip); } Builder::Builder(QWidget* parent_, bool runtime_lock_) : translations{InitializeTranslations(parent_)}, combobox_translations{ComboboxEnumeration(parent_)}, parent{parent_}, runtime_lock{ runtime_lock_} {} Builder::~Builder() = default; Widget* Builder::BuildWidget(Settings::BasicSetting* setting, std::vector>& apply_funcs, RequestType request, bool managed, float multiplier, Settings::BasicSetting* other_setting, const QString& suffix) const { if (!Settings::IsConfiguringGlobal() && !setting->Switchable()) { return nullptr; } if (setting->Specialization() == Settings::Specialization::Paired) { LOG_DEBUG(Frontend, "\"{}\" has specialization Paired: ignoring", setting->GetLabel()); return nullptr; } return new Widget(setting, *translations, *combobox_translations, parent, runtime_lock, apply_funcs, request, managed, multiplier, other_setting, suffix); } Widget* Builder::BuildWidget(Settings::BasicSetting* setting, std::vector>& apply_funcs, Settings::BasicSetting* other_setting, RequestType request, const QString& suffix) const { return BuildWidget(setting, apply_funcs, request, true, 1.0f, other_setting, suffix); } const ComboboxTranslationMap& Builder::ComboboxTranslations() const { return *combobox_translations; } } // namespace ConfigurationShared