Path: blob/master/src/duckstation-qt/controllerbindingwidgets.cpp
7362 views
// SPDX-FileCopyrightText: 2019-2026 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/QScrollArea>38#include <QtWidgets/QSpinBox>39#include <algorithm>4041#include "moc_controllerbindingwidgets.cpp"4243using namespace Qt::StringLiterals;4445LOG_CHANNEL(Host);4647ControllerBindingWidget::ControllerBindingWidget(QWidget* parent, ControllerSettingsWindow* dialog, u32 port)48: QWidget(parent), m_dialog(dialog), m_config_section(Controller::GetSettingsSection(port)), m_port_number(port)49{50m_ui.setupUi(this);51populateControllerTypes();52populateWidgets();5354connect(m_ui.controllerType, QOverload<int>::of(&QComboBox::currentIndexChanged), this,55&ControllerBindingWidget::onTypeChanged);56connect(m_ui.bindings, &QPushButton::clicked, this, &ControllerBindingWidget::onBindingsClicked);57connect(m_ui.settings, &QPushButton::clicked, this, &ControllerBindingWidget::onSettingsClicked);58connect(m_ui.macros, &QPushButton::clicked, this, &ControllerBindingWidget::onMacrosClicked);59connect(m_ui.automaticBinding, &QPushButton::clicked, this, &ControllerBindingWidget::onAutomaticBindingClicked);60connect(m_ui.clearBindings, &QPushButton::clicked, this, &ControllerBindingWidget::onClearBindingsClicked);61}6263ControllerBindingWidget::~ControllerBindingWidget() = default;6465void ControllerBindingWidget::populateControllerTypes()66{67for (const Controller::ControllerInfo* cinfo : Controller::GetControllerInfoList())68m_ui.controllerType->addItem(QtUtils::StringViewToQString(cinfo->GetDisplayName()),69QVariant(static_cast<int>(cinfo->type)));7071m_controller_info = Controller::GetControllerInfo(72m_dialog->getStringValue(m_config_section.c_str(), "Type",73Controller::GetControllerInfo(Settings::GetDefaultControllerType(m_port_number)).name));74if (!m_controller_info)75m_controller_info = &Controller::GetControllerInfo(Settings::GetDefaultControllerType(m_port_number));7677const int index = m_ui.controllerType->findData(QVariant(static_cast<int>(m_controller_info->type)));78if (index >= 0 && index != m_ui.controllerType->currentIndex())79{80QSignalBlocker sb(m_ui.controllerType);81m_ui.controllerType->setCurrentIndex(index);82}83}8485void ControllerBindingWidget::populateWidgets()86{87const bool is_initializing = (m_ui.stackedWidget->count() == 0);88if (m_bindings_widget)89{90m_ui.stackedWidget->removeWidget(m_bindings_widget);91delete m_bindings_widget;92m_bindings_widget = nullptr;93}94if (m_settings_widget)95{96m_ui.stackedWidget->removeWidget(m_settings_widget);97delete m_settings_widget;98m_settings_widget = nullptr;99}100if (m_macros_widget)101{102m_ui.stackedWidget->removeWidget(m_macros_widget);103delete m_macros_widget;104m_macros_widget = nullptr;105}106107const bool has_settings = !m_controller_info->settings.empty();108const bool has_macros = !m_controller_info->bindings.empty();109m_ui.settings->setEnabled(has_settings);110m_ui.macros->setEnabled(has_macros);111112m_bindings_widget = new QWidget(this);113switch (m_controller_info->type)114{115case ControllerType::AnalogController:116{117Ui::ControllerBindingWidget_AnalogController ui;118ui.setupUi(m_bindings_widget);119bindBindingWidgets(m_bindings_widget);120m_icon = QIcon::fromTheme("controller-line"_L1);121}122break;123124case ControllerType::AnalogJoystick:125{126Ui::ControllerBindingWidget_AnalogJoystick ui;127ui.setupUi(m_bindings_widget);128bindBindingWidgets(m_bindings_widget);129m_icon = QIcon::fromTheme("joystick-line"_L1);130}131break;132133case ControllerType::DigitalController:134{135Ui::ControllerBindingWidget_DigitalController ui;136ui.setupUi(m_bindings_widget);137bindBindingWidgets(m_bindings_widget);138m_icon = QIcon::fromTheme("controller-digital-line"_L1);139}140break;141142case ControllerType::GunCon:143{144Ui::ControllerBindingWidget_GunCon ui;145ui.setupUi(m_bindings_widget);146bindBindingWidgets(m_bindings_widget);147m_icon = QIcon::fromTheme("guncon-line"_L1);148}149break;150151case ControllerType::NeGcon:152{153Ui::ControllerBindingWidget_NeGcon ui;154ui.setupUi(m_bindings_widget);155bindBindingWidgets(m_bindings_widget);156m_icon = QIcon::fromTheme("negcon-line"_L1);157}158break;159160case ControllerType::NeGconRumble:161{162Ui::ControllerBindingWidget_NeGconRumble ui;163ui.setupUi(m_bindings_widget);164bindBindingWidgets(m_bindings_widget);165m_icon = QIcon::fromTheme("negcon-line"_L1);166}167break;168169case ControllerType::PlayStationMouse:170{171Ui::ControllerBindingWidget_Mouse ui;172ui.setupUi(m_bindings_widget);173bindBindingWidgets(m_bindings_widget);174m_icon = QIcon::fromTheme("mouse-line"_L1);175}176break;177178case ControllerType::Justifier:179{180Ui::ControllerBindingWidget_Justifier ui;181ui.setupUi(m_bindings_widget);182bindBindingWidgets(m_bindings_widget);183m_icon = QIcon::fromTheme("guncon-line"_L1);184}185break;186187case ControllerType::None:188{189m_icon = QIcon::fromTheme("controller-strike-line"_L1);190}191break;192193default:194{195createBindingWidgets(m_bindings_widget);196m_icon = QIcon::fromTheme("controller-line"_L1);197}198break;199}200201m_ui.stackedWidget->addWidget(m_bindings_widget);202m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);203204if (has_settings)205{206m_settings_widget = new ControllerCustomSettingsWidget(this);207m_ui.stackedWidget->addWidget(m_settings_widget);208}209210if (has_macros)211{212m_macros_widget = new ControllerMacroWidget(this);213m_ui.stackedWidget->addWidget(m_macros_widget);214}215216updateHeaderToolButtons();217218// no need to do this on first init, only changes219if (!is_initializing)220m_dialog->updateListDescription(m_port_number, this);221}222223void ControllerBindingWidget::updateHeaderToolButtons()224{225const QWidget* current_widget = m_ui.stackedWidget->currentWidget();226const QSignalBlocker bindings_sb(m_ui.bindings);227const QSignalBlocker settings_sb(m_ui.settings);228const QSignalBlocker macros_sb(m_ui.macros);229230const bool is_bindings = (current_widget == m_bindings_widget);231m_ui.bindings->setChecked(is_bindings);232m_ui.automaticBinding->setEnabled(is_bindings);233m_ui.clearBindings->setEnabled(is_bindings);234m_ui.macros->setChecked(current_widget == m_macros_widget);235m_ui.settings->setChecked((current_widget == m_settings_widget));236}237238void ControllerBindingWidget::onTypeChanged()239{240bool ok;241const int index = m_ui.controllerType->currentData().toInt(&ok);242if (!ok || index < 0 || index >= static_cast<int>(ControllerType::Count))243return;244245m_controller_info = &Controller::GetControllerInfo(static_cast<ControllerType>(index));246247SettingsInterface* sif = m_dialog->getEditingSettingsInterface();248if (sif)249{250sif->SetStringValue(m_config_section.c_str(), "Type", m_controller_info->name);251QtHost::SaveGameSettings(sif, false);252g_core_thread->reloadGameSettings();253}254else255{256Core::SetBaseStringSettingValue(m_config_section.c_str(), "Type", m_controller_info->name);257Host::CommitBaseSettingChanges();258g_core_thread->applySettings();259}260261populateWidgets();262}263264void ControllerBindingWidget::onAutomaticBindingClicked()265{266QMenu* const menu = QtUtils::NewPopupMenu(this);267bool added = false;268269for (const InputDeviceListModel::Device& dev : g_core_thread->getInputDeviceListModel()->getDeviceList())270{271// we set it as data, because the device list could get invalidated while the menu is up272menu->addAction(InputDeviceListModel::getIconForKey(dev.key),273QStringLiteral("%1 (%2)").arg(dev.identifier).arg(dev.display_name),274[this, device = dev.identifier]() { doDeviceAutomaticBinding(device); });275added = true;276}277278if (added)279{280menu->addAction(tr("Multiple devices..."), this,281&ControllerBindingWidget::onMultipleDeviceAutomaticBindingTriggered);282}283else284{285QAction* const action = menu->addAction(tr("No devices available"));286action->setEnabled(false);287}288289menu->popup(QCursor::pos());290}291292void ControllerBindingWidget::onClearBindingsClicked()293{294if (QtUtils::MessageBoxQuestion(295this, tr("Clear Mapping"),296tr("Are you sure you want to clear all mappings for this controller? This action cannot be undone.")) !=297QMessageBox::Yes)298{299return;300}301302if (m_dialog->isEditingGlobalSettings())303{304const auto lock = Core::GetSettingsLock();305InputManager::ClearPortBindings(*Core::GetBaseSettingsLayer(), m_port_number);306}307else308{309InputManager::ClearPortBindings(*m_dialog->getEditingSettingsInterface(), m_port_number);310}311312saveAndRefresh();313}314315void ControllerBindingWidget::onBindingsClicked()316{317m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);318updateHeaderToolButtons();319}320321void ControllerBindingWidget::onSettingsClicked()322{323if (!m_settings_widget)324return;325326m_ui.stackedWidget->setCurrentWidget(m_settings_widget);327updateHeaderToolButtons();328}329330void ControllerBindingWidget::onMacrosClicked()331{332if (!m_macros_widget)333return;334335m_ui.stackedWidget->setCurrentWidget(m_macros_widget);336updateHeaderToolButtons();337}338339void ControllerBindingWidget::doDeviceAutomaticBinding(const QString& device)340{341std::vector<std::pair<GenericInputBinding, std::string>> mapping =342InputManager::GetGenericBindingMapping(device.toStdString());343if (mapping.empty())344{345QtUtils::AsyncMessageBox(346this, QMessageBox::Critical, tr("Automatic Mapping Failed"),347tr("No generic bindings were generated for device '%1'. The controller/source may not support automatic mapping.")348.arg(device));349return;350}351352bool result;353if (m_dialog->isEditingGlobalSettings())354{355const auto lock = Core::GetSettingsLock();356result = InputManager::MapController(*Core::GetBaseSettingsLayer(), m_port_number, mapping, true);357}358else359{360result = InputManager::MapController(*m_dialog->getEditingSettingsInterface(), m_port_number, mapping, true);361QtHost::SaveGameSettings(m_dialog->getEditingSettingsInterface(), false);362g_core_thread->reloadInputBindings();363}364365// force a refresh after mapping366if (result)367saveAndRefresh();368}369370void ControllerBindingWidget::onMultipleDeviceAutomaticBindingTriggered()371{372QDialog* const dialog = new MultipleDeviceAutobindDialog(this, m_dialog, m_port_number);373dialog->setAttribute(Qt::WA_DeleteOnClose);374375// force a refresh after mapping376connect(dialog, &QDialog::accepted, this, [this] { onTypeChanged(); });377378dialog->open();379}380381void ControllerBindingWidget::saveAndRefresh()382{383onTypeChanged();384QtHost::QueueSettingsSave();385g_core_thread->applySettings();386}387388void ControllerBindingWidget::createBindingWidgets(QWidget* parent)389{390SettingsInterface* sif = getDialog()->getEditingSettingsInterface();391DebugAssert(m_controller_info);392393QGroupBox* axis_gbox = nullptr;394QGridLayout* axis_layout = nullptr;395QGroupBox* button_gbox = nullptr;396QGridLayout* button_layout = nullptr;397398QScrollArea* scrollarea = new QScrollArea(parent);399QWidget* scrollarea_widget = new QWidget(scrollarea);400scrollarea->setWidget(scrollarea_widget);401scrollarea->setWidgetResizable(true);402scrollarea->setFrameShape(QFrame::StyledPanel);403scrollarea->setFrameShadow(QFrame::Sunken);404405// We do axes and buttons separately, so we can figure out how many columns to use.406constexpr int NUM_AXIS_COLUMNS = 2;407int column = 0;408int row = 0;409for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)410{411if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||412bi.type == InputBindingInfo::Type::Pointer || bi.type == InputBindingInfo::Type::RelativePointer ||413bi.type == InputBindingInfo::Type::Device || bi.type == InputBindingInfo::Type::Motor ||414bi.type == InputBindingInfo::Type::LED)415{416if (!axis_gbox)417{418axis_gbox = new QGroupBox(tr("Axes"), scrollarea_widget);419axis_layout = new QGridLayout(axis_gbox);420}421422QGroupBox* const gbox =423new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), axis_gbox);424QVBoxLayout* const temp = new QVBoxLayout(gbox);425QWidget* const widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);426427temp->addWidget(widget);428axis_layout->addWidget(gbox, row, column);429if ((++column) == NUM_AXIS_COLUMNS)430{431column = 0;432row++;433}434}435}436437if (axis_gbox)438axis_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);439440const int num_button_columns = axis_layout ? 2 : 4;441row = 0;442column = 0;443for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)444{445if (bi.type == InputBindingInfo::Type::Button)446{447if (!button_gbox)448{449button_gbox = new QGroupBox(tr("Buttons"), scrollarea_widget);450button_layout = new QGridLayout(button_gbox);451}452453QGroupBox* gbox =454new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), button_gbox);455QVBoxLayout* temp = new QVBoxLayout(gbox);456InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);457temp->addWidget(widget);458button_layout->addWidget(gbox, row, column);459if ((++column) == num_button_columns)460{461column = 0;462row++;463}464}465}466467if (button_gbox)468button_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);469470if (!axis_gbox && !button_gbox)471{472delete scrollarea_widget;473delete scrollarea;474return;475}476477QHBoxLayout* layout = new QHBoxLayout(scrollarea_widget);478if (axis_gbox)479layout->addWidget(axis_gbox, 1);480if (button_gbox)481layout->addWidget(button_gbox, 1);482483QHBoxLayout* main_layout = new QHBoxLayout(parent);484main_layout->addWidget(scrollarea);485}486487void ControllerBindingWidget::bindBindingWidgets(QWidget* parent)488{489SettingsInterface* sif = getDialog()->getEditingSettingsInterface();490DebugAssert(m_controller_info);491492const std::string& config_section = getConfigSection();493for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)494{495if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||496bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer ||497bi.type == InputBindingInfo::Type::RelativePointer || bi.type == InputBindingInfo::Type::Motor ||498bi.type == InputBindingInfo::Type::LED)499{500InputBindingWidget* widget = parent->findChild<InputBindingWidget*>(QString::fromUtf8(bi.name));501if (!widget)502{503ERROR_LOG("No widget found for '{}' ({})", bi.name, m_controller_info->name);504continue;505}506507widget->initialize(sif, bi.type, config_section, bi.name);508}509}510}511512//////////////////////////////////////////////////////////////////////////513514ControllerMacroWidget::ControllerMacroWidget(ControllerBindingWidget* parent) : QSplitter(parent)515{516setChildrenCollapsible(false);517setWindowTitle(tr("Controller Port %1 Macros").arg(parent->getPortNumber() + 1u));518createWidgets(parent);519}520521ControllerMacroWidget::~ControllerMacroWidget() = default;522523void ControllerMacroWidget::updateListItem(u32 index)524{525QString summary = m_macros[index]->getSummary();526QListWidgetItem* item = m_macroList->item(static_cast<int>(index));527item->setText(tr("Macro %1\n%2").arg(index + 1).arg(summary));528item->setToolTip(summary);529}530531void ControllerMacroWidget::createWidgets(ControllerBindingWidget* bwidget)532{533m_macroList = new QListWidget(this);534m_macroList->setIconSize(QSize(32, 32));535m_macroList->setMinimumWidth(150);536addWidget(m_macroList);537setStretchFactor(0, 1);538539m_container = new QStackedWidget(this);540addWidget(m_container);541setStretchFactor(1, 3);542543for (u32 i = 0; i < m_macros.size(); i++)544{545m_macros[i] = new ControllerMacroEditWidget(this, bwidget, i);546m_container->addWidget(m_macros[i]);547548QListWidgetItem* item = new QListWidgetItem();549item->setIcon(QIcon::fromTheme("flashlight-line"_L1));550m_macroList->addItem(item);551updateListItem(i);552}553554m_macroList->setCurrentRow(0);555m_container->setCurrentIndex(0);556557connect(m_macroList, &QListWidget::currentRowChanged, m_container, &QStackedWidget::setCurrentIndex);558}559560//////////////////////////////////////////////////////////////////////////561562ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget,563u32 index)564: QWidget(parent), m_parent(parent), m_bwidget(bwidget), m_index(index)565{566m_ui.setupUi(this);567m_ui.increaseFrequency->setIcon(style()->standardIcon(QStyle::SP_ArrowUp));568m_ui.decreateFrequency->setIcon(style()->standardIcon(QStyle::SP_ArrowDown));569570ControllerSettingsWindow* dialog = m_bwidget->getDialog();571const std::string& section = m_bwidget->getConfigSection();572const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();573DebugAssert(cinfo);574575// load binds (single string joined by &)576const std::string binds_string(577dialog->getStringValue(section.c_str(), TinyString::from_format("Macro{}Binds", index + 1u), ""));578const std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true));579580for (const std::string_view& button : buttons_split)581{582for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)583{584if (button == bi.name)585{586m_binds.push_back(&bi);587break;588}589}590}591592// populate list view593for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)594{595if (bi.type == InputBindingInfo::Type::Motor)596continue;597598QListWidgetItem* item = new QListWidgetItem();599item->setText(QtUtils::StringViewToQString(cinfo->GetBindingDisplayName(bi)));600item->setCheckState((std::find(m_binds.begin(), m_binds.end(), &bi) != m_binds.end()) ? Qt::Checked :601Qt::Unchecked);602m_ui.bindList->addItem(item);603}604605ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(606dialog->getEditingSettingsInterface(), m_ui.pressure, section, fmt::format("Macro{}Pressure", index + 1u), 100.0f,6071.0f);608ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(609dialog->getEditingSettingsInterface(), m_ui.deadzone, section, fmt::format("Macro{}Deadzone", index + 1u), 100.0f,6100.0f);611connect(m_ui.pressure, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onPressureChanged);612connect(m_ui.deadzone, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onDeadzoneChanged);613onPressureChanged();614onDeadzoneChanged();615616m_frequency = dialog->getIntValue(section.c_str(), TinyString::from_format("Macro{}Frequency", index + 1u), 0);617ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(dialog->getEditingSettingsInterface(), m_ui.triggerToggle,618section.c_str(), fmt::format("Macro{}Toggle", index + 1u),619false);620updateFrequencyText();621622m_ui.trigger->initialize(dialog->getEditingSettingsInterface(), InputBindingInfo::Type::Macro, section,623fmt::format("Macro{}", index + 1u));624625connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); });626connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); });627connect(m_ui.setFrequency, &QAbstractButton::clicked, this, &ControllerMacroEditWidget::onSetFrequencyClicked);628connect(m_ui.bindList, &QListWidget::itemChanged, this, &ControllerMacroEditWidget::updateBinds);629}630631ControllerMacroEditWidget::~ControllerMacroEditWidget() = default;632633QString ControllerMacroEditWidget::getSummary() const634{635SmallString str;636for (const Controller::ControllerBindingInfo* bi : m_binds)637{638if (!str.empty())639str.append('/');640str.append(bi->name);641}642return str.empty() ? tr("Not Configured") : QString::fromUtf8(str.c_str(), static_cast<int>(str.length()));643}644645void ControllerMacroEditWidget::onPressureChanged()646{647m_ui.pressureValue->setText(tr("%1%").arg(m_ui.pressure->value()));648}649650void ControllerMacroEditWidget::onDeadzoneChanged()651{652m_ui.deadzoneValue->setText(tr("%1%").arg(m_ui.deadzone->value()));653}654655void ControllerMacroEditWidget::onSetFrequencyClicked()656{657bool okay;658int new_freq = QInputDialog::getInt(this, tr("Set Frequency"), tr("Frequency: "), static_cast<int>(m_frequency), 0,659std::numeric_limits<int>::max(), 1, &okay);660if (!okay)661return;662663m_frequency = static_cast<u32>(new_freq);664updateFrequency();665}666667void ControllerMacroEditWidget::modFrequency(s32 delta)668{669if (delta < 0 && m_frequency == 0)670return;671672m_frequency = static_cast<u32>(static_cast<s32>(m_frequency) + delta);673updateFrequency();674}675676void ControllerMacroEditWidget::updateFrequency()677{678m_bwidget->getDialog()->setIntValue(m_bwidget->getConfigSection().c_str(),679fmt::format("Macro{}Frequency", m_index + 1u).c_str(),680static_cast<s32>(m_frequency));681updateFrequencyText();682}683684void ControllerMacroEditWidget::updateFrequencyText()685{686if (m_frequency == 0)687m_ui.frequencyText->setText(tr("Macro will not repeat."));688else689m_ui.frequencyText->setText(tr("Macro will toggle buttons every %n frame(s).", nullptr, m_frequency));690}691692void ControllerMacroEditWidget::updateBinds()693{694ControllerSettingsWindow* dialog = m_bwidget->getDialog();695const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();696DebugAssert(cinfo);697698std::vector<const Controller::ControllerBindingInfo*> new_binds;699u32 bind_index = 0;700for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)701{702if (bi.type == InputBindingInfo::Type::Motor)703continue;704705const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index));706bind_index++;707708if (!item)709{710// shouldn't happen711continue;712}713714if (item->checkState() == Qt::Checked)715new_binds.push_back(&bi);716}717if (m_binds == new_binds)718return;719720m_binds = std::move(new_binds);721722std::string binds_string;723for (const Controller::ControllerBindingInfo* bi : m_binds)724{725if (!binds_string.empty())726binds_string.append(" & ");727binds_string.append(bi->name);728}729730const std::string& section = m_bwidget->getConfigSection();731const std::string key(fmt::format("Macro{}Binds", m_index + 1u));732if (binds_string.empty())733dialog->clearSettingValue(section.c_str(), key.c_str());734else735dialog->setStringValue(section.c_str(), key.c_str(), binds_string.c_str());736737m_parent->updateListItem(m_index);738}739740//////////////////////////////////////////////////////////////////////////741742static void createSettingWidgets(SettingsInterface* const sif, QWidget* parent_widget, QGridLayout* layout,743const std::string& section, std::span<const SettingInfo> settings,744const char* tr_context)745{746int current_row = 0;747748for (const SettingInfo& si : settings)749{750std::string key_name = si.name;751752switch (si.type)753{754case SettingInfo::Type::Boolean:755{756QCheckBox* cb = new QCheckBox(qApp->translate(tr_context, si.display_name), parent_widget);757cb->setObjectName(si.name);758ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, cb, section, std::move(key_name),759si.BooleanDefaultValue());760layout->addWidget(cb, current_row, 0, 1, 4);761current_row++;762}763break;764765case SettingInfo::Type::Integer:766{767QSpinBox* sb = new QSpinBox(parent_widget);768sb->setObjectName(si.name);769sb->setMinimum(si.IntegerMinValue());770sb->setMaximum(si.IntegerMaxValue());771sb->setSingleStep(si.IntegerStepValue());772ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, sb, section, std::move(key_name),773si.IntegerDefaultValue());774layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);775layout->addWidget(sb, current_row, 1, 1, 3);776current_row++;777}778break;779780case SettingInfo::Type::IntegerList:781{782QComboBox* cb = new QComboBox(parent_widget);783cb->setObjectName(si.name);784for (u32 j = 0; si.options[j] != nullptr; j++)785cb->addItem(qApp->translate(tr_context, si.options[j]));786ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, cb, section, std::move(key_name),787si.IntegerDefaultValue(), si.IntegerMinValue());788layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);789layout->addWidget(cb, current_row, 1, 1, 3);790current_row++;791}792break;793794case SettingInfo::Type::Float:795{796QDoubleSpinBox* sb = new QDoubleSpinBox(parent_widget);797sb->setObjectName(si.name);798if (si.multiplier != 0.0f && si.multiplier != 1.0f)799{800const float multiplier = si.multiplier;801sb->setMinimum(si.FloatMinValue() * multiplier);802sb->setMaximum(si.FloatMaxValue() * multiplier);803sb->setSingleStep(si.FloatStepValue() * multiplier);804if (std::abs(si.multiplier - 100.0f) < 0.01f)805{806sb->setDecimals(0);807sb->setSuffix("%"_L1);808}809810ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(sif, sb, section, std::move(key_name),811si.multiplier, si.FloatDefaultValue());812}813else814{815sb->setMinimum(si.FloatMinValue());816sb->setMaximum(si.FloatMaxValue());817sb->setSingleStep(si.FloatStepValue());818819ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, sb, section, std::move(key_name),820si.FloatDefaultValue());821}822layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);823layout->addWidget(sb, current_row, 1, 1, 3);824current_row++;825}826break;827828case SettingInfo::Type::String:829{830QLineEdit* le = new QLineEdit(parent_widget);831le->setObjectName(si.name);832ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),833si.StringDefaultValue());834layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);835layout->addWidget(le, current_row, 1, 1, 3);836current_row++;837}838break;839840case SettingInfo::Type::Path:841{842QLineEdit* le = new QLineEdit(parent_widget);843le->setObjectName(si.name);844QPushButton* browse_button =845new QPushButton(qApp->translate("ControllerCustomSettingsWidget", "Browse..."), parent_widget);846ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),847si.StringDefaultValue());848QObject::connect(browse_button, &QPushButton::clicked, [le, root = parent_widget]() {849QString path = QDir::toNativeSeparators(850QFileDialog::getOpenFileName(root, qApp->translate("ControllerCustomSettingsWidget", "Select File")));851if (!path.isEmpty())852le->setText(path);853});854855QHBoxLayout* hbox = new QHBoxLayout();856hbox->addWidget(le, 1);857hbox->addWidget(browse_button);858859layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);860layout->addLayout(hbox, current_row, 1, 1, 3);861current_row++;862}863break;864}865866QLabel* label = new QLabel(si.description ? qApp->translate(tr_context, si.description) : QString(), parent_widget);867label->setWordWrap(true);868layout->addWidget(label, current_row++, 0, 1, 4);869870layout->addItem(new QSpacerItem(1, 10, QSizePolicy::Minimum, QSizePolicy::Fixed), current_row++, 0, 1, 4);871}872}873874static void restoreDefaultSettingWidgets(QWidget* parent_widget, std::span<const SettingInfo> settings)875{876for (const SettingInfo& si : settings)877{878switch (si.type)879{880case SettingInfo::Type::Boolean:881{882QCheckBox* widget = parent_widget->findChild<QCheckBox*>(si.name);883if (widget)884widget->setChecked(si.BooleanDefaultValue());885}886break;887888case SettingInfo::Type::Integer:889{890QSpinBox* widget = parent_widget->findChild<QSpinBox*>(si.name);891if (widget)892widget->setValue(si.IntegerDefaultValue());893}894break;895896case SettingInfo::Type::IntegerList:897{898QComboBox* widget = parent_widget->findChild<QComboBox*>(si.name);899if (widget)900widget->setCurrentIndex(si.IntegerDefaultValue() - si.IntegerMinValue());901}902break;903904case SettingInfo::Type::Float:905{906QDoubleSpinBox* widget = parent_widget->findChild<QDoubleSpinBox*>(si.name);907if (widget)908{909if (si.multiplier != 0.0f && si.multiplier != 1.0f)910widget->setValue(si.FloatDefaultValue() * si.multiplier);911else912widget->setValue(si.FloatDefaultValue());913}914}915break;916917case SettingInfo::Type::String:918{919QLineEdit* widget = parent_widget->findChild<QLineEdit*>(si.name);920if (widget)921widget->setText(QString::fromUtf8(si.StringDefaultValue()));922}923break;924925case SettingInfo::Type::Path:926{927QLineEdit* widget = parent_widget->findChild<QLineEdit*>(si.name);928if (widget)929widget->setText(QString::fromUtf8(si.StringDefaultValue()));930}931break;932}933}934}935936ControllerCustomSettingsWidget::ControllerCustomSettingsWidget(ControllerBindingWidget* parent)937: QWidget(parent), m_parent(parent)938{939const Controller::ControllerInfo* cinfo = parent->getControllerInfo();940DebugAssert(cinfo);941if (cinfo->settings.empty())942return;943944QScrollArea* sarea = new QScrollArea(this);945QWidget* swidget = new QWidget(sarea);946sarea->setWidget(swidget);947sarea->setWidgetResizable(true);948sarea->setFrameShape(QFrame::StyledPanel);949sarea->setFrameShadow(QFrame::Sunken);950951QGridLayout* swidget_layout = new QGridLayout(swidget);952createSettingWidgets(parent->getDialog()->getEditingSettingsInterface(), swidget, swidget_layout,953parent->getConfigSection(), cinfo->settings, cinfo->name);954955int current_row = swidget_layout->rowCount();956957QHBoxLayout* bottom_hlayout = new QHBoxLayout();958QPushButton* restore_defaults = new QPushButton(tr("Restore Default Settings"), swidget);959restore_defaults->setIcon(QIcon::fromTheme("restart-line"_L1));960bottom_hlayout->addStretch(1);961bottom_hlayout->addWidget(restore_defaults);962swidget_layout->addLayout(bottom_hlayout, current_row++, 0, 1, 4);963connect(restore_defaults, &QPushButton::clicked, this, &ControllerCustomSettingsWidget::restoreDefaults);964965swidget_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), current_row++, 0, 1, 4);966967QVBoxLayout* layout = new QVBoxLayout(this);968layout->setContentsMargins(0, 0, 0, 0);969layout->addWidget(sarea);970}971972ControllerCustomSettingsWidget::~ControllerCustomSettingsWidget() = default;973974void ControllerCustomSettingsWidget::restoreDefaults()975{976const Controller::ControllerInfo* cinfo = m_parent->getControllerInfo();977DebugAssert(cinfo);978979restoreDefaultSettingWidgets(this, cinfo->settings);980}981982ControllerCustomSettingsDialog::ControllerCustomSettingsDialog(QWidget* parent, SettingsInterface* sif,983const std::string& section,984std::span<const SettingInfo> settings,985const char* tr_context, const QString& window_title)986: QDialog(parent)987{988setMinimumWidth(500);989resize(minimumWidth(), 100);990setWindowTitle(window_title);991992QGridLayout* layout = new QGridLayout(this);993createSettingWidgets(sif, this, layout, section, settings, tr_context);994995QDialogButtonBox* bbox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::RestoreDefaults, this);996bbox->button(QDialogButtonBox::Close)->setDefault(true);997connect(bbox, &QDialogButtonBox::rejected, this, &ControllerCustomSettingsDialog::accept);998connect(bbox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this,999[this, settings]() { restoreDefaultSettingWidgets(this, settings); });1000layout->addWidget(bbox, layout->rowCount(), 0, 1, 4);1001}10021003ControllerCustomSettingsDialog::~ControllerCustomSettingsDialog() = default;10041005MultipleDeviceAutobindDialog::MultipleDeviceAutobindDialog(QWidget* parent, ControllerSettingsWindow* settings_window,1006u32 port)1007: QDialog(parent), m_settings_window(settings_window), m_port(port)1008{1009QVBoxLayout* layout = new QVBoxLayout(this);1010layout->addWidget(1011new QLabel(tr("Select the devices from the list below that you want to bind to this controller."), this));10121013m_list = new QListWidget(this);1014m_list->setSelectionMode(QListWidget::SingleSelection);1015layout->addWidget(m_list);10161017for (const InputDeviceListModel::Device& dev : g_core_thread->getInputDeviceListModel()->getDeviceList())1018{1019QListWidgetItem* item = new QListWidgetItem;1020item->setIcon(InputDeviceListModel::getIconForKey(dev.key));1021item->setText(QStringLiteral("%1 (%2)").arg(dev.identifier).arg(dev.display_name));1022item->setData(Qt::UserRole, dev.identifier);1023item->setFlags(item->flags() | Qt::ItemIsUserCheckable);1024item->setCheckState(Qt::Unchecked);1025m_list->addItem(item);1026}10271028QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);1029connect(bb, &QDialogButtonBox::accepted, this, &MultipleDeviceAutobindDialog::doAutomaticBinding);1030connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject);1031layout->addWidget(bb);1032}10331034MultipleDeviceAutobindDialog::~MultipleDeviceAutobindDialog() = default;10351036void MultipleDeviceAutobindDialog::doAutomaticBinding()1037{1038auto lock = Core::GetSettingsLock();1039const bool global = (!m_settings_window || m_settings_window->isEditingGlobalSettings());1040SettingsInterface* si = global ? Core::GetBaseSettingsLayer() : m_settings_window->getEditingSettingsInterface();10411042// first device should clear mappings1043bool tried_any = false;1044bool mapped_any = false;1045const int count = m_list->count();1046for (int i = 0; i < count; i++)1047{1048const QListWidgetItem* item = m_list->item(i);1049if (item->checkState() != Qt::Checked)1050continue;10511052tried_any = true;10531054const QString identifier = item->data(Qt::UserRole).toString();1055std::vector<std::pair<GenericInputBinding, std::string>> mapping =1056InputManager::GetGenericBindingMapping(identifier.toStdString());1057if (mapping.empty())1058{1059lock.unlock();1060QtUtils::MessageBoxCritical(1061this, tr("Automatic Mapping Failed"),1062tr("No generic bindings were generated for device '%1'. The controller/source may not "1063"support automatic mapping.")1064.arg(identifier));1065lock.lock();1066continue;1067}10681069mapped_any |= InputManager::MapController(*si, m_port, mapping, !mapped_any);1070}10711072lock.unlock();10731074if (!tried_any)1075{1076QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Automatic Mapping Failed"),1077tr("No devices were selected."));1078return;1079}10801081if (mapped_any)1082{1083if (global)1084{1085QtHost::SaveGameSettings(si, false);1086g_core_thread->reloadGameSettings(false);1087}1088else1089{1090QtHost::QueueSettingsSave();1091g_core_thread->reloadInputBindings();1092}1093accept();1094}1095else1096{1097reject();1098}1099}110011011102