Skip to content

Commit e484895

Browse files
feat: add controller remapping for keyboard, mouse, and gamepad
Gamepad remapping engine (@hostInput.cpp): - InputMap now parses "Pad:A" bindings alongside keyboard/mouse - HostInputGamepadButton() routes gamepad buttons through custom map - window.cpp calls remap for every gamepad button event Launcher UI (Settings → Controller Mapping panel): - New ControllerMappingPanel: 24-row table (face, dpad, shoulders, sticks, axes, center buttons) - Click any row to capture next key, mouse button, or gamepad - Saved as host_input_mapping in the per-game config - Passed as --keymap args to the emulator on launch - Works for both wired and wireless gamepads (SDL auto-detects) 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 6b46f77 commit e484895

7 files changed

Lines changed: 342 additions & 19 deletions

File tree

src/graphics/presentation/window/hostInput.cpp

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "graphics/presentation/window/hostInput.h"
22

3+
#include "SDL_gamecontroller.h"
34
#include "SDL_keyboard.h"
45
#include "SDL_keycode.h"
56
#include "SDL_mouse.h"
@@ -55,9 +56,10 @@ static constexpr std::array CONTROL_INFO = {
5556
constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size();
5657

5758
struct Binding {
58-
SDL_Keycode key = SDLK_UNKNOWN;
59-
uint8_t mouse_button = 0;
60-
std::size_t control = INVALID_CONTROL;
59+
SDL_Keycode key = SDLK_UNKNOWN;
60+
uint8_t mouse_button = 0;
61+
int gamepad_button = -1; // SDL_GameControllerButton or -1
62+
std::size_t control = INVALID_CONTROL;
6163
};
6264

6365
std::size_t ControlFromName(std::string_view name) {
@@ -95,10 +97,17 @@ uint8_t MouseButtonFromName(std::string_view name) {
9597
return button != buttons.end() ? button->second : 0;
9698
}
9799

100+
int GamepadButtonFromName(std::string_view name) {
101+
if (!name.starts_with("Pad:")) return -1;
102+
return static_cast<int>(
103+
SDL_GameControllerGetButtonFromString(std::string(name.substr(4)).c_str()));
104+
}
105+
98106
bool Conflicts(const Binding& first, const Binding& second) {
99107
return first.control == second.control ||
100108
(first.key != SDLK_UNKNOWN && first.key == second.key) ||
101-
(first.mouse_button != 0 && first.mouse_button == second.mouse_button);
109+
(first.mouse_button != 0 && first.mouse_button == second.mouse_button) ||
110+
(first.gamepad_button >= 0 && first.gamepad_button == second.gamepad_button);
102111
}
103112

104113
class InputMap {
@@ -108,23 +117,27 @@ class InputMap {
108117
const std::string_view entry = value;
109118
const auto split = entry.find('=');
110119

111-
Binding binding;
112-
if (split != std::string_view::npos) {
113-
binding.control = ControlFromName(entry.substr(0, split));
114-
const auto host_input = entry.substr(split + 1);
115-
binding.mouse_button = MouseButtonFromName(host_input);
116-
if (binding.mouse_button == 0) {
117-
binding.key = NormalizeKey(SDL_GetKeyFromName(std::string(host_input).c_str()));
118-
}
120+
Binding binding;
121+
if (split != std::string_view::npos) {
122+
binding.control = ControlFromName(entry.substr(0, split));
123+
const auto host_input = entry.substr(split + 1);
124+
binding.mouse_button = MouseButtonFromName(host_input);
125+
if (binding.mouse_button == 0) {
126+
binding.gamepad_button = GamepadButtonFromName(host_input);
119127
}
120-
121-
const bool reserved =
122-
binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1;
123-
if (binding.control == INVALID_CONTROL || reserved ||
124-
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
125-
EXIT("Invalid input mapping: %s\n", value.c_str());
128+
if (binding.mouse_button == 0 && binding.gamepad_button < 0) {
129+
binding.key = NormalizeKey(SDL_GetKeyFromName(std::string(host_input).c_str()));
126130
}
127-
Add(binding);
131+
}
132+
133+
const bool reserved =
134+
binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1;
135+
const bool has_binding = binding.key != SDLK_UNKNOWN || binding.mouse_button != 0 ||
136+
binding.gamepad_button >= 0;
137+
if (binding.control == INVALID_CONTROL || reserved || !has_binding) {
138+
EXIT("Invalid input mapping: %s\n", value.c_str());
139+
}
140+
Add(binding);
128141
}
129142
}
130143
[[nodiscard]] bool Custom() const { return m_custom; }
@@ -144,6 +157,13 @@ class InputMap {
144157
return binding != m_bindings.begin() + m_size ? binding->control : INVALID_CONTROL;
145158
}
146159

160+
[[nodiscard]] std::size_t FindGamepadButton(int button) const {
161+
const auto binding = std::find_if(
162+
m_bindings.begin(), m_bindings.begin() + m_size,
163+
[button](const auto& item) { return item.gamepad_button == button; });
164+
return binding != m_bindings.begin() + m_size ? binding->control : INVALID_CONTROL;
165+
}
166+
147167
private:
148168
void Add(const Binding& binding) {
149169
for (std::size_t index = 0; index < m_size;) {
@@ -302,4 +322,11 @@ void HostInputMouseButton(uint8_t mouse_button, bool down) {
302322
}
303323
}
304324

325+
void HostInputGamepadButton(int gamepad_button, bool down) {
326+
const auto& map = GetInputMap();
327+
if (map.Custom() && gamepad_button >= 0) {
328+
SetControl(map.FindGamepadButton(gamepad_button), down);
329+
}
330+
}
331+
305332
} // namespace Libs::Graphics

src/graphics/presentation/window/hostInput.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Libs::Graphics {
88
void HostInputInit();
99
void HostInputKey(int key_code, bool down);
1010
void HostInputMouseButton(uint8_t mouse_button, bool down);
11+
void HostInputGamepadButton(int gamepad_button, bool down);
1112

1213
} // namespace Libs::Graphics
1314

src/graphics/presentation/window/window.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
336336
if (button != 0) {
337337
Controller::ControllerButton(f.id, button, f.down);
338338
}
339+
// Also route through custom gamepad mapping if user has one set.
340+
HostInputGamepadButton(static_cast<uint8_t>(f.button), f.down);
339341
}
340342

341343
if (f.axis) {

src/launcher/include/configurationEditDialog.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <QString>
88
#include <QStringList>
99
class QByteArray;
10+
class ControllerMappingPanel;
1011
class QGroupBox;
1112
class QListWidget;
1213
class QMoveEvent;
@@ -38,13 +39,16 @@ class ConfigurationEditDialog: public QDialog {
3839
Ui::ConfigurationEditDialog* m_ui = nullptr;
3940
Configuration& m_info;
4041
QGroupBox* m_game_dirs_group = nullptr;
42+
QGroupBox* m_controller_group = nullptr;
43+
ControllerMappingPanel* m_controller_mapping = nullptr;
4144
QListWidget* m_game_dirs_list = nullptr;
4245
QToolButton* m_remove_game_dir_button = nullptr;
4346
bool m_show_game_dirs = false;
4447

4548
protected:
4649
void Init(const Configuration& info);
4750
void InitGameDirectories();
51+
void InitControllerMapping();
4852
void AddGameDirectoryItem(const QString& dir);
4953

5054
void moveEvent(QMoveEvent* event) override;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#ifndef CONTROLLER_MAPPING_PANEL_H
2+
#define CONTROLLER_MAPPING_PANEL_H
3+
4+
#include "common.h"
5+
6+
#include <QKeyEvent>
7+
#include <QPushButton>
8+
#include <QString>
9+
#include <QStringList>
10+
#include <QWidget>
11+
12+
class QGridLayout;
13+
14+
// Editable table: each PS5 control gets a bindable cell.
15+
// Click a row to capture the next keyboard key, mouse button, or gamepad button press.
16+
class ControllerMappingPanel: public QWidget {
17+
Q_OBJECT
18+
KYTY_QT_CLASS_NO_COPY(ControllerMappingPanel);
19+
20+
public:
21+
explicit ControllerMappingPanel(QWidget* parent = nullptr);
22+
23+
// Get/set the mapping list in the emulator's keymap format.
24+
// Each entry is like "Cross=Pad:A" or "Cross=Key:Space" or "Cross=Mouse:Left".
25+
[[nodiscard]] QStringList GetMappings() const;
26+
void SetMappings(const QStringList& list);
27+
28+
private:
29+
struct ControlRow {
30+
QString control_name; // e.g. "Cross", "LeftStickUp"
31+
QPushButton* bind_button = nullptr;
32+
};
33+
34+
void BuildLayout();
35+
void SetCaptureMode(ControlRow& row);
36+
37+
void keyPressEvent(QKeyEvent* event) override;
38+
39+
QList<ControlRow> m_rows;
40+
QGridLayout* m_grid = nullptr;
41+
ControlRow* m_capturing = nullptr;
42+
};
43+
44+
#endif // CONTROLLER_MAPPING_PANEL_H

src/launcher/src/configurationEditDialog.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "configurationEditDialog.h"
22

33
#include "configuration.h"
4+
#include "controllerMappingPanel.h"
45
#include "mandatoryLineEdit.h"
56

67
#include <QAbstractItemView>
@@ -96,6 +97,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
9697
m_info(info) {
9798
m_ui->setupUi(this);
9899
InitGameDirectories();
100+
InitControllerMapping();
99101

100102
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
101103
connect(m_ui->clear_button, &QPushButton::clicked, this, &ConfigurationEditDialog::clear);
@@ -191,6 +193,21 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
191193
ListInit(m_ui->comboBox_present_mode, info.present_mode);
192194
ListInit(m_ui->comboBox_present_filter, info.present_filter);
193195
ListInit(m_ui->comboBox_aspect_ratio, info.aspect_ratio);
196+
m_controller_mapping->SetMappings(info.host_input_mapping);
197+
}
198+
199+
void ConfigurationEditDialog::InitControllerMapping() {
200+
m_controller_group = new QGroupBox(tr("Controller Mapping"), this);
201+
auto* group_layout = new QVBoxLayout(m_controller_group);
202+
group_layout->setContentsMargins(8, 8, 8, 8);
203+
group_layout->setSpacing(6);
204+
205+
m_controller_mapping = new ControllerMappingPanel(m_controller_group);
206+
m_controller_mapping->setMinimumHeight(200);
207+
group_layout->addWidget(m_controller_mapping);
208+
209+
// addRow with a plain QWidget (no label) spans both columns.
210+
m_ui->formLayout_6->addRow(m_controller_group);
194211
}
195212

196213
void ConfigurationEditDialog::InitGameDirectories() {
@@ -334,6 +351,9 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
334351

335352
void ConfigurationEditDialog::update_info() {
336353
UpdateInfo(m_info, *m_ui);
354+
if (m_controller_mapping != nullptr) {
355+
m_info.host_input_mapping = m_controller_mapping->GetMappings();
356+
}
337357
}
338358

339359
void ConfigurationEditDialog::adjust_size() {

0 commit comments

Comments
 (0)