Qt: Allow rebinding keyboard controls (#779)
Some checks failed
Android Build / x64 (release) (push) Has been cancelled
Android Build / arm64 (release) (push) Has been cancelled
HTTP Server Build / build (push) Has been cancelled
Hydra Core Build / Windows (push) Has been cancelled
Hydra Core Build / MacOS (push) Has been cancelled
Hydra Core Build / Linux (push) Has been cancelled
Hydra Core Build / Android-x64 (push) Has been cancelled
Hydra Core Build / ARM-Libretro (push) Has been cancelled
Linux AppImage Build / build (push) Has been cancelled
Linux Build / build (push) Has been cancelled
MacOS Build / MacOS-arm64 (push) Has been cancelled
MacOS Build / MacOS-x86_64 (push) Has been cancelled
Qt Build / Windows (push) Has been cancelled
Qt Build / MacOS-arm64 (push) Has been cancelled
Qt Build / MacOS-x86_64 (push) Has been cancelled
Qt Build / Linux (push) Has been cancelled
Windows Build / build (push) Has been cancelled
iOS Simulator Build / build (push) Has been cancelled
MacOS Build / MacOS-Universal (push) Has been cancelled
Qt Build / MacOS-Universal (push) Has been cancelled

* 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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 385 additions and 14 deletions

View file

@ -1,5 +1,10 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <map>
#include <optional>
#include <toml.hpp>
#include <unordered_map>
#include "helpers.hpp"
@ -15,8 +20,108 @@ struct InputMappings {
}
void setMapping(Scancode scancode, u32 key) { container[scancode] = key; }
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;
std::error_code error;
if (std::filesystem::exists(path, error)) {
try {
data = toml::parse<toml::preserve_comments, std::map>(path);
} catch (const std::exception& ex) {
Helpers::warn("Exception trying to parse mappings file. Exception: %s\n", ex.what());
return;
}
} else {
if (error) {
Helpers::warn("Filesystem error accessing %s (error: %s)\n", path.string().c_str(), error.message().c_str());
}
printf("Saving new mappings file %s\n", path.string().c_str());
}
data["Metadata"]["Name"] = name.empty() ? "Unnamed Mappings" : name;
data["Metadata"]["Device"] = device.empty() ? "Unknown Device" : device;
data["Metadata"]["Frontend"] = frontend;
data["Mappings"] = toml::table{};
for (const auto& [scancode, key] : container) {
const std::string& keyName = HID::Keys::keyToName(key);
if (!data["Mappings"].contains(keyName)) {
data["Mappings"][keyName] = toml::array{};
}
data["Mappings"][keyName].push_back(scancodeToString(scancode));
}
std::ofstream file(path, std::ios::out);
file << data;
}
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;
std::error_code error;
if (!std::filesystem::exists(path, error)) {
if (error) {
Helpers::warn("Filesystem error accessing %s (error: %s)\n", path.string().c_str(), error.message().c_str());
}
return std::nullopt;
}
InputMappings mappings;
try {
data = toml::parse<toml::preserve_comments, std::map>(path);
const auto metadata = toml::find(data, "Metadata");
mappings.name = toml::find_or<std::string>(metadata, "Name", "Unnamed Mappings");
mappings.device = toml::find_or<std::string>(metadata, "Device", "Unknown Device");
std::string haveFrontend = toml::find_or<std::string>(metadata, "Frontend", "Unknown Frontend");
bool equal = std::equal(haveFrontend.begin(), haveFrontend.end(), wantFrontend.begin(), wantFrontend.end(), [](char a, char b) {
return std::tolower(a) == std::tolower(b);
});
if (!equal) {
Helpers::warn(
"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()
);
return std::nullopt;
}
} catch (const std::exception& ex) {
Helpers::warn("Exception trying to parse config file. Exception: %s\n", ex.what());
return std::nullopt;
}
const auto& mappingsTable = toml::find_or<toml::table>(data, "Mappings", toml::table{});
for (const auto& [keyName, scancodes] : mappingsTable) {
for (const auto& scancodeVal : scancodes.as_array()) {
std::string scancodeStr = scancodeVal.as_string();
mappings.setMapping(stringToScancode(scancodeStr), HID::Keys::nameToKey(keyName));
}
}
return mappings;
}
static InputMappings defaultKeyboardMappings();
auto begin() { return container.begin(); }
auto end() { return container.end(); }
auto begin() const { return container.begin(); }
auto end() const { return container.end(); }
private:
Container container;
std::string name;
std::string device;
};

View file

@ -16,6 +16,8 @@
#include "emulator.hpp"
#include "frontend_settings.hpp"
#include "input_mappings.hpp"
#include "panda_qt/input_window.hpp"
class ConfigWindow : public QDialog {
Q_OBJECT
@ -30,8 +32,9 @@ class ConfigWindow : public QDialog {
QTextEdit* helpText = nullptr;
QListWidget* widgetList = nullptr;
QStackedWidget* widgetContainer = nullptr;
InputWindow* inputWindow = nullptr;
static constexpr size_t settingWidgetCount = 6;
static constexpr size_t settingWidgetCount = 7;
std::array<QString, settingWidgetCount> helpTexts;
// The config class holds a copy of the emulator config which it edits and sends
@ -52,6 +55,7 @@ class ConfigWindow : public QDialog {
~ConfigWindow();
EmulatorConfig& getConfig() { return config; }
InputWindow* getInputWindow() { return inputWindow; }
private:
Emulator* emu;

View file

@ -0,0 +1,32 @@
#pragma once
#include <QDialog>
#include <QKeySequence>
#include <QMap>
#include <QPushButton>
#include "input_mappings.hpp"
class InputWindow : public QDialog {
Q_OBJECT
public:
InputWindow(QWidget* parent = nullptr);
void loadFromMappings(const InputMappings& mappings);
void applyToMappings(InputMappings& mappings) const;
signals:
void mappingsChanged();
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
private:
QMap<QString, QPushButton*> buttonMap;
QMap<QString, QKeySequence> keyMappings;
QString waitingForAction;
void startKeyCapture(const QString& action);
};

View file

@ -43,7 +43,6 @@ class MainWindow : public QMainWindow {
Pause,
Resume,
TogglePause,
DumpRomFS,
PressKey,
ReleaseKey,
SetCirclePadX,
@ -134,6 +133,9 @@ class MainWindow : public QMainWindow {
void dispatchMessage(const EmulatorMessage& message);
void loadTranslation();
void loadKeybindings();
void saveKeybindings();
// Tracks whether we are using an OpenGL-backed renderer or a Vulkan-backed renderer
bool usingGL = false;
bool usingVk = false;
@ -145,6 +147,9 @@ class MainWindow : public QMainWindow {
bool keyboardAnalogX = 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:
MainWindow(QApplication* app, QWidget* parent = nullptr);
~MainWindow();

View file

@ -1,6 +1,7 @@
#pragma once
#include <array>
#include <optional>
#include <string>
#include "helpers.hpp"
#include "kernel_types.hpp"
@ -38,7 +39,10 @@ namespace HID::Keys {
CirclePadUp = 1 << 30, // Y >= 41
CirclePadDown = 1u << 31 // Y <= -41
};
}
const char* keyToName(u32 key);
u32 nameToKey(std::string name);
} // namespace HID::Keys
// Circular dependency because we need HID to spawn events
class Kernel;