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.
This commit is contained in:
Stenzek 2025-01-22 18:23:16 +10:00
parent b08ab9f712
commit 9113a6e6a6
No known key found for this signature in database
8 changed files with 201 additions and 16 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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<std::pair<GenericInputBinding, std::string>> 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();

View File

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

View File

@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// 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));
}

View File

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

View File

@ -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<std::pair<GenericInputBinding, std::string>>& mapping)
const std::vector<std::pair<GenericInputBinding, std::string>>& 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<std::string> InputManager::GetInputProfileNames()
{
FileSystem::FindResultsArray results;

View File

@ -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<std::pair<GenericInputBinding, std::string>>& mapping);
const std::vector<std::pair<GenericInputBinding, std::string>>& 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<std::string> GetInputProfileNames();