Qt: Allow rebinding keyboard controls (#779)

* Initial input UI draft

Co-Authored-By: Paris Oplopoios <parisoplop@gmail.com>

* More keybinding work

Co-Authored-By: Paris Oplopoios <parisoplop@gmail.com>

* Nit

Co-Authored-By: Paris Oplopoios <parisoplop@gmail.com>

* More nits

Co-Authored-By: Paris Oplopoios <parisoplop@gmail.com>

---------

Co-authored-by: Paris Oplopoios <parisoplop@gmail.com>
This commit is contained in:
wheremyfoodat
2025-07-18 04:08:08 +03:00
committed by GitHub
parent 3cae1bd256
commit 81f37e1699
14 changed files with 385 additions and 14 deletions

View File

@@ -1,6 +1,8 @@
#include "services/hid.hpp"
#include <bit>
#include <algorithm>
#include <cctype>
#include <unordered_map>
#include "ipc.hpp"
#include "kernel.hpp"
@@ -242,4 +244,61 @@ void HIDService::updateInputs(u64 currentTick) {
kernel.signalEvent(e.value());
}
}
}
}
// Key serialization helpers
namespace HID::Keys {
const char* keyToName(u32 key) {
static std::unordered_map<u32, const char*> keyMap = {
{A, "A"},
{B, "B"},
{Select, "Select"},
{Start, "Start"},
{Right, "D-Pad Right"},
{Left, "D-Pad Left"},
{Up, "D-Pad Up"},
{Down, "D-Pad Down"},
{R, "R"},
{L, "L"},
{X, "X"},
{Y, "Y"},
{ZL, "ZL"},
{ZR, "ZR"},
{CirclePadRight, "CirclePad Right"},
{CirclePadLeft, "CirclePad Left"},
{CirclePadUp, "CirclePad Up"},
{CirclePadDown, "CirclePad Down"},
};
auto it = keyMap.find(key);
return it != keyMap.end() ? it->second : "Unknown key";
}
u32 nameToKey(std::string name) {
static std::unordered_map<std::string, u32> keyMap = {
{"a", A},
{"b", B},
{"select", Select},
{"start", Start},
{"d-pad right", Right},
{"d-pad left", Left},
{"d-pad up", Up},
{"d-pad down", Down},
{"r", R},
{"l", L},
{"x", X},
{"y", Y},
{"zl", ZL},
{"zr", ZR},
{"circlepad right", CirclePadRight},
{"circlepad left", CirclePadLeft},
{"circlepad up", CirclePadUp},
{"circlepad down", CirclePadDown},
};
std::transform(name.begin(), name.end(), name.begin(), [](char c) { return std::tolower(c); });
auto it = keyMap.find(name);
return it != keyMap.end() ? it->second : HID::Keys::Null;
}
} // namespace HID::Keys

View File

@@ -284,8 +284,8 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win
gpuLayout->addRow(tr("Light threshold for forcing shadergen"), lightShadergenThreshold);
// Audio settings
QGroupBox* spuGroupBox = new QGroupBox(tr("Audio Settings"), this);
QFormLayout* audioLayout = new QFormLayout(spuGroupBox);
QGroupBox* dspGroupBox = new QGroupBox(tr("Audio Settings"), this);
QFormLayout* audioLayout = new QFormLayout(dspGroupBox);
audioLayout->setHorizontalSpacing(20);
audioLayout->setVerticalSpacing(10);
@@ -344,6 +344,8 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win
volumeLayout->addWidget(volumeLabel);
audioLayout->addRow(tr("Audio device volume"), volumeLayout);
inputWindow = new InputWindow(this);
// Battery settings
QGroupBox* batGroupBox = new QGroupBox(tr("Battery Settings"), this);
QFormLayout* batLayout = new QFormLayout(batGroupBox);
@@ -381,7 +383,8 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win
addWidget(guiGroupBox, tr("Interface"), ":/docs/img/sparkling_icon.png", tr("User Interface settings"));
addWidget(genGroupBox, tr("General"), ":/docs/img/settings_icon.png", tr("General emulator settings"));
addWidget(gpuGroupBox, tr("Graphics"), ":/docs/img/display_icon.png", tr("Graphics emulation and output settings"));
addWidget(spuGroupBox, tr("Audio"), ":/docs/img/speaker_icon.png", tr("Audio emulation and output settings"));
addWidget(dspGroupBox, tr("Audio"), ":/docs/img/speaker_icon.png", tr("Audio emulation and output settings"));
addWidget(inputWindow, tr("Input"), ":/docs/img/gamepad_icon.png", tr("Keyboard & controller input settings"));
addWidget(batGroupBox, tr("Battery"), ":/docs/img/battery_icon.png", tr("Battery emulation settings"));
addWidget(sdcGroupBox, tr("SD Card"), ":/docs/img/sdcard_icon.png", tr("SD Card emulation settings"));

View File

@@ -0,0 +1,127 @@
#include "panda_qt/input_window.hpp"
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include "input_mappings.hpp"
#include "services/hid.hpp"
InputWindow::InputWindow(QWidget* parent) : QDialog(parent) {
auto mainLayout = new QVBoxLayout(this);
QStringList actions = {
"A",
"B",
"X",
"Y",
"L",
"R",
"ZL",
"ZR",
"Start",
"Select",
"D-Pad Up",
"D-Pad Down",
"D-Pad Left",
"D-Pad Right",
"CirclePad Up",
"CirclePad Down",
"CirclePad Left",
"CirclePad Right",
};
for (const QString& action : actions) {
auto row = new QHBoxLayout();
row->addWidget(new QLabel(action));
auto button = new QPushButton(tr("Not set"));
buttonMap[action] = button;
keyMappings[action] = QKeySequence();
connect(button, &QPushButton::clicked, this, [=, this]() { startKeyCapture(action); });
row->addWidget(button);
mainLayout->addLayout(row);
}
auto resetButton = new QPushButton(tr("Reset Defaults"));
connect(resetButton, &QPushButton::pressed, this, [&]() {
// Restore the keymappings to the default ones for Qt
auto defaultMappings = InputMappings::defaultKeyboardMappings();
loadFromMappings(defaultMappings);
emit mappingsChanged();
});
mainLayout->addWidget(resetButton);
installEventFilter(this);
}
void InputWindow::startKeyCapture(const QString& action) {
waitingForAction = action;
grabKeyboard();
}
bool InputWindow::eventFilter(QObject* obj, QEvent* event) {
// If we're waiting for a button to be inputted, handle the keypress
if (!waitingForAction.isEmpty() && event->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
QKeySequence key(keyEvent->key());
// If this key is already bound to something else, unbind it
for (auto it = keyMappings.begin(); it != keyMappings.end(); ++it) {
if (it.key() != waitingForAction && it.value() == key) {
it.value() = QKeySequence();
buttonMap[it.key()]->setText(tr("Not set"));
break;
}
}
keyMappings[waitingForAction] = key;
buttonMap[waitingForAction]->setText(key.toString());
releaseKeyboard();
waitingForAction.clear();
emit mappingsChanged();
return true;
}
return false;
}
void InputWindow::loadFromMappings(const InputMappings& mappings) {
for (const auto& action : buttonMap.keys()) {
u32 key = HID::Keys::nameToKey(action.toStdString());
for (const auto& [scancode, mappedKey] : mappings) {
if (mappedKey == key) {
QKeySequence qkey(scancode);
keyMappings[action] = qkey;
buttonMap[action]->setText(qkey.toString());
break;
}
}
}
}
void InputWindow::applyToMappings(InputMappings& mappings) const {
// Clear existing keyboard mappings before mapping the buttons
mappings = InputMappings();
for (const auto& action : keyMappings.keys()) {
const QKeySequence& qkey = keyMappings[action];
if (!qkey.isEmpty()) {
InputMappings::Scancode scancode = qkey[0].key();
u32 key = HID::Keys::nameToKey(action.toStdString());
if (key != HID::Keys::Null) {
mappings.setMapping(scancode, key);
}
}
}
}

View File

@@ -1,7 +1,6 @@
#include <QApplication>
#include "panda_qt/main_window.hpp"
#include "panda_qt/screen.hpp"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);

View File

@@ -14,7 +14,7 @@
#include "services/dsp.hpp"
#include "version.hpp"
MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) {
MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings()) {
emu = new Emulator();
loadTranslation();
@@ -115,6 +115,13 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
[&]() { return this; }, emu->getConfig(), this
);
loadKeybindings();
connect(configWindow->getInputWindow(), &InputWindow::mappingsChanged, this, [&]() {
keybindingsChanged = true;
configWindow->getInputWindow()->applyToMappings(keyboardMappings);
});
auto args = QCoreApplication::arguments();
if (args.size() > 1) {
auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String();
@@ -274,6 +281,10 @@ void MainWindow::closeEvent(QCloseEvent* event) {
// Cleanup when the main window closes
MainWindow::~MainWindow() {
if (keybindingsChanged) {
saveKeybindings();
}
delete emu;
delete menuBar;
delete aboutWindow;
@@ -766,3 +777,23 @@ void MainWindow::setupControllerSensors(SDL_GameController* controller) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
}
}
void MainWindow::loadKeybindings() {
auto mappings = InputMappings::deserialize(emu->getAppDataRoot() / "controls_qt.toml", "Qt", [](const std::string& name) {
return InputMappings::Scancode(QKeySequence(QString::fromStdString(name))[0].key());
});
if (mappings.has_value()) {
keyboardMappings = *mappings;
} else {
keyboardMappings = InputMappings::defaultKeyboardMappings();
}
configWindow->getInputWindow()->loadFromMappings(keyboardMappings);
}
void MainWindow::saveKeybindings() {
keyboardMappings.serialize(emu->getAppDataRoot() / "controls_qt.toml", "Qt", [](InputMappings::Scancode scancode) {
return QKeySequence(scancode).toString().toStdString();
});
}

View File

@@ -1,4 +1,5 @@
#include <QKeyEvent>
#include <QKeySequence>
#include "input_mappings.hpp"

View File

@@ -1,7 +1,7 @@
#include "input_mappings.hpp"
#include <SDL.h>
#include "input_mappings.hpp"
InputMappings InputMappings::defaultKeyboardMappings() {
InputMappings mappings;
mappings.setMapping(SDLK_l, HID::Keys::A);