From fdb13a3b90236cd6c6e9c6e097b5e69f29efe04c Mon Sep 17 00:00:00 2001 From: Vasyl_Baran Date: Wed, 11 Sep 2024 08:51:18 +0300 Subject: [PATCH] Add UI to configure keyboard-to-controller mapping (#308) * Add UI to configure keyboard-to-controller mapping * Add an optional "---fix" argument to format-checking script * clang fix --------- Co-authored-by: georgemoralis --- .ci/clang-format.sh | 19 +- .github/workflows/linux-qt.yml | 8 +- .reuse/dep5 | 1 + CMakeLists.txt | 4 + src/common/config.cpp | 45 ++- src/common/config.h | 5 + src/emulator.cpp | 2 +- src/emulator.h | 2 +- src/images/PS4_controller_scheme.png | Bin 0 -> 30766 bytes src/input/keys_constants.h | 30 ++ src/qt_gui/keyboardcontrolswindow.cpp | 524 ++++++++++++++++++++++++++ src/qt_gui/keyboardcontrolswindow.h | 40 ++ src/qt_gui/keyboardcontrolswindow.ui | 439 +++++++++++++++++++++ src/qt_gui/main_window.cpp | 12 + src/qt_gui/main_window.h | 5 + src/sdl_window.cpp | 443 ++++++++++++++-------- src/sdl_window.h | 29 +- src/shadps4.qrc | 1 + 18 files changed, 1436 insertions(+), 173 deletions(-) create mode 100644 src/images/PS4_controller_scheme.png create mode 100644 src/input/keys_constants.h create mode 100644 src/qt_gui/keyboardcontrolswindow.cpp create mode 100644 src/qt_gui/keyboardcontrolswindow.h create mode 100644 src/qt_gui/keyboardcontrolswindow.ui diff --git a/.ci/clang-format.sh b/.ci/clang-format.sh index 0ccd4062d..b9018f508 100755 --- a/.ci/clang-format.sh +++ b/.ci/clang-format.sh @@ -3,6 +3,11 @@ # SPDX-FileCopyrightText: 2023 Citra Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later +fix=false +if [ "$1" == "--fix" ]; then + fix=true +fi + if grep -nrI '\s$' src *.yml *.txt *.md Doxyfile .gitignore .gitmodules .ci* dist/*.desktop \ dist/*.svg dist/*.xml; then echo Trailing whitespace found, aborting @@ -25,11 +30,15 @@ fi set +x for f in $files_to_lint; do - d=$(diff -u "$f" <($CLANG_FORMAT "$f") || true) - if ! [ -z "$d" ]; then - echo "!!! $f not compliant to coding style, here is the fix:" - echo "$d" - fail=1 + if [ "$fix" = true ]; then + $CLANG_FORMAT -i "$f" + else + d=$(diff -u "$f" <($CLANG_FORMAT "$f") || true) + if ! [ -z "$d" ]; then + echo "!!! $f not compliant to coding style, here is the fix:" + echo "$d" + fail=1 + fi fi done diff --git a/.github/workflows/linux-qt.yml b/.github/workflows/linux-qt.yml index 6848f203b..466554ca6 100644 --- a/.github/workflows/linux-qt.yml +++ b/.github/workflows/linux-qt.yml @@ -23,7 +23,13 @@ jobs: - name: Install misc packages run: > - sudo apt-get update && sudo apt install libx11-dev libxext-dev libwayland-dev libfuse2 clang build-essential qt6-base-dev qt6-tools-dev + sudo apt-get update && sudo apt install libx11-dev libxext-dev libwayland-dev libfuse2 clang build-essential + + - name: Setup Qt + uses: jurplel/install-qt-action@v4 + with: + arch: linux_gcc_64 + version: 6.7.1 - name: Cache CMake dependency source code uses: actions/cache@v4 diff --git a/.reuse/dep5 b/.reuse/dep5 index 0140c0c02..88c3e9969 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -15,6 +15,7 @@ Files: CMakeSettings.json documents/Screenshots/Undertale.png documents/Screenshots/We are DOOMED.png scripts/ps4_names.txt + src/images/PS4_controller_scheme.png src/images/about_icon.png src/images/controller_icon.png src/images/exit_icon.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 9101af9df..20e2143b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -607,6 +607,7 @@ set(IMGUI src/imgui/imgui_config.h set(INPUT src/input/controller.cpp src/input/controller.h + src/input/keys_constants.h ) set(EMULATOR src/emulator.cpp @@ -651,6 +652,9 @@ set(QT_GUI src/qt_gui/about_dialog.cpp src/qt_gui/settings_dialog.cpp src/qt_gui/settings_dialog.h src/qt_gui/settings_dialog.ui + src/qt_gui/keyboardcontrolswindow.h + src/qt_gui/keyboardcontrolswindow.cpp + src/qt_gui/keyboardcontrolswindow.ui src/qt_gui/main.cpp ${EMULATOR} ${RESOURCE_FILES} diff --git a/src/common/config.cpp b/src/common/config.cpp index fb6ee120a..1063dfca9 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -53,6 +53,7 @@ std::vector m_recent_files; std::string emulator_language = "en"; // Settings u32 m_language = 1; // english +std::map m_keyboard_binding_map; bool isNeoMode() { return isNeo; @@ -283,7 +284,12 @@ void setRecentFiles(const std::vector& recentFiles) { void setEmulatorLanguage(std::string language) { emulator_language = language; } - +void setKeyboardBindingMap(std::map map) { + m_keyboard_binding_map = map; +} +const std::map& getKeyboardBindingMap() { + return m_keyboard_binding_map; +} u32 getMainWindowGeometryX() { return main_window_geometry_x; } @@ -431,6 +437,34 @@ void load(const std::filesystem::path& path) { m_language = toml::find_or(settings, "consoleLanguage", 1); } + + if (data.contains("Controls")) { + auto controls = toml::find(data, "Controls"); + + toml::table keyboardBindings{}; + auto it = controls.find("keyboardBindings"); + if (it != controls.end() && it->second.is_table()) { + keyboardBindings = it->second.as_table(); + } + + // Convert TOML table to std::map + for (const auto& [key, value] : keyboardBindings) { + try { + Uint32 int_key = static_cast(std::stoll(key)); + if (value.is_integer()) { + // Convert the TOML integer value to KeysMapping (int) + int int_value = value.as_integer(); + + // Add to the map + m_keyboard_binding_map[int_key] = static_cast(int_value); + } else { + fmt::print("Unexpected type for value: expected integer, got other type\n"); + } + } catch (const std::exception& e) { + fmt::print("Error processing key-value pair: {}\n", e.what()); + } + } + } } void save(const std::filesystem::path& path) { toml::value data; @@ -492,6 +526,15 @@ void save(const std::filesystem::path& path) { data["GUI"]["recentFiles"] = m_recent_files; data["GUI"]["emulatorLanguage"] = emulator_language; + // Create a TOML table with keyboard bindings + toml::table keyboardBindingsTable; + // Serialize the map to the TOML table + for (const auto& [key, value] : m_keyboard_binding_map) { + keyboardBindingsTable[std::to_string(key)] = static_cast(value); + } + + data["Controls"]["keyboardBindings"] = keyboardBindingsTable; + data["Settings"]["consoleLanguage"] = m_language; std::ofstream file(path, std::ios::out); diff --git a/src/common/config.h b/src/common/config.h index 7e717fe71..ff76f7765 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -4,7 +4,10 @@ #pragma once #include +#include #include +#include "SDL3/SDL_stdinc.h" +#include "input/keys_constants.h" #include "types.h" namespace Config { @@ -79,6 +82,8 @@ void setPkgViewer(const std::vector& pkgList); void setElfViewer(const std::vector& elfList); void setRecentFiles(const std::vector& recentFiles); void setEmulatorLanguage(std::string language); +void setKeyboardBindingMap(std::map map); +const std::map& getKeyboardBindingMap(); u32 getMainWindowGeometryX(); u32 getMainWindowGeometryY(); diff --git a/src/emulator.cpp b/src/emulator.cpp index 9c41a3dbd..3006de1e8 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -146,7 +146,7 @@ void Emulator::Run(const std::filesystem::path& file) { } window = std::make_unique( Config::getScreenWidth(), Config::getScreenHeight(), controller, window_title); - + window->setKeysBindingsMap(Config::getKeyboardBindingMap()); g_window = window.get(); const auto& mount_data_dir = Common::FS::GetUserPath(Common::FS::PathType::GameDataDir) / id; diff --git a/src/emulator.h b/src/emulator.h index 01bce7e7c..b1f779898 100644 --- a/src/emulator.h +++ b/src/emulator.h @@ -34,6 +34,6 @@ private: Input::GameController* controller; Core::Linker* linker; std::unique_ptr window; + std::map m_keysBindingsMap; }; - } // namespace Core diff --git a/src/images/PS4_controller_scheme.png b/src/images/PS4_controller_scheme.png new file mode 100644 index 0000000000000000000000000000000000000000..9e095a063fb3ce8077d9ba5fed344f4687a6f7ee GIT binary patch literal 30766 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 YK%{qgoHl(3 +#include + +#include + +namespace { +static constexpr auto keyBindingsSettingsKey = "ShadPS4_Keyboard_Settings_KEY"; +static constexpr auto inputErrorTimerTimeout = 2000; +} // namespace + +void showError(const QString& message) { + QMessageBox::critical(nullptr, "Error", message, QMessageBox::Ok); +} + +void showWarning(const QString& message) { + QMessageBox::warning(nullptr, "Warning", message, QMessageBox::Ok); +} + +void showInfo(const QString& message) { + QMessageBox::information(nullptr, "Info", message, QMessageBox::Ok); +} + +KeyboardControlsWindow::KeyboardControlsWindow(QWidget* parent) + : QDialog(parent), ui(new Ui::KeyboardControlsWindow) { + ui->setupUi(this); + + m_keysMap = Config::getKeyboardBindingMap(); + + for (auto& pair : m_keysMap) { + m_reverseKeysMap.emplace(pair.second, pair.first); + } + + m_listOfKeySequenceEdits = {ui->StartKeySequenceEdit, ui->SelectKeySequenceEdit, + ui->LAnalogDownkeySequenceEdit, ui->LAnalogLeftkeySequenceEdit, + ui->LAnalogUpkeySequenceEdit, ui->LAnalogRightkeySequenceEdit, + ui->PSkeySequenceEdit, ui->RAnalogDownkeySequenceEdit, + ui->RAnalogLeftkeySequenceEdit, ui->RAnalogUpkeySequenceEdit, + ui->RAnalogRightkeySequenceEdit, ui->DPadLeftkeySequenceEdit, + ui->DPadRightkeySequenceEdit, ui->DPadUpkeySequenceEdit, + ui->DPadDownkeySequenceEdit, ui->L2keySequenceEdit, + ui->L1keySequenceEdit, ui->CrossKeySequenceEdit, + ui->R2KeySequenceEdit, ui->CircleKeySequenceEdit, + ui->R1KeySequenceEdit, ui->SquareKeySequenceEdit, + ui->TriangleKeySequenceEdit}; + + for (auto edit : m_listOfKeySequenceEdits) { + edit->setStyleSheet("QLineEdit { qproperty-alignment: AlignCenter; }"); + QObject::connect(edit, &QKeySequenceEdit::editingFinished, this, + &KeyboardControlsWindow::onEditingFinished); + } + + ui->StartKeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::Start_Key])); + ui->SelectKeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::Select_Key])); + ui->LAnalogDownkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::LAnalogDown_Key])); + ui->LAnalogLeftkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::LAnalogLeft_Key])); + ui->LAnalogUpkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::LAnalogUp_Key])); + ui->LAnalogRightkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::LAnalogRight_Key])); + ui->PSkeySequenceEdit->setKeySequence(convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::PS_Key])); + ui->RAnalogDownkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::RAnalogDown_Key])); + ui->RAnalogLeftkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::RAnalogLeft_Key])); + ui->RAnalogUpkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::RAnalogUp_Key])); + ui->RAnalogRightkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::RAnalogRight_Key])); + ui->DPadLeftkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::DPadLeft_Key])); + ui->DPadRightkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::DPadRight_Key])); + ui->DPadUpkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::DPadUp_Key])); + ui->DPadDownkeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::DPadDown_Key])); + ui->L2keySequenceEdit->setKeySequence(convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::L2_Key])); + ui->L1keySequenceEdit->setKeySequence(convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::L1_Key])); + ui->CrossKeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::Cross_Key])); + ui->R2KeySequenceEdit->setKeySequence(convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::R2_Key])); + ui->CircleKeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::Circle_Key])); + ui->R1KeySequenceEdit->setKeySequence(convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::R1_Key])); + ui->SquareKeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::Square_Key])); + ui->TriangleKeySequenceEdit->setKeySequence( + convertSDLKeyToQt(m_reverseKeysMap[KeysMapping::Triangle_Key])); + + QObject::connect(ui->applyButton, &QPushButton::clicked, + [this]() { validateAndSaveKeyBindings(); }); + + QObject::connect(ui->cancelButton, &QPushButton::clicked, [this]() { this->close(); }); +} + +KeyboardControlsWindow::~KeyboardControlsWindow() { + delete ui; +} + +const std::map& KeyboardControlsWindow::getKeysMapping() const { + return m_keysMap; +} + +void KeyboardControlsWindow::validateAndSaveKeyBindings() { + int nOfUnconfiguredButtons = 0; + for (auto& keyEdit : m_listOfKeySequenceEdits) { + auto keySequence = keyEdit->keySequence(); + // If key sequence is empty (i.e. there is no key assigned to it) we highlight it in red + if (keySequence.isEmpty()) { + keyEdit->setStyleSheet("background-color: red; qproperty-alignment: AlignCenter;"); + QTimer::singleShot(inputErrorTimerTimeout, keyEdit, [keyEdit]() { + keyEdit->setStyleSheet("qproperty-alignment: AlignCenter;"); // Reset to default + }); + + ++nOfUnconfiguredButtons; + } + } + + if (nOfUnconfiguredButtons > 0) { + showError("Some of the buttons were not configured"); + return; + } + + m_keysMap.clear(); + m_reverseKeysMap.clear(); + + m_keysMap.emplace(convertQtKeyToSDL(ui->LAnalogDownkeySequenceEdit->keySequence()[0].key()), + KeysMapping::LAnalogDown_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->LAnalogLeftkeySequenceEdit->keySequence()[0].key()), + KeysMapping::LAnalogLeft_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->LAnalogUpkeySequenceEdit->keySequence()[0].key()), + KeysMapping::LAnalogUp_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->LAnalogRightkeySequenceEdit->keySequence()[0].key()), + KeysMapping::LAnalogRight_Key); + + m_keysMap.emplace(convertQtKeyToSDL(ui->PSkeySequenceEdit->keySequence()[0].key()), + KeysMapping::PS_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->StartKeySequenceEdit->keySequence()[0].key()), + KeysMapping::Start_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->SelectKeySequenceEdit->keySequence()[0].key()), + KeysMapping::Select_Key); + + m_keysMap.emplace(convertQtKeyToSDL(ui->RAnalogDownkeySequenceEdit->keySequence()[0].key()), + KeysMapping::RAnalogDown_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->RAnalogLeftkeySequenceEdit->keySequence()[0].key()), + KeysMapping::RAnalogLeft_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->RAnalogUpkeySequenceEdit->keySequence()[0].key()), + KeysMapping::RAnalogUp_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->RAnalogRightkeySequenceEdit->keySequence()[0].key()), + KeysMapping::RAnalogRight_Key); + + m_keysMap.emplace(convertQtKeyToSDL(ui->DPadLeftkeySequenceEdit->keySequence()[0].key()), + KeysMapping::DPadLeft_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->DPadRightkeySequenceEdit->keySequence()[0].key()), + KeysMapping::DPadRight_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->DPadUpkeySequenceEdit->keySequence()[0].key()), + KeysMapping::DPadUp_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->DPadDownkeySequenceEdit->keySequence()[0].key()), + KeysMapping::DPadDown_Key); + + m_keysMap.emplace(convertQtKeyToSDL(ui->L1keySequenceEdit->keySequence()[0].key()), + KeysMapping::L1_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->L2keySequenceEdit->keySequence()[0].key()), + KeysMapping::L2_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->R1KeySequenceEdit->keySequence()[0].key()), + KeysMapping::R1_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->R2KeySequenceEdit->keySequence()[0].key()), + KeysMapping::R2_Key); + + m_keysMap.emplace(convertQtKeyToSDL(ui->CrossKeySequenceEdit->keySequence()[0].key()), + KeysMapping::Cross_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->CircleKeySequenceEdit->keySequence()[0].key()), + KeysMapping::Circle_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->SquareKeySequenceEdit->keySequence()[0].key()), + KeysMapping::Square_Key); + m_keysMap.emplace(convertQtKeyToSDL(ui->TriangleKeySequenceEdit->keySequence()[0].key()), + KeysMapping::Triangle_Key); + + for (auto& pair : m_keysMap) { + m_reverseKeysMap.emplace(pair.second, pair.first); + } + + // Saving into settings (for permanent storage) + Config::setKeyboardBindingMap(m_keysMap); + + this->close(); +} + +Qt::Key KeyboardControlsWindow::convertSDLKeyToQt(SDL_Keycode sdlKey) { + switch (sdlKey) { + case SDLK_A: + return Qt::Key_A; + case SDLK_B: + return Qt::Key_B; + case SDLK_C: + return Qt::Key_C; + case SDLK_D: + return Qt::Key_D; + case SDLK_E: + return Qt::Key_E; + case SDLK_F: + return Qt::Key_F; + case SDLK_G: + return Qt::Key_G; + case SDLK_H: + return Qt::Key_H; + case SDLK_I: + return Qt::Key_I; + case SDLK_J: + return Qt::Key_J; + case SDLK_K: + return Qt::Key_K; + case SDLK_L: + return Qt::Key_L; + case SDLK_M: + return Qt::Key_M; + case SDLK_N: + return Qt::Key_N; + case SDLK_O: + return Qt::Key_O; + case SDLK_P: + return Qt::Key_P; + case SDLK_Q: + return Qt::Key_Q; + case SDLK_R: + return Qt::Key_R; + case SDLK_S: + return Qt::Key_S; + case SDLK_T: + return Qt::Key_T; + case SDLK_U: + return Qt::Key_U; + case SDLK_V: + return Qt::Key_V; + case SDLK_W: + return Qt::Key_W; + case SDLK_X: + return Qt::Key_X; + case SDLK_Y: + return Qt::Key_Y; + case SDLK_Z: + return Qt::Key_Z; + case SDLK_0: + return Qt::Key_0; + case SDLK_1: + return Qt::Key_1; + case SDLK_2: + return Qt::Key_2; + case SDLK_3: + return Qt::Key_3; + case SDLK_4: + return Qt::Key_4; + case SDLK_5: + return Qt::Key_5; + case SDLK_6: + return Qt::Key_6; + case SDLK_7: + return Qt::Key_7; + case SDLK_8: + return Qt::Key_8; + case SDLK_9: + return Qt::Key_9; + case SDLK_SPACE: + return Qt::Key_Space; + case SDLK_RETURN: + return Qt::Key_Return; + case SDLK_ESCAPE: + return Qt::Key_Escape; + case SDLK_TAB: + return Qt::Key_Tab; + case SDLK_BACKSPACE: + return Qt::Key_Backspace; + case SDLK_DELETE: + return Qt::Key_Delete; + case SDLK_INSERT: + return Qt::Key_Insert; + case SDLK_HOME: + return Qt::Key_Home; + case SDLK_END: + return Qt::Key_End; + case SDLK_PAGEUP: + return Qt::Key_PageUp; + case SDLK_PAGEDOWN: + return Qt::Key_PageDown; + case SDLK_LEFT: + return Qt::Key_Left; + case SDLK_RIGHT: + return Qt::Key_Right; + case SDLK_UP: + return Qt::Key_Up; + case SDLK_DOWN: + return Qt::Key_Down; + case SDLK_CAPSLOCK: + return Qt::Key_CapsLock; + case SDLK_NUMLOCKCLEAR: + return Qt::Key_NumLock; + case SDLK_SCROLLLOCK: + return Qt::Key_ScrollLock; + case SDLK_F1: + return Qt::Key_F1; + case SDLK_F2: + return Qt::Key_F2; + case SDLK_F3: + return Qt::Key_F3; + case SDLK_F4: + return Qt::Key_F4; + case SDLK_F5: + return Qt::Key_F5; + case SDLK_F6: + return Qt::Key_F6; + case SDLK_F7: + return Qt::Key_F7; + case SDLK_F8: + return Qt::Key_F8; + case SDLK_F9: + return Qt::Key_F9; + case SDLK_F10: + return Qt::Key_F10; + case SDLK_F11: + return Qt::Key_F11; + case SDLK_F12: + return Qt::Key_F12; + case SDLK_LSHIFT: + return Qt::Key_Shift; + case SDLK_LCTRL: + return Qt::Key_Control; + case SDLK_LALT: + return Qt::Key_Alt; + case SDLK_LGUI: + return Qt::Key_Meta; + default: + return Qt::Key_unknown; + } +} + +SDL_Keycode KeyboardControlsWindow::convertQtKeyToSDL(Qt::Key qtKey) { + switch (qtKey) { + case Qt::Key_A: + return SDLK_A; + case Qt::Key_B: + return SDLK_B; + case Qt::Key_C: + return SDLK_C; + case Qt::Key_D: + return SDLK_D; + case Qt::Key_E: + return SDLK_E; + case Qt::Key_F: + return SDLK_F; + case Qt::Key_G: + return SDLK_G; + case Qt::Key_H: + return SDLK_H; + case Qt::Key_I: + return SDLK_I; + case Qt::Key_J: + return SDLK_J; + case Qt::Key_K: + return SDLK_K; + case Qt::Key_L: + return SDLK_L; + case Qt::Key_M: + return SDLK_M; + case Qt::Key_N: + return SDLK_N; + case Qt::Key_O: + return SDLK_O; + case Qt::Key_P: + return SDLK_P; + case Qt::Key_Q: + return SDLK_Q; + case Qt::Key_R: + return SDLK_R; + case Qt::Key_S: + return SDLK_S; + case Qt::Key_T: + return SDLK_T; + case Qt::Key_U: + return SDLK_U; + case Qt::Key_V: + return SDLK_V; + case Qt::Key_W: + return SDLK_W; + case Qt::Key_X: + return SDLK_X; + case Qt::Key_Y: + return SDLK_Y; + case Qt::Key_Z: + return SDLK_Z; + case Qt::Key_0: + return SDLK_0; + case Qt::Key_1: + return SDLK_1; + case Qt::Key_2: + return SDLK_2; + case Qt::Key_3: + return SDLK_3; + case Qt::Key_4: + return SDLK_4; + case Qt::Key_5: + return SDLK_5; + case Qt::Key_6: + return SDLK_6; + case Qt::Key_7: + return SDLK_7; + case Qt::Key_8: + return SDLK_8; + case Qt::Key_9: + return SDLK_9; + case Qt::Key_Space: + return SDLK_SPACE; + case Qt::Key_Enter: + return SDLK_RETURN; + case Qt::Key_Return: + return SDLK_RETURN; + case Qt::Key_Escape: + return SDLK_ESCAPE; + case Qt::Key_Tab: + return SDLK_TAB; + case Qt::Key_Backspace: + return SDLK_BACKSPACE; + case Qt::Key_Delete: + return SDLK_DELETE; + case Qt::Key_Insert: + return SDLK_INSERT; + case Qt::Key_Home: + return SDLK_HOME; + case Qt::Key_End: + return SDLK_END; + case Qt::Key_PageUp: + return SDLK_PAGEUP; + case Qt::Key_PageDown: + return SDLK_PAGEDOWN; + case Qt::Key_Left: + return SDLK_LEFT; + case Qt::Key_Right: + return SDLK_RIGHT; + case Qt::Key_Up: + return SDLK_UP; + case Qt::Key_Down: + return SDLK_DOWN; + case Qt::Key_CapsLock: + return SDLK_CAPSLOCK; + case Qt::Key_NumLock: + return SDLK_NUMLOCKCLEAR; + case Qt::Key_ScrollLock: + return SDLK_SCROLLLOCK; + case Qt::Key_F1: + return SDLK_F1; + case Qt::Key_F2: + return SDLK_F2; + case Qt::Key_F3: + return SDLK_F3; + case Qt::Key_F4: + return SDLK_F4; + case Qt::Key_F5: + return SDLK_F5; + case Qt::Key_F6: + return SDLK_F6; + case Qt::Key_F7: + return SDLK_F7; + case Qt::Key_F8: + return SDLK_F8; + case Qt::Key_F9: + return SDLK_F9; + case Qt::Key_F10: + return SDLK_F10; + case Qt::Key_F11: + return SDLK_F11; + case Qt::Key_F12: + return SDLK_F12; + case Qt::Key_Shift: + return SDLK_LSHIFT; + case Qt::Key_Control: + return SDLK_LCTRL; + case Qt::Key_Alt: + return SDLK_LALT; + case Qt::Key_Meta: + return SDLK_LGUI; + default: + return SDLK_UNKNOWN; + } +} + +void KeyboardControlsWindow::onEditingFinished() { + auto sender = qobject_cast(QObject::sender()); + auto new_keySequence = sender->keySequence(); + + // If new key sequence is empty (i.e. there is no key assigned to it) - skip 'duplicate' checks + // Two checks are needed for the sake of robustness (when we click on a widget but don't type + // anything it might no longer be "empty") + if (new_keySequence.isEmpty() || new_keySequence.toString().isEmpty()) { + return; + } + + // Check if sequance is not already used (i.e. making sure there are not duplicates) + for (auto& keyEdit : m_listOfKeySequenceEdits) { + if (keyEdit != sender && new_keySequence == keyEdit->keySequence()) { + sender->clear(); + sender->setStyleSheet("background-color: red; qproperty-alignment: AlignCenter;"); + QTimer::singleShot(inputErrorTimerTimeout, sender, [sender]() { + sender->setStyleSheet( + "QLineEdit { qproperty-alignment: AlignCenter; }"); // Reset to default + }); + + keyEdit->setStyleSheet("background-color: red; qproperty-alignment: AlignCenter;"); + QTimer::singleShot(inputErrorTimerTimeout, keyEdit, [keyEdit]() { + keyEdit->setStyleSheet( + "QLineEdit { qproperty-alignment: AlignCenter; }"); // Reset to default + }); + } + } +} diff --git a/src/qt_gui/keyboardcontrolswindow.h b/src/qt_gui/keyboardcontrolswindow.h new file mode 100644 index 000000000..3649917e2 --- /dev/null +++ b/src/qt_gui/keyboardcontrolswindow.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include +#include "input/keys_constants.h" + +QT_BEGIN_NAMESPACE +namespace Ui { +class KeyboardControlsWindow; +} +QT_END_NAMESPACE + +class KeyboardControlsWindow : public QDialog { + Q_OBJECT + +public: + KeyboardControlsWindow(QWidget* parent = nullptr); + ~KeyboardControlsWindow(); + + const std::map& getKeysMapping() const; + +private slots: + void onEditingFinished(); + +private: + void validateAndSaveKeyBindings(); + SDL_Keycode convertQtKeyToSDL(Qt::Key qtKey); + Qt::Key convertSDLKeyToQt(SDL_Keycode qtKey); + + Ui::KeyboardControlsWindow* ui; + QSet m_listOfKeySequenceEdits; + std::map m_keysMap; + std::map m_reverseKeysMap; +}; diff --git a/src/qt_gui/keyboardcontrolswindow.ui b/src/qt_gui/keyboardcontrolswindow.ui new file mode 100644 index 000000000..7973e0c5a --- /dev/null +++ b/src/qt_gui/keyboardcontrolswindow.ui @@ -0,0 +1,439 @@ + + + + KeyboardControlsWindow + + + + 0 + 0 + 1155 + 790 + + + + + 0 + 0 + + + + + 1148 + 731 + + + + + 1155 + 790 + + + + Configure Controller Bindings + + + true + + + + + 10 + 10 + 1131 + 711 + + + + 0 + + + + Port 1 + + + + + -10 + 20 + 1131 + 621 + + + + + + 170 + 50 + 870 + 522 + + + + false + + + QWidget { background-image: url(:images/PS4_controller_scheme.png); + background-repeat: no-repeat; + background-position: center; } + + + + + + 550 + 0 + 71 + 40 + + + + 1 + + + + + + 260 + 0 + 71 + 40 + + + + 1 + + + + + + 280 + 480 + 71 + 40 + + + + 1 + + + + + + 240 + 440 + 71 + 40 + + + + 1 + + + + + + 280 + 400 + 71 + 40 + + + + 1 + + + + + + 320 + 440 + 71 + 40 + + + + 1 + + + + + + 400 + 400 + 71 + 40 + + + + 1 + + + + + + 520 + 480 + 71 + 40 + + + + 1 + + + + + + 480 + 440 + 71 + 40 + + + + 1 + + + + + + 520 + 400 + 71 + 40 + + + + 1 + + + + + + 560 + 440 + 71 + 40 + + + + 1 + + + + + + + 10 + 230 + 71 + 40 + + + + 1 + + + + + + 90 + 230 + 71 + 40 + + + + 1 + + + + + + 50 + 190 + 71 + 40 + + + + 1 + + + + + + 50 + 270 + 71 + 40 + + + + 1 + + + + + + 90 + 40 + 71 + 40 + + + + 1 + + + + + + 90 + 110 + 71 + 40 + + + + 1 + + + + + true + + + + 1050 + 380 + 71 + 40 + + + + 1 + + + + + true + + + + 1050 + 30 + 71 + 40 + + + + 1 + + + + + true + + + + 1050 + 240 + 71 + 40 + + + + 1 + + + + + true + + + + 1050 + 100 + 71 + 40 + + + + 1 + + + + + true + + + + 1050 + 310 + 71 + 40 + + + + 1 + + + + + true + + + + 1050 + 170 + 71 + 40 + + + + 1 + + + + + + + Port 2 + + + + + + + 1030 + 740 + 100 + 32 + + + + Apply + + + + + + 890 + 740 + 100 + 32 + + + + Cancel + + + + + + diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index e5b502c58..59387a1a9 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -50,6 +50,7 @@ bool MainWindow::Init() { auto end = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(end - start); statusBar.reset(new QStatusBar); + m_controllerControlsDialog.reset(new KeyboardControlsWindow()); this->setStatusBar(statusBar.data()); // Update status bar int numGames = m_game_info->m_games.size(); @@ -90,6 +91,9 @@ void MainWindow::AddUiWidgets() { ui->toolBar->addWidget(ui->stopButton); ui->toolBar->addWidget(ui->refreshButton); ui->toolBar->addWidget(ui->settingsButton); + auto connection = QObject::connect(ui->controllerButton, &QPushButton::clicked, this, + &MainWindow::ControllerConfigurationButtonPressed); + ui->toolBar->addWidget(ui->controllerButton); QFrame* line = new QFrame(this); line->setFrameShape(QFrame::StyledPanel); @@ -99,6 +103,10 @@ void MainWindow::AddUiWidgets() { ui->toolBar->addWidget(ui->mw_searchbar); } +void MainWindow::ControllerConfigurationButtonPressed() { + m_controllerControlsDialog->show(); +} + void MainWindow::CreateDockWindows() { // place holder widget is needed for good health they say :) QWidget* phCentralWidget = new QWidget(this); @@ -781,6 +789,10 @@ void MainWindow::InstallDirectory() { RefreshGameTable(); } +std::map MainWindow::getKeysMapping() { + return m_controllerControlsDialog->getKeysMapping(); +} + void MainWindow::SetLastUsedTheme() { Theme lastTheme = static_cast(Config::getMainWindowTheme()); m_window_themes.SetWindowTheme(lastTheme, ui->mw_searchbar); diff --git a/src/qt_gui/main_window.h b/src/qt_gui/main_window.h index d3b83e619..8ffb760b0 100644 --- a/src/qt_gui/main_window.h +++ b/src/qt_gui/main_window.h @@ -17,6 +17,7 @@ #include "game_info.h" #include "game_list_frame.h" #include "game_list_utils.h" +#include "keyboardcontrolswindow.h" #include "main_window_themes.h" #include "main_window_ui.h" #include "pkg_viewer.h" @@ -37,6 +38,8 @@ public: void InstallDirectory(); void StartGame(); + std::map getKeysMapping(); + private Q_SLOTS: void ConfigureGuiFromSettings(); void SaveWindowState() const; @@ -45,6 +48,7 @@ private Q_SLOTS: void RefreshGameTable(); void HandleResize(QResizeEvent* event); void OnLanguageChanged(const std::string& locale); + void ControllerConfigurationButtonPressed(); private: Ui_MainWindow* ui; @@ -80,6 +84,7 @@ private: QScopedPointer m_elf_viewer; // Status Bar. QScopedPointer statusBar; + QScopedPointer m_controllerControlsDialog; // Available GPU devices std::vector m_physical_devices; diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index f3418c8f9..123ccce75 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -118,193 +118,310 @@ void WindowSDL::waitEvent() { } } +void WindowSDL::setKeysBindingsMap(const std::map& bindingsMap) { + keysBindingsMap = bindingsMap; +} + void WindowSDL::onResize() { SDL_GetWindowSizeInPixels(window, &width, &height); ImGui::Core::OnResize(); } +using Libraries::Pad::OrbisPadButtonDataOffset; + void WindowSDL::onKeyPress(const SDL_Event* event) { - using Libraries::Pad::OrbisPadButtonDataOffset; - -#ifdef __APPLE__ - // Use keys that are more friendly for keyboards without a keypad. - // Once there are key binding options this won't be necessary. - constexpr SDL_Keycode CrossKey = SDLK_N; - constexpr SDL_Keycode CircleKey = SDLK_B; - constexpr SDL_Keycode SquareKey = SDLK_V; - constexpr SDL_Keycode TriangleKey = SDLK_C; -#else - constexpr SDL_Keycode CrossKey = SDLK_KP_2; - constexpr SDL_Keycode CircleKey = SDLK_KP_6; - constexpr SDL_Keycode SquareKey = SDLK_KP_4; - constexpr SDL_Keycode TriangleKey = SDLK_KP_8; -#endif - u32 button = 0; Input::Axis axis = Input::Axis::AxisMax; int axisvalue = 0; int ax = 0; - switch (event->key.key) { - case SDLK_UP: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_UP; - break; - case SDLK_DOWN: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_DOWN; - break; - case SDLK_LEFT: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_LEFT; - break; - case SDLK_RIGHT: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_RIGHT; - break; - case TriangleKey: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_TRIANGLE; - break; - case CircleKey: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_CIRCLE; - break; - case CrossKey: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_CROSS; - break; - case SquareKey: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_SQUARE; - break; - case SDLK_RETURN: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_OPTIONS; - break; - case SDLK_A: - axis = Input::Axis::LeftX; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += -127; - } else { - axisvalue = 0; + + bool keyHandlingPending = true; + if (!keysBindingsMap.empty()) { + + std::optional ps4KeyOpt; + auto foundIt = keysBindingsMap.find(event->key.key); + if (foundIt != keysBindingsMap.end()) { + ps4KeyOpt = foundIt->second; } - ax = Input::GetAxis(-0x80, 0x80, axisvalue); - break; - case SDLK_D: - axis = Input::Axis::LeftX; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += 127; - } else { - axisvalue = 0; + + // No support for modifiers (yet) + if (ps4KeyOpt.has_value() && (event->key.mod == SDL_KMOD_NONE)) { + keyHandlingPending = false; + + auto ps4Key = ps4KeyOpt.value(); + if (ps4Key == KeysMapping::Start_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_OPTIONS; + if (ps4Key == KeysMapping::Triangle_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_TRIANGLE; + if (ps4Key == KeysMapping::Circle_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_CIRCLE; + if (ps4Key == KeysMapping::Cross_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_CROSS; + if (ps4Key == KeysMapping::Square_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_SQUARE; + if (ps4Key == KeysMapping::R1_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_R1; + if (ps4Key == KeysMapping::R2_Key) + handleR2Key(event, button, axis, axisvalue, ax); + if (ps4Key == KeysMapping::DPadLeft_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_LEFT; + if (ps4Key == KeysMapping::DPadRight_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_RIGHT; + if (ps4Key == KeysMapping::DPadDown_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_DOWN; + if (ps4Key == KeysMapping::DPadUp_Key) + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_UP; + if (ps4Key == KeysMapping::LAnalogLeft_Key) + handleLAnalogLeftKey(event, button, axis, axisvalue, ax); + if (ps4Key == KeysMapping::LAnalogUp_Key) + handleLAnalogUpKey(event, button, axis, axisvalue, ax); + if (ps4Key == KeysMapping::LAnalogDown_Key) + handleLAnalogDownKey(event, button, axis, axisvalue, ax); + if (ps4Key == KeysMapping::RAnalogLeft_Key) + handleRAnalogLeftKey(event, button, axis, axisvalue, ax); + if (ps4Key == KeysMapping::RAnalogRight_Key) + handleRAnalogRightKey(event, button, axis, axisvalue, ax); + if (ps4Key == KeysMapping::RAnalogUp_Key) + handleRAnalogUpKey(event, button, axis, axisvalue, ax); + if (ps4Key == KeysMapping::RAnalogDown_Key) + handleRAnalogDownKey(event, button, axis, axisvalue, ax); } - ax = Input::GetAxis(-0x80, 0x80, axisvalue); - break; - case SDLK_W: - axis = Input::Axis::LeftY; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += -127; - } else { - axisvalue = 0; - } - ax = Input::GetAxis(-0x80, 0x80, axisvalue); - break; - case SDLK_S: - axis = Input::Axis::LeftY; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += 127; - } else { - axisvalue = 0; - } - ax = Input::GetAxis(-0x80, 0x80, axisvalue); - break; - case SDLK_J: - axis = Input::Axis::RightX; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += -127; - } else { - axisvalue = 0; - } - ax = Input::GetAxis(-0x80, 0x80, axisvalue); - break; - case SDLK_L: - axis = Input::Axis::RightX; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += 127; - } else { - axisvalue = 0; - } - ax = Input::GetAxis(-0x80, 0x80, axisvalue); - break; - case SDLK_I: - axis = Input::Axis::RightY; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += -127; - } else { - axisvalue = 0; - } - ax = Input::GetAxis(-0x80, 0x80, axisvalue); - break; - case SDLK_K: - axis = Input::Axis::RightY; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += 127; - } else { - axisvalue = 0; - } - ax = Input::GetAxis(-0x80, 0x80, axisvalue); - break; - case SDLK_X: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_L3; - break; - case SDLK_M: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_R3; - break; - case SDLK_Q: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_L1; - break; - case SDLK_U: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_R1; - break; - case SDLK_E: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_L2; - axis = Input::Axis::TriggerLeft; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += 255; - } else { - axisvalue = 0; - } - ax = Input::GetAxis(0, 0x80, axisvalue); - break; - case SDLK_O: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_R2; - axis = Input::Axis::TriggerRight; - if (event->type == SDL_EVENT_KEY_DOWN) { - axisvalue += 255; - } else { - axisvalue = 0; - } - ax = Input::GetAxis(0, 0x80, axisvalue); - break; - case SDLK_SPACE: - button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_TOUCH_PAD; - break; - case SDLK_F11: - if (event->type == SDL_EVENT_KEY_DOWN) { - { + } + + if (keyHandlingPending) { + switch (event->key.key) { + case SDLK_UP: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_UP; + break; + case SDLK_DOWN: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_DOWN; + break; + case SDLK_LEFT: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_LEFT; + break; + case SDLK_RIGHT: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_RIGHT; + break; + case Triangle_Key: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_TRIANGLE; + break; + case Circle_Key: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_CIRCLE; + break; + case Cross_Key: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_CROSS; + break; + case Square_Key: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_SQUARE; + break; + case SDLK_KP_8: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_TRIANGLE; + break; + case SDLK_KP_6: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_CIRCLE; + break; + case SDLK_KP_2: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_CROSS; + break; + case SDLK_KP_4: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_SQUARE; + break; + case SDLK_RETURN: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_OPTIONS; + break; + case SDLK_A: + handleLAnalogLeftKey(event, button, axis, axisvalue, ax); + break; + case SDLK_D: + handleLAnalogRightKey(event, button, axis, axisvalue, ax); + break; + case SDLK_W: + handleLAnalogUpKey(event, button, axis, axisvalue, ax); + break; + case SDLK_S: + handleLAnalogDownKey(event, button, axis, axisvalue, ax); + if (event->key.mod == SDL_KMOD_LCTRL) { + // Trigger rdoc capture + VideoCore::TriggerCapture(); + } + break; + case SDLK_J: + handleRAnalogLeftKey(event, button, axis, axisvalue, ax); + break; + case SDLK_L: + handleRAnalogRightKey(event, button, axis, axisvalue, ax); + break; + case SDLK_I: + handleRAnalogUpKey(event, button, axis, axisvalue, ax); + break; + case SDLK_K: + handleRAnalogDownKey(event, button, axis, axisvalue, ax); + break; + case SDLK_X: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_L3; + break; + case SDLK_M: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_R3; + break; + case SDLK_Q: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_L1; + break; + case SDLK_U: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_R1; + break; + case SDLK_E: + handleL2Key(event, button, axis, axisvalue, ax); + break; + case SDLK_O: + handleR2Key(event, button, axis, axisvalue, ax); + break; + case SDLK_SPACE: + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_TOUCH_PAD; + break; + case SDLK_F11: + if (event->type == SDL_EVENT_KEY_DOWN) { SDL_WindowFlags flag = SDL_GetWindowFlags(window); bool is_fullscreen = flag & SDL_WINDOW_FULLSCREEN; SDL_SetWindowFullscreen(window, !is_fullscreen); } + break; + case SDLK_F12: + if (event->type == SDL_EVENT_KEY_DOWN) { + // Trigger rdoc capture + VideoCore::TriggerCapture(); + } + break; + default: + break; } - break; - case SDLK_F12: - if (event->type == SDL_EVENT_KEY_DOWN) { - // Trigger rdoc capture - VideoCore::TriggerCapture(); - } - break; - default: - break; } + if (button != 0) { controller->CheckButton(0, button, event->type == SDL_EVENT_KEY_DOWN); } if (axis != Input::Axis::AxisMax) { - controller->Axis(0, axis, ax); + if (event->gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || + event->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + controller->Axis(0, axis, Input::GetAxis(0, 0x8000, event->gaxis.value)); + + } else { + controller->Axis(0, axis, Input::GetAxis(-0x8000, 0x8000, event->gaxis.value)); + } } } +void WindowSDL::handleR2Key(const SDL_Event* event, u32& button, Input::Axis& axis, int& axisvalue, + int& ax) { + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_R2; + axis = Input::Axis::TriggerRight; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += 255; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(0, 0x80, axisvalue); +} + +void WindowSDL::handleL2Key(const SDL_Event* event, u32& button, Input::Axis& axis, int& axisvalue, + int& ax) { + button = OrbisPadButtonDataOffset::ORBIS_PAD_BUTTON_L2; + axis = Input::Axis::TriggerLeft; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += 255; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(0, 0x80, axisvalue); +} + +void WindowSDL::handleLAnalogRightKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax) { + axis = Input::Axis::LeftX; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += 127; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(-0x80, 0x80, axisvalue); +} + +void WindowSDL::handleLAnalogLeftKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax) { + axis = Input::Axis::LeftX; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += -127; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(-0x80, 0x80, axisvalue); +} + +void WindowSDL::handleLAnalogUpKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax) { + axis = Input::Axis::LeftY; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += -127; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(-0x80, 0x80, axisvalue); +} + +void WindowSDL::handleLAnalogDownKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax) { + axis = Input::Axis::LeftY; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += 127; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(-0x80, 0x80, axisvalue); +} + +void WindowSDL::handleRAnalogRightKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax) { + axis = Input::Axis::RightX; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += 127; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(-0x80, 0x80, axisvalue); +} + +void WindowSDL::handleRAnalogLeftKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax) { + axis = Input::Axis::RightX; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += -127; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(-0x80, 0x80, axisvalue); +} + +void WindowSDL::handleRAnalogUpKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax) { + axis = Input::Axis::RightY; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += -127; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(-0x80, 0x80, axisvalue); +} + +void WindowSDL::handleRAnalogDownKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax) { + axis = Input::Axis::RightY; + if (event->type == SDL_EVENT_KEY_DOWN) { + axisvalue += 127; + } else { + axisvalue = 0; + } + ax = Input::GetAxis(-0x80, 0x80, axisvalue); +} + void WindowSDL::onGamepadEvent(const SDL_Event* event) { using Libraries::Pad::OrbisPadButtonDataOffset; @@ -389,4 +506,4 @@ int WindowSDL::sdlGamepadToOrbisButton(u8 button) { } } -} // namespace Frontend +} // namespace Frontend \ No newline at end of file diff --git a/src/sdl_window.h b/src/sdl_window.h index 2a5aeb38c..10ecd081c 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -3,8 +3,10 @@ #pragma once +#include #include #include "common/types.h" +#include "input/keys_constants.h" struct SDL_Window; struct SDL_Gamepad; @@ -12,7 +14,8 @@ union SDL_Event; namespace Input { class GameController; -} +enum class Axis; +} // namespace Input namespace Frontend { @@ -68,6 +71,8 @@ public: void waitEvent(); + void setKeysBindingsMap(const std::map& bindingsMap); + private: void onResize(); void onKeyPress(const SDL_Event* event); @@ -75,12 +80,34 @@ private: int sdlGamepadToOrbisButton(u8 button); + void handleR2Key(const SDL_Event* event, u32& button, Input::Axis& axis, int& axisvalue, + int& ax); + void handleL2Key(const SDL_Event* event, u32& button, Input::Axis& axis, int& axisvalue, + int& ax); + void handleLAnalogRightKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax); + void handleLAnalogLeftKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax); + void handleLAnalogUpKey(const SDL_Event* event, u32& button, Input::Axis& axis, int& axisvalue, + int& ax); + void handleLAnalogDownKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax); + void handleRAnalogRightKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax); + void handleRAnalogLeftKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax); + void handleRAnalogUpKey(const SDL_Event* event, u32& button, Input::Axis& axis, int& axisvalue, + int& ax); + void handleRAnalogDownKey(const SDL_Event* event, u32& button, Input::Axis& axis, + int& axisvalue, int& ax); + private: s32 width; s32 height; Input::GameController* controller; WindowSystemInfo window_info{}; SDL_Window* window{}; + std::map keysBindingsMap; bool is_shown{}; bool is_open{true}; }; diff --git a/src/shadps4.qrc b/src/shadps4.qrc index c22b837bd..00f51bc6f 100644 --- a/src/shadps4.qrc +++ b/src/shadps4.qrc @@ -14,6 +14,7 @@ images/exit_icon.png images/settings_icon.png images/controller_icon.png + images/PS4_controller_scheme.png images/refresh_icon.png images/list_mode_icon.png images/flag_jp.png