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

controllerbindingwidgets.cpp (32029B)


      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 #include "controllerbindingwidgets.h"
      5 #include "controllersettingswindow.h"
      6 #include "controllersettingwidgetbinder.h"
      7 #include "qthost.h"
      8 #include "qtutils.h"
      9 #include "settingswindow.h"
     10 #include "settingwidgetbinder.h"
     11 
     12 #include "ui_controllerbindingwidget_analog_controller.h"
     13 #include "ui_controllerbindingwidget_analog_joystick.h"
     14 #include "ui_controllerbindingwidget_digital_controller.h"
     15 #include "ui_controllerbindingwidget_guncon.h"
     16 #include "ui_controllerbindingwidget_justifier.h"
     17 #include "ui_controllerbindingwidget_mouse.h"
     18 #include "ui_controllerbindingwidget_negcon.h"
     19 #include "ui_controllerbindingwidget_negconrumble.h"
     20 
     21 #include "core/controller.h"
     22 #include "core/host.h"
     23 
     24 #include "util/input_manager.h"
     25 
     26 #include "common/log.h"
     27 #include "common/string_util.h"
     28 
     29 #include <QtWidgets/QCheckBox>
     30 #include <QtWidgets/QDoubleSpinBox>
     31 #include <QtWidgets/QInputDialog>
     32 #include <QtWidgets/QLineEdit>
     33 #include <QtWidgets/QMenu>
     34 #include <QtWidgets/QMessageBox>
     35 #include <QtWidgets/QScrollArea>
     36 #include <QtWidgets/QSpinBox>
     37 #include <algorithm>
     38 
     39 Log_SetChannel(ControllerBindingWidget);
     40 
     41 ControllerBindingWidget::ControllerBindingWidget(QWidget* parent, ControllerSettingsWindow* dialog, u32 port)
     42   : QWidget(parent), m_dialog(dialog), m_config_section(Controller::GetSettingsSection(port)), m_port_number(port)
     43 {
     44   m_ui.setupUi(this);
     45   populateControllerTypes();
     46   populateWidgets();
     47 
     48   connect(m_ui.controllerType, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
     49           &ControllerBindingWidget::onTypeChanged);
     50   connect(m_ui.bindings, &QPushButton::clicked, this, &ControllerBindingWidget::onBindingsClicked);
     51   connect(m_ui.settings, &QPushButton::clicked, this, &ControllerBindingWidget::onSettingsClicked);
     52   connect(m_ui.macros, &QPushButton::clicked, this, &ControllerBindingWidget::onMacrosClicked);
     53   connect(m_ui.automaticBinding, &QPushButton::clicked, this, &ControllerBindingWidget::onAutomaticBindingClicked);
     54   connect(m_ui.clearBindings, &QPushButton::clicked, this, &ControllerBindingWidget::onClearBindingsClicked);
     55 }
     56 
     57 ControllerBindingWidget::~ControllerBindingWidget() = default;
     58 
     59 void ControllerBindingWidget::populateControllerTypes()
     60 {
     61   for (u32 i = 0; i < static_cast<u32>(ControllerType::Count); i++)
     62   {
     63     const ControllerType ctype = static_cast<ControllerType>(i);
     64     const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(ctype);
     65     if (!cinfo)
     66       continue;
     67 
     68     m_ui.controllerType->addItem(QString::fromUtf8(cinfo->GetDisplayName()), QVariant(static_cast<int>(i)));
     69   }
     70 
     71   m_controller_info = Controller::GetControllerInfo(
     72     m_dialog->getStringValue(m_config_section.c_str(), "Type", Controller::GetDefaultPadType(m_port_number)));
     73   if (!m_controller_info)
     74   {
     75     m_controller_info = Controller::GetControllerInfo(m_port_number == 0 ? Settings::DEFAULT_CONTROLLER_1_TYPE :
     76                                                                            Settings::DEFAULT_CONTROLLER_2_TYPE);
     77   }
     78 
     79   const int index = m_ui.controllerType->findData(QVariant(static_cast<int>(m_controller_info->type)));
     80   if (index >= 0 && index != m_ui.controllerType->currentIndex())
     81   {
     82     QSignalBlocker sb(m_ui.controllerType);
     83     m_ui.controllerType->setCurrentIndex(index);
     84   }
     85 }
     86 
     87 void ControllerBindingWidget::populateWidgets()
     88 {
     89   const bool is_initializing = (m_ui.stackedWidget->count() == 0);
     90   if (m_bindings_widget)
     91   {
     92     m_ui.stackedWidget->removeWidget(m_bindings_widget);
     93     delete m_bindings_widget;
     94     m_bindings_widget = nullptr;
     95   }
     96   if (m_settings_widget)
     97   {
     98     m_ui.stackedWidget->removeWidget(m_settings_widget);
     99     delete m_settings_widget;
    100     m_settings_widget = nullptr;
    101   }
    102   if (m_macros_widget)
    103   {
    104     m_ui.stackedWidget->removeWidget(m_macros_widget);
    105     delete m_macros_widget;
    106     m_macros_widget = nullptr;
    107   }
    108 
    109   const bool has_settings = !m_controller_info->settings.empty();
    110   const bool has_macros = !m_controller_info->bindings.empty();
    111   m_ui.settings->setEnabled(has_settings);
    112   m_ui.macros->setEnabled(has_macros);
    113 
    114   m_bindings_widget = new QWidget(this);
    115   switch (m_controller_info->type)
    116   {
    117     case ControllerType::AnalogController:
    118     {
    119       Ui::ControllerBindingWidget_AnalogController ui;
    120       ui.setupUi(m_bindings_widget);
    121       bindBindingWidgets(m_bindings_widget);
    122       m_icon = QIcon::fromTheme(QStringLiteral("controller-line"));
    123     }
    124     break;
    125 
    126     case ControllerType::AnalogJoystick:
    127     {
    128       Ui::ControllerBindingWidget_AnalogJoystick ui;
    129       ui.setupUi(m_bindings_widget);
    130       bindBindingWidgets(m_bindings_widget);
    131       m_icon = QIcon::fromTheme(QStringLiteral("joystick-line"));
    132     }
    133     break;
    134 
    135     case ControllerType::DigitalController:
    136     {
    137       Ui::ControllerBindingWidget_DigitalController ui;
    138       ui.setupUi(m_bindings_widget);
    139       bindBindingWidgets(m_bindings_widget);
    140       m_icon = QIcon::fromTheme(QStringLiteral("controller-digital-line"));
    141     }
    142     break;
    143 
    144     case ControllerType::GunCon:
    145     {
    146       Ui::ControllerBindingWidget_GunCon ui;
    147       ui.setupUi(m_bindings_widget);
    148       bindBindingWidgets(m_bindings_widget);
    149       m_icon = QIcon::fromTheme(QStringLiteral("guncon-line"));
    150     }
    151     break;
    152 
    153     case ControllerType::NeGcon:
    154     {
    155       Ui::ControllerBindingWidget_NeGcon ui;
    156       ui.setupUi(m_bindings_widget);
    157       bindBindingWidgets(m_bindings_widget);
    158       m_icon = QIcon::fromTheme(QStringLiteral("negcon-line"));
    159     }
    160     break;
    161 
    162     case ControllerType::NeGconRumble:
    163     {
    164       Ui::ControllerBindingWidget_NeGconRumble ui;
    165       ui.setupUi(m_bindings_widget);
    166       bindBindingWidgets(m_bindings_widget);
    167       m_icon = QIcon::fromTheme(QStringLiteral("negcon-line"));
    168     }
    169     break;
    170 
    171     case ControllerType::PlayStationMouse:
    172     {
    173       Ui::ControllerBindingWidget_Mouse ui;
    174       ui.setupUi(m_bindings_widget);
    175       bindBindingWidgets(m_bindings_widget);
    176       m_icon = QIcon::fromTheme(QStringLiteral("mouse-line"));
    177     }
    178     break;
    179 
    180     case ControllerType::Justifier:
    181     {
    182       Ui::ControllerBindingWidget_Justifier ui;
    183       ui.setupUi(m_bindings_widget);
    184       bindBindingWidgets(m_bindings_widget);
    185       m_icon = QIcon::fromTheme(QStringLiteral("guncon-line"));
    186     }
    187     break;
    188 
    189     case ControllerType::None:
    190     {
    191       m_icon = QIcon::fromTheme(QStringLiteral("controller-strike-line"));
    192     }
    193     break;
    194 
    195     default:
    196     {
    197       createBindingWidgets(m_bindings_widget);
    198       m_icon = QIcon::fromTheme(QStringLiteral("controller-line"));
    199     }
    200     break;
    201   }
    202 
    203   m_ui.stackedWidget->addWidget(m_bindings_widget);
    204   m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);
    205 
    206   if (has_settings)
    207   {
    208     m_settings_widget = new ControllerCustomSettingsWidget(this);
    209     m_ui.stackedWidget->addWidget(m_settings_widget);
    210   }
    211 
    212   if (has_macros)
    213   {
    214     m_macros_widget = new ControllerMacroWidget(this);
    215     m_ui.stackedWidget->addWidget(m_macros_widget);
    216   }
    217 
    218   updateHeaderToolButtons();
    219 
    220   // no need to do this on first init, only changes
    221   if (!is_initializing)
    222     m_dialog->updateListDescription(m_port_number, this);
    223 }
    224 
    225 void ControllerBindingWidget::updateHeaderToolButtons()
    226 {
    227   const QWidget* current_widget = m_ui.stackedWidget->currentWidget();
    228   const QSignalBlocker bindings_sb(m_ui.bindings);
    229   const QSignalBlocker settings_sb(m_ui.settings);
    230   const QSignalBlocker macros_sb(m_ui.macros);
    231 
    232   const bool is_bindings = (current_widget == m_bindings_widget);
    233   m_ui.bindings->setChecked(is_bindings);
    234   m_ui.automaticBinding->setEnabled(is_bindings);
    235   m_ui.clearBindings->setEnabled(is_bindings);
    236   m_ui.macros->setChecked(current_widget == m_macros_widget);
    237   m_ui.settings->setChecked((current_widget == m_settings_widget));
    238 }
    239 
    240 void ControllerBindingWidget::onTypeChanged()
    241 {
    242   bool ok;
    243   const int index = m_ui.controllerType->currentData().toInt(&ok);
    244   if (!ok || index < 0 || index >= static_cast<int>(ControllerType::Count))
    245     return;
    246 
    247   m_controller_info = Controller::GetControllerInfo(static_cast<ControllerType>(index));
    248   DebugAssert(m_controller_info);
    249 
    250   SettingsInterface* sif = m_dialog->getEditingSettingsInterface();
    251   if (sif)
    252   {
    253     sif->SetStringValue(m_config_section.c_str(), "Type", m_controller_info->name);
    254     QtHost::SaveGameSettings(sif, false);
    255     g_emu_thread->reloadGameSettings();
    256   }
    257   else
    258   {
    259     Host::SetBaseStringSettingValue(m_config_section.c_str(), "Type", m_controller_info->name);
    260     Host::CommitBaseSettingChanges();
    261     g_emu_thread->applySettings();
    262   }
    263 
    264   populateWidgets();
    265 }
    266 
    267 void ControllerBindingWidget::onAutomaticBindingClicked()
    268 {
    269   QMenu menu(this);
    270   bool added = false;
    271 
    272   for (const auto& [identifier, device_name] : m_dialog->getDeviceList())
    273   {
    274     // we set it as data, because the device list could get invalidated while the menu is up
    275     const QString qidentifier = QString::fromStdString(identifier);
    276     QAction* action =
    277       menu.addAction(QStringLiteral("%1 (%2)").arg(qidentifier).arg(QString::fromStdString(device_name)));
    278     action->setData(qidentifier);
    279     connect(action, &QAction::triggered, this,
    280             [this, action]() { doDeviceAutomaticBinding(action->data().toString()); });
    281     added = true;
    282   }
    283 
    284   if (!added)
    285   {
    286     QAction* action = menu.addAction(tr("No devices available"));
    287     action->setEnabled(false);
    288   }
    289 
    290   menu.exec(QCursor::pos());
    291 }
    292 
    293 void ControllerBindingWidget::onClearBindingsClicked()
    294 {
    295   if (QMessageBox::question(
    296         QtUtils::GetRootWidget(this), tr("Clear Mapping"),
    297         tr("Are you sure you want to clear all mappings for this controller? This action cannot be undone.")) !=
    298       QMessageBox::Yes)
    299   {
    300     return;
    301   }
    302 
    303   if (m_dialog->isEditingGlobalSettings())
    304   {
    305     auto lock = Host::GetSettingsLock();
    306     InputManager::ClearPortBindings(*Host::Internal::GetBaseSettingsLayer(), m_port_number);
    307   }
    308   else
    309   {
    310     InputManager::ClearPortBindings(*m_dialog->getEditingSettingsInterface(), m_port_number);
    311   }
    312 
    313   saveAndRefresh();
    314 }
    315 
    316 void ControllerBindingWidget::onBindingsClicked()
    317 {
    318   m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);
    319   updateHeaderToolButtons();
    320 }
    321 
    322 void ControllerBindingWidget::onSettingsClicked()
    323 {
    324   if (!m_settings_widget)
    325     return;
    326 
    327   m_ui.stackedWidget->setCurrentWidget(m_settings_widget);
    328   updateHeaderToolButtons();
    329 }
    330 
    331 void ControllerBindingWidget::onMacrosClicked()
    332 {
    333   if (!m_macros_widget)
    334     return;
    335 
    336   m_ui.stackedWidget->setCurrentWidget(m_macros_widget);
    337   updateHeaderToolButtons();
    338 }
    339 
    340 void ControllerBindingWidget::doDeviceAutomaticBinding(const QString& device)
    341 {
    342   std::vector<std::pair<GenericInputBinding, std::string>> mapping =
    343     InputManager::GetGenericBindingMapping(device.toStdString());
    344   if (mapping.empty())
    345   {
    346     QMessageBox::critical(
    347       QtUtils::GetRootWidget(this), tr("Automatic Mapping"),
    348       tr("No generic bindings were generated for device '%1'. The controller/source may not support automatic mapping.")
    349         .arg(device));
    350     return;
    351   }
    352 
    353   bool result;
    354   if (m_dialog->isEditingGlobalSettings())
    355   {
    356     auto lock = Host::GetSettingsLock();
    357     result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), m_port_number, mapping);
    358   }
    359   else
    360   {
    361     result = InputManager::MapController(*m_dialog->getEditingSettingsInterface(), m_port_number, mapping);
    362     QtHost::SaveGameSettings(m_dialog->getEditingSettingsInterface(), false);
    363     g_emu_thread->reloadInputBindings();
    364   }
    365 
    366   // force a refresh after mapping
    367   if (result)
    368     saveAndRefresh();
    369 }
    370 
    371 void ControllerBindingWidget::saveAndRefresh()
    372 {
    373   onTypeChanged();
    374   QtHost::QueueSettingsSave();
    375   g_emu_thread->applySettings();
    376 }
    377 
    378 void ControllerBindingWidget::createBindingWidgets(QWidget* parent)
    379 {
    380   SettingsInterface* sif = getDialog()->getEditingSettingsInterface();
    381   DebugAssert(m_controller_info);
    382 
    383   QGroupBox* axis_gbox = nullptr;
    384   QGridLayout* axis_layout = nullptr;
    385   QGroupBox* button_gbox = nullptr;
    386   QGridLayout* button_layout = nullptr;
    387 
    388   QScrollArea* scrollarea = new QScrollArea(parent);
    389   QWidget* scrollarea_widget = new QWidget(scrollarea);
    390   scrollarea->setWidget(scrollarea_widget);
    391   scrollarea->setWidgetResizable(true);
    392   scrollarea->setFrameShape(QFrame::StyledPanel);
    393   scrollarea->setFrameShadow(QFrame::Sunken);
    394 
    395   // We do axes and buttons separately, so we can figure out how many columns to use.
    396   constexpr int NUM_AXIS_COLUMNS = 2;
    397   int column = 0;
    398   int row = 0;
    399   for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
    400   {
    401     if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||
    402         bi.type == InputBindingInfo::Type::Pointer)
    403     {
    404       if (!axis_gbox)
    405       {
    406         axis_gbox = new QGroupBox(tr("Axes"), scrollarea_widget);
    407         axis_layout = new QGridLayout(axis_gbox);
    408       }
    409 
    410       QGroupBox* gbox = new QGroupBox(qApp->translate("USB", bi.display_name), axis_gbox);
    411       QVBoxLayout* temp = new QVBoxLayout(gbox);
    412       InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);
    413       temp->addWidget(widget);
    414       axis_layout->addWidget(gbox, row, column);
    415       if ((++column) == NUM_AXIS_COLUMNS)
    416       {
    417         column = 0;
    418         row++;
    419       }
    420     }
    421   }
    422   if (axis_gbox)
    423     axis_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);
    424 
    425   const int num_button_columns = axis_layout ? 2 : 4;
    426   row = 0;
    427   column = 0;
    428   for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
    429   {
    430     if (bi.type == InputBindingInfo::Type::Button)
    431     {
    432       if (!button_gbox)
    433       {
    434         button_gbox = new QGroupBox(tr("Buttons"), scrollarea_widget);
    435         button_layout = new QGridLayout(button_gbox);
    436       }
    437 
    438       QGroupBox* gbox = new QGroupBox(qApp->translate("USB", bi.display_name), button_gbox);
    439       QVBoxLayout* temp = new QVBoxLayout(gbox);
    440       InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);
    441       temp->addWidget(widget);
    442       button_layout->addWidget(gbox, row, column);
    443       if ((++column) == num_button_columns)
    444       {
    445         column = 0;
    446         row++;
    447       }
    448     }
    449   }
    450 
    451   if (button_gbox)
    452     button_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);
    453 
    454   if (!axis_gbox && !button_gbox)
    455   {
    456     delete scrollarea_widget;
    457     delete scrollarea;
    458     return;
    459   }
    460 
    461   QHBoxLayout* layout = new QHBoxLayout(scrollarea_widget);
    462   if (axis_gbox)
    463     layout->addWidget(axis_gbox);
    464   if (button_gbox)
    465     layout->addWidget(button_gbox);
    466   layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Minimum));
    467 
    468   QHBoxLayout* main_layout = new QHBoxLayout(parent);
    469   main_layout->addWidget(scrollarea);
    470 }
    471 
    472 void ControllerBindingWidget::bindBindingWidgets(QWidget* parent)
    473 {
    474   SettingsInterface* sif = getDialog()->getEditingSettingsInterface();
    475   DebugAssert(m_controller_info);
    476 
    477   const std::string& config_section = getConfigSection();
    478   for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
    479   {
    480     if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||
    481         bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer)
    482     {
    483       InputBindingWidget* widget = parent->findChild<InputBindingWidget*>(QString::fromUtf8(bi.name));
    484       if (!widget)
    485       {
    486         ERROR_LOG("No widget found for '{}' ({})", bi.name, m_controller_info->name);
    487         continue;
    488       }
    489 
    490       widget->initialize(sif, bi.type, config_section, bi.name);
    491     }
    492   }
    493 
    494   switch (m_controller_info->vibration_caps)
    495   {
    496     case Controller::VibrationCapabilities::LargeSmallMotors:
    497     {
    498       InputVibrationBindingWidget* widget =
    499         parent->findChild<InputVibrationBindingWidget*>(QStringLiteral("LargeMotor"));
    500       if (widget)
    501         widget->setKey(getDialog(), config_section, "LargeMotor");
    502 
    503       widget = parent->findChild<InputVibrationBindingWidget*>(QStringLiteral("SmallMotor"));
    504       if (widget)
    505         widget->setKey(getDialog(), config_section, "SmallMotor");
    506     }
    507     break;
    508 
    509     case Controller::VibrationCapabilities::SingleMotor:
    510     {
    511       InputVibrationBindingWidget* widget = parent->findChild<InputVibrationBindingWidget*>(QStringLiteral("Motor"));
    512       if (widget)
    513         widget->setKey(getDialog(), config_section, "Motor");
    514     }
    515     break;
    516 
    517     case Controller::VibrationCapabilities::NoVibration:
    518     default:
    519       break;
    520   }
    521 }
    522 
    523 //////////////////////////////////////////////////////////////////////////
    524 
    525 ControllerMacroWidget::ControllerMacroWidget(ControllerBindingWidget* parent) : QWidget(parent)
    526 {
    527   m_ui.setupUi(this);
    528   setWindowTitle(tr("Controller Port %1 Macros").arg(parent->getPortNumber() + 1u));
    529   createWidgets(parent);
    530 }
    531 
    532 ControllerMacroWidget::~ControllerMacroWidget() = default;
    533 
    534 void ControllerMacroWidget::updateListItem(u32 index)
    535 {
    536   m_ui.portList->item(static_cast<int>(index))
    537     ->setText(tr("Macro %1\n%2").arg(index + 1).arg(m_macros[index]->getSummary()));
    538 }
    539 
    540 void ControllerMacroWidget::createWidgets(ControllerBindingWidget* parent)
    541 {
    542   for (u32 i = 0; i < NUM_MACROS; i++)
    543   {
    544     m_macros[i] = new ControllerMacroEditWidget(this, parent, i);
    545     m_ui.container->addWidget(m_macros[i]);
    546 
    547     QListWidgetItem* item = new QListWidgetItem();
    548     item->setIcon(QIcon::fromTheme(QStringLiteral("flashlight-line")));
    549     m_ui.portList->addItem(item);
    550     updateListItem(i);
    551   }
    552 
    553   m_ui.portList->setCurrentRow(0);
    554   m_ui.container->setCurrentIndex(0);
    555 
    556   connect(m_ui.portList, &QListWidget::currentRowChanged, m_ui.container, &QStackedWidget::setCurrentIndex);
    557 }
    558 
    559 //////////////////////////////////////////////////////////////////////////
    560 
    561 ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget,
    562                                                      u32 index)
    563   : QWidget(parent), m_parent(parent), m_bwidget(bwidget), m_index(index)
    564 {
    565   m_ui.setupUi(this);
    566 
    567   ControllerSettingsWindow* dialog = m_bwidget->getDialog();
    568   const std::string& section = m_bwidget->getConfigSection();
    569   const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();
    570   DebugAssert(cinfo);
    571 
    572   // load binds (single string joined by &)
    573   const std::string binds_string(
    574     dialog->getStringValue(section.c_str(), TinyString::from_format("Macro{}Binds", index + 1u), ""));
    575   const std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true));
    576 
    577   for (const std::string_view& button : buttons_split)
    578   {
    579     for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
    580     {
    581       if (button == bi.name)
    582       {
    583         m_binds.push_back(&bi);
    584         break;
    585       }
    586     }
    587   }
    588 
    589   // populate list view
    590   for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
    591   {
    592     if (bi.type == InputBindingInfo::Type::Motor)
    593       continue;
    594 
    595     QListWidgetItem* item = new QListWidgetItem();
    596     item->setText(qApp->translate(cinfo->name, bi.display_name));
    597     item->setCheckState((std::find(m_binds.begin(), m_binds.end(), &bi) != m_binds.end()) ? Qt::Checked :
    598                                                                                             Qt::Unchecked);
    599     m_ui.bindList->addItem(item);
    600   }
    601 
    602   m_frequency = dialog->getIntValue(section.c_str(), TinyString::from_format("Macro{}Frequency", index + 1u), 0);
    603   ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(dialog->getEditingSettingsInterface(), m_ui.triggerToggle,
    604                                                               section.c_str(), fmt::format("Macro{}Toggle", index + 1u),
    605                                                               false);
    606   updateFrequencyText();
    607 
    608   m_ui.trigger->initialize(dialog->getEditingSettingsInterface(), InputBindingInfo::Type::Macro, section,
    609                            fmt::format("Macro{}", index + 1u));
    610 
    611   connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); });
    612   connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); });
    613   connect(m_ui.setFrequency, &QAbstractButton::clicked, this, &ControllerMacroEditWidget::onSetFrequencyClicked);
    614   connect(m_ui.bindList, &QListWidget::itemChanged, this, &ControllerMacroEditWidget::updateBinds);
    615 }
    616 
    617 ControllerMacroEditWidget::~ControllerMacroEditWidget() = default;
    618 
    619 QString ControllerMacroEditWidget::getSummary() const
    620 {
    621   SmallString str;
    622   for (const Controller::ControllerBindingInfo* bi : m_binds)
    623   {
    624     if (!str.empty())
    625       str.append('/');
    626     str.append(bi->name);
    627   }
    628   return str.empty() ? tr("Not Configured") : QString::fromUtf8(str.c_str(), static_cast<int>(str.length()));
    629 }
    630 
    631 void ControllerMacroEditWidget::onSetFrequencyClicked()
    632 {
    633   bool okay;
    634   int new_freq = QInputDialog::getInt(this, tr("Set Frequency"), tr("Frequency: "), static_cast<int>(m_frequency), 0,
    635                                       std::numeric_limits<int>::max(), 1, &okay);
    636   if (!okay)
    637     return;
    638 
    639   m_frequency = static_cast<u32>(new_freq);
    640   updateFrequency();
    641 }
    642 
    643 void ControllerMacroEditWidget::modFrequency(s32 delta)
    644 {
    645   if (delta < 0 && m_frequency == 0)
    646     return;
    647 
    648   m_frequency = static_cast<u32>(static_cast<s32>(m_frequency) + delta);
    649   updateFrequency();
    650 }
    651 
    652 void ControllerMacroEditWidget::updateFrequency()
    653 {
    654   m_bwidget->getDialog()->setIntValue(m_bwidget->getConfigSection().c_str(),
    655                                       fmt::format("Macro{}Frequency", m_index + 1u).c_str(),
    656                                       static_cast<s32>(m_frequency));
    657   updateFrequencyText();
    658 }
    659 
    660 void ControllerMacroEditWidget::updateFrequencyText()
    661 {
    662   if (m_frequency == 0)
    663     m_ui.frequencyText->setText(tr("Macro will not repeat."));
    664   else
    665     m_ui.frequencyText->setText(tr("Macro will toggle buttons every %1 frames.").arg(m_frequency));
    666 }
    667 
    668 void ControllerMacroEditWidget::updateBinds()
    669 {
    670   ControllerSettingsWindow* dialog = m_bwidget->getDialog();
    671   const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();
    672   DebugAssert(cinfo);
    673 
    674   std::vector<const Controller::ControllerBindingInfo*> new_binds;
    675   u32 bind_index = 0;
    676   for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
    677   {
    678     if (bi.type == InputBindingInfo::Type::Motor)
    679       continue;
    680 
    681     const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index));
    682     bind_index++;
    683 
    684     if (!item)
    685     {
    686       // shouldn't happen
    687       continue;
    688     }
    689 
    690     if (item->checkState() == Qt::Checked)
    691       new_binds.push_back(&bi);
    692   }
    693   if (m_binds == new_binds)
    694     return;
    695 
    696   m_binds = std::move(new_binds);
    697 
    698   std::string binds_string;
    699   for (const Controller::ControllerBindingInfo* bi : m_binds)
    700   {
    701     if (!binds_string.empty())
    702       binds_string.append(" & ");
    703     binds_string.append(bi->name);
    704   }
    705 
    706   const std::string& section = m_bwidget->getConfigSection();
    707   const std::string key(fmt::format("Macro{}Binds", m_index + 1u));
    708   if (binds_string.empty())
    709     dialog->clearSettingValue(section.c_str(), key.c_str());
    710   else
    711     dialog->setStringValue(section.c_str(), key.c_str(), binds_string.c_str());
    712 
    713   m_parent->updateListItem(m_index);
    714 }
    715 
    716 //////////////////////////////////////////////////////////////////////////
    717 
    718 ControllerCustomSettingsWidget::ControllerCustomSettingsWidget(ControllerBindingWidget* parent)
    719   : QWidget(parent), m_parent(parent)
    720 {
    721   const Controller::ControllerInfo* cinfo = parent->getControllerInfo();
    722   DebugAssert(cinfo);
    723   if (cinfo->settings.empty())
    724     return;
    725 
    726   QScrollArea* sarea = new QScrollArea(this);
    727   QWidget* swidget = new QWidget(sarea);
    728   sarea->setWidget(swidget);
    729   sarea->setWidgetResizable(true);
    730   sarea->setFrameShape(QFrame::StyledPanel);
    731   sarea->setFrameShadow(QFrame::Sunken);
    732 
    733   QGridLayout* swidget_layout = new QGridLayout(swidget);
    734   createSettingWidgets(parent, swidget, swidget_layout, cinfo);
    735 
    736   QVBoxLayout* layout = new QVBoxLayout(this);
    737   layout->setContentsMargins(0, 0, 0, 0);
    738   layout->addWidget(sarea);
    739 }
    740 
    741 ControllerCustomSettingsWidget::~ControllerCustomSettingsWidget()
    742 {
    743 }
    744 
    745 void ControllerCustomSettingsWidget::createSettingWidgets(ControllerBindingWidget* parent, QWidget* parent_widget,
    746                                                           QGridLayout* layout, const Controller::ControllerInfo* cinfo)
    747 {
    748   const std::string& section = parent->getConfigSection();
    749   SettingsInterface* sif = parent->getDialog()->getEditingSettingsInterface();
    750   int current_row = 0;
    751 
    752   for (const SettingInfo& si : cinfo->settings)
    753   {
    754     std::string key_name = si.name;
    755 
    756     switch (si.type)
    757     {
    758       case SettingInfo::Type::Boolean:
    759       {
    760         QCheckBox* cb = new QCheckBox(qApp->translate(cinfo->name, si.display_name), this);
    761         cb->setObjectName(QString::fromUtf8(si.name));
    762         ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, cb, section, std::move(key_name),
    763                                                                     si.BooleanDefaultValue());
    764         layout->addWidget(cb, current_row, 0, 1, 4);
    765         current_row++;
    766       }
    767       break;
    768 
    769       case SettingInfo::Type::Integer:
    770       {
    771         QSpinBox* sb = new QSpinBox(this);
    772         sb->setObjectName(QString::fromUtf8(si.name));
    773         sb->setMinimum(si.IntegerMinValue());
    774         sb->setMaximum(si.IntegerMaxValue());
    775         sb->setSingleStep(si.IntegerStepValue());
    776         SettingWidgetBinder::BindWidgetToIntSetting(sif, sb, section, std::move(key_name), si.IntegerDefaultValue());
    777         layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0);
    778         layout->addWidget(sb, current_row, 1, 1, 3);
    779         current_row++;
    780       }
    781       break;
    782 
    783       case SettingInfo::Type::IntegerList:
    784       {
    785         QComboBox* cb = new QComboBox(this);
    786         cb->setObjectName(QString::fromUtf8(si.name));
    787         for (u32 j = 0; si.options[j] != nullptr; j++)
    788           cb->addItem(qApp->translate(cinfo->name, si.options[j]));
    789         SettingWidgetBinder::BindWidgetToIntSetting(sif, cb, section, std::move(key_name), si.IntegerDefaultValue(),
    790                                                     si.IntegerMinValue());
    791         layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0);
    792         layout->addWidget(cb, current_row, 1, 1, 3);
    793         current_row++;
    794       }
    795       break;
    796 
    797       case SettingInfo::Type::Float:
    798       {
    799         QDoubleSpinBox* sb = new QDoubleSpinBox(this);
    800         sb->setObjectName(QString::fromUtf8(si.name));
    801         if (si.multiplier != 0.0f && si.multiplier != 1.0f)
    802         {
    803           const float multiplier = si.multiplier;
    804           sb->setMinimum(si.FloatMinValue() * multiplier);
    805           sb->setMaximum(si.FloatMaxValue() * multiplier);
    806           sb->setSingleStep(si.FloatStepValue() * multiplier);
    807           if (std::abs(si.multiplier - 100.0f) < 0.01f)
    808           {
    809             sb->setDecimals(0);
    810             sb->setSuffix(QStringLiteral("%"));
    811           }
    812 
    813           SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, sb, section, std::move(key_name), si.multiplier,
    814                                                              si.FloatDefaultValue());
    815         }
    816         else
    817         {
    818           sb->setMinimum(si.FloatMinValue());
    819           sb->setMaximum(si.FloatMaxValue());
    820           sb->setSingleStep(si.FloatStepValue());
    821 
    822           SettingWidgetBinder::BindWidgetToFloatSetting(sif, sb, section, std::move(key_name), si.FloatDefaultValue());
    823         }
    824         layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0);
    825         layout->addWidget(sb, current_row, 1, 1, 3);
    826         current_row++;
    827       }
    828       break;
    829 
    830       case SettingInfo::Type::String:
    831       {
    832         QLineEdit* le = new QLineEdit(this);
    833         le->setObjectName(QString::fromUtf8(si.name));
    834         SettingWidgetBinder::BindWidgetToStringSetting(sif, le, section, std::move(key_name), si.StringDefaultValue());
    835         layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0);
    836         layout->addWidget(le, current_row, 1, 1, 3);
    837         current_row++;
    838       }
    839       break;
    840 
    841       case SettingInfo::Type::Path:
    842       {
    843         QLineEdit* le = new QLineEdit(this);
    844         le->setObjectName(QString::fromUtf8(si.name));
    845         QPushButton* browse_button = new QPushButton(tr("Browse..."), this);
    846         SettingWidgetBinder::BindWidgetToStringSetting(sif, le, section, std::move(key_name), si.StringDefaultValue());
    847         connect(browse_button, &QPushButton::clicked, [this, le]() {
    848           QString path = QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Select File")));
    849           if (!path.isEmpty())
    850             le->setText(path);
    851         });
    852 
    853         QHBoxLayout* hbox = new QHBoxLayout();
    854         hbox->addWidget(le, 1);
    855         hbox->addWidget(browse_button);
    856 
    857         layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0);
    858         layout->addLayout(hbox, current_row, 1, 1, 3);
    859         current_row++;
    860       }
    861       break;
    862     }
    863 
    864     QLabel* label = new QLabel(si.description ? qApp->translate(cinfo->name, si.description) : QString(), this);
    865     label->setWordWrap(true);
    866     layout->addWidget(label, current_row++, 0, 1, 4);
    867 
    868     layout->addItem(new QSpacerItem(1, 10, QSizePolicy::Minimum, QSizePolicy::Fixed), current_row++, 0, 1, 4);
    869   }
    870 
    871   QHBoxLayout* bottom_hlayout = new QHBoxLayout();
    872   QPushButton* restore_defaults = new QPushButton(tr("Restore Default Settings"), this);
    873   restore_defaults->setIcon(QIcon::fromTheme(QStringLiteral("restart-line")));
    874   connect(restore_defaults, &QPushButton::clicked, this, &ControllerCustomSettingsWidget::restoreDefaults);
    875   bottom_hlayout->addStretch(1);
    876   bottom_hlayout->addWidget(restore_defaults);
    877   layout->addLayout(bottom_hlayout, current_row++, 0, 1, 4);
    878 
    879   layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), current_row++, 0, 1, 4);
    880 }
    881 
    882 void ControllerCustomSettingsWidget::restoreDefaults()
    883 {
    884   const Controller::ControllerInfo* cinfo = m_parent->getControllerInfo();
    885   DebugAssert(cinfo);
    886   if (cinfo->settings.empty())
    887     return;
    888 
    889   for (const SettingInfo& si : cinfo->settings)
    890   {
    891     const QString key(QString::fromStdString(si.name));
    892 
    893     switch (si.type)
    894     {
    895       case SettingInfo::Type::Boolean:
    896       {
    897         QCheckBox* widget = findChild<QCheckBox*>(QString::fromStdString(si.name));
    898         if (widget)
    899           widget->setChecked(si.BooleanDefaultValue());
    900       }
    901       break;
    902 
    903       case SettingInfo::Type::Integer:
    904       {
    905         QSpinBox* widget = findChild<QSpinBox*>(QString::fromStdString(si.name));
    906         if (widget)
    907           widget->setValue(si.IntegerDefaultValue());
    908       }
    909       break;
    910 
    911       case SettingInfo::Type::IntegerList:
    912       {
    913         QComboBox* widget = findChild<QComboBox*>(QString::fromStdString(si.name));
    914         if (widget)
    915           widget->setCurrentIndex(si.IntegerDefaultValue() - si.IntegerMinValue());
    916       }
    917       break;
    918 
    919       case SettingInfo::Type::Float:
    920       {
    921         QDoubleSpinBox* widget = findChild<QDoubleSpinBox*>(QString::fromStdString(si.name));
    922         if (widget)
    923         {
    924           if (si.multiplier != 0.0f && si.multiplier != 1.0f)
    925             widget->setValue(si.FloatDefaultValue() * si.multiplier);
    926           else
    927             widget->setValue(si.FloatDefaultValue());
    928         }
    929       }
    930       break;
    931 
    932       case SettingInfo::Type::String:
    933       {
    934         QLineEdit* widget = findChild<QLineEdit*>(QString::fromStdString(si.name));
    935         if (widget)
    936           widget->setText(QString::fromUtf8(si.StringDefaultValue()));
    937       }
    938       break;
    939 
    940       case SettingInfo::Type::Path:
    941       {
    942         QLineEdit* widget = findChild<QLineEdit*>(QString::fromStdString(si.name));
    943         if (widget)
    944           widget->setText(QString::fromUtf8(si.StringDefaultValue()));
    945       }
    946       break;
    947     }
    948   }
    949 }