duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

settingwidgetbinder.h (53886B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #pragma once
      5 
      6 #include "qthost.h"
      7 #include "qtutils.h"
      8 #include "settingswindow.h"
      9 
     10 #include "core/host.h"
     11 #include "core/settings.h"
     12 
     13 #include "common/assert.h"
     14 #include "common/file_system.h"
     15 #include "common/path.h"
     16 
     17 #include <QtCore/QtCore>
     18 #include <QtGui/QAction>
     19 #include <QtWidgets/QAbstractButton>
     20 #include <QtWidgets/QCheckBox>
     21 #include <QtWidgets/QComboBox>
     22 #include <QtWidgets/QDoubleSpinBox>
     23 #include <QtWidgets/QFileDialog>
     24 #include <QtWidgets/QLabel>
     25 #include <QtWidgets/QLineEdit>
     26 #include <QtWidgets/QMenu>
     27 #include <QtWidgets/QMessageBox>
     28 #include <QtWidgets/QSlider>
     29 #include <QtWidgets/QSpinBox>
     30 #include <optional>
     31 #include <type_traits>
     32 
     33 namespace SettingWidgetBinder {
     34 static constexpr const char* NULLABLE_PROPERTY = "SettingWidgetBinder_isNullable";
     35 static constexpr const char* IS_NULL_PROPERTY = "SettingWidgetBinder_isNull";
     36 static constexpr const char* GLOBAL_VALUE_PROPERTY = "SettingWidgetBinder_globalValue";
     37 
     38 template<typename T>
     39 struct SettingAccessor
     40 {
     41   static bool getBoolValue(const T* widget);
     42   static void setBoolValue(T* widget, bool value);
     43   static void makeNullableBool(T* widget, bool globalValue);
     44   static std::optional<bool> getNullableBoolValue(const T* widget);
     45   static void setNullableBoolValue(T* widget, std::optional<bool> value);
     46 
     47   static int getIntValue(const T* widget);
     48   static void setIntValue(T* widget, bool nullable, int value);
     49   static void makeNullableInt(T* widget, int globalValue);
     50   static std::optional<int> getNullableIntValue(const T* widget);
     51   static void setNullableIntValue(T* widget, std::optional<int> value);
     52 
     53   static int getFloatValue(const T* widget);
     54   static void setFloatValue(T* widget, int value);
     55   static void makeNullableFloat(T* widget, float globalValue);
     56   static std::optional<float> getNullableFloatValue(const T* widget);
     57   static void setNullableFloatValue(T* widget, std::optional<float> value);
     58 
     59   static QString getStringValue(const T* widget);
     60   static void setStringValue(T* widget, const QString& value);
     61   static void makeNullableString(T* widget, const QString& globalValue);
     62   static std::optional<QString> getNullableStringValue(const T* widget);
     63   static void setNullableFloatValue(T* widget, std::optional<QString> value);
     64 
     65   template<typename F>
     66   static void connectValueChanged(T* widget, F func);
     67 };
     68 
     69 template<>
     70 struct SettingAccessor<QLineEdit>
     71 {
     72   static bool getBoolValue(const QLineEdit* widget) { return widget->text().toInt() != 0; }
     73   static void setBoolValue(QLineEdit* widget, bool value)
     74   {
     75     widget->setText(value ? QStringLiteral("1") : QStringLiteral("0"));
     76   }
     77   static void makeNullableBool(QLineEdit* widget, bool globalValue) { widget->setEnabled(false); }
     78   static std::optional<bool> getNullableBoolValue(const QLineEdit* widget) { return getBoolValue(widget); }
     79   static void setNullableBoolValue(QLineEdit* widget, std::optional<bool> value)
     80   {
     81     setBoolValue(widget, value.value_or(false));
     82   }
     83 
     84   static int getIntValue(const QLineEdit* widget) { return widget->text().toInt(); }
     85   static void setIntValue(QLineEdit* widget, int value) { widget->setText(QString::number(value)); }
     86   static void makeNullableInt(QLineEdit* widget, int globalValue) { widget->setEnabled(false); }
     87   static std::optional<int> getNullableIntValue(const QLineEdit* widget) { return getIntValue(widget); }
     88   static void setNullableIntValue(QLineEdit* widget, std::optional<int> value)
     89   {
     90     setIntValue(widget, value.value_or(0));
     91   }
     92 
     93   static float getFloatValue(const QLineEdit* widget) { return widget->text().toFloat(); }
     94   static void setFloatValue(QLineEdit* widget, float value) { widget->setText(QString::number(value)); }
     95   static void makeNullableFloat(QLineEdit* widget, float globalValue) { widget->setEnabled(false); }
     96   static std::optional<float> getNullableFloatValue(const QLineEdit* widget) { return getFloatValue(widget); }
     97   static void setNullableFloatValue(QLineEdit* widget, std::optional<float> value)
     98   {
     99     setFloatValue(widget, value.value_or(0.0f));
    100   }
    101 
    102   static QString getStringValue(const QLineEdit* widget) { return widget->text(); }
    103   static void setStringValue(QLineEdit* widget, const QString& value) { widget->setText(value); }
    104   static void makeNullableString(QLineEdit* widget, const QString& globalValue) { widget->setEnabled(false); }
    105   static std::optional<QString> getNullableStringValue(const QLineEdit* widget) { return getStringValue(widget); }
    106   static void setNullableStringValue(QLineEdit* widget, std::optional<QString> value)
    107   {
    108     setStringValue(widget, value.value_or(QString()));
    109   }
    110 
    111   template<typename F>
    112   static void connectValueChanged(QLineEdit* widget, F func)
    113   {
    114     widget->connect(widget, &QLineEdit::textChanged, func);
    115   }
    116 };
    117 
    118 template<>
    119 struct SettingAccessor<QComboBox>
    120 {
    121   static bool isNullValue(const QComboBox* widget) { return (widget->currentIndex() == 0); }
    122 
    123   static bool getBoolValue(const QComboBox* widget) { return widget->currentIndex() > 0; }
    124   static void setBoolValue(QComboBox* widget, bool value) { widget->setCurrentIndex(value ? 1 : 0); }
    125   static void makeNullableBool(QComboBox* widget, bool globalValue)
    126   {
    127     widget->insertItem(0, globalValue ? qApp->translate("SettingsDialog", "Use Global Setting [Enabled]") :
    128                                         qApp->translate("SettingsDialog", "Use Global Setting [Disabled]"));
    129   }
    130 
    131   static int getIntValue(const QComboBox* widget) { return widget->currentIndex(); }
    132   static void setIntValue(QComboBox* widget, int value) { widget->setCurrentIndex(value); }
    133   static void makeNullableInt(QComboBox* widget, int globalValue)
    134   {
    135     widget->insertItem(
    136       0, qApp->translate("SettingsDialog", "Use Global Setting [%1]")
    137            .arg((globalValue >= 0 && globalValue < widget->count()) ? widget->itemText(globalValue) : QString()));
    138   }
    139   static std::optional<int> getNullableIntValue(const QComboBox* widget)
    140   {
    141     return isNullValue(widget) ? std::nullopt : std::optional<int>(widget->currentIndex() - 1);
    142   }
    143   static void setNullableIntValue(QComboBox* widget, std::optional<int> value)
    144   {
    145     widget->setCurrentIndex(value.has_value() ? (value.value() + 1) : 0);
    146   }
    147 
    148   static float getFloatValue(const QComboBox* widget) { return static_cast<float>(widget->currentIndex()); }
    149   static void setFloatValue(QComboBox* widget, float value) { widget->setCurrentIndex(static_cast<int>(value)); }
    150   static void makeNullableFloat(QComboBox* widget, float globalValue)
    151   {
    152     widget->insertItem(0, qApp->translate("SettingsDialog", "Use Global Setting [%1]")
    153                             .arg((globalValue >= 0.0f && static_cast<int>(globalValue) < widget->count()) ?
    154                                    widget->itemText(static_cast<int>(globalValue)) :
    155                                    QString()));
    156   }
    157   static std::optional<float> getNullableFloatValue(const QComboBox* widget)
    158   {
    159     return isNullValue(widget) ? std::nullopt : std::optional<float>(static_cast<float>(widget->currentIndex() + 1));
    160   }
    161   static void setNullableFloatValue(QComboBox* widget, std::optional<float> value)
    162   {
    163     widget->setCurrentIndex(value.has_value() ? static_cast<int>(value.value() + 1.0f) : 0);
    164   }
    165 
    166   static QString getStringValue(const QComboBox* widget)
    167   {
    168     const QVariant currentData(widget->currentData());
    169     if (currentData.metaType().id() == QMetaType::QString)
    170       return currentData.toString();
    171 
    172     return widget->currentText();
    173   }
    174   static void setStringValue(QComboBox* widget, const QString& value)
    175   {
    176     const int index = widget->findData(value);
    177     if (index >= 0)
    178     {
    179       widget->setCurrentIndex(index);
    180       return;
    181     }
    182 
    183     widget->setCurrentText(value);
    184   }
    185   static void makeNullableString(QComboBox* widget, const QString& globalValue)
    186   {
    187     makeNullableInt(widget, widget->findData(globalValue));
    188   }
    189   static std::optional<QString> getNullableStringValue(const QComboBox* widget)
    190   {
    191     return isNullValue(widget) ? std::nullopt : std::optional<QString>(getStringValue(widget));
    192   }
    193   static void setNullableStringValue(QComboBox* widget, std::optional<QString> value)
    194   {
    195     value.has_value() ? setStringValue(widget, value.value()) : widget->setCurrentIndex(0);
    196   }
    197 
    198   template<typename F>
    199   static void connectValueChanged(QComboBox* widget, F func)
    200   {
    201     widget->connect(widget, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), func);
    202   }
    203 };
    204 
    205 template<>
    206 struct SettingAccessor<QCheckBox>
    207 {
    208   static bool getBoolValue(const QCheckBox* widget) { return widget->isChecked(); }
    209   static void setBoolValue(QCheckBox* widget, bool value) { widget->setChecked(value); }
    210   static void makeNullableBool(QCheckBox* widget, bool globalValue) { widget->setTristate(true); }
    211   static std::optional<bool> getNullableBoolValue(const QCheckBox* widget)
    212   {
    213     return (widget->checkState() == Qt::PartiallyChecked) ? std::nullopt : std::optional<bool>(widget->isChecked());
    214   }
    215   static void setNullableBoolValue(QCheckBox* widget, std::optional<bool> value)
    216   {
    217     widget->setCheckState(value.has_value() ? (value.value() ? Qt::Checked : Qt::Unchecked) : Qt::PartiallyChecked);
    218   }
    219 
    220   static int getIntValue(const QCheckBox* widget) { return widget->isChecked() ? 1 : 0; }
    221   static void setIntValue(QCheckBox* widget, int value) { widget->setChecked(value != 0); }
    222   static void makeNullableInt(QCheckBox* widget, int globalValue) { widget->setTristate(true); }
    223   static std::optional<int> getNullableIntValue(const QCheckBox* widget)
    224   {
    225     return (widget->checkState() == Qt::PartiallyChecked) ? std::nullopt :
    226                                                             std::optional<int>(widget->isChecked() ? 1 : 0);
    227   }
    228   static void setNullableIntValue(QCheckBox* widget, std::optional<int> value)
    229   {
    230     widget->setCheckState(value.has_value() ? ((value.value() != 0) ? Qt::Checked : Qt::Unchecked) :
    231                                               Qt::PartiallyChecked);
    232   }
    233 
    234   static float getFloatValue(const QCheckBox* widget) { return widget->isChecked() ? 1.0f : 0.0f; }
    235   static void setFloatValue(QCheckBox* widget, float value) { widget->setChecked(value != 0.0f); }
    236   static void makeNullableFloat(QCheckBox* widget, float globalValue) { widget->setTristate(true); }
    237   static std::optional<float> getNullableFloatValue(const QCheckBox* widget)
    238   {
    239     return (widget->checkState() == Qt::PartiallyChecked) ? std::nullopt :
    240                                                             std::optional<float>(widget->isChecked() ? 1.0f : 0.0f);
    241   }
    242   static void setNullableFloatValue(QCheckBox* widget, std::optional<float> value)
    243   {
    244     widget->setCheckState(value.has_value() ? ((value.value() != 0.0f) ? Qt::Checked : Qt::Unchecked) :
    245                                               Qt::PartiallyChecked);
    246   }
    247 
    248   static QString getStringValue(const QCheckBox* widget)
    249   {
    250     return widget->isChecked() ? QStringLiteral("1") : QStringLiteral("0");
    251   }
    252   static void setStringValue(QCheckBox* widget, const QString& value) { widget->setChecked(value.toInt() != 0); }
    253   static void makeNullableString(QCheckBox* widget, const QString& globalValue) { widget->setTristate(true); }
    254   static std::optional<QString> getNullableStringValue(const QCheckBox* widget)
    255   {
    256     return (widget->checkState() == Qt::PartiallyChecked) ?
    257              std::nullopt :
    258              std::optional<QString>(widget->isChecked() ? QStringLiteral("1") : QStringLiteral("0"));
    259   }
    260   static void setNullableStringValue(QCheckBox* widget, std::optional<QString> value)
    261   {
    262     widget->setCheckState(value.has_value() ? ((value->toInt() != 0) ? Qt::Checked : Qt::Unchecked) :
    263                                               Qt::PartiallyChecked);
    264   }
    265 
    266   template<typename F>
    267   static void connectValueChanged(QCheckBox* widget, F func)
    268   {
    269     widget->connect(widget, &QCheckBox::checkStateChanged, func);
    270   }
    271 };
    272 
    273 template<>
    274 struct SettingAccessor<QSlider>
    275 {
    276   static bool isNullable(const QSlider* widget) { return widget->property(NULLABLE_PROPERTY).toBool(); }
    277 
    278   static bool getBoolValue(const QSlider* widget) { return widget->value() > 0; }
    279   static void setBoolValue(QSlider* widget, bool value) { widget->setValue(value ? 1 : 0); }
    280   static void makeNullableBool(QSlider* widget, bool globalSetting)
    281   {
    282     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    283     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalSetting));
    284   }
    285   static std::optional<bool> getNullableBoolValue(const QSlider* widget)
    286   {
    287     if (widget->property(IS_NULL_PROPERTY).toBool())
    288       return std::nullopt;
    289 
    290     return getBoolValue(widget);
    291   }
    292   static void setNullableBoolValue(QSlider* widget, std::optional<bool> value)
    293   {
    294     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    295     setBoolValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toBool());
    296   }
    297 
    298   static int getIntValue(const QSlider* widget) { return widget->value(); }
    299   static void setIntValue(QSlider* widget, int value) { widget->setValue(value); }
    300   static void makeNullableInt(QSlider* widget, int globalValue)
    301   {
    302     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    303     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalValue));
    304   }
    305   static std::optional<int> getNullableIntValue(const QSlider* widget)
    306   {
    307     if (widget->property(IS_NULL_PROPERTY).toBool())
    308       return std::nullopt;
    309 
    310     return getIntValue(widget);
    311   }
    312   static void setNullableIntValue(QSlider* widget, std::optional<int> value)
    313   {
    314     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    315     setIntValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toInt());
    316   }
    317 
    318   static float getFloatValue(const QSlider* widget) { return static_cast<float>(widget->value()); }
    319   static void setFloatValue(QSlider* widget, float value) { widget->setValue(static_cast<int>(value)); }
    320   static void makeNullableFloat(QSlider* widget, float globalValue) { widget->setEnabled(false); }
    321   static std::optional<float> getNullableFloatValue(const QSlider* widget)
    322   {
    323     if (widget->property(IS_NULL_PROPERTY).toBool())
    324       return std::nullopt;
    325 
    326     return getFloatValue(widget);
    327   }
    328   static void setNullableFloatValue(QSlider* widget, std::optional<float> value)
    329   {
    330     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    331     setFloatValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toFloat());
    332   }
    333 
    334   static QString getStringValue(const QSlider* widget) { return QString::number(widget->value()); }
    335   static void setStringValue(QSlider* widget, const QString& value) { widget->setValue(value.toInt()); }
    336   static void makeNullableString(QSlider* widget, const QString& globalValue) { widget->setEnabled(false); }
    337   static std::optional<QString> getNullableStringValue(const QSlider* widget)
    338   {
    339     if (widget->property(IS_NULL_PROPERTY).toBool())
    340       return std::nullopt;
    341 
    342     return getStringValue(widget);
    343   }
    344   static void setNullableStringValue(QSlider* widget, std::optional<QString> value)
    345   {
    346     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    347     setStringValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toString());
    348   }
    349 
    350   template<typename F>
    351   static void connectValueChanged(QSlider* widget, F func)
    352   {
    353     if (!isNullable(widget))
    354     {
    355       widget->connect(widget, &QSlider::valueChanged, func);
    356     }
    357     else
    358     {
    359       widget->setContextMenuPolicy(Qt::CustomContextMenu);
    360       widget->connect(widget, &QSlider::customContextMenuRequested, widget, [widget, func](const QPoint& pt) {
    361         QMenu menu(widget);
    362         widget->connect(menu.addAction(qApp->translate("SettingWidgetBinder", "Reset")), &QAction::triggered, widget,
    363                         [widget, func = std::move(func)]() {
    364                           const bool old = widget->blockSignals(true);
    365                           setNullableIntValue(widget, std::nullopt);
    366                           widget->blockSignals(old);
    367                           func();
    368                         });
    369         menu.exec(widget->mapToGlobal(pt));
    370       });
    371       widget->connect(widget, &QSlider::valueChanged, widget, [widget, func = std::move(func)]() {
    372         if (widget->property(IS_NULL_PROPERTY).toBool())
    373           widget->setProperty(IS_NULL_PROPERTY, QVariant(false));
    374         func();
    375       });
    376     }
    377   }
    378 };
    379 
    380 template<>
    381 struct SettingAccessor<QSpinBox>
    382 {
    383   static bool isNullable(const QSpinBox* widget) { return widget->property(NULLABLE_PROPERTY).toBool(); }
    384 
    385   static void updateFont(QSpinBox* widget, bool isNull)
    386   {
    387     // We should be able to use QFont here.. but it doesn't update on change.
    388     widget->setStyleSheet(isNull ? QStringLiteral("font-style: italic;") : QString());
    389     widget->setPrefix(isNull ? qApp->translate("SettingWidgetBinder", "Default: ") : QString());
    390   }
    391 
    392   static bool getBoolValue(const QSpinBox* widget) { return widget->value() > 0; }
    393   static void setBoolValue(QSpinBox* widget, bool value) { widget->setValue(value ? 1 : 0); }
    394   static void makeNullableBool(QSpinBox* widget, bool globalSetting)
    395   {
    396     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    397     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalSetting));
    398   }
    399   static std::optional<bool> getNullableBoolValue(const QSpinBox* widget)
    400   {
    401     if (widget->property(IS_NULL_PROPERTY).toBool())
    402       return std::nullopt;
    403 
    404     return getBoolValue(widget);
    405   }
    406   static void setNullableBoolValue(QSpinBox* widget, std::optional<bool> value)
    407   {
    408     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    409     setBoolValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toBool());
    410     updateFont(widget, !value.has_value());
    411   }
    412 
    413   static int getIntValue(const QSpinBox* widget) { return widget->value(); }
    414   static void setIntValue(QSpinBox* widget, int value) { widget->setValue(value); }
    415   static void makeNullableInt(QSpinBox* widget, int globalValue)
    416   {
    417     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    418     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalValue));
    419   }
    420   static std::optional<int> getNullableIntValue(const QSpinBox* widget)
    421   {
    422     if (widget->property(IS_NULL_PROPERTY).toBool())
    423       return std::nullopt;
    424 
    425     return getIntValue(widget);
    426   }
    427   static void setNullableIntValue(QSpinBox* widget, std::optional<int> value)
    428   {
    429     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    430     setIntValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toInt());
    431     updateFont(widget, !value.has_value());
    432   }
    433 
    434   static float getFloatValue(const QSpinBox* widget) { return static_cast<float>(widget->value()); }
    435   static void setFloatValue(QSpinBox* widget, float value) { widget->setValue(static_cast<int>(value)); }
    436   static void makeNullableFloat(QSpinBox* widget, float globalValue)
    437   {
    438     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    439     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalValue));
    440   }
    441   static std::optional<float> getNullableFloatValue(const QSpinBox* widget)
    442   {
    443     if (widget->property(IS_NULL_PROPERTY).toBool())
    444       return std::nullopt;
    445 
    446     return getFloatValue(widget);
    447   }
    448   static void setNullableFloatValue(QSpinBox* widget, std::optional<float> value)
    449   {
    450     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    451     setFloatValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toFloat());
    452     updateFont(widget, !value.has_value());
    453   }
    454 
    455   static QString getStringValue(const QSpinBox* widget) { return QString::number(widget->value()); }
    456   static void setStringValue(QSpinBox* widget, const QString& value) { widget->setValue(value.toInt()); }
    457   static void makeNullableString(QSpinBox* widget, const QString& globalValue)
    458   {
    459     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    460     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalValue));
    461   }
    462   static std::optional<QString> getNullableStringValue(const QSpinBox* widget)
    463   {
    464     if (widget->property(IS_NULL_PROPERTY).toBool())
    465       return std::nullopt;
    466 
    467     return getStringValue(widget);
    468   }
    469   static void setNullableStringValue(QSpinBox* widget, std::optional<QString> value)
    470   {
    471     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    472     setStringValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toString());
    473     updateFont(widget, !value.has_value());
    474   }
    475 
    476   template<typename F>
    477   static void connectValueChanged(QSpinBox* widget, F func)
    478   {
    479     if (!isNullable(widget))
    480     {
    481       widget->connect(widget, QOverload<int>::of(&QSpinBox::valueChanged), func);
    482     }
    483     else
    484     {
    485       widget->setContextMenuPolicy(Qt::CustomContextMenu);
    486       widget->connect(widget, &QSpinBox::customContextMenuRequested, widget, [widget, func](const QPoint& pt) {
    487         QMenu menu(widget);
    488         widget->connect(menu.addAction(qApp->translate("SettingWidgetBinder", "Reset")), &QAction::triggered, widget,
    489                         [widget, func = std::move(func)]() {
    490                           const bool old = widget->blockSignals(true);
    491                           setNullableIntValue(widget, std::nullopt);
    492                           widget->blockSignals(old);
    493                           updateFont(widget, true);
    494                           func();
    495                         });
    496         menu.exec(widget->mapToGlobal(pt));
    497       });
    498       widget->connect(widget, &QSpinBox::valueChanged, widget, [widget, func = std::move(func)]() {
    499         if (widget->property(IS_NULL_PROPERTY).toBool())
    500         {
    501           widget->setProperty(IS_NULL_PROPERTY, QVariant(false));
    502           updateFont(widget, false);
    503         }
    504         func();
    505       });
    506     }
    507   }
    508 };
    509 
    510 template<>
    511 struct SettingAccessor<QDoubleSpinBox>
    512 {
    513   static bool isNullable(const QDoubleSpinBox* widget) { return widget->property(NULLABLE_PROPERTY).toBool(); }
    514 
    515   static void updateFont(QDoubleSpinBox* widget, bool isNull)
    516   {
    517     // We should be able to use QFont here.. but it doesn't update on change.
    518     widget->setStyleSheet(isNull ? QStringLiteral("font-style: italic;") : QString());
    519     widget->setPrefix(isNull ? qApp->translate("SettingWidgetBinder", "Default: ") : QString());
    520   }
    521 
    522   static bool getBoolValue(const QDoubleSpinBox* widget) { return widget->value() > 0.0; }
    523   static void setBoolValue(QDoubleSpinBox* widget, bool value) { widget->setValue(value ? 1.0 : 0.0); }
    524   static void makeNullableBool(QDoubleSpinBox* widget, bool globalSetting)
    525   {
    526     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    527     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalSetting));
    528   }
    529   static std::optional<bool> getNullableBoolValue(const QDoubleSpinBox* widget)
    530   {
    531     if (widget->property(IS_NULL_PROPERTY).toBool())
    532       return std::nullopt;
    533 
    534     return getBoolValue(widget);
    535   }
    536   static void setNullableBoolValue(QDoubleSpinBox* widget, std::optional<bool> value)
    537   {
    538     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    539     setBoolValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toBool());
    540     updateFont(widget, !value.has_value());
    541   }
    542 
    543   static int getIntValue(const QDoubleSpinBox* widget) { return static_cast<int>(widget->value()); }
    544   static void setIntValue(QDoubleSpinBox* widget, int value) { widget->setValue(static_cast<double>(value)); }
    545   static void makeNullableInt(QDoubleSpinBox* widget, int globalValue)
    546   {
    547     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    548     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalValue));
    549   }
    550   static std::optional<int> getNullableIntValue(const QDoubleSpinBox* widget)
    551   {
    552     if (widget->property(IS_NULL_PROPERTY).toBool())
    553       return std::nullopt;
    554 
    555     return getIntValue(widget);
    556   }
    557   static void setNullableIntValue(QDoubleSpinBox* widget, std::optional<int> value)
    558   {
    559     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    560     setIntValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toInt());
    561     updateFont(widget, !value.has_value());
    562   }
    563 
    564   static float getFloatValue(const QDoubleSpinBox* widget) { return static_cast<float>(widget->value()); }
    565   static void setFloatValue(QDoubleSpinBox* widget, float value) { widget->setValue(static_cast<double>(value)); }
    566   static void makeNullableFloat(QDoubleSpinBox* widget, float globalValue)
    567   {
    568     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    569     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalValue));
    570   }
    571   static std::optional<float> getNullableFloatValue(const QDoubleSpinBox* widget)
    572   {
    573     if (widget->property(IS_NULL_PROPERTY).toBool())
    574       return std::nullopt;
    575 
    576     return getFloatValue(widget);
    577   }
    578   static void setNullableFloatValue(QDoubleSpinBox* widget, std::optional<float> value)
    579   {
    580     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    581     setFloatValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toFloat());
    582     updateFont(widget, !value.has_value());
    583   }
    584 
    585   static QString getStringValue(const QDoubleSpinBox* widget) { return QString::number(widget->value()); }
    586   static void setStringValue(QDoubleSpinBox* widget, const QString& value) { widget->setValue(value.toDouble()); }
    587   static void makeNullableString(QDoubleSpinBox* widget, const QString& globalValue)
    588   {
    589     widget->setProperty(NULLABLE_PROPERTY, QVariant(true));
    590     widget->setProperty(GLOBAL_VALUE_PROPERTY, QVariant(globalValue));
    591   }
    592   static std::optional<QString> getNullableStringValue(const QDoubleSpinBox* widget)
    593   {
    594     if (widget->property(IS_NULL_PROPERTY).toBool())
    595       return std::nullopt;
    596 
    597     return getStringValue(widget);
    598   }
    599   static void setNullableStringValue(QDoubleSpinBox* widget, std::optional<QString> value)
    600   {
    601     widget->setProperty(IS_NULL_PROPERTY, QVariant(!value.has_value()));
    602     setStringValue(widget, value.has_value() ? value.value() : widget->property(GLOBAL_VALUE_PROPERTY).toString());
    603     updateFont(widget, !value.has_value());
    604   }
    605 
    606   template<typename F>
    607   static void connectValueChanged(QDoubleSpinBox* widget, F func)
    608   {
    609     if (!isNullable(widget))
    610     {
    611       widget->connect(widget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), func);
    612     }
    613     else
    614     {
    615       widget->setContextMenuPolicy(Qt::CustomContextMenu);
    616       widget->connect(widget, &QDoubleSpinBox::customContextMenuRequested, widget, [widget, func](const QPoint& pt) {
    617         QMenu menu(widget);
    618         widget->connect(menu.addAction(qApp->translate("SettingWidgetBinder", "Reset")), &QAction::triggered, widget,
    619                         [widget, func = std::move(func)]() {
    620                           const bool old = widget->blockSignals(true);
    621                           setNullableFloatValue(widget, std::nullopt);
    622                           widget->blockSignals(old);
    623                           updateFont(widget, true);
    624                           func();
    625                         });
    626         menu.exec(widget->mapToGlobal(pt));
    627       });
    628       widget->connect(widget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), widget,
    629                       [widget, func = std::move(func)]() {
    630                         if (widget->property(IS_NULL_PROPERTY).toBool())
    631                         {
    632                           widget->setProperty(IS_NULL_PROPERTY, QVariant(false));
    633                           updateFont(widget, false);
    634                         }
    635                         func();
    636                       });
    637     }
    638   }
    639 };
    640 
    641 template<>
    642 struct SettingAccessor<QAction>
    643 {
    644   static bool getBoolValue(const QAction* widget) { return widget->isChecked(); }
    645   static void setBoolValue(QAction* widget, bool value) { widget->setChecked(value); }
    646   static void makeNullableBool(QAction* widget, bool globalSetting) { widget->setEnabled(false); }
    647   static std::optional<bool> getNullableBoolValue(const QAction* widget) { return getBoolValue(widget); }
    648   static void setNullableBoolValue(QAction* widget, std::optional<bool> value)
    649   {
    650     setBoolValue(widget, value.value_or(false));
    651   }
    652 
    653   static int getIntValue(const QAction* widget) { return widget->isChecked() ? 1 : 0; }
    654   static void setIntValue(QAction* widget, int value) { widget->setChecked(value != 0); }
    655   static void makeNullableInt(QAction* widget, int globalValue) { widget->setEnabled(false); }
    656   static std::optional<int> getNullableIntValue(const QAction* widget) { return getIntValue(widget); }
    657   static void setNullableIntValue(QAction* widget, std::optional<int> value) { setIntValue(widget, value.value_or(0)); }
    658 
    659   static float getFloatValue(const QAction* widget) { return widget->isChecked() ? 1.0f : 0.0f; }
    660   static void setFloatValue(QAction* widget, float value) { widget->setChecked(value != 0.0f); }
    661   static void makeNullableFloat(QAction* widget, float globalValue) { widget->setEnabled(false); }
    662   static std::optional<float> getNullableFloatValue(const QAction* widget) { return getFloatValue(widget); }
    663   static void setNullableFloatValue(QAction* widget, std::optional<float> value)
    664   {
    665     setFloatValue(widget, value.value_or(0.0f));
    666   }
    667 
    668   static QString getStringValue(const QAction* widget)
    669   {
    670     return widget->isChecked() ? QStringLiteral("1") : QStringLiteral("0");
    671   }
    672   static void setStringValue(QAction* widget, const QString& value) { widget->setChecked(value.toInt() != 0); }
    673   static void makeNullableString(QAction* widget, const QString& globalValue) { widget->setEnabled(false); }
    674   static std::optional<QString> getNullableStringValue(const QAction* widget) { return getStringValue(widget); }
    675   static void setNullableStringValue(QAction* widget, std::optional<QString> value)
    676   {
    677     setStringValue(widget, value.value_or(QString()));
    678   }
    679 
    680   template<typename F>
    681   static void connectValueChanged(QAction* widget, F func)
    682   {
    683     widget->connect(widget, &QAction::toggled, func);
    684   }
    685 };
    686 
    687 /// Binds a widget's value to a setting, updating it when the value changes.
    688 
    689 template<typename WidgetType>
    690 static void BindWidgetToBoolSetting(SettingsInterface* sif, WidgetType* widget, std::string section, std::string key,
    691                                     bool default_value)
    692 {
    693   using Accessor = SettingAccessor<WidgetType>;
    694 
    695   const bool value = Host::GetBaseBoolSettingValue(section.c_str(), key.c_str(), default_value);
    696 
    697   if (sif)
    698   {
    699     Accessor::makeNullableBool(widget, value);
    700 
    701     bool sif_value;
    702     if (sif->GetBoolValue(section.c_str(), key.c_str(), &sif_value))
    703       Accessor::setNullableBoolValue(widget, sif_value);
    704     else
    705       Accessor::setNullableBoolValue(widget, std::nullopt);
    706 
    707     Accessor::connectValueChanged(widget, [sif, widget, section = std::move(section), key = std::move(key)]() {
    708       if (std::optional<bool> new_value = Accessor::getNullableBoolValue(widget); new_value.has_value())
    709         sif->SetBoolValue(section.c_str(), key.c_str(), new_value.value());
    710       else
    711         sif->DeleteValue(section.c_str(), key.c_str());
    712 
    713       QtHost::SaveGameSettings(sif, true);
    714       g_emu_thread->reloadGameSettings();
    715     });
    716   }
    717   else
    718   {
    719     Accessor::setBoolValue(widget, value);
    720 
    721     Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() {
    722       const bool new_value = Accessor::getBoolValue(widget);
    723       Host::SetBaseBoolSettingValue(section.c_str(), key.c_str(), new_value);
    724       Host::CommitBaseSettingChanges();
    725       g_emu_thread->applySettings();
    726     });
    727   }
    728 }
    729 
    730 template<typename WidgetType>
    731 static void BindWidgetToIntSetting(SettingsInterface* sif, WidgetType* widget, std::string section, std::string key,
    732                                    int default_value, int option_offset = 0)
    733 {
    734   using Accessor = SettingAccessor<WidgetType>;
    735 
    736   const s32 value =
    737     Host::GetBaseIntSettingValue(section.c_str(), key.c_str(), static_cast<s32>(default_value)) - option_offset;
    738 
    739   if (sif)
    740   {
    741     Accessor::makeNullableInt(widget, value);
    742 
    743     int sif_value;
    744     if (sif->GetIntValue(section.c_str(), key.c_str(), &sif_value))
    745       Accessor::setNullableIntValue(widget, sif_value - option_offset);
    746     else
    747       Accessor::setNullableIntValue(widget, std::nullopt);
    748 
    749     Accessor::connectValueChanged(
    750       widget, [sif, widget, section = std::move(section), key = std::move(key), option_offset]() {
    751         if (std::optional<int> new_value = Accessor::getNullableIntValue(widget); new_value.has_value())
    752           sif->SetIntValue(section.c_str(), key.c_str(), new_value.value() + option_offset);
    753         else
    754           sif->DeleteValue(section.c_str(), key.c_str());
    755 
    756         QtHost::SaveGameSettings(sif, true);
    757         g_emu_thread->reloadGameSettings();
    758       });
    759   }
    760   else
    761   {
    762     Accessor::setIntValue(widget, static_cast<int>(value));
    763 
    764     Accessor::connectValueChanged(
    765       widget, [widget, section = std::move(section), key = std::move(key), option_offset]() {
    766         const int new_value = Accessor::getIntValue(widget);
    767         Host::SetBaseIntSettingValue(section.c_str(), key.c_str(), new_value + option_offset);
    768         Host::CommitBaseSettingChanges();
    769         g_emu_thread->applySettings();
    770       });
    771   }
    772 }
    773 
    774 template<typename WidgetType>
    775 static inline void BindWidgetAndLabelToIntSetting(SettingsInterface* sif, WidgetType* widget, QLabel* label,
    776                                                   const QString& label_suffix, std::string section, std::string key,
    777                                                   int default_value, int option_offset = 0)
    778 {
    779   using Accessor = SettingAccessor<WidgetType>;
    780 
    781   const s32 global_value =
    782     Host::GetBaseIntSettingValue(section.c_str(), key.c_str(), static_cast<s32>(default_value)) - option_offset;
    783 
    784   if (sif)
    785   {
    786     QFont orig_font(label->font());
    787     QFont bold_font(orig_font);
    788     bold_font.setBold(true);
    789 
    790     Accessor::makeNullableInt(widget, global_value);
    791 
    792     int sif_value;
    793     if (sif->GetIntValue(section.c_str(), key.c_str(), &sif_value))
    794     {
    795       Accessor::setNullableIntValue(widget, sif_value - option_offset);
    796       if (label)
    797       {
    798         label->setText(QStringLiteral("%1%2").arg(sif_value).arg(label_suffix));
    799         label->setFont(bold_font);
    800       }
    801     }
    802     else
    803     {
    804       Accessor::setNullableIntValue(widget, std::nullopt);
    805       if (label)
    806         label->setText(QStringLiteral("%1%2").arg(global_value).arg(label_suffix));
    807     }
    808 
    809     Accessor::connectValueChanged(widget, [sif, widget, label, label_suffix, section = std::move(section),
    810                                            key = std::move(key), option_offset, global_value,
    811                                            bold_font = std::move(bold_font), orig_font = std::move(orig_font)]() {
    812       if (std::optional<int> new_value = Accessor::getNullableIntValue(widget); new_value.has_value())
    813       {
    814         sif->SetIntValue(section.c_str(), key.c_str(), new_value.value() + option_offset);
    815         if (label)
    816         {
    817           label->setFont(bold_font);
    818           label->setText(QStringLiteral("%1%2").arg(new_value.value()).arg(label_suffix));
    819         }
    820       }
    821       else
    822       {
    823         sif->DeleteValue(section.c_str(), key.c_str());
    824         if (label)
    825         {
    826           label->setFont(orig_font);
    827           label->setText(QStringLiteral("%1%2").arg(global_value).arg(label_suffix));
    828         }
    829       }
    830 
    831       QtHost::SaveGameSettings(sif, true);
    832       g_emu_thread->reloadGameSettings();
    833     });
    834   }
    835   else
    836   {
    837     Accessor::setIntValue(widget, static_cast<int>(global_value));
    838 
    839     if (label)
    840       label->setText(QStringLiteral("%1%2").arg(global_value).arg(label_suffix));
    841 
    842     Accessor::connectValueChanged(
    843       widget, [widget, label, label_suffix, section = std::move(section), key = std::move(key), option_offset]() {
    844         const int new_value = Accessor::getIntValue(widget);
    845         Host::SetBaseIntSettingValue(section.c_str(), key.c_str(), new_value + option_offset);
    846         Host::CommitBaseSettingChanges();
    847         g_emu_thread->applySettings();
    848 
    849         if (label)
    850           label->setText(QStringLiteral("%1%2").arg(new_value).arg(label_suffix));
    851       });
    852   }
    853 }
    854 
    855 template<typename WidgetType>
    856 static void BindWidgetToFloatSetting(SettingsInterface* sif, WidgetType* widget, std::string section, std::string key,
    857                                      float default_value)
    858 {
    859   using Accessor = SettingAccessor<WidgetType>;
    860 
    861   const float value = Host::GetBaseFloatSettingValue(section.c_str(), key.c_str(), default_value);
    862 
    863   if (sif)
    864   {
    865     Accessor::makeNullableFloat(widget, value);
    866 
    867     float sif_value;
    868     if (sif->GetFloatValue(section.c_str(), key.c_str(), &sif_value))
    869       Accessor::setNullableFloatValue(widget, sif_value);
    870     else
    871       Accessor::setNullableFloatValue(widget, std::nullopt);
    872 
    873     Accessor::connectValueChanged(widget, [sif, widget, section = std::move(section), key = std::move(key)]() {
    874       if (std::optional<float> new_value = Accessor::getNullableFloatValue(widget); new_value.has_value())
    875         sif->SetFloatValue(section.c_str(), key.c_str(), new_value.value());
    876       else
    877         sif->DeleteValue(section.c_str(), key.c_str());
    878 
    879       QtHost::SaveGameSettings(sif, true);
    880       g_emu_thread->reloadGameSettings();
    881     });
    882   }
    883   else
    884   {
    885     Accessor::setFloatValue(widget, value);
    886 
    887     Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() {
    888       const float new_value = Accessor::getFloatValue(widget);
    889       Host::SetBaseFloatSettingValue(section.c_str(), key.c_str(), new_value);
    890       Host::CommitBaseSettingChanges();
    891       g_emu_thread->applySettings();
    892     });
    893   }
    894 }
    895 
    896 template<typename WidgetType>
    897 static void BindWidgetToNormalizedSetting(SettingsInterface* sif, WidgetType* widget, std::string section,
    898                                           std::string key, float range, float default_value)
    899 {
    900   using Accessor = SettingAccessor<WidgetType>;
    901 
    902   const float value = Host::GetBaseFloatSettingValue(section.c_str(), key.c_str(), default_value);
    903 
    904   if (sif)
    905   {
    906     Accessor::makeNullableInt(widget, static_cast<int>(value * range));
    907 
    908     float sif_value;
    909     if (sif->GetFloatValue(section.c_str(), key.c_str(), &sif_value))
    910       Accessor::setNullableIntValue(widget, static_cast<int>(sif_value * range));
    911     else
    912       Accessor::setNullableIntValue(widget, std::nullopt);
    913 
    914     Accessor::connectValueChanged(widget, [sif, widget, section = std::move(section), key = std::move(key), range]() {
    915       if (std::optional<int> new_value = Accessor::getNullableIntValue(widget); new_value.has_value())
    916         sif->SetFloatValue(section.c_str(), key.c_str(), static_cast<float>(new_value.value()) / range);
    917       else
    918         sif->DeleteValue(section.c_str(), key.c_str());
    919 
    920       QtHost::SaveGameSettings(sif, true);
    921       g_emu_thread->reloadGameSettings();
    922     });
    923   }
    924   else
    925   {
    926     Accessor::setIntValue(widget, static_cast<int>(value * range));
    927 
    928     Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), range]() {
    929       const float new_value = (static_cast<float>(Accessor::getIntValue(widget)) / range);
    930       Host::SetBaseFloatSettingValue(section.c_str(), key.c_str(), new_value);
    931       Host::CommitBaseSettingChanges();
    932       g_emu_thread->applySettings();
    933     });
    934   }
    935 }
    936 
    937 template<typename WidgetType>
    938 static void BindWidgetToStringSetting(SettingsInterface* sif, WidgetType* widget, std::string section, std::string key,
    939                                       std::string default_value = std::string())
    940 {
    941   using Accessor = SettingAccessor<WidgetType>;
    942 
    943   const QString value(
    944     QString::fromStdString(Host::GetBaseStringSettingValue(section.c_str(), key.c_str(), default_value.c_str())));
    945 
    946   if (sif)
    947   {
    948     Accessor::makeNullableString(widget, value);
    949 
    950     std::string sif_value;
    951     if (sif->GetStringValue(section.c_str(), key.c_str(), &sif_value))
    952       Accessor::setNullableStringValue(widget, QString::fromStdString(sif_value));
    953     else
    954       Accessor::setNullableStringValue(widget, std::nullopt);
    955 
    956     Accessor::connectValueChanged(widget, [widget, sif, section = std::move(section), key = std::move(key)]() {
    957       if (std::optional<QString> new_value = Accessor::getNullableStringValue(widget); new_value.has_value())
    958         sif->SetStringValue(section.c_str(), key.c_str(), new_value->toUtf8().constData());
    959       else
    960         sif->DeleteValue(section.c_str(), key.c_str());
    961 
    962       QtHost::SaveGameSettings(sif, true);
    963       g_emu_thread->reloadGameSettings();
    964     });
    965   }
    966   else
    967   {
    968     Accessor::setStringValue(widget, value);
    969 
    970     Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() {
    971       const QString new_value = Accessor::getStringValue(widget);
    972       if (!new_value.isEmpty())
    973         Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), new_value.toUtf8().constData());
    974       else
    975         Host::DeleteBaseSettingValue(section.c_str(), key.c_str());
    976 
    977       Host::CommitBaseSettingChanges();
    978       g_emu_thread->applySettings();
    979     });
    980   }
    981 }
    982 
    983 template<typename WidgetType, typename DataType>
    984 static void BindWidgetToEnumSetting(SettingsInterface* sif, WidgetType* widget, std::string section, std::string key,
    985                                     std::optional<DataType> (*from_string_function)(const char* str),
    986                                     const char* (*to_string_function)(DataType value), DataType default_value)
    987 {
    988   using Accessor = SettingAccessor<WidgetType>;
    989   using UnderlyingType = std::underlying_type_t<DataType>;
    990 
    991   const std::string value(
    992     Host::GetBaseStringSettingValue(section.c_str(), key.c_str(), to_string_function(default_value)));
    993   const std::optional<DataType> typed_value = from_string_function(value.c_str());
    994 
    995   if (sif)
    996   {
    997     Accessor::makeNullableInt(
    998       widget, typed_value.has_value() ? static_cast<int>(static_cast<UnderlyingType>(typed_value.value())) : 0);
    999 
   1000     std::string sif_value;
   1001     if (sif->GetStringValue(section.c_str(), key.c_str(), &sif_value))
   1002     {
   1003       const std::optional<DataType> old_setting_value = from_string_function(sif_value.c_str());
   1004       if (old_setting_value.has_value())
   1005         Accessor::setNullableIntValue(widget, static_cast<int>(static_cast<UnderlyingType>(old_setting_value.value())));
   1006       else
   1007         Accessor::setNullableIntValue(widget, std::nullopt);
   1008     }
   1009     else
   1010     {
   1011       Accessor::setNullableIntValue(widget, std::nullopt);
   1012     }
   1013 
   1014     Accessor::connectValueChanged(
   1015       widget, [sif, widget, section = std::move(section), key = std::move(key), to_string_function]() {
   1016         if (std::optional<int> new_value = Accessor::getNullableIntValue(widget); new_value.has_value())
   1017         {
   1018           const char* string_value =
   1019             to_string_function(static_cast<DataType>(static_cast<UnderlyingType>(new_value.value())));
   1020           sif->SetStringValue(section.c_str(), key.c_str(), string_value);
   1021         }
   1022         else
   1023         {
   1024           sif->DeleteValue(section.c_str(), key.c_str());
   1025         }
   1026 
   1027         QtHost::SaveGameSettings(sif, true);
   1028         g_emu_thread->reloadGameSettings();
   1029       });
   1030   }
   1031   else
   1032   {
   1033     if (typed_value.has_value())
   1034       Accessor::setIntValue(widget, static_cast<int>(static_cast<UnderlyingType>(typed_value.value())));
   1035     else
   1036       Accessor::setIntValue(widget, static_cast<int>(static_cast<UnderlyingType>(default_value)));
   1037 
   1038     Accessor::connectValueChanged(
   1039       widget, [widget, section = std::move(section), key = std::move(key), to_string_function]() {
   1040         const DataType value = static_cast<DataType>(static_cast<UnderlyingType>(Accessor::getIntValue(widget)));
   1041         const char* string_value = to_string_function(value);
   1042         Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), string_value);
   1043         Host::CommitBaseSettingChanges();
   1044         g_emu_thread->applySettings();
   1045       });
   1046   }
   1047 }
   1048 
   1049 template<typename WidgetType, typename DataType>
   1050 static void BindWidgetToEnumSetting(SettingsInterface* sif, WidgetType* widget, std::string section, std::string key,
   1051                                     const char** enum_names, DataType default_value)
   1052 {
   1053   using Accessor = SettingAccessor<WidgetType>;
   1054   using UnderlyingType = std::underlying_type_t<DataType>;
   1055 
   1056   const std::string value(Host::GetBaseStringSettingValue(section.c_str(), key.c_str(),
   1057                                                           enum_names[static_cast<UnderlyingType>(default_value)]));
   1058 
   1059   UnderlyingType enum_index = static_cast<UnderlyingType>(default_value);
   1060   for (UnderlyingType i = 0; enum_names[i] != nullptr; i++)
   1061   {
   1062     if (value == enum_names[i])
   1063     {
   1064       enum_index = i;
   1065       break;
   1066     }
   1067   }
   1068 
   1069   if (sif)
   1070   {
   1071     Accessor::makeNullableInt(widget, static_cast<int>(enum_index));
   1072 
   1073     std::string sif_value;
   1074     std::optional<int> sif_int_value;
   1075     if (sif->GetStringValue(section.c_str(), key.c_str(), &sif_value))
   1076     {
   1077       for (UnderlyingType i = 0; enum_names[i] != nullptr; i++)
   1078       {
   1079         if (sif_value == enum_names[i])
   1080         {
   1081           sif_int_value = static_cast<int>(i);
   1082           break;
   1083         }
   1084       }
   1085     }
   1086     Accessor::setNullableIntValue(widget, sif_int_value);
   1087 
   1088     Accessor::connectValueChanged(
   1089       widget, [sif, widget, section = std::move(section), key = std::move(key), enum_names]() {
   1090         if (std::optional<int> new_value = Accessor::getNullableIntValue(widget); new_value.has_value())
   1091           sif->SetStringValue(section.c_str(), key.c_str(), enum_names[new_value.value()]);
   1092         else
   1093           sif->DeleteValue(section.c_str(), key.c_str());
   1094 
   1095         QtHost::SaveGameSettings(sif, true);
   1096         g_emu_thread->reloadGameSettings();
   1097       });
   1098   }
   1099   else
   1100   {
   1101     Accessor::setIntValue(widget, static_cast<int>(enum_index));
   1102 
   1103     Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), enum_names]() {
   1104       const UnderlyingType value = static_cast<UnderlyingType>(Accessor::getIntValue(widget));
   1105       Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), enum_names[value]);
   1106       Host::CommitBaseSettingChanges();
   1107       g_emu_thread->applySettings();
   1108     });
   1109   }
   1110 }
   1111 
   1112 template<typename WidgetType>
   1113 static void BindWidgetToEnumSetting(SettingsInterface* sif, WidgetType* widget, std::string section, std::string key,
   1114                                     const char** enum_names, const char** enum_values, const char* default_value,
   1115                                     const char* translation_ctx = nullptr)
   1116 {
   1117   using Accessor = SettingAccessor<WidgetType>;
   1118 
   1119   const std::string value = Host::GetBaseStringSettingValue(section.c_str(), key.c_str(), default_value);
   1120 
   1121   for (int i = 0; enum_names[i] != nullptr; i++)
   1122   {
   1123     widget->addItem(translation_ctx ? qApp->translate(translation_ctx, enum_names[i]) :
   1124                                       QString::fromUtf8(enum_names[i]));
   1125   }
   1126 
   1127   int enum_index = -1;
   1128   for (int i = 0; enum_values[i] != nullptr; i++)
   1129   {
   1130     if (value == enum_values[i])
   1131     {
   1132       enum_index = i;
   1133       break;
   1134     }
   1135   }
   1136 
   1137   if (sif)
   1138   {
   1139     Accessor::makeNullableInt(widget, enum_index);
   1140 
   1141     std::string sif_value;
   1142     std::optional<int> sif_int_value;
   1143     if (sif->GetStringValue(section.c_str(), key.c_str(), &sif_value))
   1144     {
   1145       for (int i = 0; enum_values[i] != nullptr; i++)
   1146       {
   1147         if (sif_value == enum_values[i])
   1148         {
   1149           sif_int_value = i;
   1150           break;
   1151         }
   1152       }
   1153     }
   1154     Accessor::setNullableIntValue(widget, sif_int_value);
   1155 
   1156     Accessor::connectValueChanged(
   1157       widget, [sif, widget, section = std::move(section), key = std::move(key), enum_values]() {
   1158         if (std::optional<int> new_value = Accessor::getNullableIntValue(widget); new_value.has_value())
   1159           sif->SetStringValue(section.c_str(), key.c_str(), enum_values[new_value.value()]);
   1160         else
   1161           sif->DeleteValue(section.c_str(), key.c_str());
   1162 
   1163         QtHost::SaveGameSettings(sif, true);
   1164         g_emu_thread->reloadGameSettings();
   1165       });
   1166   }
   1167   else
   1168   {
   1169     if (enum_index >= 0)
   1170       Accessor::setIntValue(widget, enum_index);
   1171 
   1172     Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), enum_values]() {
   1173       const int value = Accessor::getIntValue(widget);
   1174       Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), enum_values[value]);
   1175       Host::CommitBaseSettingChanges();
   1176       g_emu_thread->applySettings();
   1177     });
   1178   }
   1179 }
   1180 
   1181 static inline void BindWidgetToFolderSetting(SettingsInterface* sif, QLineEdit* widget, QAbstractButton* browse_button,
   1182                                              QString browse_title, QAbstractButton* open_button,
   1183                                              QAbstractButton* reset_button, std::string section, std::string key,
   1184                                              std::string default_value, bool use_relative = true)
   1185 {
   1186   using Accessor = SettingAccessor<QLineEdit>;
   1187 
   1188   std::string current_path(Host::GetBaseStringSettingValue(section.c_str(), key.c_str(), default_value.c_str()));
   1189   if (current_path.empty())
   1190     current_path = default_value;
   1191   else if (use_relative && !Path::IsAbsolute(current_path))
   1192     current_path = Path::Canonicalize(Path::Combine(EmuFolders::DataRoot, current_path));
   1193   const QString value(QString::fromStdString(current_path));
   1194   Accessor::setStringValue(widget, value);
   1195   // if we're doing per-game settings, disable the widget, we only allow folder changes in the base config
   1196   if (sif)
   1197   {
   1198     widget->setEnabled(false);
   1199     if (browse_button)
   1200       browse_button->setEnabled(false);
   1201     if (reset_button)
   1202       reset_button->setEnabled(false);
   1203     return;
   1204   }
   1205 
   1206   auto value_changed = [widget, section = std::move(section), key = std::move(key), default_value, use_relative]() {
   1207     const std::string new_value(widget->text().toStdString());
   1208     if (!new_value.empty())
   1209     {
   1210       if (FileSystem::DirectoryExists(new_value.c_str()) ||
   1211           QMessageBox::question(
   1212             QtUtils::GetRootWidget(widget), qApp->translate("SettingWidgetBinder", "Confirm Folder"),
   1213             qApp
   1214               ->translate(
   1215                 "SettingWidgetBinder",
   1216                 "The chosen directory does not currently exist:\n\n%1\n\nDo you want to create this directory?")
   1217               .arg(QString::fromStdString(new_value)),
   1218             QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes)
   1219       {
   1220         if (use_relative)
   1221         {
   1222           const std::string relative_path(Path::MakeRelative(new_value, EmuFolders::DataRoot));
   1223           Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), relative_path.c_str());
   1224         }
   1225         else
   1226         {
   1227           Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), new_value.c_str());
   1228         }
   1229 
   1230         Host::CommitBaseSettingChanges();
   1231         g_emu_thread->updateEmuFolders();
   1232         return;
   1233       }
   1234     }
   1235     else
   1236     {
   1237       QMessageBox::critical(QtUtils::GetRootWidget(widget), qApp->translate("SettingWidgetBinder", "Error"),
   1238                             qApp->translate("SettingWidgetBinder", "Folder path cannot be empty."));
   1239     }
   1240 
   1241     // reset to old value
   1242     std::string current_path(Host::GetBaseStringSettingValue(section.c_str(), key.c_str(), default_value.c_str()));
   1243     if (current_path.empty())
   1244       current_path = default_value;
   1245     else if (use_relative && !Path::IsAbsolute(current_path))
   1246       current_path = Path::Canonicalize(Path::Combine(EmuFolders::DataRoot, current_path));
   1247 
   1248     widget->setText(QString::fromStdString(current_path));
   1249   };
   1250 
   1251   if (browse_button)
   1252   {
   1253     QObject::connect(browse_button, &QAbstractButton::clicked, browse_button,
   1254                      [widget, browse_title = std::move(browse_title), value_changed]() {
   1255                        const QString path = QDir::toNativeSeparators(
   1256                          QFileDialog::getExistingDirectory(QtUtils::GetRootWidget(widget), browse_title));
   1257                        if (path.isEmpty())
   1258                          return;
   1259 
   1260                        widget->setText(path);
   1261                        value_changed();
   1262                      });
   1263   }
   1264   if (open_button)
   1265   {
   1266     QObject::connect(open_button, &QAbstractButton::clicked, open_button, [widget]() {
   1267       QString path(Accessor::getStringValue(widget));
   1268       if (!path.isEmpty())
   1269         QtUtils::OpenURL(QtUtils::GetRootWidget(widget), QUrl::fromLocalFile(path));
   1270     });
   1271   }
   1272   if (reset_button)
   1273   {
   1274     QObject::connect(reset_button, &QAbstractButton::clicked, reset_button,
   1275                      [widget, default_value = std::move(default_value), value_changed]() {
   1276                        widget->setText(QString::fromStdString(default_value));
   1277                        value_changed();
   1278                      });
   1279   }
   1280 
   1281   widget->connect(widget, &QLineEdit::editingFinished, widget, std::move(value_changed));
   1282 }
   1283 
   1284 template<typename WidgetType>
   1285 static inline void SetAvailability(WidgetType* widget, bool available)
   1286 {
   1287   if (available)
   1288     return;
   1289 
   1290   widget->disconnect();
   1291 
   1292   if constexpr (std::is_same_v<WidgetType, QComboBox>)
   1293   {
   1294     widget->clear();
   1295     widget->addItem(qApp->translate("SettingWidgetBinder", "Incompatible with this game."));
   1296   }
   1297   else if constexpr (std::is_same_v<WidgetType, QLineEdit>)
   1298   {
   1299     widget->setText(qApp->translate("SettingWidgetBinder", "Incompatible with this game."));
   1300   }
   1301   else if constexpr (std::is_same_v<WidgetType, QCheckBox>)
   1302   {
   1303     widget->setText(widget->text() + qApp->translate("SettingWidgetBinder", " [incompatible]"));
   1304     widget->setCheckState(Qt::Unchecked);
   1305   }
   1306   else if constexpr (std::is_same_v<WidgetType, QSlider>)
   1307   {
   1308     widget->setTickPosition(0);
   1309   }
   1310   else if constexpr (std::is_same_v<WidgetType, QSpinBox> || std::is_same_v<WidgetType, QDoubleSpinBox>)
   1311   {
   1312     widget->setValue(0);
   1313   }
   1314 
   1315   widget->setEnabled(false);
   1316 }
   1317 
   1318 } // namespace SettingWidgetBinder