Path: blob/master/src/duckstation-qt/controllerbindingwidgets.cpp
4246 views
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>1// SPDX-License-Identifier: CC-BY-NC-ND-4.023#include "controllerbindingwidgets.h"4#include "controllersettingswindow.h"5#include "controllersettingwidgetbinder.h"6#include "mainwindow.h"7#include "qthost.h"8#include "qtutils.h"9#include "settingswindow.h"10#include "settingwidgetbinder.h"1112#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"2021#include "core/controller.h"22#include "core/host.h"2324#include "util/input_manager.h"2526#include "common/log.h"27#include "common/string_util.h"2829#include "fmt/format.h"3031#include <QtWidgets/QCheckBox>32#include <QtWidgets/QDialogButtonBox>33#include <QtWidgets/QDoubleSpinBox>34#include <QtWidgets/QInputDialog>35#include <QtWidgets/QLineEdit>36#include <QtWidgets/QMenu>37#include <QtWidgets/QMessageBox>38#include <QtWidgets/QScrollArea>39#include <QtWidgets/QSpinBox>40#include <algorithm>4142#include "moc_controllerbindingwidgets.cpp"4344LOG_CHANNEL(Host);4546ControllerBindingWidget::ControllerBindingWidget(QWidget* parent, ControllerSettingsWindow* dialog, u32 port)47: QWidget(parent), m_dialog(dialog), m_config_section(Controller::GetSettingsSection(port)), m_port_number(port)48{49m_ui.setupUi(this);50populateControllerTypes();51populateWidgets();5253connect(m_ui.controllerType, QOverload<int>::of(&QComboBox::currentIndexChanged), this,54&ControllerBindingWidget::onTypeChanged);55connect(m_ui.bindings, &QPushButton::clicked, this, &ControllerBindingWidget::onBindingsClicked);56connect(m_ui.settings, &QPushButton::clicked, this, &ControllerBindingWidget::onSettingsClicked);57connect(m_ui.macros, &QPushButton::clicked, this, &ControllerBindingWidget::onMacrosClicked);58connect(m_ui.automaticBinding, &QPushButton::clicked, this, &ControllerBindingWidget::onAutomaticBindingClicked);59connect(m_ui.clearBindings, &QPushButton::clicked, this, &ControllerBindingWidget::onClearBindingsClicked);60}6162ControllerBindingWidget::~ControllerBindingWidget() = default;6364void ControllerBindingWidget::populateControllerTypes()65{66for (const Controller::ControllerInfo* cinfo : Controller::GetControllerInfoList())67m_ui.controllerType->addItem(QtUtils::StringViewToQString(cinfo->GetDisplayName()),68QVariant(static_cast<int>(cinfo->type)));6970m_controller_info = Controller::GetControllerInfo(71m_dialog->getStringValue(m_config_section.c_str(), "Type",72Controller::GetControllerInfo(Settings::GetDefaultControllerType(m_port_number)).name));73if (!m_controller_info)74m_controller_info = &Controller::GetControllerInfo(Settings::GetDefaultControllerType(m_port_number));7576const int index = m_ui.controllerType->findData(QVariant(static_cast<int>(m_controller_info->type)));77if (index >= 0 && index != m_ui.controllerType->currentIndex())78{79QSignalBlocker sb(m_ui.controllerType);80m_ui.controllerType->setCurrentIndex(index);81}82}8384void ControllerBindingWidget::populateWidgets()85{86const bool is_initializing = (m_ui.stackedWidget->count() == 0);87if (m_bindings_widget)88{89m_ui.stackedWidget->removeWidget(m_bindings_widget);90delete m_bindings_widget;91m_bindings_widget = nullptr;92}93if (m_settings_widget)94{95m_ui.stackedWidget->removeWidget(m_settings_widget);96delete m_settings_widget;97m_settings_widget = nullptr;98}99if (m_macros_widget)100{101m_ui.stackedWidget->removeWidget(m_macros_widget);102delete m_macros_widget;103m_macros_widget = nullptr;104}105106const bool has_settings = !m_controller_info->settings.empty();107const bool has_macros = !m_controller_info->bindings.empty();108m_ui.settings->setEnabled(has_settings);109m_ui.macros->setEnabled(has_macros);110111m_bindings_widget = new QWidget(this);112switch (m_controller_info->type)113{114case ControllerType::AnalogController:115{116Ui::ControllerBindingWidget_AnalogController ui;117ui.setupUi(m_bindings_widget);118bindBindingWidgets(m_bindings_widget);119m_icon = QIcon::fromTheme(QStringLiteral("controller-line"));120}121break;122123case ControllerType::AnalogJoystick:124{125Ui::ControllerBindingWidget_AnalogJoystick ui;126ui.setupUi(m_bindings_widget);127bindBindingWidgets(m_bindings_widget);128m_icon = QIcon::fromTheme(QStringLiteral("joystick-line"));129}130break;131132case ControllerType::DigitalController:133{134Ui::ControllerBindingWidget_DigitalController ui;135ui.setupUi(m_bindings_widget);136bindBindingWidgets(m_bindings_widget);137m_icon = QIcon::fromTheme(QStringLiteral("controller-digital-line"));138}139break;140141case ControllerType::GunCon:142{143Ui::ControllerBindingWidget_GunCon ui;144ui.setupUi(m_bindings_widget);145bindBindingWidgets(m_bindings_widget);146m_icon = QIcon::fromTheme(QStringLiteral("guncon-line"));147}148break;149150case ControllerType::NeGcon:151{152Ui::ControllerBindingWidget_NeGcon ui;153ui.setupUi(m_bindings_widget);154bindBindingWidgets(m_bindings_widget);155m_icon = QIcon::fromTheme(QStringLiteral("negcon-line"));156}157break;158159case ControllerType::NeGconRumble:160{161Ui::ControllerBindingWidget_NeGconRumble ui;162ui.setupUi(m_bindings_widget);163bindBindingWidgets(m_bindings_widget);164m_icon = QIcon::fromTheme(QStringLiteral("negcon-line"));165}166break;167168case ControllerType::PlayStationMouse:169{170Ui::ControllerBindingWidget_Mouse ui;171ui.setupUi(m_bindings_widget);172bindBindingWidgets(m_bindings_widget);173m_icon = QIcon::fromTheme(QStringLiteral("mouse-line"));174}175break;176177case ControllerType::Justifier:178{179Ui::ControllerBindingWidget_Justifier ui;180ui.setupUi(m_bindings_widget);181bindBindingWidgets(m_bindings_widget);182m_icon = QIcon::fromTheme(QStringLiteral("guncon-line"));183}184break;185186case ControllerType::None:187{188m_icon = QIcon::fromTheme(QStringLiteral("controller-strike-line"));189}190break;191192default:193{194createBindingWidgets(m_bindings_widget);195m_icon = QIcon::fromTheme(QStringLiteral("controller-line"));196}197break;198}199200m_ui.stackedWidget->addWidget(m_bindings_widget);201m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);202203if (has_settings)204{205m_settings_widget = new ControllerCustomSettingsWidget(this);206m_ui.stackedWidget->addWidget(m_settings_widget);207}208209if (has_macros)210{211m_macros_widget = new ControllerMacroWidget(this);212m_ui.stackedWidget->addWidget(m_macros_widget);213}214215updateHeaderToolButtons();216217// no need to do this on first init, only changes218if (!is_initializing)219m_dialog->updateListDescription(m_port_number, this);220}221222void ControllerBindingWidget::updateHeaderToolButtons()223{224const QWidget* current_widget = m_ui.stackedWidget->currentWidget();225const QSignalBlocker bindings_sb(m_ui.bindings);226const QSignalBlocker settings_sb(m_ui.settings);227const QSignalBlocker macros_sb(m_ui.macros);228229const bool is_bindings = (current_widget == m_bindings_widget);230m_ui.bindings->setChecked(is_bindings);231m_ui.automaticBinding->setEnabled(is_bindings);232m_ui.clearBindings->setEnabled(is_bindings);233m_ui.macros->setChecked(current_widget == m_macros_widget);234m_ui.settings->setChecked((current_widget == m_settings_widget));235}236237void ControllerBindingWidget::onTypeChanged()238{239bool ok;240const int index = m_ui.controllerType->currentData().toInt(&ok);241if (!ok || index < 0 || index >= static_cast<int>(ControllerType::Count))242return;243244m_controller_info = &Controller::GetControllerInfo(static_cast<ControllerType>(index));245246SettingsInterface* sif = m_dialog->getEditingSettingsInterface();247if (sif)248{249sif->SetStringValue(m_config_section.c_str(), "Type", m_controller_info->name);250QtHost::SaveGameSettings(sif, false);251g_emu_thread->reloadGameSettings();252}253else254{255Host::SetBaseStringSettingValue(m_config_section.c_str(), "Type", m_controller_info->name);256Host::CommitBaseSettingChanges();257g_emu_thread->applySettings();258}259260populateWidgets();261}262263void ControllerBindingWidget::onAutomaticBindingClicked()264{265QMenu menu(this);266bool added = false;267268for (const InputDeviceListModel::Device& dev : g_emu_thread->getInputDeviceListModel()->getDeviceList())269{270// we set it as data, because the device list could get invalidated while the menu is up271QAction* action = menu.addAction(QStringLiteral("%1 (%2)").arg(dev.identifier).arg(dev.display_name));272action->setIcon(InputDeviceListModel::getIconForKey(dev.key));273action->setData(dev.identifier);274connect(action, &QAction::triggered, this,275[this, action]() { doDeviceAutomaticBinding(action->data().toString()); });276added = true;277}278279if (added)280{281QAction* action = menu.addAction(tr("Multiple devices..."));282connect(action, &QAction::triggered, this, &ControllerBindingWidget::onMultipleDeviceAutomaticBindingTriggered);283}284else285{286QAction* action = menu.addAction(tr("No devices available"));287action->setEnabled(false);288}289290menu.exec(QCursor::pos());291}292293void ControllerBindingWidget::onClearBindingsClicked()294{295if (QMessageBox::question(296QtUtils::GetRootWidget(this), tr("Clear Mapping"),297tr("Are you sure you want to clear all mappings for this controller? This action cannot be undone.")) !=298QMessageBox::Yes)299{300return;301}302303if (m_dialog->isEditingGlobalSettings())304{305auto lock = Host::GetSettingsLock();306InputManager::ClearPortBindings(*Host::Internal::GetBaseSettingsLayer(), m_port_number);307}308else309{310InputManager::ClearPortBindings(*m_dialog->getEditingSettingsInterface(), m_port_number);311}312313saveAndRefresh();314}315316void ControllerBindingWidget::onBindingsClicked()317{318m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);319updateHeaderToolButtons();320}321322void ControllerBindingWidget::onSettingsClicked()323{324if (!m_settings_widget)325return;326327m_ui.stackedWidget->setCurrentWidget(m_settings_widget);328updateHeaderToolButtons();329}330331void ControllerBindingWidget::onMacrosClicked()332{333if (!m_macros_widget)334return;335336m_ui.stackedWidget->setCurrentWidget(m_macros_widget);337updateHeaderToolButtons();338}339340void ControllerBindingWidget::doDeviceAutomaticBinding(const QString& device)341{342std::vector<std::pair<GenericInputBinding, std::string>> mapping =343InputManager::GetGenericBindingMapping(device.toStdString());344if (mapping.empty())345{346QMessageBox::critical(347QtUtils::GetRootWidget(this), tr("Automatic Mapping"),348tr("No generic bindings were generated for device '%1'. The controller/source may not support automatic mapping.")349.arg(device));350return;351}352353bool result;354if (m_dialog->isEditingGlobalSettings())355{356auto lock = Host::GetSettingsLock();357result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), m_port_number, mapping, true);358}359else360{361result = InputManager::MapController(*m_dialog->getEditingSettingsInterface(), m_port_number, mapping, true);362QtHost::SaveGameSettings(m_dialog->getEditingSettingsInterface(), false);363g_emu_thread->reloadInputBindings();364}365366// force a refresh after mapping367if (result)368saveAndRefresh();369}370371void ControllerBindingWidget::onMultipleDeviceAutomaticBindingTriggered()372{373// force a refresh after mapping374if (doMultipleDeviceAutomaticBinding(this, m_dialog, m_port_number))375onTypeChanged();376}377378bool ControllerBindingWidget::doMultipleDeviceAutomaticBinding(QWidget* parent, ControllerSettingsWindow* parent_dialog,379u32 port)380{381QDialog dialog(parent);382383QVBoxLayout* layout = new QVBoxLayout(&dialog);384QLabel help(tr("Select the devices from the list below that you want to bind to this controller."), &dialog);385layout->addWidget(&help);386387QListWidget list(&dialog);388list.setSelectionMode(QListWidget::SingleSelection);389layout->addWidget(&list);390391for (const InputDeviceListModel::Device& dev : g_emu_thread->getInputDeviceListModel()->getDeviceList())392{393QListWidgetItem* item = new QListWidgetItem;394item->setText(QStringLiteral("%1 (%2)").arg(dev.identifier).arg(dev.display_name));395item->setData(Qt::UserRole, dev.identifier);396item->setIcon(InputDeviceListModel::getIconForKey(dev.key));397item->setFlags(item->flags() | Qt::ItemIsUserCheckable);398item->setCheckState(Qt::Unchecked);399list.addItem(item);400}401402QDialogButtonBox bb(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);403connect(&bb, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);404connect(&bb, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);405layout->addWidget(&bb);406407if (dialog.exec() == QDialog::Rejected)408return false;409410auto lock = Host::GetSettingsLock();411const bool global = (!parent_dialog || parent_dialog->isEditingGlobalSettings());412SettingsInterface& si =413*(global ? Host::Internal::GetBaseSettingsLayer() : parent_dialog->getEditingSettingsInterface());414415// first device should clear mappings416bool tried_any = false;417bool mapped_any = false;418const int count = list.count();419for (int i = 0; i < count; i++)420{421QListWidgetItem* item = list.item(i);422if (item->checkState() != Qt::Checked)423continue;424425tried_any = true;426427const QString identifier = item->data(Qt::UserRole).toString();428std::vector<std::pair<GenericInputBinding, std::string>> mapping =429InputManager::GetGenericBindingMapping(identifier.toStdString());430if (mapping.empty())431{432lock.unlock();433QMessageBox::critical(QtUtils::GetRootWidget(parent), tr("Automatic Mapping"),434tr("No generic bindings were generated for device '%1'. The controller/source may not "435"support automatic mapping.")436.arg(identifier));437lock.lock();438continue;439}440441mapped_any |= InputManager::MapController(si, port, mapping, !mapped_any);442}443444lock.unlock();445446if (!tried_any)447{448QMessageBox::information(QtUtils::GetRootWidget(parent), tr("Automatic Mapping"), tr("No devices were selected."));449return false;450}451452if (mapped_any)453{454if (global)455{456QtHost::SaveGameSettings(&si, false);457g_emu_thread->reloadGameSettings(false);458}459else460{461QtHost::QueueSettingsSave();462g_emu_thread->reloadInputBindings();463}464}465466return mapped_any;467}468469void ControllerBindingWidget::saveAndRefresh()470{471onTypeChanged();472QtHost::QueueSettingsSave();473g_emu_thread->applySettings();474}475476void ControllerBindingWidget::createBindingWidgets(QWidget* parent)477{478SettingsInterface* sif = getDialog()->getEditingSettingsInterface();479DebugAssert(m_controller_info);480481QGroupBox* axis_gbox = nullptr;482QGridLayout* axis_layout = nullptr;483QGroupBox* button_gbox = nullptr;484QGridLayout* button_layout = nullptr;485486QScrollArea* scrollarea = new QScrollArea(parent);487QWidget* scrollarea_widget = new QWidget(scrollarea);488scrollarea->setWidget(scrollarea_widget);489scrollarea->setWidgetResizable(true);490scrollarea->setFrameShape(QFrame::StyledPanel);491scrollarea->setFrameShadow(QFrame::Sunken);492493// We do axes and buttons separately, so we can figure out how many columns to use.494constexpr int NUM_AXIS_COLUMNS = 2;495int column = 0;496int row = 0;497for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)498{499if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||500bi.type == InputBindingInfo::Type::Pointer || bi.type == InputBindingInfo::Type::RelativePointer ||501bi.type == InputBindingInfo::Type::Device || bi.type == InputBindingInfo::Type::Motor)502{503if (!axis_gbox)504{505axis_gbox = new QGroupBox(tr("Axes"), scrollarea_widget);506axis_layout = new QGridLayout(axis_gbox);507}508509QGroupBox* gbox =510new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), axis_gbox);511QVBoxLayout* temp = new QVBoxLayout(gbox);512QWidget* widget;513if (bi.type != InputBindingInfo::Type::Motor)514widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);515else516widget = new InputVibrationBindingWidget(gbox, getDialog(), getConfigSection(), bi.name);517518temp->addWidget(widget);519axis_layout->addWidget(gbox, row, column);520if ((++column) == NUM_AXIS_COLUMNS)521{522column = 0;523row++;524}525}526}527528if (axis_gbox)529axis_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);530531const int num_button_columns = axis_layout ? 2 : 4;532row = 0;533column = 0;534for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)535{536if (bi.type == InputBindingInfo::Type::Button)537{538if (!button_gbox)539{540button_gbox = new QGroupBox(tr("Buttons"), scrollarea_widget);541button_layout = new QGridLayout(button_gbox);542}543544QGroupBox* gbox =545new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), button_gbox);546QVBoxLayout* temp = new QVBoxLayout(gbox);547InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);548temp->addWidget(widget);549button_layout->addWidget(gbox, row, column);550if ((++column) == num_button_columns)551{552column = 0;553row++;554}555}556}557558if (button_gbox)559button_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);560561if (!axis_gbox && !button_gbox)562{563delete scrollarea_widget;564delete scrollarea;565return;566}567568QHBoxLayout* layout = new QHBoxLayout(scrollarea_widget);569if (axis_gbox)570layout->addWidget(axis_gbox, 1);571if (button_gbox)572layout->addWidget(button_gbox, 1);573574QHBoxLayout* main_layout = new QHBoxLayout(parent);575main_layout->addWidget(scrollarea);576}577578void ControllerBindingWidget::bindBindingWidgets(QWidget* parent)579{580SettingsInterface* sif = getDialog()->getEditingSettingsInterface();581DebugAssert(m_controller_info);582583const std::string& config_section = getConfigSection();584for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)585{586if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||587bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer ||588bi.type == InputBindingInfo::Type::RelativePointer)589{590InputBindingWidget* widget = parent->findChild<InputBindingWidget*>(QString::fromUtf8(bi.name));591if (!widget)592{593ERROR_LOG("No widget found for '{}' ({})", bi.name, m_controller_info->name);594continue;595}596597widget->initialize(sif, bi.type, config_section, bi.name);598}599else if (bi.type == InputBindingInfo::Type::Motor)600{601InputVibrationBindingWidget* widget = parent->findChild<InputVibrationBindingWidget*>(QString::fromUtf8(bi.name));602if (widget)603widget->setKey(getDialog(), config_section, bi.name);604}605}606}607608//////////////////////////////////////////////////////////////////////////609610ControllerMacroWidget::ControllerMacroWidget(ControllerBindingWidget* parent) : QWidget(parent)611{612m_ui.setupUi(this);613setWindowTitle(tr("Controller Port %1 Macros").arg(parent->getPortNumber() + 1u));614createWidgets(parent);615}616617ControllerMacroWidget::~ControllerMacroWidget() = default;618619void ControllerMacroWidget::updateListItem(u32 index)620{621m_ui.portList->item(static_cast<int>(index))622->setText(tr("Macro %1\n%2").arg(index + 1).arg(m_macros[index]->getSummary()));623}624625void ControllerMacroWidget::createWidgets(ControllerBindingWidget* parent)626{627for (u32 i = 0; i < NUM_MACROS; i++)628{629m_macros[i] = new ControllerMacroEditWidget(this, parent, i);630m_ui.container->addWidget(m_macros[i]);631632QListWidgetItem* item = new QListWidgetItem();633item->setIcon(QIcon::fromTheme(QStringLiteral("flashlight-line")));634m_ui.portList->addItem(item);635updateListItem(i);636}637638m_ui.portList->setCurrentRow(0);639m_ui.container->setCurrentIndex(0);640641connect(m_ui.portList, &QListWidget::currentRowChanged, m_ui.container, &QStackedWidget::setCurrentIndex);642}643644//////////////////////////////////////////////////////////////////////////645646ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget,647u32 index)648: QWidget(parent), m_parent(parent), m_bwidget(bwidget), m_index(index)649{650m_ui.setupUi(this);651652ControllerSettingsWindow* dialog = m_bwidget->getDialog();653const std::string& section = m_bwidget->getConfigSection();654const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();655DebugAssert(cinfo);656657// load binds (single string joined by &)658const std::string binds_string(659dialog->getStringValue(section.c_str(), TinyString::from_format("Macro{}Binds", index + 1u), ""));660const std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true));661662for (const std::string_view& button : buttons_split)663{664for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)665{666if (button == bi.name)667{668m_binds.push_back(&bi);669break;670}671}672}673674// populate list view675for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)676{677if (bi.type == InputBindingInfo::Type::Motor)678continue;679680QListWidgetItem* item = new QListWidgetItem();681item->setText(QtUtils::StringViewToQString(cinfo->GetBindingDisplayName(bi)));682item->setCheckState((std::find(m_binds.begin(), m_binds.end(), &bi) != m_binds.end()) ? Qt::Checked :683Qt::Unchecked);684m_ui.bindList->addItem(item);685}686687ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(688dialog->getEditingSettingsInterface(), m_ui.pressure, section, fmt::format("Macro{}Pressure", index + 1u), 100.0f,6891.0f);690ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(691dialog->getEditingSettingsInterface(), m_ui.deadzone, section, fmt::format("Macro{}Deadzone", index + 1u), 100.0f,6920.0f);693connect(m_ui.pressure, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onPressureChanged);694connect(m_ui.deadzone, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onDeadzoneChanged);695onPressureChanged();696onDeadzoneChanged();697698m_frequency = dialog->getIntValue(section.c_str(), TinyString::from_format("Macro{}Frequency", index + 1u), 0);699ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(dialog->getEditingSettingsInterface(), m_ui.triggerToggle,700section.c_str(), fmt::format("Macro{}Toggle", index + 1u),701false);702updateFrequencyText();703704m_ui.trigger->initialize(dialog->getEditingSettingsInterface(), InputBindingInfo::Type::Macro, section,705fmt::format("Macro{}", index + 1u));706707connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); });708connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); });709connect(m_ui.setFrequency, &QAbstractButton::clicked, this, &ControllerMacroEditWidget::onSetFrequencyClicked);710connect(m_ui.bindList, &QListWidget::itemChanged, this, &ControllerMacroEditWidget::updateBinds);711}712713ControllerMacroEditWidget::~ControllerMacroEditWidget() = default;714715QString ControllerMacroEditWidget::getSummary() const716{717SmallString str;718for (const Controller::ControllerBindingInfo* bi : m_binds)719{720if (!str.empty())721str.append('/');722str.append(bi->name);723}724return str.empty() ? tr("Not Configured") : QString::fromUtf8(str.c_str(), static_cast<int>(str.length()));725}726727void ControllerMacroEditWidget::onPressureChanged()728{729m_ui.pressureValue->setText(tr("%1%").arg(m_ui.pressure->value()));730}731732void ControllerMacroEditWidget::onDeadzoneChanged()733{734m_ui.deadzoneValue->setText(tr("%1%").arg(m_ui.deadzone->value()));735}736737void ControllerMacroEditWidget::onSetFrequencyClicked()738{739bool okay;740int new_freq = QInputDialog::getInt(this, tr("Set Frequency"), tr("Frequency: "), static_cast<int>(m_frequency), 0,741std::numeric_limits<int>::max(), 1, &okay);742if (!okay)743return;744745m_frequency = static_cast<u32>(new_freq);746updateFrequency();747}748749void ControllerMacroEditWidget::modFrequency(s32 delta)750{751if (delta < 0 && m_frequency == 0)752return;753754m_frequency = static_cast<u32>(static_cast<s32>(m_frequency) + delta);755updateFrequency();756}757758void ControllerMacroEditWidget::updateFrequency()759{760m_bwidget->getDialog()->setIntValue(m_bwidget->getConfigSection().c_str(),761fmt::format("Macro{}Frequency", m_index + 1u).c_str(),762static_cast<s32>(m_frequency));763updateFrequencyText();764}765766void ControllerMacroEditWidget::updateFrequencyText()767{768if (m_frequency == 0)769m_ui.frequencyText->setText(tr("Macro will not repeat."));770else771m_ui.frequencyText->setText(tr("Macro will toggle buttons every %1 frames.").arg(m_frequency));772}773774void ControllerMacroEditWidget::updateBinds()775{776ControllerSettingsWindow* dialog = m_bwidget->getDialog();777const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();778DebugAssert(cinfo);779780std::vector<const Controller::ControllerBindingInfo*> new_binds;781u32 bind_index = 0;782for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)783{784if (bi.type == InputBindingInfo::Type::Motor)785continue;786787const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index));788bind_index++;789790if (!item)791{792// shouldn't happen793continue;794}795796if (item->checkState() == Qt::Checked)797new_binds.push_back(&bi);798}799if (m_binds == new_binds)800return;801802m_binds = std::move(new_binds);803804std::string binds_string;805for (const Controller::ControllerBindingInfo* bi : m_binds)806{807if (!binds_string.empty())808binds_string.append(" & ");809binds_string.append(bi->name);810}811812const std::string& section = m_bwidget->getConfigSection();813const std::string key(fmt::format("Macro{}Binds", m_index + 1u));814if (binds_string.empty())815dialog->clearSettingValue(section.c_str(), key.c_str());816else817dialog->setStringValue(section.c_str(), key.c_str(), binds_string.c_str());818819m_parent->updateListItem(m_index);820}821822//////////////////////////////////////////////////////////////////////////823824static void createSettingWidgets(SettingsInterface* const sif, QWidget* parent_widget, QGridLayout* layout,825const std::string& section, std::span<const SettingInfo> settings,826const char* tr_context)827{828int current_row = 0;829830for (const SettingInfo& si : settings)831{832std::string key_name = si.name;833834switch (si.type)835{836case SettingInfo::Type::Boolean:837{838QCheckBox* cb = new QCheckBox(qApp->translate(tr_context, si.display_name), parent_widget);839cb->setObjectName(QString::fromUtf8(si.name));840ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, cb, section, std::move(key_name),841si.BooleanDefaultValue());842layout->addWidget(cb, current_row, 0, 1, 4);843current_row++;844}845break;846847case SettingInfo::Type::Integer:848{849QSpinBox* sb = new QSpinBox(parent_widget);850sb->setObjectName(QString::fromUtf8(si.name));851sb->setMinimum(si.IntegerMinValue());852sb->setMaximum(si.IntegerMaxValue());853sb->setSingleStep(si.IntegerStepValue());854ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, sb, section, std::move(key_name),855si.IntegerDefaultValue());856layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);857layout->addWidget(sb, current_row, 1, 1, 3);858current_row++;859}860break;861862case SettingInfo::Type::IntegerList:863{864QComboBox* cb = new QComboBox(parent_widget);865cb->setObjectName(QString::fromUtf8(si.name));866for (u32 j = 0; si.options[j] != nullptr; j++)867cb->addItem(qApp->translate(tr_context, si.options[j]));868ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, cb, section, std::move(key_name),869si.IntegerDefaultValue(), si.IntegerMinValue());870layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);871layout->addWidget(cb, current_row, 1, 1, 3);872current_row++;873}874break;875876case SettingInfo::Type::Float:877{878QDoubleSpinBox* sb = new QDoubleSpinBox(parent_widget);879sb->setObjectName(QString::fromUtf8(si.name));880if (si.multiplier != 0.0f && si.multiplier != 1.0f)881{882const float multiplier = si.multiplier;883sb->setMinimum(si.FloatMinValue() * multiplier);884sb->setMaximum(si.FloatMaxValue() * multiplier);885sb->setSingleStep(si.FloatStepValue() * multiplier);886if (std::abs(si.multiplier - 100.0f) < 0.01f)887{888sb->setDecimals(0);889sb->setSuffix(QStringLiteral("%"));890}891892ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(sif, sb, section, std::move(key_name),893si.multiplier, si.FloatDefaultValue());894}895else896{897sb->setMinimum(si.FloatMinValue());898sb->setMaximum(si.FloatMaxValue());899sb->setSingleStep(si.FloatStepValue());900901ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, sb, section, std::move(key_name),902si.FloatDefaultValue());903}904layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);905layout->addWidget(sb, current_row, 1, 1, 3);906current_row++;907}908break;909910case SettingInfo::Type::String:911{912QLineEdit* le = new QLineEdit(parent_widget);913le->setObjectName(QString::fromUtf8(si.name));914ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),915si.StringDefaultValue());916layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);917layout->addWidget(le, current_row, 1, 1, 3);918current_row++;919}920break;921922case SettingInfo::Type::Path:923{924QLineEdit* le = new QLineEdit(parent_widget);925le->setObjectName(QString::fromUtf8(si.name));926QPushButton* browse_button =927new QPushButton(qApp->translate("ControllerCustomSettingsWidget", "Browse..."), parent_widget);928ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),929si.StringDefaultValue());930QObject::connect(browse_button, &QPushButton::clicked, [le, root = QtUtils::GetRootWidget(parent_widget)]() {931QString path = QDir::toNativeSeparators(932QFileDialog::getOpenFileName(root, qApp->translate("ControllerCustomSettingsWidget", "Select File")));933if (!path.isEmpty())934le->setText(path);935});936937QHBoxLayout* hbox = new QHBoxLayout();938hbox->addWidget(le, 1);939hbox->addWidget(browse_button);940941layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);942layout->addLayout(hbox, current_row, 1, 1, 3);943current_row++;944}945break;946}947948QLabel* label = new QLabel(si.description ? qApp->translate(tr_context, si.description) : QString(), parent_widget);949label->setWordWrap(true);950layout->addWidget(label, current_row++, 0, 1, 4);951952layout->addItem(new QSpacerItem(1, 10, QSizePolicy::Minimum, QSizePolicy::Fixed), current_row++, 0, 1, 4);953}954}955956static void restoreDefaultSettingWidgets(QWidget* parent_widget, std::span<const SettingInfo> settings)957{958for (const SettingInfo& si : settings)959{960const QString key(QString::fromStdString(si.name));961962switch (si.type)963{964case SettingInfo::Type::Boolean:965{966QCheckBox* widget = parent_widget->findChild<QCheckBox*>(QString::fromStdString(si.name));967if (widget)968widget->setChecked(si.BooleanDefaultValue());969}970break;971972case SettingInfo::Type::Integer:973{974QSpinBox* widget = parent_widget->findChild<QSpinBox*>(QString::fromStdString(si.name));975if (widget)976widget->setValue(si.IntegerDefaultValue());977}978break;979980case SettingInfo::Type::IntegerList:981{982QComboBox* widget = parent_widget->findChild<QComboBox*>(QString::fromStdString(si.name));983if (widget)984widget->setCurrentIndex(si.IntegerDefaultValue() - si.IntegerMinValue());985}986break;987988case SettingInfo::Type::Float:989{990QDoubleSpinBox* widget = parent_widget->findChild<QDoubleSpinBox*>(QString::fromStdString(si.name));991if (widget)992{993if (si.multiplier != 0.0f && si.multiplier != 1.0f)994widget->setValue(si.FloatDefaultValue() * si.multiplier);995else996widget->setValue(si.FloatDefaultValue());997}998}999break;10001001case SettingInfo::Type::String:1002{1003QLineEdit* widget = parent_widget->findChild<QLineEdit*>(QString::fromStdString(si.name));1004if (widget)1005widget->setText(QString::fromUtf8(si.StringDefaultValue()));1006}1007break;10081009case SettingInfo::Type::Path:1010{1011QLineEdit* widget = parent_widget->findChild<QLineEdit*>(QString::fromStdString(si.name));1012if (widget)1013widget->setText(QString::fromUtf8(si.StringDefaultValue()));1014}1015break;1016}1017}1018}10191020ControllerCustomSettingsWidget::ControllerCustomSettingsWidget(ControllerBindingWidget* parent)1021: QWidget(parent), m_parent(parent)1022{1023const Controller::ControllerInfo* cinfo = parent->getControllerInfo();1024DebugAssert(cinfo);1025if (cinfo->settings.empty())1026return;10271028QScrollArea* sarea = new QScrollArea(this);1029QWidget* swidget = new QWidget(sarea);1030sarea->setWidget(swidget);1031sarea->setWidgetResizable(true);1032sarea->setFrameShape(QFrame::StyledPanel);1033sarea->setFrameShadow(QFrame::Sunken);10341035QGridLayout* swidget_layout = new QGridLayout(swidget);1036createSettingWidgets(parent->getDialog()->getEditingSettingsInterface(), swidget, swidget_layout,1037parent->getConfigSection(), cinfo->settings, cinfo->name);10381039int current_row = swidget_layout->rowCount();10401041QHBoxLayout* bottom_hlayout = new QHBoxLayout();1042QPushButton* restore_defaults = new QPushButton(tr("Restore Default Settings"), swidget);1043restore_defaults->setIcon(QIcon::fromTheme(QStringLiteral("restart-line")));1044bottom_hlayout->addStretch(1);1045bottom_hlayout->addWidget(restore_defaults);1046swidget_layout->addLayout(bottom_hlayout, current_row++, 0, 1, 4);1047connect(restore_defaults, &QPushButton::clicked, this, &ControllerCustomSettingsWidget::restoreDefaults);10481049swidget_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), current_row++, 0, 1, 4);10501051QVBoxLayout* layout = new QVBoxLayout(this);1052layout->setContentsMargins(0, 0, 0, 0);1053layout->addWidget(sarea);1054}10551056ControllerCustomSettingsWidget::~ControllerCustomSettingsWidget() = default;10571058void ControllerCustomSettingsWidget::restoreDefaults()1059{1060const Controller::ControllerInfo* cinfo = m_parent->getControllerInfo();1061DebugAssert(cinfo);10621063restoreDefaultSettingWidgets(this, cinfo->settings);1064}10651066ControllerCustomSettingsDialog::ControllerCustomSettingsDialog(QWidget* parent, SettingsInterface* sif,1067const std::string& section,1068std::span<const SettingInfo> settings,1069const char* tr_context, const QString& window_title)1070: QDialog(parent)1071{1072setMinimumWidth(500);1073resize(minimumWidth(), 100);1074setWindowTitle(window_title);10751076QGridLayout* layout = new QGridLayout(this);1077createSettingWidgets(sif, this, layout, section, settings, tr_context);10781079QDialogButtonBox* bbox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::RestoreDefaults, this);1080connect(bbox, &QDialogButtonBox::accepted, this, &ControllerCustomSettingsDialog::accept);1081connect(bbox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this,1082[this, settings]() { restoreDefaultSettingWidgets(this, settings); });1083layout->addWidget(bbox, layout->rowCount(), 0, 1, 4);1084}10851086ControllerCustomSettingsDialog::~ControllerCustomSettingsDialog() = default;108710881089