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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 xfV@uPBinh8r&+kSXGEt}PElXvD*oSosZ9KfSF +#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