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 000000000..64777056f Binary files /dev/null and b/src/images/hotkey.png differ diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index 67980ed0c..1f36612d0 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -60,6 +60,10 @@ std::pair 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