Qt: add customizable controller hotkeys (#3369)

* customizable controller hotkeys - initial

* Update input_handler.h
This commit is contained in:
rainmakerv2 2025-08-03 16:59:12 +08:00 committed by GitHub
commit c7f1c66b82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1067 additions and 15 deletions

View file

@ -1079,6 +1079,9 @@ set(QT_GUI src/qt_gui/about_dialog.cpp
src/qt_gui/settings.h src/qt_gui/settings.h
src/qt_gui/sdl_event_wrapper.cpp src/qt_gui/sdl_event_wrapper.cpp
src/qt_gui/sdl_event_wrapper.h src/qt_gui/sdl_event_wrapper.h
src/qt_gui/hotkeys.h
src/qt_gui/hotkeys.cpp
src/qt_gui/hotkeys.ui
${EMULATOR} ${EMULATOR}
${RESOURCE_FILES} ${RESOURCE_FILES}
${TRANSLATIONS} ${TRANSLATIONS}

View file

@ -74,6 +74,7 @@ path = [
"src/images/website.svg", "src/images/website.svg",
"src/images/youtube.svg", "src/images/youtube.svg",
"src/images/trophy.wav", "src/images/trophy.wav",
"src/images/hotkey.png",
"src/shadps4.qrc", "src/shadps4.qrc",
"src/shadps4.rc", "src/shadps4.rc",
"src/qt_gui/translations/update_translation.sh", "src/qt_gui/translations/update_translation.sh",

View file

@ -3,6 +3,7 @@
#include "layer.h" #include "layer.h"
#include <SDL3/SDL_events.h>
#include <imgui.h> #include <imgui.h>
#include "SDL3/SDL_log.h" #include "SDL3/SDL_log.h"
@ -28,6 +29,7 @@ using L = ::Core::Devtools::Layer;
static bool show_simple_fps = false; static bool show_simple_fps = false;
static bool visibility_toggled = false; static bool visibility_toggled = false;
static bool show_quit_window = false;
static float fps_scale = 1.0f; static float fps_scale = 1.0f;
static int dump_frame_count = 1; static int dump_frame_count = 1;
@ -138,15 +140,8 @@ void L::DrawAdvanced() {
const auto& ctx = *GImGui; const auto& ctx = *GImGui;
const auto& io = ctx.IO; const auto& io = ctx.IO;
auto isSystemPaused = DebugState.IsGuestThreadsPaused();
frame_graph.Draw(); 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()) { if (DebugState.should_show_frame_dump && DebugState.waiting_reg_dumps.empty()) {
DebugState.should_show_frame_dump = false; DebugState.should_show_frame_dump = false;
std::unique_lock lock{DebugState.frame_dump_list_mutex}; std::unique_lock lock{DebugState.frame_dump_list_mutex};
@ -383,20 +378,17 @@ void L::Draw() {
if (DebugState.IsGuestThreadsPaused()) { if (DebugState.IsGuestThreadsPaused()) {
DebugState.ResumeGuestThreads(); DebugState.ResumeGuestThreads();
SDL_Log("Game resumed from Keyboard"); SDL_Log("Game resumed from Keyboard");
show_pause_status = false;
} else { } else {
DebugState.PauseGuestThreads(); DebugState.PauseGuestThreads();
SDL_Log("Game paused from Keyboard"); SDL_Log("Game paused from Keyboard");
show_pause_status = true;
} }
visibility_toggled = true; visibility_toggled = true;
} }
} }
if (show_pause_status) { if (DebugState.IsGuestThreadsPaused()) {
ImVec2 pos = ImVec2(10, 10); ImVec2 pos = ImVec2(10, 10);
ImU32 color = IM_COL32(255, 255, 255, 255); ImU32 color = IM_COL32(255, 255, 255, 255);
ImGui::GetForegroundDrawList()->AddText(pos, color, "Game Paused Press F9 to Resume"); ImGui::GetForegroundDrawList()->AddText(pos, color, "Game Paused Press F9 to Resume");
} }
@ -436,5 +428,56 @@ void L::Draw() {
PopFont(); 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(); 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

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <string>
#include "imgui/imgui_layer.h" #include "imgui/imgui_layer.h"
@ -19,7 +20,14 @@ public:
static void SetupSettings(); static void SetupSettings();
void Draw() override; void Draw() override;
bool show_pause_status = false; void TextCentered(const std::string& text);
}; };
} // namespace Core::Devtools } // namespace Core::Devtools
namespace Overlay {
void ToggleSimpleFps();
void ToggleQuitWindow();
} // namespace Overlay

