From 9113a6e6a6b532ff3c0add7a925a4b547b5fc864 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 22 Jan 2025 18:23:16 +1000 Subject: [PATCH] Qt: Add 'Multiple Devices' to automatic mapping Also populate the "current device" label with the device from the config when running the setup wizard, instead of always setting the label to Keyboard. --- src/core/fullscreen_ui.cpp | 2 +- src/core/settings.cpp | 2 +- .../controllerbindingwidgets.cpp | 109 +++++++++++++++++- src/duckstation-qt/controllerbindingwidgets.h | 3 + src/duckstation-qt/setupwizarddialog.cpp | 30 ++++- src/duckstation-qt/setupwizarddialog.h | 3 + src/util/input_manager.cpp | 62 +++++++++- src/util/input_manager.h | 6 +- 8 files changed, 201 insertions(+), 16 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 7f9232952..85ac1948d 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -3236,7 +3236,7 @@ void FullscreenUI::StartAutomaticBindingForPort(u32 port) auto lock = Host::GetSettingsLock(); SettingsInterface* bsi = GetEditingSettingsInterface(); const bool result = - InputManager::MapController(*bsi, port, InputManager::GetGenericBindingMapping(name)); + InputManager::MapController(*bsi, port, InputManager::GetGenericBindingMapping(name), true); SetSettingsChanged(bsi); // and the toast needs to happen on the UI thread. diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 3d73f94cd..140b46f54 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -1187,7 +1187,7 @@ void Settings::SetDefaultControllerConfig(SettingsInterface& si) #ifndef __ANDROID__ // Use the automapper to set this up. - InputManager::MapController(si, 0, InputManager::GetGenericBindingMapping("Keyboard")); + InputManager::MapController(si, 0, InputManager::GetGenericBindingMapping("Keyboard"), true); #endif } diff --git a/src/duckstation-qt/controllerbindingwidgets.cpp b/src/duckstation-qt/controllerbindingwidgets.cpp index d2bb9f9f3..67e6b6ada 100644 --- a/src/duckstation-qt/controllerbindingwidgets.cpp +++ b/src/duckstation-qt/controllerbindingwidgets.cpp @@ -273,7 +273,12 @@ void ControllerBindingWidget::onAutomaticBindingClicked() added = true; } - if (!added) + if (added) + { + QAction* action = menu.addAction(tr("Multiple devices...")); + connect(action, &QAction::triggered, this, &ControllerBindingWidget::onMultipleDeviceAutomaticBindingTriggered); + } + else { QAction* action = menu.addAction(tr("No devices available")); action->setEnabled(false); @@ -346,11 +351,11 @@ void ControllerBindingWidget::doDeviceAutomaticBinding(const QString& device) if (m_dialog->isEditingGlobalSettings()) { auto lock = Host::GetSettingsLock(); - result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), m_port_number, mapping); + result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), m_port_number, mapping, true); } else { - result = InputManager::MapController(*m_dialog->getEditingSettingsInterface(), m_port_number, mapping); + result = InputManager::MapController(*m_dialog->getEditingSettingsInterface(), m_port_number, mapping, true); QtHost::SaveGameSettings(m_dialog->getEditingSettingsInterface(), false); g_emu_thread->reloadInputBindings(); } @@ -360,6 +365,104 @@ void ControllerBindingWidget::doDeviceAutomaticBinding(const QString& device) saveAndRefresh(); } +void ControllerBindingWidget::onMultipleDeviceAutomaticBindingTriggered() +{ + // force a refresh after mapping + if (doMultipleDeviceAutomaticBinding(this, m_dialog, m_port_number)) + onTypeChanged(); +} + +bool ControllerBindingWidget::doMultipleDeviceAutomaticBinding(QWidget* parent, ControllerSettingsWindow* parent_dialog, + u32 port) +{ + QDialog dialog(parent); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + QLabel help(tr("Select the devices from the list below that you want to bind to this controller."), &dialog); + layout->addWidget(&help); + + QListWidget list(&dialog); + list.setSelectionMode(QListWidget::SingleSelection); + layout->addWidget(&list); + + for (const InputDeviceListModel::Device& dev : g_emu_thread->getInputDeviceListModel()->getDeviceList()) + { + QListWidgetItem* item = new QListWidgetItem; + item->setText(QStringLiteral("%1 (%2)").arg(dev.identifier).arg(dev.display_name)); + item->setData(Qt::UserRole, dev.identifier); + item->setIcon(InputDeviceListModel::getIconForKey(dev.key)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + list.addItem(item); + } + + QDialogButtonBox bb(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + connect(&bb, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(&bb, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(&bb); + + if (dialog.exec() == 0) + return false; + + auto lock = Host::GetSettingsLock(); + const bool global = (!parent_dialog || parent_dialog->isEditingGlobalSettings()); + SettingsInterface& si = + *(global ? Host::Internal::GetBaseSettingsLayer() : parent_dialog->getEditingSettingsInterface()); + + // first device should clear mappings + bool tried_any = false; + bool mapped_any = false; + const int count = list.count(); + for (int i = 0; i < count; i++) + { + QListWidgetItem* item = list.item(i); + if (item->checkState() != Qt::Checked) + continue; + + tried_any = true; + + const QString identifier = item->data(Qt::UserRole).toString(); + std::vector> mapping = + InputManager::GetGenericBindingMapping(identifier.toStdString()); + if (mapping.empty()) + { + lock.unlock(); + QMessageBox::critical(QtUtils::GetRootWidget(parent), tr("Automatic Mapping"), + tr("No generic bindings were generated for device '%1'. The controller/source may not " + "support automatic mapping.") + .arg(identifier)); + lock.lock(); + continue; + } + + mapped_any |= InputManager::MapController(si, port, mapping, !mapped_any); + } + + lock.unlock(); + + if (!tried_any) + { + QMessageBox::information(QtUtils::GetRootWidget(parent), tr("Automatic Mapping"), tr("No devices were selected.")); + return false; + } + + if (mapped_any) + { + if (global) + { + QtHost::SaveGameSettings(&si, false); + g_emu_thread->reloadGameSettings(false); + } + else + { + QtHost::QueueSettingsSave(); + g_emu_thread->reloadInputBindings(); + } + } + + return mapped_any; +} + void ControllerBindingWidget::saveAndRefresh() { onTypeChanged(); diff --git a/src/duckstation-qt/controllerbindingwidgets.h b/src/duckstation-qt/controllerbindingwidgets.h index 0284b078a..4a4787d34 100644 --- a/src/duckstation-qt/controllerbindingwidgets.h +++ b/src/duckstation-qt/controllerbindingwidgets.h @@ -37,6 +37,8 @@ public: ALWAYS_INLINE u32 getPortNumber() const { return m_port_number; } ALWAYS_INLINE const QIcon& getIcon() { return m_icon; } + static bool doMultipleDeviceAutomaticBinding(QWidget* parent, ControllerSettingsWindow* parent_dialog, u32 port); + private Q_SLOTS: void onTypeChanged(); void onAutomaticBindingClicked(); @@ -44,6 +46,7 @@ private Q_SLOTS: void onBindingsClicked(); void onSettingsClicked(); void onMacrosClicked(); + void onMultipleDeviceAutomaticBindingTriggered(); private: void populateControllerTypes(); diff --git a/src/duckstation-qt/setupwizarddialog.cpp b/src/duckstation-qt/setupwizarddialog.cpp index dacaaecba..d9071a133 100644 --- a/src/duckstation-qt/setupwizarddialog.cpp +++ b/src/duckstation-qt/setupwizarddialog.cpp @@ -1,7 +1,8 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "setupwizarddialog.h" +#include "controllerbindingwidgets.h" #include "controllersettingwidgetbinder.h" #include "interfacesettingswidget.h" #include "mainwindow.h" @@ -411,7 +412,7 @@ void SetupWizardDialog::setupControllerPage(bool initial) nullptr, w.type_combo, section, "Type", Controller::GetControllerInfo(Settings::GetDefaultControllerType(port)).name); - w.mapping_result->setText((port == 0) ? tr("Default (Keyboard)") : tr("Default (None)")); + w.mapping_result->setText(findCurrentDeviceForPort(port)); if (initial) { @@ -431,6 +432,13 @@ void SetupWizardDialog::updateStylesheets() { } +QString SetupWizardDialog::findCurrentDeviceForPort(u32 port) const +{ + auto lock = Host::GetSettingsLock(); + return QString::fromStdString( + InputManager::GetPhysicalDeviceForController(*Host::Internal::GetBaseSettingsLayer(), port)); +} + void SetupWizardDialog::openAutomaticMappingMenu(u32 port, QLabel* update_label) { QMenu menu(this); @@ -448,7 +456,13 @@ void SetupWizardDialog::openAutomaticMappingMenu(u32 port, QLabel* update_label) added = true; } - if (!added) + if (added) + { + QAction* action = menu.addAction(tr("Multiple Devices...")); + connect(action, &QAction::triggered, this, + [this, port, update_label]() { doMultipleDeviceAutomaticBinding(port, update_label); }); + } + else { QAction* action = menu.addAction(tr("No devices available")); action->setEnabled(false); @@ -474,7 +488,7 @@ void SetupWizardDialog::doDeviceAutomaticBinding(u32 port, QLabel* update_label, bool result; { auto lock = Host::GetSettingsLock(); - result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), port, mapping); + result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), port, mapping, true); } if (!result) return; @@ -483,3 +497,11 @@ void SetupWizardDialog::doDeviceAutomaticBinding(u32 port, QLabel* update_label, update_label->setText(device); } + +void SetupWizardDialog::doMultipleDeviceAutomaticBinding(u32 port, QLabel* update_label) +{ + if (!ControllerBindingWidget::doMultipleDeviceAutomaticBinding(this, nullptr, port)) + return; + + update_label->setText(findCurrentDeviceForPort(port)); +} diff --git a/src/duckstation-qt/setupwizarddialog.h b/src/duckstation-qt/setupwizarddialog.h index 4bb775d62..471f326a0 100644 --- a/src/duckstation-qt/setupwizarddialog.h +++ b/src/duckstation-qt/setupwizarddialog.h @@ -45,6 +45,8 @@ private Q_SLOTS: void refreshDirectoryList(); void resizeDirectoryListColumns(); + void doMultipleDeviceAutomaticBinding(u32 port, QLabel* update_label); + protected: void resizeEvent(QResizeEvent* event); @@ -72,6 +74,7 @@ private: void addPathToTable(const std::string& path, bool recursive); + QString findCurrentDeviceForPort(u32 port) const; void openAutomaticMappingMenu(u32 port, QLabel* update_label); void doDeviceAutomaticBinding(u32 port, QLabel* update_label, const QString& device); diff --git a/src/util/input_manager.cpp b/src/util/input_manager.cpp index d79e4969d..4ba18483d 100644 --- a/src/util/input_manager.cpp +++ b/src/util/input_manager.cpp @@ -1530,7 +1530,7 @@ void InputManager::CopyConfiguration(SettingsInterface* dest_si, const SettingsI static u32 TryMapGenericMapping(SettingsInterface& si, const std::string& section, const GenericInputBindingMapping& mapping, GenericInputBinding generic_name, - const char* bind_name) + const char* bind_name, bool clear_existing_mappings) { // find the mapping it corresponds to const std::string* found_mapping = nullptr; @@ -1546,18 +1546,25 @@ static u32 TryMapGenericMapping(SettingsInterface& si, const std::string& sectio if (found_mapping) { INFO_LOG("Map {}/{} to '{}'", section, bind_name, *found_mapping); - si.SetStringValue(section.c_str(), bind_name, found_mapping->c_str()); + if (clear_existing_mappings) + si.SetStringValue(section.c_str(), bind_name, found_mapping->c_str()); + else + si.AddToStringList(section.c_str(), bind_name, found_mapping->c_str()); + return 1; } else { - si.DeleteValue(section.c_str(), bind_name); + if (clear_existing_mappings) + si.DeleteValue(section.c_str(), bind_name); + return 0; } } bool InputManager::MapController(SettingsInterface& si, u32 controller, - const std::vector>& mapping) + const std::vector>& mapping, + bool clear_existing_mappings) { const std::string section = Controller::GetSettingsSection(controller); const TinyString type = si.GetTinyStringValue( @@ -1572,11 +1579,15 @@ bool InputManager::MapController(SettingsInterface& si, u32 controller, if (bi.generic_mapping == GenericInputBinding::Unknown) continue; - u32 mappings_added = TryMapGenericMapping(si, section, mapping, bi.generic_mapping, bi.name); + u32 mappings_added = + TryMapGenericMapping(si, section, mapping, bi.generic_mapping, bi.name, clear_existing_mappings); // try to map to small motor if we tried big motor if (mappings_added == 0 && bi.generic_mapping == GenericInputBinding::LargeMotor) - mappings_added += TryMapGenericMapping(si, section, mapping, GenericInputBinding::SmallMotor, bi.name); + { + mappings_added += + TryMapGenericMapping(si, section, mapping, GenericInputBinding::SmallMotor, bi.name, clear_existing_mappings); + } num_mappings += mappings_added; } @@ -1584,6 +1595,45 @@ bool InputManager::MapController(SettingsInterface& si, u32 controller, return (num_mappings > 0); } +std::string InputManager::GetPhysicalDeviceForController(SettingsInterface& si, u32 controller) +{ + std::string ret; + + const std::string section = Controller::GetSettingsSection(controller); + const TinyString type = si.GetTinyStringValue( + section.c_str(), "Type", Controller::GetControllerInfo(Settings::GetDefaultControllerType(controller)).name); + const Controller::ControllerInfo* info = Controller::GetControllerInfo(type); + if (info) + { + for (const Controller::ControllerBindingInfo& bi : info->bindings) + { + for (const std::string& binding : si.GetStringList(section.c_str(), bi.name)) + { + std::string_view source, sub_binding; + if (!SplitBinding(binding, &source, &sub_binding)) + continue; + + if (ret.empty()) + { + ret = source; + continue; + } + + if (ret != source) + { + ret = TRANSLATE_STR("InputManager", "Multiple Devices"); + return ret; + } + } + } + } + + if (ret.empty()) + ret = TRANSLATE_STR("InputManager", "None"); + + return ret; +} + std::vector InputManager::GetInputProfileNames() { FileSystem::FindResultsArray results; diff --git a/src/util/input_manager.h b/src/util/input_manager.h index f5ade5a10..9cb1474f3 100644 --- a/src/util/input_manager.h +++ b/src/util/input_manager.h @@ -384,7 +384,11 @@ void CopyConfiguration(SettingsInterface* dest_si, const SettingsInterface& src_ /// Performs automatic controller mapping with the provided list of generic mappings. bool MapController(SettingsInterface& si, u32 controller, - const std::vector>& mapping); + const std::vector>& mapping, + bool clear_existing_mappings); + +/// Returns the name of the first physical device mapped to the emulated controller, "None", or "Multiple Devices". +std::string GetPhysicalDeviceForController(SettingsInterface& si, u32 controller); /// Returns a list of input profiles available. std::vector GetInputProfileNames();