More keybinding work

Co-Authored-By: Paris Oplopoios <parisoplop@gmail.com>
This commit is contained in:
wheremyfoodat 2025-07-18 03:16:28 +03:00
commit 2d0b22761e
7 changed files with 97 additions and 36 deletions

View file

@ -21,7 +21,8 @@ struct InputMappings {
void setMapping(Scancode scancode, u32 key) { container[scancode] = key; } void setMapping(Scancode scancode, u32 key) { container[scancode] = key; }
void serialize(const std::filesystem::path& path, const std::string& frontend) const { template <typename ScancodeToString>
void serialize(const std::filesystem::path& path, const std::string& frontend, ScancodeToString scancodeToString) const {
toml::basic_value<toml::preserve_comments, std::map> data; toml::basic_value<toml::preserve_comments, std::map> data;
std::error_code error; std::error_code error;
@ -46,18 +47,21 @@ struct InputMappings {
data["Mappings"] = toml::table{}; data["Mappings"] = toml::table{};
for (const auto& [scancode, key] : container) { for (const auto& [scancode, key] : container) {
if (!data["Mappings"].contains(HID::Keys::keyToName(key))) { const std::string& keyName = HID::Keys::keyToName(key);
data["Mappings"][HID::Keys::keyToName(key)] = toml::array{}; if (!data["Mappings"].contains(keyName)) {
data["Mappings"][keyName] = toml::array{};
} }
data["Mappings"][keyName].push_back(scancodeToString(scancode));
data["Mappings"][HID::Keys::keyToName(key)].push_back(scancodeToName(scancode));
} }
std::ofstream file(path, std::ios::out); std::ofstream file(path, std::ios::out);
file << data; file << data;
} }
static std::optional<InputMappings> deserialize(const std::filesystem::path& path, const std::string& wantFrontend) { template <typename ScancodeFromString>
static std::optional<InputMappings> deserialize(
const std::filesystem::path& path, const std::string& wantFrontend, ScancodeFromString stringToScancode
) {
toml::basic_value<toml::preserve_comments, std::map> data; toml::basic_value<toml::preserve_comments, std::map> data;
std::error_code error; std::error_code error;
@ -84,23 +88,24 @@ struct InputMappings {
return std::tolower(a) == std::tolower(b); return std::tolower(a) == std::tolower(b);
}); });
if (equal) { if (!equal) {
Helpers::warn( Helpers::warn(
"Mappings file %s was created for frontend %s, but we are using frontend %s\n", path.string().c_str(), haveFrontend.c_str(), "Mappings file %s was created for frontend %s, but we are using frontend %s\n", path.string().c_str(), haveFrontend.c_str(),
wantFrontend.c_str() wantFrontend.c_str()
); );
return std::nullopt; return std::nullopt;
} }
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
Helpers::warn("Exception trying to parse config file. Exception: %s\n", ex.what()); Helpers::warn("Exception trying to parse config file. Exception: %s\n", ex.what());
return std::nullopt; return std::nullopt;
} }
const auto& mappingsTable = toml::find_or<toml::table>(data, "Mappings", toml::table{}); const auto& mappingsTable = toml::find_or<toml::table>(data, "Mappings", toml::table{});
for (const auto& [key, scancodes] : mappingsTable) { for (const auto& [keyName, scancodes] : mappingsTable) {
for (const auto& scancode : scancodes.as_array()) { for (const auto& scancodeVal : scancodes.as_array()) {
mappings.setMapping(nameToScancode(scancode.as_string()), HID::Keys::nameToKey(key)); std::string scancodeStr = scancodeVal.as_string();
mappings.setMapping(stringToScancode(scancodeStr), HID::Keys::nameToKey(keyName));
} }
} }
@ -108,8 +113,6 @@ struct InputMappings {
} }
static InputMappings defaultKeyboardMappings(); static InputMappings defaultKeyboardMappings();
static std::string scancodeToName(Scancode scancode);
static Scancode nameToScancode(const std::string& name);
auto begin() { return container.begin(); } auto begin() { return container.begin(); }
auto end() { return container.end(); } auto end() { return container.end(); }
@ -121,4 +124,4 @@ struct InputMappings {
Container container; Container container;
std::string name; std::string name;
std::string device; std::string device;
}; };

