Skip to content

Commit

Permalink
Qt: Add 'Multiple Devices' to automatic mapping
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stenzek committed Jan 22, 2025
1 parent b08ab9f commit 9113a6e
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 16 deletions.
2 changes: 1 addition & 1 deletion src/core/fullscreen_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/core/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
109 changes: 106 additions & 3 deletions src/duckstation-qt/controllerbindingwidgets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
Expand All @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions src/duckstation-qt/controllerbindingwidgets.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ class ControllerBindingWidget final : public QWidget
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();
void onClearBindingsClicked();
void onBindingsClicked();
void onSettingsClicked();
void onMacrosClicked();
void onMultipleDeviceAutomaticBindingTriggered();

private:
void populateControllerTypes();
Expand Down
30 changes: 26 additions & 4 deletions src/duckstation-qt/setupwizarddialog.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <[email protected]>
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0

#include "setupwizarddialog.h"
#include "controllerbindingwidgets.h"
#include "controllersettingwidgetbinder.h"
#include "interfacesettingswidget.h"
#include "mainwindow.h"
Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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));
}
3 changes: 3 additions & 0 deletions src/duckstation-qt/setupwizarddialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ private Q_SLOTS:
void refreshDirectoryList();
void resizeDirectoryListColumns();

void doMultipleDeviceAutomaticBinding(u32 port, QLabel* update_label);

protected:
void resizeEvent(QResizeEvent* event);

Expand Down Expand Up @@ -72,6 +74,7 @@ private Q_SLOTS:

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

Expand Down
62 changes: 56 additions & 6 deletions src/util/input_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -1572,18 +1579,61 @@ 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;
}

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;
Expand Down
6 changes: 5 additions & 1 deletion src/util/input_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 9113a6e

Please sign in to comment.