RMG-Input: add 'Auto-Configure' feature

This commit is contained in:
Rosalie Wanders
2025-10-19 15:50:33 +02:00
parent dc10430315
commit 2679fd41d5
9 changed files with 365 additions and 62 deletions
+1
View File
@@ -29,6 +29,7 @@ set(RMG_INPUT_SOURCES
UserInterface/MainDialog.cpp
UserInterface/UIResources.qrc
Utilities/Sdl3ButtonLabelToString.cpp
Utilities/InputProfileDB.cpp
Utilities/QtKeyToSdl3Key.cpp
Thread/SDLThread.cpp
Thread/HotkeysThread.cpp
+21 -14
View File
@@ -59,6 +59,9 @@ MainDialog::MainDialog(QWidget* parent, Thread::SDLThread* sdlThread, bool romCo
&MainDialog::on_ControllerWidget_UserProfileRemoved);
}
// initialize input profile DB
this->inputProfileDB.Load();
// always add keyboard device
for (auto& controllerWidget : this->controllerWidgets)
{
@@ -68,14 +71,14 @@ MainDialog::MainDialog(QWidget* parent, Thread::SDLThread* sdlThread, bool romCo
// so we only have to expose it there
if (controllerWidget == this->controllerWidgets.last())
{
controllerWidget->AddInputDevice({ InputDeviceType::EmulateVRU, "Voice Recognition Unit" });
controllerWidget->AddInputDevice({ InputDeviceType::EmulateVRU, "Voice Recognition Unit" }, {});
}
#endif // VRU
controllerWidget->SetAllowKeyboardForAutomatic(controllerWidget == this->controllerWidgets.first());
controllerWidget->AddInputDevice({ InputDeviceType::None, "None" });
controllerWidget->AddInputDevice({ InputDeviceType::Automatic, "Automatic" });
controllerWidget->AddInputDevice({ InputDeviceType::Keyboard, "Keyboard" });
this->addInputDevice(controllerWidget, { InputDeviceType::None, "None" });
this->addInputDevice(controllerWidget, { InputDeviceType::Automatic, "Automatic" });
this->addInputDevice(controllerWidget, { InputDeviceType::Keyboard, "Keyboard" });
controllerWidget->SetInitialized(true);
}
@@ -92,11 +95,24 @@ MainDialog::~MainDialog()
this->closeInputDevice();
}
void MainDialog::addInputDevice(Widget::ControllerWidget* controllerWidget, const InputDevice& device)
{
// only fallback to fallback profile when
// we have a controller device
InputProfileDBEntry inputProfile = this->inputProfileDB.FindEntry(device,
device.type == InputDeviceType::Automatic ||
device.type == InputDeviceType::Joystick);
controllerWidget->AddInputDevice(device, inputProfile);
}
void MainDialog::addInputDevice(const InputDevice& device)
{
InputProfileDBEntry inputProfile = this->inputProfileDB.FindEntry(device);
for (auto& controllerWidget : this->controllerWidgets)
{
controllerWidget->AddInputDevice(device);
controllerWidget->AddInputDevice(device, inputProfile);
}
}
@@ -202,15 +218,6 @@ void MainDialog::on_InputPollTimer_triggered()
break;
}
// check if controller has been disconnected,
// if so, keep trying to re-open it
if ((this->currentJoystick != nullptr && !SDL_JoystickConnected(this->currentJoystick)) ||
(this->currentGamepad != nullptr && !SDL_GamepadConnected(this->currentGamepad)))
{
this->closeInputDevice();
this->openInputDevice(this->currentDevice);
}
// process SDL events
SDL_Event event;
while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST) == 1)
@@ -13,6 +13,7 @@
#include "common.hpp"
#include <QDialog>
#include "Utilities/InputProfileDB.hpp"
#include "Widget/ControllerWidget.hpp"
#include "Thread/SDLThread.hpp"
#include "EventFilter.hpp"
@@ -31,6 +32,8 @@ private:
QTimer* inputPollTimer;
Thread::SDLThread* sdlThread;
Utilities::InputProfileDB inputProfileDB;
QList<InputDevice> oldInputDeviceList;
QList<InputDevice> inputDeviceList;
bool updatingDeviceList = false;
@@ -44,6 +47,7 @@ private:
EventFilter* eventFilter;
void addInputDevice(Widget::ControllerWidget* controllerWidget, const InputDevice& device);
void addInputDevice(const InputDevice& device);
void removeInputDevice(const InputDevice& device);
@@ -239,6 +239,7 @@ void ControllerWidget::initializeProfileButtons()
void ControllerWidget::initializeMiscButtons()
{
this->inputDeviceRefreshButton->setIcon(QIcon::fromTheme("refresh-line"));
this->autoConfigButton->setIcon(QIcon::fromTheme("magic-line"));
this->resetButton->setIcon(QIcon::fromTheme("restart-line"));
this->optionsButton->setIcon(QIcon::fromTheme("settings-3-line"));
this->hotkeysButton->setIcon(QIcon::fromTheme("gamepad-line"));
@@ -246,10 +247,10 @@ void ControllerWidget::initializeMiscButtons()
bool ControllerWidget::isCurrentDeviceKeyboard()
{
const InputDevice device = this->inputDeviceComboBox->currentData().value<InputDevice>();
const inputDeviceData deviceData = this->inputDeviceComboBox->currentData().value<inputDeviceData>();
return device.type == InputDeviceType::Keyboard ||
(this->allowKeyboardForAutomatic && device.type == InputDeviceType::Automatic);
return deviceData.device.type == InputDeviceType::Keyboard ||
(this->allowKeyboardForAutomatic && deviceData.device.type == InputDeviceType::Automatic);
}
bool ControllerWidget::isCurrentDeviceNotFound()
@@ -339,6 +340,32 @@ QString ControllerWidget::getUserProfileSectionName(QString profile)
return profileSection;
}
QString ControllerWidget::getGamepadButtonText(SDL_GamepadButton button)
{
QString text = SDL_GetGamepadStringForButton(button);
if (this->currentGamepad == nullptr)
{
return text;
}
// convert A/B/X/Y buttons to their native counterpart,
// this makes the button text look better in the UI
if (button == SDL_GAMEPAD_BUTTON_SOUTH ||
button == SDL_GAMEPAD_BUTTON_EAST ||
button == SDL_GAMEPAD_BUTTON_WEST ||
button == SDL_GAMEPAD_BUTTON_NORTH)
{
SDL_GamepadButtonLabel buttonLabel = SDL_GetGamepadButtonLabel(this->currentGamepad, button);
QString labelString = Utilities::Sdl3ButtonLabelToString(buttonLabel);
if (!labelString.isEmpty())
{ // replace only when we have a name
text = labelString;
}
}
return text;
}
bool ControllerWidget::isSectionUserProfile(QString section)
{
return section.contains("User Profile");
@@ -500,7 +527,7 @@ void ControllerWidget::showErrorMessage(QString text, QString details)
msgBox.exec();
}
void ControllerWidget::AddInputDevice(const InputDevice& device)
void ControllerWidget::AddInputDevice(const InputDevice& device, const InputProfileDBEntry& inputProfile)
{
QString deviceName = QString::fromStdString(device.name);
QString name = deviceName;
@@ -512,18 +539,19 @@ void ControllerWidget::AddInputDevice(const InputDevice& device)
name += ")";
}
this->inputDeviceNameList.append(deviceName);
this->inputDeviceComboBox->addItem(name, QVariant::fromValue<InputDevice>(device));
this->inputDeviceComboBox->addItem(name, QVariant::fromValue<inputDeviceData>({ device, inputProfile }));
}
void ControllerWidget::RemoveInputDevice(const InputDevice& device)
{
inputDeviceNameList.removeOne(QString::fromStdString(device.name));
int index = this->inputDeviceComboBox->findData(QVariant::fromValue<InputDevice>(device));
if (index >= 0)
inputDeviceData deviceData;
for (int i = 0; i < this->inputDeviceComboBox->count(); i++)
{
this->inputDeviceComboBox->removeItem(index);
deviceData = this->inputDeviceComboBox->itemData(i).value<inputDeviceData>();
if (device == deviceData.device)
{
this->inputDeviceComboBox->removeItem(i);
}
}
}
@@ -600,7 +628,6 @@ void ControllerWidget::CheckInputDeviceSettings(QString sectionQString)
int notFoundIndex = this->inputDeviceComboBox->findText("(not found)", Qt::MatchFlag::MatchEndsWith);
if (notFoundIndex != -1)
{
this->inputDeviceNameList.removeAt(notFoundIndex);
this->inputDeviceComboBox->removeItem(notFoundIndex);
}
@@ -609,7 +636,8 @@ void ControllerWidget::CheckInputDeviceSettings(QString sectionQString)
for (int i = 0; i < this->inputDeviceComboBox->count(); i++)
{
InputDevice otherDevice = this->inputDeviceComboBox->itemData(i).value<InputDevice>();
inputDeviceData otherDeviceData = this->inputDeviceComboBox->itemData(i).value<inputDeviceData>();
InputDevice otherDevice = otherDeviceData.device;
if (device.name == otherDevice.name &&
device.serial == otherDevice.serial)
{
@@ -644,9 +672,8 @@ void ControllerWidget::CheckInputDeviceSettings(QString sectionQString)
{ // no match
QString title = QString::fromStdString(deviceName);
title += " (not found)";
this->inputDeviceNameList.append(QString::fromStdString(deviceName));
this->inputDeviceComboBox->addItem(title, QVariant::fromValue<InputDevice>(device));
this->inputDeviceComboBox->setCurrentIndex(this->inputDeviceNameList.count() - 1);
this->inputDeviceComboBox->addItem(title, QVariant::fromValue<inputDeviceData>({ device, {} }));
this->inputDeviceComboBox->setCurrentIndex(this->inputDeviceComboBox->count() - 1);
}
}
@@ -670,7 +697,7 @@ void ControllerWidget::GetCurrentInputDevice(InputDevice& device, bool ignoreDev
}
else
{
device = this->inputDeviceComboBox->itemData(currentIndex).value<InputDevice>();
device = this->inputDeviceComboBox->itemData(currentIndex).value<inputDeviceData>().device;
}
}
@@ -725,20 +752,23 @@ void ControllerWidget::on_inputDeviceComboBox_currentIndexChanged(int value)
return;
}
InputDevice device = this->inputDeviceComboBox->itemData(value).value<InputDevice>();
inputDeviceData deviceData = this->inputDeviceComboBox->itemData(value).value<inputDeviceData>();
this->ClearControllerImage();
if (this->isCurrentDeviceNotFound())
{
device = { };
deviceData.device = { };
}
// set plugged in state
this->setPluggedIn(device.type != InputDeviceType::None &&
device.type != InputDeviceType::EmulateVRU);
// set whether auto configure button is enabled
this->autoConfigButton->setEnabled(deviceData.inputProfile.valid);
emit this->CurrentInputDeviceChanged(this, device);
// set plugged in state
this->setPluggedIn(deviceData.device.type != InputDeviceType::None &&
deviceData.device.type != InputDeviceType::EmulateVRU);
emit this->CurrentInputDeviceChanged(this, deviceData.device);
}
void ControllerWidget::on_inputDeviceRefreshButton_clicked()
@@ -870,6 +900,57 @@ void ControllerWidget::on_removeProfileButton_clicked()
}
}
void ControllerWidget::on_autoConfigButton_clicked()
{
inputDeviceData deviceData = this->inputDeviceComboBox->currentData().value<inputDeviceData>();
QString name;
for (const auto& inputEntry : deviceData.inputProfile.inputEntries)
{
if (inputEntry.button != N64ControllerButton::Invalid)
{
for (const auto& buttonMapping : this->buttonWidgetMappings)
{
if (buttonMapping.button == inputEntry.button)
{
buttonMapping.buttonWidget->Clear();
for (int i = 0; i < inputEntry.data.size(); i++)
{
if (inputEntry.inputTypes[i] == InputType::GamepadButton)
{
name = this->getGamepadButtonText(static_cast<SDL_GamepadButton>(inputEntry.data[i]));
}
else
{
name = inputEntry.names[i];
}
buttonMapping.buttonWidget->AddInputData(inputEntry.inputTypes[i], inputEntry.data[i],
inputEntry.extraData[i], name);
}
break;
}
}
}
else if (inputEntry.axis != InputAxisDirection::Invalid)
{
for (const auto& axisMapping : this->joystickWidgetMappings)
{
if (axisMapping.direction == inputEntry.axis)
{
axisMapping.buttonWidget->Clear();
for (int i = 0; i < inputEntry.data.size(); i++)
{
axisMapping.buttonWidget->AddInputData(inputEntry.inputTypes[i], inputEntry.data[i],
inputEntry.extraData[i], inputEntry.names[i]);
}
break;
}
}
}
}
}
void ControllerWidget::on_resetButton_clicked()
{
const QString section = this->getCurrentSettingsSection();
@@ -886,8 +967,8 @@ void ControllerWidget::on_resetButton_clicked()
void ControllerWidget::on_optionsButton_clicked()
{
InputDevice device = this->inputDeviceComboBox->currentData().value<InputDevice>();
const bool isKeyboard = device.type == InputDeviceType::Keyboard;
inputDeviceData deviceData = this->inputDeviceComboBox->currentData().value<inputDeviceData>();
const bool isKeyboard = deviceData.device.type == InputDeviceType::Keyboard;
OptionsDialog dialog(this, this->optionsDialogSettings,
isKeyboard ? nullptr : this->currentJoystick,
@@ -1050,24 +1131,7 @@ void ControllerWidget::on_MainDialog_SdlEvent(SDL_Event* event)
inputType = InputType::GamepadButton;
sdlButton = event->gbutton.button;
sdlButtonPressed = (event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN);
sdlButtonName = SDL_GetGamepadStringForButton(static_cast<SDL_GamepadButton>(sdlButton));
// convert A/B/X/Y buttons to their native counterpart,
// this makes the button text look better in the UI
if (sdlButton == SDL_GAMEPAD_BUTTON_SOUTH ||
sdlButton == SDL_GAMEPAD_BUTTON_EAST ||
sdlButton == SDL_GAMEPAD_BUTTON_WEST ||
sdlButton == SDL_GAMEPAD_BUTTON_NORTH)
{
SDL_GamepadButtonLabel sdlLabel = SDL_GetGamepadButtonLabel(this->currentGamepad,
static_cast<SDL_GamepadButton>(sdlButton));
QString sdlLabelName = Utilities::Sdl3ButtonLabelToString(sdlLabel);
if (!sdlLabelName.isEmpty())
{ // replace only when we have a name
sdlButtonName = sdlLabelName;
}
}
sdlButtonName = this->getGamepadButtonText(static_cast<SDL_GamepadButton>(sdlButton));
}
else if ((event->type == SDL_EVENT_JOYSTICK_BUTTON_DOWN) ||
(event->type == SDL_EVENT_JOYSTICK_BUTTON_UP))
@@ -1526,8 +1590,8 @@ void ControllerWidget::on_MainDialog_SdlEventPollFinished()
bool ControllerWidget::IsPluggedIn()
{
const InputDevice device = this->inputDeviceComboBox->currentData().value<InputDevice>();
return device.type != InputDeviceType::None;
const inputDeviceData deviceData = this->inputDeviceComboBox->currentData().value<inputDeviceData>();
return deviceData.device.type != InputDeviceType::None;
}
void ControllerWidget::SetOnlyLoadGameProfile(bool value, CoreRomHeader romHeader, CoreRomSettings romSettings)
@@ -1975,6 +2039,9 @@ void ControllerWidget::SetCurrentJoystick(SDL_Joystick* joystick, SDL_Gamepad* g
{
this->currentJoystick = joystick;
this->currentGamepad = gamepad;
// we can enable the auto-configure button when we have a gamepad
this->autoConfigButton->setEnabled(gamepad != nullptr);
}
void ControllerWidget::AddUserProfile(QString name, QString section)
@@ -16,6 +16,7 @@
#include "UserInterface/OptionsDialog.hpp"
#include "UserInterface/HotkeysDialog.hpp"
#include "UserInterface/EventFilter.hpp"
#include "Utilities/InputProfileDB.hpp"
using namespace UserInterface::Widget;
@@ -43,7 +44,6 @@ private:
OptionsDialogSettings optionsDialogSettings;
QList<QString> inputDeviceNameList;
MappingButton* currentButton = nullptr;
bool addMappingToButton = false;
@@ -72,6 +72,12 @@ private:
SettingsID extraDataSettingsId;
};
struct inputDeviceData
{
InputDevice device;
InputProfileDBEntry inputProfile;
};
QList<buttonWidgetMapping> buttonWidgetMappings;
QList<axisWidgetMapping> joystickWidgetMappings;
QList<buttonSettingMapping> buttonSettingMappings;
@@ -94,6 +100,8 @@ private:
QString getUserProfileSectionName(QString profile);
QString getGamepadButtonText(SDL_GamepadButton button);
bool isSectionUserProfile(QString section);
bool isSectionGameProfile(QString section);
@@ -123,7 +131,7 @@ public:
ControllerWidget(QWidget* parent, EventFilter* eventFilter);
~ControllerWidget();
void AddInputDevice(const InputDevice& device);
void AddInputDevice(const InputDevice& device, const InputProfileDBEntry& inputProfile);
void RemoveInputDevice(const InputDevice& device);
void CheckInputDeviceSettings();
void CheckInputDeviceSettings(QString section);
@@ -170,6 +178,7 @@ private slots:
void on_addProfileButton_clicked();
void on_removeProfileButton_clicked();
void on_autoConfigButton_clicked();
void on_resetButton_clicked();
void on_optionsButton_clicked();
void on_hotkeysButton_clicked();
@@ -1253,6 +1253,13 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="autoConfigButton">
<property name="text">
<string>Auto-Configure</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resetButton">
<property name="text">
@@ -0,0 +1,158 @@
/*
* Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG
* Copyright (C) 2020-2025 Rosalie Wanders <rosalie@mailbox.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "InputProfileDB.hpp"
#include "main.hpp"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#include <RMG-Core/m64p/api/m64p_types.h>
#include <RMG-Core/Directories.hpp>
//
// Public Functions
//
bool Utilities::InputProfileDB::Load(void)
{
std::filesystem::path profileDatabaseFilePath;
profileDatabaseFilePath = CoreGetSharedDataDirectory();
profileDatabaseFilePath += "/InputProfileDB.json";
QFile jsonFile(profileDatabaseFilePath);
if (!jsonFile.open(QFile::ReadOnly))
{
std::string message = "InputProfileDB::Load(): Failed to open DB";
PluginDebugMessage(M64MSG_WARNING, message);
return false;
}
QJsonParseError error;
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonFile.readAll(), &error);
if (error.error != QJsonParseError::NoError)
{
std::string message = "InputProfileDB::Load(): Failed to load json: ";
message += error.errorString().toStdString();
PluginDebugMessage(M64MSG_WARNING, message);
return false;
}
const struct
{
N64ControllerButton button;
InputAxisDirection axis;
QString baseKey;
} jsonButtonMappings[] =
{
{ N64ControllerButton::A, InputAxisDirection::Invalid, "A_" },
{ N64ControllerButton::B, InputAxisDirection::Invalid, "B_" },
{ N64ControllerButton::Start, InputAxisDirection::Invalid, "Start_" },
{ N64ControllerButton::DpadUp, InputAxisDirection::Invalid, "DpadUp_" },
{ N64ControllerButton::DpadDown, InputAxisDirection::Invalid, "DpadDown_" },
{ N64ControllerButton::DpadLeft, InputAxisDirection::Invalid, "DpadLeft_" },
{ N64ControllerButton::DpadRight, InputAxisDirection::Invalid, "DpadRight_" },
{ N64ControllerButton::CButtonUp, InputAxisDirection::Invalid, "CButtonUp_" },
{ N64ControllerButton::CButtonDown, InputAxisDirection::Invalid, "CButtonDown_" },
{ N64ControllerButton::CButtonLeft, InputAxisDirection::Invalid, "CButtonLeft_" },
{ N64ControllerButton::CButtonRight, InputAxisDirection::Invalid, "CButtonRight_" },
{ N64ControllerButton::LeftShoulder, InputAxisDirection::Invalid, "LeftTrigger_", },
{ N64ControllerButton::RightShoulder, InputAxisDirection::Invalid, "RightTrigger_" },
{ N64ControllerButton::ZTrigger, InputAxisDirection::Invalid, "ZTrigger_" },
{ N64ControllerButton::Invalid, InputAxisDirection::Up, "AnalogStickUp_" },
{ N64ControllerButton::Invalid, InputAxisDirection::Down, "AnalogStickDown_" },
{ N64ControllerButton::Invalid, InputAxisDirection::Left, "AnalogStickLeft_" },
{ N64ControllerButton::Invalid, InputAxisDirection::Right, "AnalogStickRight_" },
};
InputProfileDBEntry entry;
InputProfileDBInputEntry inputEntry;
for (const auto& arrayValue : jsonDocument.array())
{
// reset entry state
entry = {};
entry.valid = true;
QJsonObject jsonObject = arrayValue.toObject();
QJsonArray deviceNameArray = jsonObject["DeviceName"].toArray();
for (const auto& deviceName : deviceNameArray)
{
entry.deviceNames.push_back(deviceName.toString());
}
entry.deviceType = static_cast<InputDeviceType>(jsonObject["DeviceType"].toInt());
// add all button mappings
for (const auto& buttonMapping : jsonButtonMappings)
{
// reset input entry state
inputEntry = {};
QStringList inputTypes = jsonObject[buttonMapping.baseKey + "InputType"].toString().split(';');
QStringList names = jsonObject[buttonMapping.baseKey + "Name"].toString().split(';');
QStringList data = jsonObject[buttonMapping.baseKey + "Data"].toString().split(';');
QStringList extraData = jsonObject[buttonMapping.baseKey + "ExtraData"].toString().split(';');
inputEntry.button = buttonMapping.button;
inputEntry.axis = buttonMapping.axis;
inputEntry.names = names;
size_t size = std::min(inputTypes.size(), std::min(names.size(), std::min(data.size(), extraData.size())));
for (int i = 0; i < size; i++)
{
inputEntry.inputTypes.push_back(static_cast<InputType>(inputTypes[i].toInt()));
inputEntry.data.push_back(data[i].toInt());
inputEntry.extraData.push_back(extraData[i].toInt());
}
entry.inputEntries.push_back(inputEntry);
}
// add to list
this->entries.push_back(entry);
}
return true;
}
InputProfileDBEntry Utilities::InputProfileDB::FindEntry(const InputDevice& device, bool fallback)
{
QString deviceName = QString::fromStdString(device.name);
for (const auto& dbEntry : this->entries)
{
if (dbEntry.deviceType == device.type)
{
for (const auto& name : dbEntry.deviceNames)
{
if (name == deviceName)
{
return dbEntry;
}
}
}
}
if (fallback)
{
return this->FindEntry({ InputDeviceType::Joystick, "fallback_profile" }, false);
}
// nothing found, return invalid entry
return { false };
}
@@ -0,0 +1,49 @@
#ifndef INPUTPROFILEDB_HPP
#define INPUTPROFILEDB_HPP
#include <QStringList>
#include <QString>
#include <QList>
#include "common.hpp"
struct InputProfileDBInputEntry
{
N64ControllerButton button;
InputAxisDirection axis;
QList<InputType> inputTypes;
QStringList names;
QList<int> data;
QList<int> extraData;
};
struct InputProfileDBEntry
{
bool valid = false;
QStringList deviceNames;
InputDeviceType deviceType;
QList<InputProfileDBInputEntry> inputEntries;
};
namespace Utilities
{
class InputProfileDB
{
private:
QList<InputProfileDBEntry> entries;
public:
// attempts to load DB in memory
bool Load(void);
// attempts to find entry using device name and type
InputProfileDBEntry FindEntry(const InputDevice& device, bool fallback = true);
};
} // namespace Utilities
#endif // INPUTPROFILEDB_HPP
+2 -1
View File
@@ -61,7 +61,8 @@ enum class InputAxisDirection
Up = 0,
Down,
Left,
Right
Right,
Invalid
};
enum class N64ControllerPak