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();