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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 ULFa 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