View file

@ -43,7 +43,6 @@ class MainWindow : public QMainWindow {
Pause, Pause,
Resume, Resume,
TogglePause, TogglePause,
DumpRomFS,
PressKey, PressKey,
ReleaseKey, ReleaseKey,
SetCirclePadX, SetCirclePadX,
@ -134,6 +133,9 @@ class MainWindow : public QMainWindow {
void dispatchMessage(const EmulatorMessage& message); void dispatchMessage(const EmulatorMessage& message);
void loadTranslation(); void loadTranslation();
void loadKeybindings();
void saveKeybindings();
// Tracks whether we are using an OpenGL-backed renderer or a Vulkan-backed renderer // Tracks whether we are using an OpenGL-backed renderer or a Vulkan-backed renderer
bool usingGL = false; bool usingGL = false;
bool usingVk = false; bool usingVk = false;
@ -145,6 +147,9 @@ class MainWindow : public QMainWindow {
bool keyboardAnalogX = false; bool keyboardAnalogX = false;
bool keyboardAnalogY = false; bool keyboardAnalogY = false;
// Tracks if keybindings changed, in which case we should update the keybindings file when closing the emulator
bool keybindingsChanged = false;
public: public:
MainWindow(QApplication* app, QWidget* parent = nullptr); MainWindow(QApplication* app, QWidget* parent = nullptr);
~MainWindow(); ~MainWindow();

View file

@ -3,31 +3,60 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QKeyEvent> #include <QKeyEvent>
#include <QLabel> #include <QLabel>
#include <QPushButton>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "input_mappings.hpp"
#include "services/hid.hpp" #include "services/hid.hpp"
InputWindow::InputWindow(QWidget* parent) : QDialog(parent) { InputWindow::InputWindow(QWidget* parent) : QDialog(parent) {
auto mainLayout = new QVBoxLayout(this); auto mainLayout = new QVBoxLayout(this);
QStringList actions = { QStringList actions = {
"A", "B", "X", "Y", "L", "R", "ZL", "ZR", "Start", "Select", "D-Pad Up", "D-Pad Down", "D-Pad Left", "D-Pad Right", "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) { for (const QString& action : actions) {
auto row = new QHBoxLayout; auto row = new QHBoxLayout();
row->addWidget(new QLabel(action)); row->addWidget(new QLabel(action));
auto button = new QPushButton("Not set"); auto button = new QPushButton(tr("Not set"));
buttonMap[action] = button; buttonMap[action] = button;
keyMappings[action] = QKeySequence(); keyMappings[action] = QKeySequence();
connect(button, &QPushButton::clicked, this, [=]() { startKeyCapture(action); }); connect(button, &QPushButton::clicked, this, [=, this]() { startKeyCapture(action); });
row->addWidget(button); row->addWidget(button);
mainLayout->addLayout(row); 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); installEventFilter(this);
} }
@ -37,10 +66,20 @@ void InputWindow::startKeyCapture(const QString& action) {
} }
bool InputWindow::eventFilter(QObject* obj, QEvent* event) { 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) { if (!waitingForAction.isEmpty() && event->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
QKeySequence key(keyEvent->key()); 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; keyMappings[waitingForAction] = key;
buttonMap[waitingForAction]->setText(key.toString()); buttonMap[waitingForAction]->setText(key.toString());
@ -60,7 +99,7 @@ void InputWindow::loadFromMappings(const InputMappings& mappings) {
for (const auto& [scancode, mappedKey] : mappings) { for (const auto& [scancode, mappedKey] : mappings) {
if (mappedKey == key) { if (mappedKey == key) {
QKeySequence qkey(QString::fromStdString(InputMappings::scancodeToName(scancode))); QKeySequence qkey(scancode);
keyMappings[action] = qkey; keyMappings[action] = qkey;
buttonMap[action]->setText(qkey.toString()); buttonMap[action]->setText(qkey.toString());
break; break;
@ -77,7 +116,7 @@ void InputWindow::applyToMappings(InputMappings& mappings) const {
const QKeySequence& qkey = keyMappings[action]; const QKeySequence& qkey = keyMappings[action];
if (!qkey.isEmpty()) { if (!qkey.isEmpty()) {
InputMappings::Scancode scancode = InputMappings::nameToScancode(qkey.toString().toStdString()); InputMappings::Scancode scancode = qkey[0].key();
u32 key = HID::Keys::nameToKey(action.toStdString()); u32 key = HID::Keys::nameToKey(action.toStdString());
if (key != HID::Keys::Null) { if (key != HID::Keys::Null) {

View file

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

View file

@ -14,7 +14,7 @@
#include "services/dsp.hpp" #include "services/dsp.hpp"
#include "version.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(); emu = new Emulator();
loadTranslation(); loadTranslation();
@ -115,9 +115,10 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
[&]() { return this; }, emu->getConfig(), this [&]() { return this; }, emu->getConfig(), this
); );
configWindow->getInputWindow()->loadFromMappings(keyboardMappings); loadKeybindings();
connect(configWindow->getInputWindow(), &InputWindow::mappingsChanged, this, [&]() { connect(configWindow->getInputWindow(), &InputWindow::mappingsChanged, this, [&]() {
keybindingsChanged = true;
configWindow->getInputWindow()->applyToMappings(keyboardMappings); configWindow->getInputWindow()->applyToMappings(keyboardMappings);
}); });
@ -280,6 +281,10 @@ void MainWindow::closeEvent(QCloseEvent* event) {
// Cleanup when the main window closes // Cleanup when the main window closes
MainWindow::~MainWindow() { MainWindow::~MainWindow() {
if (keybindingsChanged) {
saveKeybindings();
}
delete emu; delete emu;
delete menuBar; delete menuBar;
delete aboutWindow; delete aboutWindow;
@ -772,3 +777,23 @@ void MainWindow::setupControllerSensors(SDL_GameController* controller) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
} }
} }
void MainWindow::loadKeybindings() {
auto keyMappings = InputMappings::deserialize(emu->getAppDataRoot() / "controls_qt.toml", "Qt", [](const std::string& name) {
return InputMappings::Scancode(QKeySequence(QString::fromStdString(name))[0].key());
});
if (keyMappings) {
keyboardMappings = *keyMappings;
} 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,6 +1,5 @@
#include <QKeyEvent> #include <QKeyEvent>
#include <QKeySequence> #include <QKeySequence>
#include <string>
#include "input_mappings.hpp" #include "input_mappings.hpp"
@ -26,8 +25,4 @@ InputMappings InputMappings::defaultKeyboardMappings() {
mappings.setMapping(Qt::Key_A, HID::Keys::CirclePadLeft); mappings.setMapping(Qt::Key_A, HID::Keys::CirclePadLeft);
return mappings; return mappings;
} }
std::string InputMappings::scancodeToName(Scancode scancode) { return QKeySequence(scancode).toString().toStdString(); }
InputMappings::Scancode InputMappings::nameToScancode(const std::string& name) { return QKeySequence(QString::fromStdString(name))[0].key(); }

View file

@ -1,7 +1,5 @@
#include <SDL.h> #include <SDL.h>
#include <string>
#include "input_mappings.hpp" #include "input_mappings.hpp"
InputMappings InputMappings::defaultKeyboardMappings() { InputMappings InputMappings::defaultKeyboardMappings() {
@ -26,7 +24,4 @@ InputMappings InputMappings::defaultKeyboardMappings() {
mappings.setMapping(SDLK_a, HID::Keys::CirclePadLeft); mappings.setMapping(SDLK_a, HID::Keys::CirclePadLeft);
return mappings; return mappings;
} }
std::string InputMappings::scancodeToName(Scancode scancode) { return SDL_GetKeyName(scancode); }
InputMappings::Scancode InputMappings::nameToScancode(const std::string& name) { return SDL_GetKeyFromName(name.c_str()); }