mirror of
https://github.com/wheremyfoodat/Panda3DS.git
synced 2025-07-20 14:01:44 +12:00
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
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:
parent
3cae1bd256
commit
81f37e1699
14 changed files with 385 additions and 14 deletions
|
@ -733,13 +733,13 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE)
|
||||||
set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp
|
set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp
|
||||||
src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp
|
src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp
|
||||||
src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp src/panda_qt/shader_editor.cpp src/panda_qt/translations.cpp
|
src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp src/panda_qt/shader_editor.cpp src/panda_qt/translations.cpp
|
||||||
src/panda_qt/thread_debugger.cpp src/panda_qt/cpu_debugger.cpp src/panda_qt/dsp_debugger.cpp
|
src/panda_qt/thread_debugger.cpp src/panda_qt/cpu_debugger.cpp src/panda_qt/dsp_debugger.cpp src/panda_qt/input_window.cpp
|
||||||
)
|
)
|
||||||
set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp
|
set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp
|
||||||
include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp
|
include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp
|
||||||
include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp include/panda_qt/shader_editor.hpp
|
include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp include/panda_qt/shader_editor.hpp
|
||||||
include/panda_qt/thread_debugger.hpp include/panda_qt/cpu_debugger.hpp include/panda_qt/dsp_debugger.hpp
|
include/panda_qt/thread_debugger.hpp include/panda_qt/cpu_debugger.hpp include/panda_qt/dsp_debugger.hpp
|
||||||
include/panda_qt/disabled_widget_overlay.hpp
|
include/panda_qt/disabled_widget_overlay.hpp include/panda_qt/input_window.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES})
|
source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES})
|
||||||
|
@ -787,6 +787,7 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE)
|
||||||
docs/img/settings_icon.png docs/img/display_icon.png docs/img/speaker_icon.png
|
docs/img/settings_icon.png docs/img/display_icon.png docs/img/speaker_icon.png
|
||||||
docs/img/sparkling_icon.png docs/img/battery_icon.png docs/img/sdcard_icon.png
|
docs/img/sparkling_icon.png docs/img/battery_icon.png docs/img/sdcard_icon.png
|
||||||
docs/img/rnap_icon.png docs/img/rcow_icon.png docs/img/skyemu_icon.png docs/img/runpog_icon.png
|
docs/img/rnap_icon.png docs/img/rcow_icon.png docs/img/skyemu_icon.png docs/img/runpog_icon.png
|
||||||
|
docs/img/gamepad_icon.png
|
||||||
)
|
)
|
||||||
|
|
||||||
# Translation files in Qt's .ts format. Will be converted into binary files and embedded into the executable
|
# Translation files in Qt's .ts format. Will be converted into binary files and embedded into the executable
|
||||||
|
|
BIN
docs/img/gamepad_icon.png
Normal file
BIN
docs/img/gamepad_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 311 B |
|
@ -1,5 +1,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <toml.hpp>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "helpers.hpp"
|
#include "helpers.hpp"
|
||||||
|
@ -15,8 +20,108 @@ struct InputMappings {
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMapping(Scancode scancode, u32 key) { container[scancode] = key; }
|
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();
|
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:
|
private:
|
||||||
Container container;
|
Container container;
|
||||||
|
std::string name;
|
||||||
|
std::string device;
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
#include "emulator.hpp"
|
#include "emulator.hpp"
|
||||||
#include "frontend_settings.hpp"
|
#include "frontend_settings.hpp"
|
||||||
|
#include "input_mappings.hpp"
|
||||||
|
#include "panda_qt/input_window.hpp"
|
||||||
|
|
||||||
class ConfigWindow : public QDialog {
|
class ConfigWindow : public QDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -30,8 +32,9 @@ class ConfigWindow : public QDialog {
|
||||||
QTextEdit* helpText = nullptr;
|
QTextEdit* helpText = nullptr;
|
||||||
QListWidget* widgetList = nullptr;
|
QListWidget* widgetList = nullptr;
|
||||||
QStackedWidget* widgetContainer = nullptr;
|
QStackedWidget* widgetContainer = nullptr;
|
||||||
|
InputWindow* inputWindow = nullptr;
|
||||||
|
|
||||||
static constexpr size_t settingWidgetCount = 6;
|
static constexpr size_t settingWidgetCount = 7;
|
||||||
std::array<QString, settingWidgetCount> helpTexts;
|
std::array<QString, settingWidgetCount> helpTexts;
|
||||||
|
|
||||||
// The config class holds a copy of the emulator config which it edits and sends
|
// The config class holds a copy of the emulator config which it edits and sends
|
||||||
|
@ -52,6 +55,7 @@ class ConfigWindow : public QDialog {
|
||||||
~ConfigWindow();
|
~ConfigWindow();
|
||||||
|
|
||||||
EmulatorConfig& getConfig() { return config; }
|
EmulatorConfig& getConfig() { return config; }
|
||||||
|
InputWindow* getInputWindow() { return inputWindow; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Emulator* emu;
|
Emulator* emu;
|
||||||
|
|
32
include/panda_qt/input_window.hpp
Normal file
32
include/panda_qt/input_window.hpp
Normal 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);
|
||||||
|
};
|
|
@ -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();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "helpers.hpp"
|
#include "helpers.hpp"
|
||||||
#include "kernel_types.hpp"
|
#include "kernel_types.hpp"
|
||||||
|
@ -38,7 +39,10 @@ namespace HID::Keys {
|
||||||
CirclePadUp = 1 << 30, // Y >= 41
|
CirclePadUp = 1 << 30, // Y >= 41
|
||||||
CirclePadDown = 1u << 31 // 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
|
// Circular dependency because we need HID to spawn events
|
||||||
class Kernel;
|
class Kernel;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#include "services/hid.hpp"
|
#include "services/hid.hpp"
|
||||||
|
|
||||||
#include <bit>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "ipc.hpp"
|
#include "ipc.hpp"
|
||||||
#include "kernel.hpp"
|
#include "kernel.hpp"
|
||||||
|
@ -242,4 +244,61 @@ void HIDService::updateInputs(u64 currentTick) {
|
||||||
kernel.signalEvent(e.value());
|
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
|
||||||
|
|
|
@ -284,8 +284,8 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win
|
||||||
gpuLayout->addRow(tr("Light threshold for forcing shadergen"), lightShadergenThreshold);
|
gpuLayout->addRow(tr("Light threshold for forcing shadergen"), lightShadergenThreshold);
|
||||||
|
|
||||||
// Audio settings
|
// Audio settings
|
||||||
QGroupBox* spuGroupBox = new QGroupBox(tr("Audio Settings"), this);
|
QGroupBox* dspGroupBox = new QGroupBox(tr("Audio Settings"), this);
|
||||||
QFormLayout* audioLayout = new QFormLayout(spuGroupBox);
|
QFormLayout* audioLayout = new QFormLayout(dspGroupBox);
|
||||||
audioLayout->setHorizontalSpacing(20);
|
audioLayout->setHorizontalSpacing(20);
|
||||||
audioLayout->setVerticalSpacing(10);
|
audioLayout->setVerticalSpacing(10);
|
||||||
|
|
||||||
|
@ -344,6 +344,8 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win
|
||||||
volumeLayout->addWidget(volumeLabel);
|
volumeLayout->addWidget(volumeLabel);
|
||||||
audioLayout->addRow(tr("Audio device volume"), volumeLayout);
|
audioLayout->addRow(tr("Audio device volume"), volumeLayout);
|
||||||
|
|
||||||
|
inputWindow = new InputWindow(this);
|
||||||
|
|
||||||
// Battery settings
|
// Battery settings
|
||||||
QGroupBox* batGroupBox = new QGroupBox(tr("Battery Settings"), this);
|
QGroupBox* batGroupBox = new QGroupBox(tr("Battery Settings"), this);
|
||||||
QFormLayout* batLayout = new QFormLayout(batGroupBox);
|
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(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(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(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(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"));
|
addWidget(sdcGroupBox, tr("SD Card"), ":/docs/img/sdcard_icon.png", tr("SD Card emulation settings"));
|
||||||
|
|
||||||
|
|
127
src/panda_qt/input_window.cpp
Normal file
127
src/panda_qt/input_window.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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,6 +115,13 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
|
||||||
[&]() { return this; }, emu->getConfig(), this
|
[&]() { return this; }, emu->getConfig(), this
|
||||||
);
|
);
|
||||||
|
|
||||||
|
loadKeybindings();
|
||||||
|
|
||||||
|
connect(configWindow->getInputWindow(), &InputWindow::mappingsChanged, this, [&]() {
|
||||||
|
keybindingsChanged = true;
|
||||||
|
configWindow->getInputWindow()->applyToMappings(keyboardMappings);
|
||||||
|
});
|
||||||
|
|
||||||
auto args = QCoreApplication::arguments();
|
auto args = QCoreApplication::arguments();
|
||||||
if (args.size() > 1) {
|
if (args.size() > 1) {
|
||||||
auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String();
|
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
|
// 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;
|
||||||
|
@ -766,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 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
|
#include <QKeySequence>
|
||||||
|
|
||||||
#include "input_mappings.hpp"
|
#include "input_mappings.hpp"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#include "input_mappings.hpp"
|
|
||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include "input_mappings.hpp"
|
||||||
|
|
||||||
InputMappings InputMappings::defaultKeyboardMappings() {
|
InputMappings InputMappings::defaultKeyboardMappings() {
|
||||||
InputMappings mappings;
|
InputMappings mappings;
|
||||||
mappings.setMapping(SDLK_l, HID::Keys::A);
|
mappings.setMapping(SDLK_l, HID::Keys::A);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue