Path: blob/master/src/duckstation-qt/controllerbindingwidgets.cpp
4802 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 ||502bi.type == InputBindingInfo::Type::LED)503{504if (!axis_gbox)505{506axis_gbox = new QGroupBox(tr("Axes"), scrollarea_widget);507axis_layout = new QGridLayout(axis_gbox);508}509510QGroupBox* const gbox =511new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), axis_gbox);512QVBoxLayout* const temp = new QVBoxLayout(gbox);513QWidget* const widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);514515temp->addWidget(widget);516axis_layout->addWidget(gbox, row, column);517if ((++column) == NUM_AXIS_COLUMNS)518{519column = 0;520row++;521}522}523}524525if (axis_gbox)526axis_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);527528const int num_button_columns = axis_layout ? 2 : 4;529row = 0;530column = 0;531for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)532{533if (bi.type == InputBindingInfo::Type::Button)534{535if (!button_gbox)536{537button_gbox = new QGroupBox(tr("Buttons"), scrollarea_widget);538button_layout = new QGridLayout(button_gbox);539}540541QGroupBox* gbox =542new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), button_gbox);543QVBoxLayout* temp = new QVBoxLayout(gbox);544InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);545temp->addWidget(widget);546button_layout->addWidget(gbox, row, column);547if ((++column) == num_button_columns)548{549column = 0;550row++;551}552}553}554555if (button_gbox)556button_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);557558if (!axis_gbox && !button_gbox)559{560delete scrollarea_widget;561delete scrollarea;562return;563}564565QHBoxLayout* layout = new QHBoxLayout(scrollarea_widget);566if (axis_gbox)567layout->addWidget(axis_gbox, 1);568if (button_gbox)569layout->addWidget(button_gbox, 1);570571QHBoxLayout* main_layout = new QHBoxLayout(parent);572main_layout->addWidget(scrollarea);573}574575void ControllerBindingWidget::bindBindingWidgets(QWidget* parent)576{577SettingsInterface* sif = getDialog()->getEditingSettingsInterface();578DebugAssert(m_controller_info);579580const std::string& config_section = getConfigSection();581for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)582{583if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||584bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer ||585bi.type == InputBindingInfo::Type::RelativePointer || bi.type == InputBindingInfo::Type::Motor ||586bi.type == InputBindingInfo::Type::LED)587{588InputBindingWidget* widget = parent->findChild<InputBindingWidget*>(QString::fromUtf8(bi.name));589if (!widget)590{591ERROR_LOG("No widget found for '{}' ({})", bi.name, m_controller_info->name);592continue;593}594595widget->initialize(sif, bi.type, config_section, bi.name);596}597}598}599600//////////////////////////////////////////////////////////////////////////601602ControllerMacroWidget::ControllerMacroWidget(ControllerBindingWidget* parent) : QWidget(parent)603{604m_ui.setupUi(this);605setWindowTitle(tr("Controller Port %1 Macros").arg(parent->getPortNumber() + 1u));606createWidgets(parent);607}608609ControllerMacroWidget::~ControllerMacroWidget() = default;610611void ControllerMacroWidget::updateListItem(u32 index)612{613m_ui.portList->item(static_cast<int>(index))614->setText(tr("Macro %1\n%2").arg(index + 1).arg(m_macros[index]->getSummary()));615}616617void ControllerMacroWidget::createWidgets(ControllerBindingWidget* parent)618{619for (u32 i = 0; i < NUM_MACROS; i++)620{621m_macros[i] = new ControllerMacroEditWidget(this, parent, i);622m_ui.container->addWidget(m_macros[i]);623624QListWidgetItem* item = new QListWidgetItem();625item->setIcon(QIcon::fromTheme(QStringLiteral("flashlight-line")));626m_ui.portList->addItem(item);627updateListItem(i);628}629630m_ui.portList->setCurrentRow(0);631m_ui.container->setCurrentIndex(0);632633connect(m_ui.portList, &QListWidget::currentRowChanged, m_ui.container, &QStackedWidget::setCurrentIndex);634}635636//////////////////////////////////////////////////////////////////////////637638ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget,639u32 index)640: QWidget(parent), m_parent(parent), m_bwidget(bwidget), m_index(index)641{642m_ui.setupUi(this);643644ControllerSettingsWindow* dialog = m_bwidget->getDialog();645const std::string& section = m_bwidget->getConfigSection();646const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();647DebugAssert(cinfo);648649// load binds (single string joined by &)650const std::string binds_string(651dialog->getStringValue(section.c_str(), TinyString::from_format("Macro{}Binds", index + 1u), ""));652const std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true));653654for (const std::string_view& button : buttons_split)655{656for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)657{658if (button == bi.name)659{660m_binds.push_back(&bi);661break;662}663}664}665666// populate list view667for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)668{669if (bi.type == InputBindingInfo::Type::Motor)670continue;671672QListWidgetItem* item = new QListWidgetItem();673item->setText(QtUtils::StringViewToQString(cinfo->GetBindingDisplayName(bi)));674item->setCheckState((std::find(m_binds.begin(), m_binds.end(), &bi) != m_binds.end()) ? Qt::Checked :675Qt::Unchecked);676m_ui.bindList->addItem(item);677}678679ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(680dialog->getEditingSettingsInterface(), m_ui.pressure, section, fmt::format("Macro{}Pressure", index + 1u), 100.0f,6811.0f);682ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(683dialog->getEditingSettingsInterface(), m_ui.deadzone, section, fmt::format("Macro{}Deadzone", index + 1u), 100.0f,6840.0f);685connect(m_ui.pressure, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onPressureChanged);686connect(m_ui.deadzone, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onDeadzoneChanged);687onPressureChanged();688onDeadzoneChanged();689690m_frequency = dialog->getIntValue(section.c_str(), TinyString::from_format("Macro{}Frequency", index + 1u), 0);691ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(dialog->getEditingSettingsInterface(), m_ui.triggerToggle,692section.c_str(), fmt::format("Macro{}Toggle", index + 1u),693false);694updateFrequencyText();695696m_ui.trigger->initialize(dialog->getEditingSettingsInterface(), InputBindingInfo::Type::Macro, section,697fmt::format("Macro{}", index + 1u));698699connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); });700connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); });701connect(m_ui.setFrequency, &QAbstractButton::clicked, this, &ControllerMacroEditWidget::onSetFrequencyClicked);702connect(m_ui.bindList, &QListWidget::itemChanged, this, &ControllerMacroEditWidget::updateBinds);703}704705ControllerMacroEditWidget::~ControllerMacroEditWidget() = default;706707QString ControllerMacroEditWidget::getSummary() const708{709SmallString str;710for (const Controller::ControllerBindingInfo* bi : m_binds)711{712if (!str.empty())713str.append('/');714str.append(bi->name);715}716return str.empty() ? tr("Not Configured") : QString::fromUtf8(str.c_str(), static_cast<int>(str.length()));717}718719void ControllerMacroEditWidget::onPressureChanged()720{721m_ui.pressureValue->setText(tr("%1%").arg(m_ui.pressure->value()));722}723724void ControllerMacroEditWidget::onDeadzoneChanged()725{726m_ui.deadzoneValue->setText(tr("%1%").arg(m_ui.deadzone->value()));727}728729void ControllerMacroEditWidget::onSetFrequencyClicked()730{731bool okay;732int new_freq = QInputDialog::getInt(this, tr("Set Frequency"), tr("Frequency: "), static_cast<int>(m_frequency), 0,733std::numeric_limits<int>::max(), 1, &okay);734if (!okay)735return;736737m_frequency = static_cast<u32>(new_freq);738updateFrequency();739}740741void ControllerMacroEditWidget::modFrequency(s32 delta)742{743if (delta < 0 && m_frequency == 0)744return;745746m_frequency = static_cast<u32>(static_cast<s32>(m_frequency) + delta);747updateFrequency();748}749750void ControllerMacroEditWidget::updateFrequency()751{752m_bwidget->getDialog()->setIntValue(m_bwidget->getConfigSection().c_str(),753fmt::format("Macro{}Frequency", m_index + 1u).c_str(),754static_cast<s32>(m_frequency));755updateFrequencyText();756}757758void ControllerMacroEditWidget::updateFrequencyText()759{760if (m_frequency == 0)761m_ui.frequencyText->setText(tr("Macro will not repeat."));762else763m_ui.frequencyText->setText(tr("Macro will toggle buttons every %1 frames.").arg(m_frequency));764}765766void ControllerMacroEditWidget::updateBinds()767{768ControllerSettingsWindow* dialog = m_bwidget->getDialog();769const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();770DebugAssert(cinfo);771772std::vector<const Controller::ControllerBindingInfo*> new_binds;773u32 bind_index = 0;774for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)775{776if (bi.type == InputBindingInfo::Type::Motor)777continue;778779const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index));780bind_index++;781782if (!item)783{784// shouldn't happen785continue;786}787788if (item->checkState() == Qt::Checked)789new_binds.push_back(&bi);790}791if (m_binds == new_binds)792return;793794m_binds = std::move(new_binds);795796std::string binds_string;797for (const Controller::ControllerBindingInfo* bi : m_binds)798{799if (!binds_string.empty())800binds_string.append(" & ");801binds_string.append(bi->name);802}803804const std::string& section = m_bwidget->getConfigSection();805const std::string key(fmt::format("Macro{}Binds", m_index + 1u));806if (binds_string.empty())807dialog->clearSettingValue(section.c_str(), key.c_str());808else809dialog->setStringValue(section.c_str(), key.c_str(), binds_string.c_str());810811m_parent->updateListItem(m_index);812}813814//////////////////////////////////////////////////////////////////////////815816static void createSettingWidgets(SettingsInterface* const sif, QWidget* parent_widget, QGridLayout* layout,817const std::string& section, std::span<const SettingInfo> settings,818const char* tr_context)819{820int current_row = 0;821822for (const SettingInfo& si : settings)823{824std::string key_name = si.name;825826switch (si.type)827{828case SettingInfo::Type::Boolean:829{830QCheckBox* cb = new QCheckBox(qApp->translate(tr_context, si.display_name), parent_widget);831cb->setObjectName(QString::fromUtf8(si.name));832ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, cb, section, std::move(key_name),833si.BooleanDefaultValue());834layout->addWidget(cb, current_row, 0, 1, 4);835current_row++;836}837break;838839case SettingInfo::Type::Integer:840{841QSpinBox* sb = new QSpinBox(parent_widget);842sb->setObjectName(QString::fromUtf8(si.name));843sb->setMinimum(si.IntegerMinValue());844sb->setMaximum(si.IntegerMaxValue());845sb->setSingleStep(si.IntegerStepValue());846ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, sb, section, std::move(key_name),847si.IntegerDefaultValue());848layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);849layout->addWidget(sb, current_row, 1, 1, 3);850current_row++;851}852break;853854case SettingInfo::Type::IntegerList:855{856QComboBox* cb = new QComboBox(parent_widget);857cb->setObjectName(QString::fromUtf8(si.name));858for (u32 j = 0; si.options[j] != nullptr; j++)859cb->addItem(qApp->translate(tr_context, si.options[j]));860ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, cb, section, std::move(key_name),861si.IntegerDefaultValue(), si.IntegerMinValue());862layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);863layout->addWidget(cb, current_row, 1, 1, 3);864current_row++;865}866break;867868case SettingInfo::Type::Float:869{870QDoubleSpinBox* sb = new QDoubleSpinBox(parent_widget);871sb->setObjectName(QString::fromUtf8(si.name));872if (si.multiplier != 0.0f && si.multiplier != 1.0f)873{874const float multiplier = si.multiplier;875sb->setMinimum(si.FloatMinValue() * multiplier);876sb->setMaximum(si.FloatMaxValue() * multiplier);877sb->setSingleStep(si.FloatStepValue() * multiplier);878if (std::abs(si.multiplier - 100.0f) < 0.01f)879{880sb->setDecimals(0);881sb->setSuffix(QStringLiteral("%"));882}883884ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(sif, sb, section, std::move(key_name),885si.multiplier, si.FloatDefaultValue());886}887else888{889sb->setMinimum(si.FloatMinValue());890sb->setMaximum(si.FloatMaxValue());891sb->setSingleStep(si.FloatStepValue());892893ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, sb, section, std::move(key_name),894si.FloatDefaultValue());895}896layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);897layout->addWidget(sb, current_row, 1, 1, 3);898current_row++;899}900break;901902case SettingInfo::Type::String:903{904QLineEdit* le = new QLineEdit(parent_widget);905le->setObjectName(QString::fromUtf8(si.name));906ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),907si.StringDefaultValue());908layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);909layout->addWidget(le, current_row, 1, 1, 3);910current_row++;911}912break;913914case SettingInfo::Type::Path:915{916QLineEdit* le = new QLineEdit(parent_widget);917le->setObjectName(QString::fromUtf8(si.name));918QPushButton* browse_button =919new QPushButton(qApp->translate("ControllerCustomSettingsWidget", "Browse..."), parent_widget);920ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),921si.StringDefaultValue());922QObject::connect(browse_button, &QPushButton::clicked, [le, root = QtUtils::GetRootWidget(parent_widget)]() {923QString path = QDir::toNativeSeparators(924QFileDialog::getOpenFileName(root, qApp->translate("ControllerCustomSettingsWidget", "Select File")));925if (!path.isEmpty())926le->setText(path);927});928929QHBoxLayout* hbox = new QHBoxLayout();930hbox->addWidget(le, 1);931hbox->addWidget(browse_button);932933layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);934layout->addLayout(hbox, current_row, 1, 1, 3);935current_row++;936}937break;938}939940QLabel* label = new QLabel(si.description ? qApp->translate(tr_context, si.description) : QString(), parent_widget);941label->setWordWrap(true);942layout->addWidget(label, current_row++, 0, 1, 4);943944layout->addItem(new QSpacerItem(1, 10, QSizePolicy::Minimum, QSizePolicy::Fixed), current_row++, 0, 1, 4);945}946}947948static void restoreDefaultSettingWidgets(QWidget* parent_widget, std::span<const SettingInfo> settings)949{950for (const SettingInfo& si : settings)951{952const QString key(QString::fromStdString(si.name));953954switch (si.type)955{956case SettingInfo::Type::Boolean:957{958QCheckBox* widget = parent_widget->findChild<QCheckBox*>(QString::fromStdString(si.name));959if (widget)960widget->setChecked(si.BooleanDefaultValue());961}962break;963964case SettingInfo::Type::Integer:965{966QSpinBox* widget = parent_widget->findChild<QSpinBox*>(QString::fromStdString(si.name));967if (widget)968widget->setValue(si.IntegerDefaultValue());969}970break;971972case SettingInfo::Type::IntegerList:973{974QComboBox* widget = parent_widget->findChild<QComboBox*>(QString::fromStdString(si.name));975if (widget)976widget->setCurrentIndex(si.IntegerDefaultValue() - si.IntegerMinValue());977}978break;979980case SettingInfo::Type::Float:981{982QDoubleSpinBox* widget = parent_widget->findChild<QDoubleSpinBox*>(QString::fromStdString(si.name));983if (widget)984{985if (si.multiplier != 0.0f && si.multiplier != 1.0f)986widget->setValue(si.FloatDefaultValue() * si.multiplier);987else988widget->setValue(si.FloatDefaultValue());989}990}991break;992993case SettingInfo::Type::String:994{995QLineEdit* widget = parent_widget->findChild<QLineEdit*>(QString::fromStdString(si.name));996if (widget)997widget->setText(QString::fromUtf8(si.StringDefaultValue()));998}999break;10001001case SettingInfo::Type::Path:1002{1003QLineEdit* widget = parent_widget->findChild<QLineEdit*>(QString::fromStdString(si.name));1004if (widget)1005widget->setText(QString::fromUtf8(si.StringDefaultValue()));1006}1007break;1008}1009}1010}10111012ControllerCustomSettingsWidget::ControllerCustomSettingsWidget(ControllerBindingWidget* parent)1013: QWidget(parent), m_parent(parent)1014{1015const Controller::ControllerInfo* cinfo = parent->getControllerInfo();1016DebugAssert(cinfo);1017if (cinfo->settings.empty())1018return;10191020QScrollArea* sarea = new QScrollArea(this);1021QWidget* swidget = new QWidget(sarea);1022sarea->setWidget(swidget);1023sarea->setWidgetResizable(true);1024sarea->setFrameShape(QFrame::StyledPanel);1025sarea->setFrameShadow(QFrame::Sunken);10261027QGridLayout* swidget_layout = new QGridLayout(swidget);1028createSettingWidgets(parent->getDialog()->getEditingSettingsInterface(), swidget, swidget_layout,1029parent->getConfigSection(), cinfo->settings, cinfo->name);10301031int current_row = swidget_layout->rowCount();10321033QHBoxLayout* bottom_hlayout = new QHBoxLayout();1034QPushButton* restore_defaults = new QPushButton(tr("Restore Default Settings"), swidget);1035restore_defaults->setIcon(QIcon::fromTheme(QStringLiteral("restart-line")));1036bottom_hlayout->addStretch(1);1037bottom_hlayout->addWidget(restore_defaults);1038swidget_layout->addLayout(bottom_hlayout, current_row++, 0, 1, 4);1039connect(restore_defaults, &QPushButton::clicked, this, &ControllerCustomSettingsWidget::restoreDefaults);10401041swidget_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), current_row++, 0, 1, 4);10421043QVBoxLayout* layout = new QVBoxLayout(this);1044layout->setContentsMargins(0, 0, 0, 0);1045layout->addWidget(sarea);1046}10471048ControllerCustomSettingsWidget::~ControllerCustomSettingsWidget() = default;10491050void ControllerCustomSettingsWidget::restoreDefaults()1051{1052const Controller::ControllerInfo* cinfo = m_parent->getControllerInfo();1053DebugAssert(cinfo);10541055restoreDefaultSettingWidgets(this, cinfo->settings);1056}10571058ControllerCustomSettingsDialog::ControllerCustomSettingsDialog(QWidget* parent, SettingsInterface* sif,1059const std::string& section,1060std::span<const SettingInfo> settings,1061const char* tr_context, const QString& window_title)1062: QDialog(parent)1063{1064setMinimumWidth(500);1065resize(minimumWidth(), 100);1066setWindowTitle(window_title);10671068QGridLayout* layout = new QGridLayout(this);1069createSettingWidgets(sif, this, layout, section, settings, tr_context);10701071QDialogButtonBox* bbox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::RestoreDefaults, this);1072connect(bbox, &QDialogButtonBox::accepted, this, &ControllerCustomSettingsDialog::accept);1073connect(bbox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this,1074[this, settings]() { restoreDefaultSettingWidgets(this, settings); });1075layout->addWidget(bbox, layout->rowCount(), 0, 1, 4);1076}10771078ControllerCustomSettingsDialog::~ControllerCustomSettingsDialog() = default;107910801081