BIN
src/images/hotkey.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -60,6 +60,10 @@ std::pair<int, int> leftjoystick_deadzone, rightjoystick_deadzone, lefttrigger_d
std::list<std::pair<InputEvent, bool>> pressed_keys; std::list<std::pair<InputEvent, bool>> pressed_keys;
std::list<InputID> toggled_keys; std::list<InputID> toggled_keys;
static std::vector<BindingConnection> connections; static std::vector<BindingConnection> connections;
static std::vector<std::string> fullscreenHotkeyInputsPad(3, "");
static std::vector<std::string> pauseHotkeyInputsPad(3, "");
static std::vector<std::string> simpleFpsHotkeyInputsPad(3, "");
static std::vector<std::string> quitHotkeyInputsPad(3, "");
auto output_array = std::array{ auto output_array = std::array{
// Important: these have to be the first, or else they will update in the wrong order // Important: these have to be the first, or else they will update in the wrong order
@ -731,4 +735,158 @@ void ActivateOutputsFromInputs() {
} }
} }
std::vector<std::string> 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<std::string>& 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<std::string> inputs) {
if (inputs[0] == "unmapped") {
return false;
}
auto controller = Common::Singleton<Input::GameController>::Instance();
auto engine = controller->GetEngine();
SDL_Gamepad* gamepad = engine->m_gamepad;
if (!gamepad) {
return false;
}
std::vector<bool> 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 } // namespace Input

View file

@ -4,9 +4,11 @@
#pragma once #pragma once
#include <array> #include <array>
#include <filesystem>
#include <map> #include <map>
#include <string> #include <string>
#include <unordered_set> #include <unordered_set>
#include <vector>
#include "SDL3/SDL_events.h" #include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.h" #include "SDL3/SDL_timer.h"
@ -448,10 +450,16 @@ public:
InputEvent ProcessBinding(); InputEvent ProcessBinding();
}; };
enum HotkeyPad { FullscreenPad, PausePad, SimpleFpsPad, QuitPad };
// Updates the list of pressed keys with the given input. // Updates the list of pressed keys with the given input.
// Returns whether the list was updated or not. // Returns whether the list was updated or not.
bool UpdatePressedKeys(InputEvent event); bool UpdatePressedKeys(InputEvent event);
void ActivateOutputsFromInputs(); void ActivateOutputsFromInputs();
void LoadHotkeyInputs();
bool HotkeyInputsPressed(std::vector<std::string> inputs);
std::vector<std::string> GetHotkeyInputs(Input::HotkeyPad hotkey);
void createHotkeyFile(std::filesystem::path hotkey_file);
} // namespace Input } // namespace Input

392
src/qt_gui/hotkeys.cpp Normal file
View file

@ -0,0 +1,392 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <QKeyEvent>
#include <QtConcurrent>
#include <SDL3/SDL.h>
#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<std::string> 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<QKeyEvent*>(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() {}

62
src/qt_gui/hotkeys.h Normal file
View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QDialog>
#include <QFuture>
#include <SDL3/SDL_gamepad.h>
#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<int, QString> pressedButtons;
QList<QPushButton*> ButtonsList;
QFuture<void> Polling;
Ui::hotkeys* ui;
protected:
void closeEvent(QCloseEvent* event) override {
Cleanup();
}
};

313
src/qt_gui/hotkeys.ui Normal file
View file

