From c7f1c66b825971a773a99d29b13ac88189569824 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Sun, 3 Aug 2025 16:59:12 +0800 Subject: [PATCH] Qt: add customizable controller hotkeys (#3369) * customizable controller hotkeys - initial * Update input_handler.h --- CMakeLists.txt | 3 + REUSE.toml | 1 + src/core/devtools/layer.cpp | 65 +++++- src/core/devtools/layer.h | 10 +- src/images/hotkey.png | Bin 0 -> 18480 bytes src/input/input_handler.cpp | 158 +++++++++++++++ src/input/input_handler.h | 8 + src/qt_gui/hotkeys.cpp | 392 ++++++++++++++++++++++++++++++++++++ src/qt_gui/hotkeys.h | 62 ++++++ src/qt_gui/hotkeys.ui | 313 ++++++++++++++++++++++++++++ src/qt_gui/main_window.cpp | 6 + src/qt_gui/main_window_ui.h | 7 + src/sdl_window.cpp | 50 ++++- src/sdl_window.h | 6 +- src/shadps4.qrc | 1 + 15 files changed, 1067 insertions(+), 15 deletions(-) create mode 100644 src/images/hotkey.png create mode 100644 src/qt_gui/hotkeys.cpp create mode 100644 src/qt_gui/hotkeys.h create mode 100644 src/qt_gui/hotkeys.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index fd4cde787..d0aafa533 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1079,6 +1079,9 @@ set(QT_GUI src/qt_gui/about_dialog.cpp src/qt_gui/settings.h src/qt_gui/sdl_event_wrapper.cpp src/qt_gui/sdl_event_wrapper.h + src/qt_gui/hotkeys.h + src/qt_gui/hotkeys.cpp + src/qt_gui/hotkeys.ui ${EMULATOR} ${RESOURCE_FILES} ${TRANSLATIONS} diff --git a/REUSE.toml b/REUSE.toml index c58fd0944..99583b516 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -74,6 +74,7 @@ path = [ "src/images/website.svg", "src/images/youtube.svg", "src/images/trophy.wav", + "src/images/hotkey.png", "src/shadps4.qrc", "src/shadps4.rc", "src/qt_gui/translations/update_translation.sh", diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index 5380d3be9..8e8c7b969 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -3,6 +3,7 @@ #include "layer.h" +#include #include #include "SDL3/SDL_log.h" @@ -28,6 +29,7 @@ using L = ::Core::Devtools::Layer; static bool show_simple_fps = false; static bool visibility_toggled = false; +static bool show_quit_window = false; static float fps_scale = 1.0f; static int dump_frame_count = 1; @@ -138,15 +140,8 @@ void L::DrawAdvanced() { const auto& ctx = *GImGui; const auto& io = ctx.IO; - auto isSystemPaused = DebugState.IsGuestThreadsPaused(); - frame_graph.Draw(); - if (isSystemPaused) { - GetForegroundDrawList(GetMainViewport()) - ->AddText({10.0f, io.DisplaySize.y - 40.0f}, IM_COL32_WHITE, "Emulator paused"); - } - if (DebugState.should_show_frame_dump && DebugState.waiting_reg_dumps.empty()) { DebugState.should_show_frame_dump = false; std::unique_lock lock{DebugState.frame_dump_list_mutex}; @@ -383,20 +378,17 @@ void L::Draw() { if (DebugState.IsGuestThreadsPaused()) { DebugState.ResumeGuestThreads(); SDL_Log("Game resumed from Keyboard"); - show_pause_status = false; } else { DebugState.PauseGuestThreads(); SDL_Log("Game paused from Keyboard"); - show_pause_status = true; } visibility_toggled = true; } } - if (show_pause_status) { + if (DebugState.IsGuestThreadsPaused()) { ImVec2 pos = ImVec2(10, 10); ImU32 color = IM_COL32(255, 255, 255, 255); - ImGui::GetForegroundDrawList()->AddText(pos, color, "Game Paused Press F9 to Resume"); } @@ -436,5 +428,56 @@ void L::Draw() { PopFont(); } + if (show_quit_window) { + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + if (Begin("Quit Notification", nullptr, + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) { + SetWindowFontScale(1.5f); + TextCentered("Are you sure you want to quit?"); + NewLine(); + Text("Press Escape or Circle/B button to cancel"); + Text("Press Enter or Cross/A button to quit"); + + if (IsKeyPressed(ImGuiKey_Escape, false) || + (IsKeyPressed(ImGuiKey_GamepadFaceRight, false))) { + show_quit_window = false; + } + + if (IsKeyPressed(ImGuiKey_Enter, false) || + (IsKeyPressed(ImGuiKey_GamepadFaceDown, false))) { + SDL_Event event; + SDL_memset(&event, 0, sizeof(event)); + event.type = SDL_EVENT_QUIT; + SDL_PushEvent(&event); + } + } + End(); + } + PopID(); } + +void L::TextCentered(const std::string& text) { + float window_width = ImGui::GetWindowSize().x; + float text_width = ImGui::CalcTextSize(text.c_str()).x; + float text_indentation = (window_width - text_width) * 0.5f; + + ImGui::SameLine(text_indentation); + ImGui::Text("%s", text.c_str()); +} + +namespace Overlay { + +void ToggleSimpleFps() { + show_simple_fps = !show_simple_fps; + visibility_toggled = true; +} + +void ToggleQuitWindow() { + show_quit_window = !show_quit_window; +} + +} // namespace Overlay diff --git a/src/core/devtools/layer.h b/src/core/devtools/layer.h index 9e949c8e9..8abd52f2f 100644 --- a/src/core/devtools/layer.h +++ b/src/core/devtools/layer.h @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include "imgui/imgui_layer.h" @@ -19,7 +20,14 @@ public: static void SetupSettings(); void Draw() override; - bool show_pause_status = false; + void TextCentered(const std::string& text); }; } // namespace Core::Devtools + +namespace Overlay { + +void ToggleSimpleFps(); +void ToggleQuitWindow(); + +} // namespace Overlay diff --git a/src/images/hotkey.png b/src/images/hotkey.png new file mode 100644 index 0000000000000000000000000000000000000000..64777056f34dea6fc124ddc7befb8198beb38b3a GIT binary patch literal 18480 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 leftjoystick_deadzone, rightjoystick_deadzone, lefttrigger_d std::list> pressed_keys; std::list toggled_keys; static std::vector connections; +static std::vector fullscreenHotkeyInputsPad(3, ""); +static std::vector pauseHotkeyInputsPad(3, ""); +static std::vector simpleFpsHotkeyInputsPad(3, ""); +static std::vector quitHotkeyInputsPad(3, ""); auto output_array = std::array{ // Important: these have to be the first, or else they will update in the wrong order @@ -731,4 +735,158 @@ void ActivateOutputsFromInputs() { } } +std::vector GetHotkeyInputs(Input::HotkeyPad hotkey) { + switch (hotkey) { + case Input::HotkeyPad::FullscreenPad: + return fullscreenHotkeyInputsPad; + case Input::HotkeyPad::PausePad: + return pauseHotkeyInputsPad; + case Input::HotkeyPad::SimpleFpsPad: + return simpleFpsHotkeyInputsPad; + case Input::HotkeyPad::QuitPad: + return quitHotkeyInputsPad; + default: + return {}; + } +} + +void LoadHotkeyInputs() { + const auto hotkey_file = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "hotkeys.ini"; + if (!std::filesystem::exists(hotkey_file)) { + createHotkeyFile(hotkey_file); + } + + std::string controllerFullscreenString, controllerPauseString, controllerFpsString, + controllerQuitString = ""; + std::ifstream file(hotkey_file); + int lineCount = 0; + std::string line = ""; + + while (std::getline(file, line)) { + lineCount++; + + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) + continue; + + if (line.contains("controllerFullscreen")) { + controllerFullscreenString = line.substr(equal_pos + 2); + } else if (line.contains("controllerQuit")) { + controllerQuitString = line.substr(equal_pos + 2); + } else if (line.contains("controllerFps")) { + controllerFpsString = line.substr(equal_pos + 2); + } else if (line.contains("controllerPause")) { + controllerPauseString = line.substr(equal_pos + 2); + } + } + + file.close(); + + auto getVectorFromString = [&](std::vector& inputVector, + const std::string& inputString) { + std::size_t comma_pos = inputString.find(','); + if (comma_pos == std::string::npos) { + inputVector[0] = inputString; + inputVector[1] = "unused"; + inputVector[2] = "unused"; + } else { + inputVector[0] = inputString.substr(0, comma_pos); + std::string substring = inputString.substr(comma_pos + 1); + std::size_t comma2_pos = substring.find(','); + + if (comma2_pos == std::string::npos) { + inputVector[1] = substring; + inputVector[2] = "unused"; + } else { + inputVector[1] = substring.substr(0, comma2_pos); + inputVector[2] = substring.substr(comma2_pos + 1); + } + } + }; + + getVectorFromString(fullscreenHotkeyInputsPad, controllerFullscreenString); + getVectorFromString(quitHotkeyInputsPad, controllerQuitString); + getVectorFromString(pauseHotkeyInputsPad, controllerPauseString); + getVectorFromString(simpleFpsHotkeyInputsPad, controllerFpsString); +} + +bool HotkeyInputsPressed(std::vector inputs) { + if (inputs[0] == "unmapped") { + return false; + } + + auto controller = Common::Singleton::Instance(); + auto engine = controller->GetEngine(); + SDL_Gamepad* gamepad = engine->m_gamepad; + + if (!gamepad) { + return false; + } + + std::vector isPressed(3, false); + for (int i = 0; i < 3; i++) { + if (inputs[i] == "cross") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_SOUTH); + } else if (inputs[i] == "circle") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_EAST); + } else if (inputs[i] == "square") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_WEST); + } else if (inputs[i] == "triangle") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_NORTH); + } else if (inputs[i] == "pad_up") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP); + } else if (inputs[i] == "pad_down") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN); + } else if (inputs[i] == "pad_left") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT); + } else if (inputs[i] == "pad_right") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); + } else if (inputs[i] == "l1") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER); + } else if (inputs[i] == "r1") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER); + } else if (inputs[i] == "l3") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_STICK); + } else if (inputs[i] == "r3") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_STICK); + } else if (inputs[i] == "options") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_START); + } else if (inputs[i] == "back") { + isPressed[i] = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_BACK); + } else if (inputs[i] == "l2") { + isPressed[i] = (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 16000); + } else if (inputs[i] == "r2") { + isPressed[i] = (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 16000); + } else if (inputs[i] == "unused") { + isPressed[i] = true; + } else { + isPressed[i] = false; + } + } + + if (isPressed[0] && isPressed[1] && isPressed[2]) { + return true; + } + + return false; +} + +void createHotkeyFile(std::filesystem::path hotkey_file) { + std::string_view default_hotkeys = R"(controllerStop = unmapped +controllerFps = l2,r2,r3 +controllerPause = l2,r2,options +controllerFullscreen = l2,r2,l3 + +keyboardStop = placeholder +keyboardFps = placeholder +keyboardPause = placeholder +keyboardFullscreen = placeholder +)"; + + std::ofstream default_hotkeys_stream(hotkey_file); + if (default_hotkeys_stream) { + default_hotkeys_stream << default_hotkeys; + } +} + } // namespace Input diff --git a/src/input/input_handler.h b/src/input/input_handler.h index daef22f21..8befb2e16 100644 --- a/src/input/input_handler.h +++ b/src/input/input_handler.h @@ -4,9 +4,11 @@ #pragma once #include +#include #include #include #include +#include #include "SDL3/SDL_events.h" #include "SDL3/SDL_timer.h" @@ -448,10 +450,16 @@ public: InputEvent ProcessBinding(); }; +enum HotkeyPad { FullscreenPad, PausePad, SimpleFpsPad, QuitPad }; + // Updates the list of pressed keys with the given input. // Returns whether the list was updated or not. bool UpdatePressedKeys(InputEvent event); void ActivateOutputsFromInputs(); +void LoadHotkeyInputs(); +bool HotkeyInputsPressed(std::vector inputs); +std::vector GetHotkeyInputs(Input::HotkeyPad hotkey); +void createHotkeyFile(std::filesystem::path hotkey_file); } // namespace Input diff --git a/src/qt_gui/hotkeys.cpp b/src/qt_gui/hotkeys.cpp new file mode 100644 index 000000000..4fb6a12b8 --- /dev/null +++ b/src/qt_gui/hotkeys.cpp @@ -0,0 +1,392 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include + +#include "common/config.h" +#include "common/logging/log.h" +#include "common/path_util.h" +#include "hotkeys.h" +#include "input/input_handler.h" +#include "ui_hotkeys.h" + +hotkeys::hotkeys(bool isGameRunning, QWidget* parent) + : QDialog(parent), GameRunning(isGameRunning), ui(new Ui::hotkeys) { + + ui->setupUi(this); + installEventFilter(this); + + if (!GameRunning) { + SDL_InitSubSystem(SDL_INIT_GAMEPAD); + SDL_InitSubSystem(SDL_INIT_EVENTS); + } else { + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); + } + + LoadHotkeys(); + CheckGamePad(); + + ButtonsList = { + ui->fpsButtonPad, + ui->quitButtonPad, + ui->fullscreenButtonPad, + ui->pauseButtonPad, + }; + + ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save")); + ui->buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Apply")); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton* button) { + if (button == ui->buttonBox->button(QDialogButtonBox::Save)) { + SaveHotkeys(true); + } else if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) { + SaveHotkeys(false); + } else if (button == ui->buttonBox->button(QDialogButtonBox::Cancel)) { + QWidget::close(); + } + }); + + for (auto& button : ButtonsList) { + connect(button, &QPushButton::clicked, this, + [this, &button]() { StartTimer(button, true); }); + } + + connect(this, &hotkeys::PushGamepadEvent, this, [this]() { CheckMapping(MappingButton); }); + + SdlEventWrapper::Wrapper::wrapperActive = true; + QObject::connect(SdlEventWrapper::Wrapper::GetInstance(), &SdlEventWrapper::Wrapper::SDLEvent, + this, &hotkeys::processSDLEvents); + + if (!GameRunning) { + Polling = QtConcurrent::run(&hotkeys::pollSDLEvents, this); + } +} + +void hotkeys::DisableMappingButtons() { + for (const auto& i : ButtonsList) { + i->setEnabled(false); + } +} + +void hotkeys::EnableMappingButtons() { + for (const auto& i : ButtonsList) { + i->setEnabled(true); + } +} + +void hotkeys::SaveHotkeys(bool CloseOnSave) { + const auto hotkey_file = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "hotkeys.ini"; + if (!std::filesystem::exists(hotkey_file)) { + Input::createHotkeyFile(hotkey_file); + } + + QString controllerFullscreenString, controllerPauseString, controllerFpsString, + controllerQuitString = ""; + std::ifstream file(hotkey_file); + int lineCount = 0; + std::string line = ""; + std::vector lines; + + while (std::getline(file, line)) { + lineCount++; + + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) { + lines.push_back(line); + continue; + } + + if (line.contains("controllerFullscreen")) { + line = "controllerFullscreen = " + ui->fullscreenButtonPad->text().toStdString(); + } else if (line.contains("controllerQuit")) { + line = "controllerQuit = " + ui->quitButtonPad->text().toStdString(); + } else if (line.contains("controllerFps")) { + line = "controllerFps = " + ui->fpsButtonPad->text().toStdString(); + } else if (line.contains("controllerPause")) { + line = "controllerPause = " + ui->pauseButtonPad->text().toStdString(); + } + + lines.push_back(line); + } + + file.close(); + + std::ofstream output_file(hotkey_file); + for (auto const& line : lines) { + output_file << line << '\n'; + } + output_file.close(); + + Input::LoadHotkeyInputs(); + + if (CloseOnSave) + QWidget::close(); +} + +void hotkeys::LoadHotkeys() { + const auto hotkey_file = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "hotkeys.ini"; + if (!std::filesystem::exists(hotkey_file)) { + Input::createHotkeyFile(hotkey_file); + } + + QString controllerFullscreenString, controllerPauseString, controllerFpsString, + controllerQuitString = ""; + std::ifstream file(hotkey_file); + int lineCount = 0; + std::string line = ""; + + while (std::getline(file, line)) { + lineCount++; + + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) + continue; + + if (line.contains("controllerFullscreen")) { + controllerFullscreenString = QString::fromStdString(line.substr(equal_pos + 2)); + } else if (line.contains("controllerQuit")) { + controllerQuitString = QString::fromStdString(line.substr(equal_pos + 2)); + } else if (line.contains("controllerFps")) { + controllerFpsString = QString::fromStdString(line.substr(equal_pos + 2)); + } else if (line.contains("controllerPause")) { + controllerPauseString = QString::fromStdString(line.substr(equal_pos + 2)); + } + } + + file.close(); + + ui->fpsButtonPad->setText(controllerFpsString); + ui->quitButtonPad->setText(controllerQuitString); + ui->fullscreenButtonPad->setText(controllerFullscreenString); + ui->pauseButtonPad->setText(controllerPauseString); +} + +void hotkeys::CheckGamePad() { + if (h_gamepad) { + SDL_CloseGamepad(h_gamepad); + h_gamepad = nullptr; + } + + h_gamepads = SDL_GetGamepads(&gamepad_count); + + if (!h_gamepads) { + LOG_ERROR(Input, "Cannot get gamepad list: {}", SDL_GetError()); + } + + int defaultIndex = GamepadSelect::GetIndexfromGUID(h_gamepads, gamepad_count, + Config::getDefaultControllerID()); + int activeIndex = GamepadSelect::GetIndexfromGUID(h_gamepads, gamepad_count, + GamepadSelect::GetSelectedGamepad()); + + if (!GameRunning) { + if (activeIndex != -1) { + h_gamepad = SDL_OpenGamepad(h_gamepads[activeIndex]); + } else if (defaultIndex != -1) { + h_gamepad = SDL_OpenGamepad(h_gamepads[defaultIndex]); + } else { + LOG_INFO(Input, "Got {} gamepads. Opening the first one.", gamepad_count); + h_gamepad = SDL_OpenGamepad(h_gamepads[0]); + } + + if (!h_gamepad) { + LOG_ERROR(Input, "Failed to open gamepad: {}", SDL_GetError()); + } + } +} + +void hotkeys::StartTimer(QPushButton*& button, bool isButton) { + MappingTimer = 3; + EnableButtonMapping = true; + MappingCompleted = false; + L2Pressed = false; + R2Pressed = false; + mapping = button->text(); + DisableMappingButtons(); + + button->setText(tr("Press a button") + " [" + QString::number(MappingTimer) + "]"); + + timer = new QTimer(this); + MappingButton = button; + timer->start(1000); + connect(timer, &QTimer::timeout, this, [this]() { CheckMapping(MappingButton); }); +} + +void hotkeys::CheckMapping(QPushButton*& button) { + MappingTimer -= 1; + button->setText(tr("Press a button") + " [" + QString::number(MappingTimer) + "]"); + + if (pressedButtons.size() > 0) { + QStringList keyStrings; + + for (const QString& buttonAction : pressedButtons) { + keyStrings << buttonAction; + } + + QString combo = keyStrings.join(","); + SetMapping(combo); + MappingButton->setText(combo); + pressedButtons.clear(); + } + + if (MappingCompleted || MappingTimer <= 0) { + button->setText(mapping); + EnableButtonMapping = false; + EnableMappingButtons(); + timer->stop(); + } +} + +void hotkeys::SetMapping(QString input) { + mapping = input; + MappingCompleted = true; +} + +// use QT events instead of SDL to override default event closing the window with escape +bool hotkeys::eventFilter(QObject* obj, QEvent* event) { + if (event->type() == QEvent::KeyPress && EnableButtonMapping) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape) { + SetMapping("unmapped"); + PushGamepadEvent(); + return true; + } + } + return QDialog::eventFilter(obj, event); +} + +void hotkeys::processSDLEvents(int Type, int Input, int Value) { + if (EnableButtonMapping) { + + if (pressedButtons.size() >= 3) { + return; + } + + if (Type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) { + switch (Input) { + case SDL_GAMEPAD_BUTTON_SOUTH: + pressedButtons.insert(5, "cross"); + break; + case SDL_GAMEPAD_BUTTON_EAST: + pressedButtons.insert(6, "circle"); + break; + case SDL_GAMEPAD_BUTTON_NORTH: + pressedButtons.insert(7, "triangle"); + break; + case SDL_GAMEPAD_BUTTON_WEST: + pressedButtons.insert(8, "square"); + break; + case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: + pressedButtons.insert(3, "l1"); + break; + case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: + pressedButtons.insert(4, "r1"); + break; + case SDL_GAMEPAD_BUTTON_LEFT_STICK: + pressedButtons.insert(9, "l3"); + break; + case SDL_GAMEPAD_BUTTON_RIGHT_STICK: + pressedButtons.insert(10, "r3"); + break; + case SDL_GAMEPAD_BUTTON_DPAD_UP: + pressedButtons.insert(13, "pad_up"); + break; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + pressedButtons.insert(14, "pad_down"); + break; + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + pressedButtons.insert(15, "pad_left"); + break; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + pressedButtons.insert(16, "pad_right"); + break; + case SDL_GAMEPAD_BUTTON_BACK: + pressedButtons.insert(11, "back"); + break; + case SDL_GAMEPAD_BUTTON_START: + pressedButtons.insert(12, "options"); + break; + default: + break; + } + } + + if (Type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { + // SDL trigger axis values range from 0 to 32000, set mapping on half movement + // Set zone for trigger release signal arbitrarily at 5000 + switch (Input) { + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: + if (Value > 16000) { + pressedButtons.insert(1, "l2"); + L2Pressed = true; + } else if (Value < 5000) { + if (L2Pressed && !R2Pressed) + emit PushGamepadEvent(); + } + break; + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: + if (Value > 16000) { + pressedButtons.insert(2, "r2"); + R2Pressed = true; + } else if (Value < 5000) { + if (R2Pressed && !L2Pressed) + emit PushGamepadEvent(); + } + break; + default: + break; + } + } + + if (Type == SDL_EVENT_GAMEPAD_BUTTON_UP) + emit PushGamepadEvent(); + } + + if (Type == SDL_EVENT_GAMEPAD_ADDED || SDL_EVENT_GAMEPAD_REMOVED) { + CheckGamePad(); + } +} + +void hotkeys::pollSDLEvents() { + SDL_Event event; + while (SdlEventWrapper::Wrapper::wrapperActive) { + + if (!SDL_WaitEvent(&event)) { + return; + } + + if (event.type == SDL_EVENT_QUIT) { + return; + } + + SdlEventWrapper::Wrapper::GetInstance()->Wrapper::ProcessEvent(&event); + } +} + +void hotkeys::Cleanup() { + SdlEventWrapper::Wrapper::wrapperActive = false; + if (h_gamepad) { + SDL_CloseGamepad(h_gamepad); + h_gamepad = nullptr; + } + + SDL_free(h_gamepads); + + if (!GameRunning) { + SDL_Event quitLoop{}; + quitLoop.type = SDL_EVENT_QUIT; + SDL_PushEvent(&quitLoop); + Polling.waitForFinished(); + + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); + SDL_QuitSubSystem(SDL_INIT_EVENTS); + SDL_Quit(); + } else { + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0"); + } +} + +hotkeys::~hotkeys() {} diff --git a/src/qt_gui/hotkeys.h b/src/qt_gui/hotkeys.h new file mode 100644 index 000000000..dd34fee27 --- /dev/null +++ b/src/qt_gui/hotkeys.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "sdl_event_wrapper.h" + +namespace Ui { +class hotkeys; +} + +class hotkeys : public QDialog { + Q_OBJECT + +public: + explicit hotkeys(bool GameRunning, QWidget* parent = nullptr); + ~hotkeys(); + +signals: + void PushGamepadEvent(); + +private: + bool eventFilter(QObject* obj, QEvent* event) override; + void CheckMapping(QPushButton*& button); + void StartTimer(QPushButton*& button, bool isButton); + void DisableMappingButtons(); + void EnableMappingButtons(); + void SaveHotkeys(bool CloseOnSave); + void LoadHotkeys(); + void processSDLEvents(int Type, int Input, int Value); + void pollSDLEvents(); + void CheckGamePad(); + void SetMapping(QString input); + void Cleanup(); + + bool GameRunning; + bool EnableButtonMapping = false; + bool MappingCompleted = false; + bool L2Pressed = false; + bool R2Pressed = false; + int MappingTimer; + int gamepad_count; + QString mapping; + QTimer* timer; + QPushButton* MappingButton; + SDL_Gamepad* h_gamepad = nullptr; + SDL_JoystickID* h_gamepads; + + // use QMap instead of QSet to maintain order of inserted strings + QMap pressedButtons; + QList ButtonsList; + QFuture Polling; + + Ui::hotkeys* ui; + +protected: + void closeEvent(QCloseEvent* event) override { + Cleanup(); + } +}; diff --git a/src/qt_gui/hotkeys.ui b/src/qt_gui/hotkeys.ui new file mode 100644 index 000000000..29dd638fd --- /dev/null +++ b/src/qt_gui/hotkeys.ui @@ -0,0 +1,313 @@ + + + + hotkeys + + + + 0 + 0 + 849 + 496 + + + + Customize Hotkeys + + + + + 750 + 200 + 81 + 81 + + + + Qt::Orientation::Vertical + + + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save + + + + + + 30 + 10 + 681 + 231 + + + + + + + + 0 + 0 + + + + + 19 + true + + + + Controller Hotkeys + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + Show FPS Counter + + + + + + Qt::FocusPolicy::NoFocus + + + unmapped + + + + + + + + + + Stop Emulator + + + + + + Qt::FocusPolicy::NoFocus + + + unmapped + + + + + + + + + + + + + + Toggle Fullscreen + + + + + + Qt::FocusPolicy::NoFocus + + + unmapped + + + + + + + + + + Toggle Pause + + + + + + Qt::FocusPolicy::NoFocus + + + unmapped + + + + + + + + + + + + + + 30 + 250 + 681 + 191 + + + + + + + + 0 + 0 + + + + + 19 + true + + + + Keyboard Hotkeys + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + + + + 11 + + + + QFrame::Shape::Box + + + Show Fps Counter: F10 + + + + + + + + 11 + + + + QFrame::Shape::Box + + + Stop Emulator: n/a + + + + + + + + + + + + 11 + + + + QFrame::Shape::Box + + + Toggle Fullscreen: F11 + + + + + + + + 11 + + + + QFrame::Shape::Box + + + Toggle Pause: F9 + + + + + + + + + + + + + 50 + 450 + 631 + 31 + + + + + 12 + true + + + + Tip: Up to three inputs can be assigned for each function + + + + + + + buttonBox + accepted() + hotkeys + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + hotkeys + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index f561bf392..afa5030e9 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -20,6 +20,7 @@ #include "common/string_util.h" #include "control_settings.h" #include "game_install_dialog.h" +#include "hotkeys.h" #include "kbm_gui.h" #include "main_window.h" #include "settings_dialog.h" @@ -495,6 +496,11 @@ void MainWindow::CreateConnects() { aboutDialog->exec(); }); + connect(ui->configureHotkeys, &QAction::triggered, this, [this]() { + auto hotkeyDialog = new hotkeys(isGameRunning, this); + hotkeyDialog->exec(); + }); + connect(ui->setIconSizeTinyAct, &QAction::triggered, this, [this]() { if (isTableList) { m_game_list_frame->icon_size = diff --git a/src/qt_gui/main_window_ui.h b/src/qt_gui/main_window_ui.h index 4ce71013e..5339a021d 100644 --- a/src/qt_gui/main_window_ui.h +++ b/src/qt_gui/main_window_ui.h @@ -32,6 +32,7 @@ public: #endif QAction* aboutAct; QAction* configureAct; + QAction* configureHotkeys; QAction* setThemeDark; QAction* setThemeLight; QAction* setThemeGreen; @@ -155,6 +156,9 @@ public: configureAct = new QAction(MainWindow); configureAct->setObjectName("configureAct"); configureAct->setIcon(QIcon(":images/settings_icon.png")); + configureHotkeys = new QAction(MainWindow); + configureHotkeys->setObjectName("configureHotkeys"); + configureHotkeys->setIcon(QIcon(":images/hotkey.png")); setThemeDark = new QAction(MainWindow); setThemeDark->setObjectName("setThemeDark"); setThemeDark->setCheckable(true); @@ -330,6 +334,7 @@ public: menuGame_List_Mode->addAction(setlistElfAct); menuSettings->addAction(configureAct); menuSettings->addAction(gameInstallPathAct); + menuSettings->addAction(configureHotkeys); menuSettings->addAction(menuUtils->menuAction()); menuUtils->addAction(downloadCheatsPatchesAct); menuUtils->addAction(dumpGameListAct); @@ -355,6 +360,8 @@ public: #endif aboutAct->setText(QCoreApplication::translate("MainWindow", "About shadPS4", nullptr)); configureAct->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr)); + configureHotkeys->setText( + QCoreApplication::translate("MainWindow", "Customize Hotkeys", nullptr)); #if QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip) menuRecent->setTitle(QCoreApplication::translate("MainWindow", "Recent Games", nullptr)); diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 69aa5f4c3..8c45b243a 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -11,6 +11,7 @@ #include "common/config.h" #include "common/elf_info.h" #include "core/debug_state.h" +#include "core/devtools/layer.h" #include "core/libraries/kernel/time.h" #include "core/libraries/pad/pad.h" #include "imgui/renderer/imgui_core.h" @@ -351,6 +352,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ Input::ControllerOutput::SetControllerOutputController(controller); Input::ControllerOutput::LinkJoystickAxes(); Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); + Input::LoadHotkeyInputs(); } WindowSDL::~WindowSDL() = default; @@ -549,7 +551,6 @@ void WindowSDL::OnKeyboardMouseInput(const SDL_Event* event) { } void WindowSDL::OnGamepadEvent(const SDL_Event* event) { - bool input_down = event->type == SDL_EVENT_GAMEPAD_AXIS_MOTION || event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN; Input::InputEvent input_event = Input::InputBinding::GetInputEventFromSDLEvent(*event); @@ -565,10 +566,55 @@ void WindowSDL::OnGamepadEvent(const SDL_Event* event) { // add/remove it from the list bool inputs_changed = Input::UpdatePressedKeys(input_event); - // update bindings if (inputs_changed) { + // process hotkeys + if (event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) { + process_hotkeys = true; + } else if (event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) { + if (event->gbutton.timestamp) + CheckHotkeys(); + } else if (event->type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { + if (event->gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || + event->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + if (event->gaxis.value < 5000) { + process_hotkeys = true; + } else if (event->gaxis.value > 16000) { + CheckHotkeys(); + } + } + } + + // update bindings Input::ActivateOutputsFromInputs(); } } +void WindowSDL::CheckHotkeys() { + if (Input::HotkeyInputsPressed(Input::GetHotkeyInputs(Input::HotkeyPad::FullscreenPad))) { + SDL_Event event; + SDL_memset(&event, 0, sizeof(event)); + event.type = SDL_EVENT_TOGGLE_FULLSCREEN; + SDL_PushEvent(&event); + process_hotkeys = false; + } + + if (Input::HotkeyInputsPressed(Input::GetHotkeyInputs(Input::HotkeyPad::PausePad))) { + SDL_Event event; + SDL_memset(&event, 0, sizeof(event)); + event.type = SDL_EVENT_TOGGLE_PAUSE; + SDL_PushEvent(&event); + process_hotkeys = false; + } + + if (Input::HotkeyInputsPressed(Input::GetHotkeyInputs(Input::HotkeyPad::SimpleFpsPad))) { + Overlay::ToggleSimpleFps(); + process_hotkeys = false; + } + + if (Input::HotkeyInputsPressed(Input::GetHotkeyInputs(Input::HotkeyPad::QuitPad))) { + Overlay::ToggleQuitWindow(); + process_hotkeys = false; + } +} + } // namespace Frontend diff --git a/src/sdl_window.h b/src/sdl_window.h index 83713af57..c05860b4a 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -3,10 +3,12 @@ #pragma once +#include + #include "common/types.h" #include "core/libraries/pad/pad.h" #include "input/controller.h" -#include "string" + #define SDL_EVENT_TOGGLE_FULLSCREEN (SDL_EVENT_USER + 1) #define SDL_EVENT_TOGGLE_PAUSE (SDL_EVENT_USER + 2) #define SDL_EVENT_CHANGE_CONTROLLER (SDL_EVENT_USER + 3) @@ -98,6 +100,7 @@ private: void OnResize(); void OnKeyboardMouseInput(const SDL_Event* event); void OnGamepadEvent(const SDL_Event* event); + void CheckHotkeys(); private: s32 width; @@ -107,6 +110,7 @@ private: SDL_Window* window{}; bool is_shown{}; bool is_open{true}; + bool process_hotkeys{true}; }; } // namespace Frontend diff --git a/src/shadps4.qrc b/src/shadps4.qrc index 707fc89b0..71b7c776f 100644 --- a/src/shadps4.qrc +++ b/src/shadps4.qrc @@ -38,5 +38,6 @@ images/refreshlist_icon.png images/favorite_icon.png images/trophy_icon.png + images/hotkey.png