@ -0,0 +1,313 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later -->
<ui version="4.0">
<class>hotkeys</class>
<widget class="QDialog" name="hotkeys">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>849</width>
<height>496</height>
</rect>
</property>
<property name="windowTitle">
<string>Customize Hotkeys</string>
</property>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>750</x>
<y>200</y>
<width>81</width>
<height>81</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save</set>
</property>
</widget>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>30</x>
<y>10</y>
<width>681</width>
<height>231</height>
</rect>
</property>
<layout class="QVBoxLayout" name="controllerLayout">
<item>
<widget class="QLabel" name="controllerLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>19</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Controller Hotkeys</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutPad1">
<item>
<widget class="QGroupBox" name="fpsGroupbox">
<property name="title">
<string>Show FPS Counter</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="fpsButtonPad">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="stopGroupbox">
<property name="title">
<string>Stop Emulator</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QPushButton" name="quitButtonPad">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutPad2">
<item>
<widget class="QGroupBox" name="fullscreenGroupbox">
<property name="title">
<string>Toggle Fullscreen</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QPushButton" name="fullscreenButtonPad">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="pauseGroupbox">
<property name="title">
<string>Toggle Pause</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QPushButton" name="pauseButtonPad">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry">
<rect>
<x>30</x>
<y>250</y>
<width>681</width>
<height>191</height>
</rect>
</property>
<layout class="QVBoxLayout" name="keyboardLayout">
<item>
<widget class="QLabel" name="keyboardLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>19</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Keyboard Hotkeys</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="KBLabelsLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayoutKB1">
<item>
<widget class="QLabel" name="fpsLabelKB">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="frameShape">
<enum>QFrame::Shape::Box</enum>
</property>
<property name="text">
<string>Show Fps Counter: F10</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="stopLabelKB">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="frameShape">
<enum>QFrame::Shape::Box</enum>
</property>
<property name="text">
<string>Stop Emulator: n/a</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutKB2">
<item>
<widget class="QLabel" name="fullscreenLabelKB">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="frameShape">
<enum>QFrame::Shape::Box</enum>
</property>
<property name="text">
<string>Toggle Fullscreen: F11</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pauseLabelKB">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="frameShape">
<enum>QFrame::Shape::Box</enum>
</property>
<property name="text">
<string>Toggle Pause: F9</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QLabel" name="tipLabel">
<property name="geometry">
<rect>
<x>50</x>
<y>450</y>
<width>631</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Tip: Up to three inputs can be assigned for each function</string>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>hotkeys</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>hotkeys</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -20,6 +20,7 @@
#include "common/string_util.h" #include "common/string_util.h"
#include "control_settings.h" #include "control_settings.h"
#include "game_install_dialog.h" #include "game_install_dialog.h"
#include "hotkeys.h"
#include "kbm_gui.h" #include "kbm_gui.h"
#include "main_window.h" #include "main_window.h"
#include "settings_dialog.h" #include "settings_dialog.h"
@ -495,6 +496,11 @@ void MainWindow::CreateConnects() {
aboutDialog->exec(); aboutDialog->exec();
}); });
connect(ui->configureHotkeys, &QAction::triggered, this, [this]() {
auto hotkeyDialog = new hotkeys(isGameRunning, this);
hotkeyDialog->exec();
});
connect(ui->setIconSizeTinyAct, &QAction::triggered, this, [this]() { connect(ui->setIconSizeTinyAct, &QAction::triggered, this, [this]() {
if (isTableList) { if (isTableList) {
m_game_list_frame->icon_size = m_game_list_frame->icon_size =

View file

@ -32,6 +32,7 @@ public:
#endif #endif
QAction* aboutAct; QAction* aboutAct;
QAction* configureAct; QAction* configureAct;
QAction* configureHotkeys;
QAction* setThemeDark; QAction* setThemeDark;
QAction* setThemeLight; QAction* setThemeLight;
QAction* setThemeGreen; QAction* setThemeGreen;
@ -155,6 +156,9 @@ public:
configureAct = new QAction(MainWindow); configureAct = new QAction(MainWindow);
configureAct->setObjectName("configureAct"); configureAct->setObjectName("configureAct");
configureAct->setIcon(QIcon(":images/settings_icon.png")); 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 = new QAction(MainWindow);
setThemeDark->setObjectName("setThemeDark"); setThemeDark->setObjectName("setThemeDark");
setThemeDark->setCheckable(true); setThemeDark->setCheckable(true);
@ -330,6 +334,7 @@ public:
menuGame_List_Mode->addAction(setlistElfAct); menuGame_List_Mode->addAction(setlistElfAct);
menuSettings->addAction(configureAct); menuSettings->addAction(configureAct);
menuSettings->addAction(gameInstallPathAct); menuSettings->addAction(gameInstallPathAct);
menuSettings->addAction(configureHotkeys);
menuSettings->addAction(menuUtils->menuAction()); menuSettings->addAction(menuUtils->menuAction());
menuUtils->addAction(downloadCheatsPatchesAct); menuUtils->addAction(downloadCheatsPatchesAct);
menuUtils->addAction(dumpGameListAct); menuUtils->addAction(dumpGameListAct);
@ -355,6 +360,8 @@ public:
#endif #endif
aboutAct->setText(QCoreApplication::translate("MainWindow", "About shadPS4", nullptr)); aboutAct->setText(QCoreApplication::translate("MainWindow", "About shadPS4", nullptr));
configureAct->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr)); configureAct->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr));
configureHotkeys->setText(
QCoreApplication::translate("MainWindow", "Customize Hotkeys", nullptr));
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
menuRecent->setTitle(QCoreApplication::translate("MainWindow", "Recent Games", nullptr)); menuRecent->setTitle(QCoreApplication::translate("MainWindow", "Recent Games", nullptr));

View file

@ -11,6 +11,7 @@
#include "common/config.h" #include "common/config.h"
#include "common/elf_info.h" #include "common/elf_info.h"
#include "core/debug_state.h" #include "core/debug_state.h"
#include "core/devtools/layer.h"
#include "core/libraries/kernel/time.h" #include "core/libraries/kernel/time.h"
#include "core/libraries/pad/pad.h" #include "core/libraries/pad/pad.h"
#include "imgui/renderer/imgui_core.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::SetControllerOutputController(controller);
Input::ControllerOutput::LinkJoystickAxes(); Input::ControllerOutput::LinkJoystickAxes();
Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
Input::LoadHotkeyInputs();
} }
WindowSDL::~WindowSDL() = default; WindowSDL::~WindowSDL() = default;
@ -549,7 +551,6 @@ void WindowSDL::OnKeyboardMouseInput(const SDL_Event* event) {
} }
void WindowSDL::OnGamepadEvent(const SDL_Event* event) { void WindowSDL::OnGamepadEvent(const SDL_Event* event) {
bool input_down = event->type == SDL_EVENT_GAMEPAD_AXIS_MOTION || bool input_down = event->type == SDL_EVENT_GAMEPAD_AXIS_MOTION ||
event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN; event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN;
Input::InputEvent input_event = Input::InputBinding::GetInputEventFromSDLEvent(*event); 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 // add/remove it from the list
bool inputs_changed = Input::UpdatePressedKeys(input_event); bool inputs_changed = Input::UpdatePressedKeys(input_event);
// update bindings
if (inputs_changed) { 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(); 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 } // namespace Frontend

View file

@ -3,10 +3,12 @@
#pragma once #pragma once
#include <string>
#include "common/types.h" #include "common/types.h"
#include "core/libraries/pad/pad.h" #include "core/libraries/pad/pad.h"
#include "input/controller.h" #include "input/controller.h"
#include "string"
#define SDL_EVENT_TOGGLE_FULLSCREEN (SDL_EVENT_USER + 1) #define SDL_EVENT_TOGGLE_FULLSCREEN (SDL_EVENT_USER + 1)
#define SDL_EVENT_TOGGLE_PAUSE (SDL_EVENT_USER + 2) #define SDL_EVENT_TOGGLE_PAUSE (SDL_EVENT_USER + 2)
#define SDL_EVENT_CHANGE_CONTROLLER (SDL_EVENT_USER + 3) #define SDL_EVENT_CHANGE_CONTROLLER (SDL_EVENT_USER + 3)
@ -98,6 +100,7 @@ private:
void OnResize(); void OnResize();
void OnKeyboardMouseInput(const SDL_Event* event); void OnKeyboardMouseInput(const SDL_Event* event);
void OnGamepadEvent(const SDL_Event* event); void OnGamepadEvent(const SDL_Event* event);
void CheckHotkeys();
private: private:
s32 width; s32 width;
@ -107,6 +110,7 @@ private:
SDL_Window* window{}; SDL_Window* window{};
bool is_shown{}; bool is_shown{};
bool is_open{true}; bool is_open{true};
bool process_hotkeys{true};
}; };
} // namespace Frontend } // namespace Frontend

View file

@ -38,5 +38,6 @@
<file>images/refreshlist_icon.png</file> <file>images/refreshlist_icon.png</file>
<file>images/favorite_icon.png</file> <file>images/favorite_icon.png</file>
<file>images/trophy_icon.png</file> <file>images/trophy_icon.png</file>
<file>images/hotkey.png</file>
</qresource> </qresource>
</RCC> </RCC>