Added keyboard and mouse input remapping, mouse movement to joystick logic, GUI and more (#1356)

* added support for loading keyboard config from file

* final minor update before pull request

* fix messing up the merge

* fix waitEvent to correctly handle mouse inputs

* add license

* Applied coding style fixes

* clang-format fucked up the .ini file

* actually fix clang changing ini syntax
use relative path for the ini file

* remove big commented out code blocks,
and fixed platform-dependent code

* fix windows hating me

* added mouse config option

* added toggle for mouse movement input (f7)

* fix license and style

* add numpad support i accidentally left out

* added support for mouse wheel (to buttons only)

* if keyboard config doesn't exist, autogenerate it

* added keybinds for "walk mode"

* Mouse movement input is now off by default

* code cleanup and misc fixes

* delete config file since it is now autogenerated

* F6 = F7 + F9

* added better mouse handling with config options

* Added capslock support

* fix clang-format

* Added support for mod key toggle key

* F6 and F7 are removed, F9 captures and enables the mouse

* Encapsulated globals and new classes in a new namespace

* Added mouse side button support

* Added per-game config

* relocated input parser to the new namespace

* changed parser parameters to make it possible to use it from the gui

* added home, end, pgup and pgdown

* Resolved merge conflict and refactored code

* Updated default keybindings

* Changed input handling to be single-threaded

* General code cleanup

* Start working on new backend

* Mouse polling, CMakeLists, and basic framework

* Output update handling, and reworked file creating, reading and parsing

* Parsing works now

* Single key button inputs work now

* Axis outputs work now

* Wheel works now (for me), l2/r2 handling improvements, and misc bugfixes

* Downgraded prints to log_debug, and implemented input hierarchy

* Implemented key toggle

* Added mouse parameter parsing

* clang-format

* Fixed clang and added a const keyword for mac

* Fix input hierarchy

* Fixed joysick halfmodes, and possibly the last update on input hierarchy

* clang-format

* Rewrote the default config to reflect new changes

* clang

* Update code style

* Updated sorting to accomodate for that one specific edge case

* Fix default config and the latest bug with input hiearchy

* Fix typo

* Temporarily added my GUI

* Update cmakelists

* Possible fix for Gravity Rush

* Update Help text, default config, and clang

* Updated README with the new keybind info

* okay so maybe the gravity rush fix might have slightly broken the joystick halfmode and key toggle

* Fixed mistakenly overwriting the last opened config with the default one if the GUI is opened multiple times in a session

* Updated Help descriptions and fixed mouse movement default parameters

* Fix crash if the Help dialog was opened a second time
If it's closed with the top right close button instead of clicking the Help button again, a required flag wasn't reset, making the next click on Help try to close a nonexistent window and segfault

* Added closing the config also closing the Help window, and fixed more segfaults due to mismatched flags

* Initial controller support

* clang and debug print cleanup

* Initial axis-to-button logic

* Updated Help text

* Added 'Reset to Default' button in GUI

* Minor text and description updates + fixed an issue with Help text box rendering

* Fix button-to-touchpad logic and l2/r2 handling, as they are both axes and buttons
The touchpad's button state was correctly handled, so games that use that were fine, but the touchDown flag was always set to true, so games that use this flag had problems, like Gravity Rush

* Fix merge conflict

* Clang

* Added back back button to touchpad binding

* Added touchpad button handling

* Added end-of-line comments and fixed some crashes happening with the VS debugger

* Apply recent changes from kbm-only

* Deadzone + initial directional axis-to-button mapping

* Added that one missing space in the README. Are you all happy now?

* Fixups from making everything use SDL

* Revert directional joystick code and fix a memory leak

* Change config directory name again to conform to project standards

* Clang

* Revert the old deeadzone code and properly add the new one

* Clang
This commit is contained in:
kalaposfos13 2025-01-31 15:36:14 +01:00 committed by GitHub
parent f3810cebea
commit c4bfaa6031
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1996 additions and 286 deletions

View file

@ -854,6 +854,10 @@ set(IMGUI src/imgui/imgui_config.h
set(INPUT src/input/controller.cpp
src/input/controller.h
src/input/input_handler.cpp
src/input/input_handler.h
src/input/input_mouse.cpp
src/input/input_mouse.h
)
set(EMULATOR src/emulator.cpp
@ -903,6 +907,10 @@ set(QT_GUI src/qt_gui/about_dialog.cpp
src/qt_gui/trophy_viewer.h
src/qt_gui/elf_viewer.cpp
src/qt_gui/elf_viewer.h
src/qt_gui/kbm_config_dialog.cpp
src/qt_gui/kbm_config_dialog.h
src/qt_gui/kbm_help_dialog.cpp
src/qt_gui/kbm_help_dialog.h
src/qt_gui/main_window_themes.cpp
src/qt_gui/main_window_themes.h
src/qt_gui/settings_dialog.cpp

View file

@ -77,7 +77,7 @@ Check the build instructions for [**macOS**](https://github.com/shadps4-emu/shad
For more information on how to test, debug and report issues with the emulator or games, read the [**Debugging documentation**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/Debugging/Debugging.md).
# Keyboard mapping
# Keyboard and Mouse Mappings
> [!NOTE]
> Some keyboards may also require you to hold the Fn key to use the F\* keys. Mac users should use the Command key instead of Control, and need to use Command+F11 for full screen to avoid conflicting with system key bindings.
@ -92,32 +92,34 @@ F12 | Trigger RenderDoc Capture
> [!NOTE]
> Xbox and DualShock controllers work out of the box.
| Controller button | Keyboard equivalent |
|-------------|-------------|
LEFT AXIS UP | W |
LEFT AXIS DOWN | S |
LEFT AXIS LEFT | A |
LEFT AXIS RIGHT | D |
RIGHT AXIS UP | I |
RIGHT AXIS DOWN | K |
RIGHT AXIS LEFT | J |
RIGHT AXIS RIGHT | L |
TRIANGLE | Numpad 8 or C |
CIRCLE | Numpad 6 or B |
CROSS | Numpad 2 or N |
SQUARE | Numpad 4 or V |
PAD UP | UP |
PAD DOWN | DOWN |
PAD LEFT | LEFT |
PAD RIGHT | RIGHT |
OPTIONS | RETURN |
BACK BUTTON / TOUCH PAD | SPACE |
L1 | Q |
R1 | U |
L2 | E |
R2 | O |
L3 | X |
R3 | M |
The default controls are inspired by the *Elden Ring* PC controls. Inputs support up to three keys per binding, mouse buttons, mouse movement mapped to joystick input, and more.
| Action | Default Key(s) |
|-------------|-----------------------------|
| Triangle | F |
| Circle | Space |
| Cross | E |
| Square | R |
| Pad Up | W, LAlt / Mouse Wheel Up |
| Pad Down | S, LAlt / Mouse Wheel Down |
| Pad Left | A, LAlt / Mouse Wheel Left |
| Pad Right | D, LAlt / Mouse Wheel Right |
| L1 | Right Button, LShift |
| R1 | Left Button |
| L2 | Right Button |
| R2 | Left Button, LShift |
| L3 | X |
| R3 | Q / Middle Button |
| Options | Escape |
| Touchpad | G |
| Joystick | Default Input |
|--------------------|----------------|
| Left Joystick | WASD |
| Right Joystick | Mouse movement |
Keyboard and mouse inputs can be customized in the settings menu by clicking the Controller button, and further details and help on controls are also found there. Custom bindings are saved per-game.
# Main team

View file

@ -46,8 +46,6 @@ static std::string logType = "async";
static std::string userName = "shadPS4";
static std::string updateChannel;
static std::string chooseHomeTab;
static u16 deadZoneLeft = 2.0;
static u16 deadZoneRight = 2.0;
static std::string backButtonBehavior = "left";
static bool useSpecialPad = false;
static int specialPadClass = 1;
@ -151,14 +149,6 @@ bool getEnableDiscordRPC() {
return enableDiscordRPC;
}
u16 leftDeadZone() {
return deadZoneLeft;
}
u16 rightDeadZone() {
return deadZoneRight;
}
s16 getCursorState() {
return cursorState;
}
@ -661,8 +651,6 @@ void load(const std::filesystem::path& path) {
if (data.contains("Input")) {
const toml::value& input = data.at("Input");
deadZoneLeft = toml::find_or<float>(input, "deadZoneLeft", 2.0);
deadZoneRight = toml::find_or<float>(input, "deadZoneRight", 2.0);
cursorState = toml::find_or<int>(input, "cursorState", HideCursorState::Idle);
cursorHideTimeout = toml::find_or<int>(input, "cursorHideTimeout", 5);
backButtonBehavior = toml::find_or<std::string>(input, "backButtonBehavior", "left");
@ -785,8 +773,6 @@ void save(const std::filesystem::path& path) {
data["General"]["separateUpdateEnabled"] = separateupdatefolder;
data["General"]["compatibilityEnabled"] = compatibilityData;
data["General"]["checkCompatibilityOnStartup"] = checkCompatibilityOnStartup;
data["Input"]["deadZoneLeft"] = deadZoneLeft;
data["Input"]["deadZoneRight"] = deadZoneRight;
data["Input"]["cursorState"] = cursorState;
data["Input"]["cursorHideTimeout"] = cursorHideTimeout;
data["Input"]["backButtonBehavior"] = backButtonBehavior;
@ -919,4 +905,109 @@ void setDefaultValues() {
checkCompatibilityOnStartup = false;
}
} // namespace Config
constexpr std::string_view GetDefaultKeyboardConfig() {
return R"(#Feeling lost? Check out the Help section!
#Keyboard bindings
triangle = f
circle = space
cross = e
square = r
pad_up = w, lalt
pad_up = mousewheelup
pad_down = s, lalt
pad_down = mousewheeldown
pad_left = a, lalt
pad_left = mousewheelleft
pad_right = d, lalt
pad_right = mousewheelright
l1 = rightbutton, lshift
r1 = leftbutton
l2 = rightbutton
r2 = leftbutton, lshift
l3 = x
r3 = q
r3 = middlebutton
options = escape
touchpad = g
key_toggle = i, lalt
mouse_to_joystick = right
mouse_movement_params = 0.5, 1, 0.125
leftjoystick_halfmode = lctrl
axis_left_x_minus = a
axis_left_x_plus = d
axis_left_y_minus = w
axis_left_y_plus = s
#Controller bindings
triangle = triangle
cross = cross
square = square
circle = circle
l1 = l1
l2 = l2
l3 = l3
r1 = r1
r2 = r2
r3 = r3
pad_up = pad_up
pad_down = pad_down
pad_left = pad_left
pad_right = pad_right
options = options
touchpad = back
axis_left_x = axis_left_x
axis_left_y = axis_left_y
axis_right_x = axis_right_x
axis_right_y = axis_right_y
)";
}
std::filesystem::path GetFoolproofKbmConfigFile(const std::string& game_id) {
// Read configuration file of the game, and if it doesn't exist, generate it from default
// If that doesn't exist either, generate that from getDefaultConfig() and try again
// If even the folder is missing, we start with that.
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "input_config";
const auto config_file = config_dir / (game_id + ".ini");
const auto default_config_file = config_dir / "default.ini";
// Ensure the config directory exists
if (!std::filesystem::exists(config_dir)) {
std::filesystem::create_directories(config_dir);
}
// Check if the default config exists
if (!std::filesystem::exists(default_config_file)) {
// If the default config is also missing, create it from getDefaultConfig()
const auto default_config = GetDefaultKeyboardConfig();
std::ofstream default_config_stream(default_config_file);
if (default_config_stream) {
default_config_stream << default_config;
}
}
// if empty, we only need to execute the function up until this point
if (game_id.empty()) {
return default_config_file;
}
// If game-specific config doesn't exist, create it from the default config
if (!std::filesystem::exists(config_file)) {
std::filesystem::copy(default_config_file, config_file);
}
return config_file;
}
} // namespace Config

View file

@ -37,8 +37,6 @@ std::string getUserName();
std::string getUpdateChannel();
std::string getChooseHomeTab();
u16 leftDeadZone();
u16 rightDeadZone();
s16 getCursorState();
int getCursorHideTimeout();
std::string getBackButtonBehavior();
@ -152,6 +150,9 @@ std::string getEmulatorLanguage();
void setDefaultValues();
// todo: name and function location pending
std::filesystem::path GetFoolproofKbmConfigFile(const std::string& game_id = "");
// settings
u32 GetLanguage();
}; // namespace Config

View file

@ -95,8 +95,8 @@ int PS4_SYSV_ABI scePadGetControllerInformation(s32 handle, OrbisPadControllerIn
pInfo->touchPadInfo.pixelDensity = 1;
pInfo->touchPadInfo.resolution.x = 1920;
pInfo->touchPadInfo.resolution.y = 950;
pInfo->stickInfo.deadZoneLeft = Config::leftDeadZone();
pInfo->stickInfo.deadZoneRight = Config::rightDeadZone();
pInfo->stickInfo.deadZoneLeft = 1;
pInfo->stickInfo.deadZoneRight = 1;
pInfo->connectionType = ORBIS_PAD_PORT_TYPE_STANDARD;
pInfo->connectedCount = 1;
pInfo->connected = false;
@ -106,8 +106,8 @@ int PS4_SYSV_ABI scePadGetControllerInformation(s32 handle, OrbisPadControllerIn
pInfo->touchPadInfo.pixelDensity = 1;
pInfo->touchPadInfo.resolution.x = 1920;
pInfo->touchPadInfo.resolution.y = 950;
pInfo->stickInfo.deadZoneLeft = Config::leftDeadZone();
pInfo->stickInfo.deadZoneRight = Config::rightDeadZone();
pInfo->stickInfo.deadZoneLeft = 1;
pInfo->stickInfo.deadZoneRight = 1;
pInfo->connectionType = ORBIS_PAD_PORT_TYPE_STANDARD;
pInfo->connectedCount = 1;
pInfo->connected = true;

View file

@ -148,7 +148,7 @@ bool ProcessEvent(SDL_Event* event) {
case SDL_EVENT_MOUSE_BUTTON_DOWN: {
const auto& io = GetIO();
return io.WantCaptureMouse && io.Ctx->NavWindow != nullptr &&
io.Ctx->NavWindow->ID != dock_id;
(io.Ctx->NavWindow->Flags & ImGuiWindowFlags_NoNav) == 0;
}
case SDL_EVENT_TEXT_INPUT:
case SDL_EVENT_KEY_DOWN: {

676
src/input/input_handler.cpp Normal file
View file

@ -0,0 +1,676 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "input_handler.h"
#include <fstream>
#include <iostream>
#include <list>
#include <map>
#include <ranges>
#include <sstream>
#include <string>
#include <string_view>
#include <typeinfo>
#include <unordered_map>
#include <vector>
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.h"
#include "common/config.h"
#include "common/elf_info.h"
#include "common/io_file.h"
#include "common/path_util.h"
#include "common/version.h"
#include "input/controller.h"
#include "input/input_mouse.h"
namespace Input {
/*
Project structure:
n to m connection between inputs and outputs
Keyup and keydown events update a dynamic list* of u32 'flags' (what is currently in the list is
'pressed') On every event, after flag updates, we check for every input binding -> controller output
pair if all their flags are 'on' If not, disable; if so, enable them. For axes, we gather their data
into a struct cumulatively from all inputs, then after we checked all of those, we update them all
at once. Wheel inputs generate a timer that doesn't turn off their outputs automatically, but push a
userevent to do so.
What structs are needed?
InputBinding(key1, key2, key3)
ControllerOutput(button, axis) - we only need a const array of these, and one of the attr-s is
always 0 BindingConnection(inputBinding (member), controllerOutput (ref to the array element))
Things to always test before pushing like a dumbass:
Button outputs
Axis outputs
Input hierarchy
Multi key inputs
Mouse to joystick
Key toggle
Joystick halfmode
Don't be an idiot and test only the changed part expecting everything else to not be broken
*/
bool leftjoystick_halfmode = false, rightjoystick_halfmode = false;
int leftjoystick_deadzone, rightjoystick_deadzone, lefttrigger_deadzone, righttrigger_deadzone;
std::list<std::pair<InputEvent, bool>> pressed_keys;
std::list<InputID> toggled_keys;
static std::vector<BindingConnection> connections;
auto output_array = std::array{
// Important: these have to be the first, or else they will update in the wrong order
ControllerOutput(LEFTJOYSTICK_HALFMODE),
ControllerOutput(RIGHTJOYSTICK_HALFMODE),
ControllerOutput(KEY_TOGGLE),
// Button mappings
ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle
ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle
ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross
ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square
ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1
ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3
ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1
ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3
ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options
ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD), // TouchPad
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right
// Axis mappings
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID),
};
void ControllerOutput::LinkJoystickAxes() {
// for (int i = 17; i < 23; i += 2) {
// delete output_array[i].new_param;
// output_array[i].new_param = output_array[i + 1].new_param;
// }
}
static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) {
using OPBDO = OrbisPadButtonDataOffset;
switch (button) {
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
return OPBDO::Down;
case SDL_GAMEPAD_BUTTON_DPAD_UP:
return OPBDO::Up;
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
return OPBDO::Left;
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
return OPBDO::Right;
case SDL_GAMEPAD_BUTTON_SOUTH:
return OPBDO::Cross;
case SDL_GAMEPAD_BUTTON_NORTH:
return OPBDO::Triangle;
case SDL_GAMEPAD_BUTTON_WEST:
return OPBDO::Square;
case SDL_GAMEPAD_BUTTON_EAST:
return OPBDO::Circle;
case SDL_GAMEPAD_BUTTON_START:
return OPBDO::Options;
case SDL_GAMEPAD_BUTTON_TOUCHPAD:
return OPBDO::TouchPad;
case SDL_GAMEPAD_BUTTON_BACK:
return OPBDO::TouchPad;
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
return OPBDO::L1;
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
return OPBDO::R1;
case SDL_GAMEPAD_BUTTON_LEFT_STICK:
return OPBDO::L3;
case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
return OPBDO::R3;
default:
return OPBDO::None;
}
}
Axis GetAxisFromSDLAxis(u8 sdl_axis) {
switch (sdl_axis) {
case SDL_GAMEPAD_AXIS_LEFTX:
return Axis::LeftX;
case SDL_GAMEPAD_AXIS_LEFTY:
return Axis::LeftY;
case SDL_GAMEPAD_AXIS_RIGHTX:
return Axis::RightX;
case SDL_GAMEPAD_AXIS_RIGHTY:
return Axis::RightY;
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
return Axis::TriggerLeft;
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
return Axis::TriggerRight;
default:
return Axis::AxisMax;
}
}
// syntax: 'name, name,name' or 'name,name' or 'name'
InputBinding GetBindingFromString(std::string& line) {
std::array<InputID, 3> keys = {InputID(), InputID(), InputID()};
// Check and process tokens
for (const auto token : std::views::split(line, ',')) { // Split by comma
const std::string t(token.begin(), token.end());
InputID input;
if (string_to_keyboard_key_map.find(t) != string_to_keyboard_key_map.end()) {
input = InputID(InputType::KeyboardMouse, string_to_keyboard_key_map.at(t));
} else if (string_to_axis_map.find(t) != string_to_axis_map.end()) {
input = InputID(InputType::Axis, (u32)string_to_axis_map.at(t).axis);
} else if (string_to_cbutton_map.find(t) != string_to_cbutton_map.end()) {
input = InputID(InputType::Controller, string_to_cbutton_map.at(t));
} else {
// Invalid token found; return default binding
LOG_DEBUG(Input, "Invalid token found: {}", t);
return InputBinding();
}
// Assign to the first available slot
for (auto& key : keys) {
if (!key.IsValid()) {
key = input;
break;
}
}
}
LOG_DEBUG(Input, "Parsed line: {}", InputBinding(keys[0], keys[1], keys[2]).ToString());
return InputBinding(keys[0], keys[1], keys[2]);
}
void ParseInputConfig(const std::string game_id = "") {
const auto config_file = Config::GetFoolproofKbmConfigFile(game_id);
if (game_id == "") {
return;
}
// we reset these here so in case the user fucks up or doesn't include some of these,
// we can fall back to default
connections.clear();
float mouse_deadzone_offset = 0.5;
float mouse_speed = 1;
float mouse_speed_offset = 0.125;
leftjoystick_deadzone = 1;
rightjoystick_deadzone = 1;
lefttrigger_deadzone = 1;
righttrigger_deadzone = 1;
int lineCount = 0;
std::ifstream file(config_file);
std::string line = "";
while (std::getline(file, line)) {
lineCount++;
// Strip the ; and whitespace
line.erase(std::remove_if(line.begin(), line.end(),
[](unsigned char c) { return std::isspace(c); }),
line.end());
if (line.empty()) {
continue;
}
// Truncate lines starting at #
std::size_t comment_pos = line.find('#');
if (comment_pos != std::string::npos) {
line = line.substr(0, comment_pos);
}
// Remove trailing semicolon
if (!line.empty() && line[line.length() - 1] == ';') {
line = line.substr(0, line.length() - 1);
}
if (line.empty()) {
continue;
}
// Split the line by '='
std::size_t equal_pos = line.find('=');
if (equal_pos == std::string::npos) {
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
}
std::string output_string = line.substr(0, equal_pos);
std::string input_string = line.substr(equal_pos + 1);
std::size_t comma_pos = input_string.find(',');
if (output_string == "mouse_to_joystick") {
if (input_string == "left") {
SetMouseToJoystick(1);
} else if (input_string == "right") {
SetMouseToJoystick(2);
} else {
LOG_WARNING(Input, "Invalid argument for mouse-to-joystick binding");
SetMouseToJoystick(0);
}
continue;
} else if (output_string == "key_toggle") {
if (comma_pos != std::string::npos) {
// handle key-to-key toggling (separate list?)
InputBinding toggle_keys = GetBindingFromString(input_string);
if (toggle_keys.KeyCount() != 2) {
LOG_WARNING(Input,
"Syntax error: Please provide exactly 2 keys: "
"first is the toggler, the second is the key to toggle: {}",
line);
continue;
}
ControllerOutput* toggle_out =
&*std::ranges::find(output_array, ControllerOutput(KEY_TOGGLE));
BindingConnection toggle_connection = BindingConnection(
InputBinding(toggle_keys.keys[0]), toggle_out, 0, toggle_keys.keys[1]);
connections.insert(connections.end(), toggle_connection);
continue;
}
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
} else if (output_string == "mouse_movement_params") {
std::stringstream ss(input_string);
char comma; // To hold the comma separators between the floats
ss >> mouse_deadzone_offset >> comma >> mouse_speed >> comma >> mouse_speed_offset;
// Check for invalid input (in case there's an unexpected format)
if (ss.fail()) {
LOG_WARNING(Input, "Failed to parse mouse movement parameters from line: {}", line);
continue;
}
SetMouseParams(mouse_deadzone_offset, mouse_speed, mouse_speed_offset);
continue;
} else if (output_string == "analog_deadzone") {
std::stringstream ss(input_string);
std::string device;
int deadzone;
std::getline(ss, device, ',');
ss >> deadzone;
if (ss.fail()) {
LOG_WARNING(Input, "Failed to parse deadzone config from line: {}", line);
continue;
} else {
LOG_DEBUG(Input, "Parsed deadzone: {} {}", device, deadzone);
}
if (device == "leftjoystick") {
leftjoystick_deadzone = deadzone;
} else if (device == "rightjoystick") {
rightjoystick_deadzone = deadzone;
} else if (device == "l2") {
lefttrigger_deadzone = deadzone;
} else if (device == "r2") {
righttrigger_deadzone = deadzone;
} else {
LOG_WARNING(Input, "Invalid axis name at line: {}, data: \"{}\", skipping line.",
lineCount, line);
}
continue;
}
// normal cases
InputBinding binding = GetBindingFromString(input_string);
BindingConnection connection(InputID(), nullptr);
auto button_it = string_to_cbutton_map.find(output_string);
auto axis_it = string_to_axis_map.find(output_string);
if (binding.IsEmpty()) {
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
}
if (button_it != string_to_cbutton_map.end()) {
connection = BindingConnection(
binding, &*std::ranges::find(output_array, ControllerOutput(button_it->second)));
connections.insert(connections.end(), connection);
} else if (axis_it != string_to_axis_map.end()) {
int value_to_set = binding.keys[2].type == InputType::Axis ? 0 : axis_it->second.value;
connection = BindingConnection(
binding,
&*std::ranges::find(output_array, ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID,
axis_it->second.axis,
axis_it->second.value >= 0)),
value_to_set);
connections.insert(connections.end(), connection);
} else {
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
}
LOG_DEBUG(Input, "Succesfully parsed line {}", lineCount);
}
file.close();
std::sort(connections.begin(), connections.end());
for (auto& c : connections) {
LOG_DEBUG(Input, "Binding: {} : {}", c.output->ToString(), c.binding.ToString());
}
LOG_DEBUG(Input, "Done parsing the input config!");
}
u32 GetMouseWheelEvent(const SDL_Event& event) {
if (event.type != SDL_EVENT_MOUSE_WHEEL && event.type != SDL_EVENT_MOUSE_WHEEL_OFF) {
LOG_WARNING(Input, "Something went wrong with wheel input parsing!");
return (u32)-1;
}
if (event.wheel.y > 0) {
return SDL_MOUSE_WHEEL_UP;
} else if (event.wheel.y < 0) {
return SDL_MOUSE_WHEEL_DOWN;
} else if (event.wheel.x > 0) {
return SDL_MOUSE_WHEEL_RIGHT;
} else if (event.wheel.x < 0) {
return SDL_MOUSE_WHEEL_LEFT;
}
return (u32)-1;
}
InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) {
switch (e.type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
return InputEvent(InputType::KeyboardMouse, e.key.key, e.key.down, 0);
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
return InputEvent(InputType::KeyboardMouse, (u32)e.button.button, e.button.down, 0);
case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_MOUSE_WHEEL_OFF:
return InputEvent(InputType::KeyboardMouse, GetMouseWheelEvent(e),
e.type == SDL_EVENT_MOUSE_WHEEL, 0);
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
return InputEvent(InputType::Controller, (u32)e.gbutton.button, e.gbutton.down, 0);
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
return InputEvent(InputType::Axis, (u32)e.gaxis.axis, true, e.gaxis.value / 256);
default:
return InputEvent();
}
}
GameController* ControllerOutput::controller = nullptr;
void ControllerOutput::SetControllerOutputController(GameController* c) {
ControllerOutput::controller = c;
}
void ToggleKeyInList(InputID input) {
if (input.type == InputType::Axis) {
LOG_ERROR(Input, "Toggling analog inputs is not supported!");
return;
}
auto it = std::find(toggled_keys.begin(), toggled_keys.end(), input);
if (it == toggled_keys.end()) {
toggled_keys.insert(toggled_keys.end(), input);
LOG_DEBUG(Input, "Added {} to toggled keys", input.ToString());
} else {
toggled_keys.erase(it);
LOG_DEBUG(Input, "Removed {} from toggled keys", input.ToString());
}
}
void ControllerOutput::ResetUpdate() {
state_changed = false;
new_button_state = false;
*new_param = 0; // bruh
}
void ControllerOutput::AddUpdate(InputEvent event) {
state_changed = true;
if (button == KEY_TOGGLE) {
if (event.active) {
ToggleKeyInList(event.input);
}
} else if (button != SDL_GAMEPAD_BUTTON_INVALID) {
if (event.input.type == InputType::Axis) {
bool temp = event.axis_value * (positive_axis ? 1 : -1) > 0x40;
new_button_state |= event.active && event.axis_value * (positive_axis ? 1 : -1) > 0x40;
if (temp) {
LOG_DEBUG(Input, "Toggled a button from an axis");
}
} else {
new_button_state |= event.active;
}
} else if (axis != SDL_GAMEPAD_AXIS_INVALID) {
switch (axis) {
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
// if it's a button input, then we know the value to set, so the param is 0.
// if it's an analog input, then the param isn't 0
*new_param = (event.active ? event.axis_value : 0) + *new_param;
break;
default:
*new_param = (event.active ? event.axis_value : 0) + *new_param;
break;
}
}
}
void ControllerOutput::FinalizeUpdate() {
if (!state_changed) {
// return;
}
old_button_state = new_button_state;
old_param = *new_param;
float touchpad_x = 0;
if (button != SDL_GAMEPAD_BUTTON_INVALID) {
switch (button) {
case SDL_GAMEPAD_BUTTON_TOUCHPAD:
touchpad_x = Config::getBackButtonBehavior() == "left" ? 0.25f
: Config::getBackButtonBehavior() == "right" ? 0.75f
: 0.5f;
controller->SetTouchpadState(0, new_button_state, touchpad_x, 0.5f);
controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state);
break;
case LEFTJOYSTICK_HALFMODE:
leftjoystick_halfmode = new_button_state;
break;
case RIGHTJOYSTICK_HALFMODE:
rightjoystick_halfmode = new_button_state;
break;
// KEY_TOGGLE isn't handled here anymore, as this function doesn't have the necessary data
// to do it, and it would be inconvenient to force it here, when AddUpdate does the job just
// fine, and a toggle doesn't have to checked against every input that's bound to it, it's
// enough that one is pressed
default: // is a normal key (hopefully)
controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state);
break;
}
} else if (axis != SDL_GAMEPAD_AXIS_INVALID && positive_axis) {
// avoid double-updating axes, but don't skip directional button bindings
float multiplier = 1.0;
int deadzone = 0;
auto ApplyDeadzone = [](s16* value, int deadzone) {
if (std::abs(*value) <= deadzone) {
*value = 0;
}
};
Axis c_axis = GetAxisFromSDLAxis(axis);
switch (c_axis) {
case Axis::LeftX:
case Axis::LeftY:
ApplyDeadzone(new_param, leftjoystick_deadzone);
multiplier = leftjoystick_halfmode ? 0.5 : 1.0;
break;
case Axis::RightX:
case Axis::RightY:
ApplyDeadzone(new_param, rightjoystick_deadzone);
multiplier = rightjoystick_halfmode ? 0.5 : 1.0;
break;
case Axis::TriggerLeft:
ApplyDeadzone(new_param, lefttrigger_deadzone);
controller->Axis(0, c_axis, GetAxis(0x0, 0x80, *new_param));
controller->CheckButton(0, OrbisPadButtonDataOffset::L2, *new_param > 0x20);
return;
case Axis::TriggerRight:
ApplyDeadzone(new_param, righttrigger_deadzone);
controller->Axis(0, c_axis, GetAxis(0x0, 0x80, *new_param));
controller->CheckButton(0, OrbisPadButtonDataOffset::R2, *new_param > 0x20);
return;
default:
break;
}
controller->Axis(0, c_axis, GetAxis(-0x80, 0x80, *new_param * multiplier));
}
}
// Updates the list of pressed keys with the given input.
// Returns whether the list was updated or not.
bool UpdatePressedKeys(InputEvent event) {
// Skip invalid inputs
InputID input = event.input;
if (input.sdl_id == (u32)-1) {
return false;
}
if (input.type == InputType::Axis) {
// analog input, it gets added when it first sends an event,
// and from there, it only changes the parameter
auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input,
[](const std::pair<InputEvent, bool>& e, InputID i) {
return std::tie(e.first.input.type, e.first.input.sdl_id) <
std::tie(i.type, i.sdl_id);
});
if (it == pressed_keys.end() || it->first.input != input) {
pressed_keys.insert(it, {event, false});
LOG_DEBUG(Input, "Added axis {} to the input list", event.input.sdl_id);
} else {
it->first.axis_value = event.axis_value;
}
return true;
} else if (event.active) {
// Find the correct position for insertion to maintain order
auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input,
[](const std::pair<InputEvent, bool>& e, InputID i) {
return std::tie(e.first.input.type, e.first.input.sdl_id) <
std::tie(i.type, i.sdl_id);
});
// Insert only if 'value' is not already in the list
if (it == pressed_keys.end() || it->first.input != input) {
pressed_keys.insert(it, {event, false});
return true;
}
} else {
// Remove 'value' from the list if it's not pressed
auto it = std::find_if(
pressed_keys.begin(), pressed_keys.end(),
[input](const std::pair<InputEvent, bool>& e) { return e.first.input == input; });
if (it != pressed_keys.end()) {
pressed_keys.erase(it);
return true;
}
}
LOG_DEBUG(Input, "No change was made!");
return false;
}
// Check if the binding's all keys are currently active.
// It also extracts the analog inputs' parameters, and updates the input hierarchy flags.
InputEvent BindingConnection::ProcessBinding() {
// the last key is always set (if the connection isn't empty),
// and the analog inputs are always the last one due to how they are sorted,
// so this signifies whether or not the input is analog
InputEvent event = InputEvent(binding.keys[0]);
if (pressed_keys.empty()) {
return event;
}
if (event.input.type != InputType::Axis) {
// for button inputs
event.axis_value = axis_param;
}
// it's a bit scuffed, but if the output is a toggle, then we put the key here
if (output->button == KEY_TOGGLE) {
event.input = toggle;
}
// Extract keys from InputBinding and ignore unused or toggled keys
std::list<InputID> input_keys = {binding.keys[0], binding.keys[1], binding.keys[2]};
input_keys.remove(InputID());
for (auto key = input_keys.begin(); key != input_keys.end();) {
if (std::find(toggled_keys.begin(), toggled_keys.end(), *key) != toggled_keys.end()) {
key = input_keys.erase(key); // Use the returned iterator
} else {
++key; // Increment only if no erase happened
}
}
if (input_keys.empty()) {
LOG_DEBUG(Input, "No actual inputs to check, returning true");
event.active = true;
return event;
}
// Iterator for pressed_keys, starting from the beginning
auto pressed_it = pressed_keys.begin();
// Store pointers to flags in pressed_keys that need to be set if all keys are active
std::list<bool*> flags_to_set;
// Check if all keys in input_keys are active
for (InputID key : input_keys) {
bool key_found = false;
while (pressed_it != pressed_keys.end()) {
if (pressed_it->first.input == key && (pressed_it->second == false)) {
key_found = true;
if (output->positive_axis) {
flags_to_set.push_back(&pressed_it->second);
}
if (pressed_it->first.input.type == InputType::Axis) {
event.axis_value = pressed_it->first.axis_value;
}
++pressed_it;
break;
}
++pressed_it;
}
if (!key_found) {
return event;
}
}
for (bool* flag : flags_to_set) {
*flag = true;
}
if (binding.keys[0].type != InputType::Axis) { // the axes spam inputs, making this unreadable
LOG_DEBUG(Input, "Input found: {}", binding.ToString());
}
event.active = true;
return event; // All keys are active
}
void ActivateOutputsFromInputs() {
// Reset values and flags
for (auto& it : pressed_keys) {
it.second = false;
}
for (auto& it : output_array) {
it.ResetUpdate();
}
// Iterate over all inputs, and update their respecive outputs accordingly
for (auto& it : connections) {
it.output->AddUpdate(it.ProcessBinding());
}
// Update all outputs
for (auto& it : output_array) {
it.FinalizeUpdate();
}
}
} // namespace Input

407
src/input/input_handler.h Normal file
View file

@ -0,0 +1,407 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <map>
#include <string>
#include <unordered_set>
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.h"
#include "common/logging/log.h"
#include "common/types.h"
#include "core/libraries/pad/pad.h"
#include "fmt/format.h"
#include "input/controller.h"
// +1 and +2 is taken
#define SDL_MOUSE_WHEEL_UP SDL_EVENT_MOUSE_WHEEL + 3
#define SDL_MOUSE_WHEEL_DOWN SDL_EVENT_MOUSE_WHEEL + 4
#define SDL_MOUSE_WHEEL_LEFT SDL_EVENT_MOUSE_WHEEL + 5
#define SDL_MOUSE_WHEEL_RIGHT SDL_EVENT_MOUSE_WHEEL + 7
// idk who already used what where so I just chose a big number
#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 10
#define LEFTJOYSTICK_HALFMODE 0x00010000
#define RIGHTJOYSTICK_HALFMODE 0x00020000
#define BACK_BUTTON 0x00040000
#define KEY_TOGGLE 0x00200000
namespace Input {
using Input::Axis;
using Libraries::Pad::OrbisPadButtonDataOffset;
struct AxisMapping {
u32 axis;
s16 value;
AxisMapping(SDL_GamepadAxis a, s16 v) : axis(a), value(v) {}
};
enum class InputType { Axis, KeyboardMouse, Controller, Count };
const std::array<std::string, 4> input_type_names = {"Axis", "KBM", "Controller", "Unknown"};
class InputID {
public:
InputType type;
u32 sdl_id;
InputID(InputType d = InputType::Count, u32 i = (u32)-1) : type(d), sdl_id(i) {}
bool operator==(const InputID& o) const {
return type == o.type && sdl_id == o.sdl_id;
}
bool operator!=(const InputID& o) const {
return type != o.type || sdl_id != o.sdl_id;
}
bool operator<=(const InputID& o) const {
return type <= o.type && sdl_id <= o.sdl_id;
}
bool IsValid() const {
return *this != InputID();
}
std::string ToString() {
return fmt::format("({}: {:x})", input_type_names[(u8)type], sdl_id);
}
};
class InputEvent {
public:
InputID input;
bool active;
s8 axis_value;
InputEvent(InputID i = InputID(), bool a = false, s8 v = 0)
: input(i), active(a), axis_value(v) {}
InputEvent(InputType d, u32 i, bool a = false, s8 v = 0)
: input(d, i), active(a), axis_value(v) {}
};
// i strongly suggest you collapse these maps
const std::map<std::string, u32> string_to_cbutton_map = {
{"triangle", SDL_GAMEPAD_BUTTON_NORTH},
{"circle", SDL_GAMEPAD_BUTTON_EAST},
{"cross", SDL_GAMEPAD_BUTTON_SOUTH},
{"square", SDL_GAMEPAD_BUTTON_WEST},
{"l1", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"r1", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"l3", SDL_GAMEPAD_BUTTON_LEFT_STICK},
{"r3", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"pad_up", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"pad_down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"pad_left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"pad_right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"options", SDL_GAMEPAD_BUTTON_START},
// these are outputs only (touchpad can only be bound to itself)
{"touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
{"leftjoystick_halfmode", LEFTJOYSTICK_HALFMODE},
{"rightjoystick_halfmode", RIGHTJOYSTICK_HALFMODE},
// this is only for input
{"back", SDL_GAMEPAD_BUTTON_BACK},
};
const std::map<std::string, AxisMapping> string_to_axis_map = {
{"axis_left_x_plus", {SDL_GAMEPAD_AXIS_LEFTX, 127}},
{"axis_left_x_minus", {SDL_GAMEPAD_AXIS_LEFTX, -127}},
{"axis_left_y_plus", {SDL_GAMEPAD_AXIS_LEFTY, 127}},
{"axis_left_y_minus", {SDL_GAMEPAD_AXIS_LEFTY, -127}},
{"axis_right_x_plus", {SDL_GAMEPAD_AXIS_RIGHTX, 127}},
{"axis_right_x_minus", {SDL_GAMEPAD_AXIS_RIGHTX, -127}},
{"axis_right_y_plus", {SDL_GAMEPAD_AXIS_RIGHTY, 127}},
{"axis_right_y_minus", {SDL_GAMEPAD_AXIS_RIGHTY, -127}},
{"l2", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 127}},
{"r2", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 127}},
// should only use these to bind analog inputs to analog outputs
{"axis_left_x", {SDL_GAMEPAD_AXIS_LEFTX, 127}},
{"axis_left_y", {SDL_GAMEPAD_AXIS_LEFTY, 127}},
{"axis_right_x", {SDL_GAMEPAD_AXIS_RIGHTX, 127}},
{"axis_right_y", {SDL_GAMEPAD_AXIS_RIGHTY, 127}},
};
const std::map<std::string, u32> string_to_keyboard_key_map = {
{"a", SDLK_A},
{"b", SDLK_B},
{"c", SDLK_C},
{"d", SDLK_D},
{"e", SDLK_E},
{"f", SDLK_F},
{"g", SDLK_G},
{"h", SDLK_H},
{"i", SDLK_I},
{"j", SDLK_J},
{"k", SDLK_K},
{"l", SDLK_L},
{"m", SDLK_M},
{"n", SDLK_N},
{"o", SDLK_O},
{"p", SDLK_P},
{"q", SDLK_Q},
{"r", SDLK_R},
{"s", SDLK_S},
{"t", SDLK_T},
{"u", SDLK_U},
{"v", SDLK_V},
{"w", SDLK_W},
{"x", SDLK_X},
{"y", SDLK_Y},
{"z", SDLK_Z},
{"0", SDLK_0},
{"1", SDLK_1},
{"2", SDLK_2},
{"3", SDLK_3},
{"4", SDLK_4},
{"5", SDLK_5},
{"6", SDLK_6},
{"7", SDLK_7},
{"8", SDLK_8},
{"9", SDLK_9},
{"kp0", SDLK_KP_0},
{"kp1", SDLK_KP_1},
{"kp2", SDLK_KP_2},
{"kp3", SDLK_KP_3},
{"kp4", SDLK_KP_4},
{"kp5", SDLK_KP_5},
{"kp6", SDLK_KP_6},
{"kp7", SDLK_KP_7},
{"kp8", SDLK_KP_8},
{"kp9", SDLK_KP_9},
{"comma", SDLK_COMMA},
{"period", SDLK_PERIOD},
{"question", SDLK_QUESTION},
{"semicolon", SDLK_SEMICOLON},
{"minus", SDLK_MINUS},
{"underscore", SDLK_UNDERSCORE},
{"lparenthesis", SDLK_LEFTPAREN},
{"rparenthesis", SDLK_RIGHTPAREN},
{"lbracket", SDLK_LEFTBRACKET},
{"rbracket", SDLK_RIGHTBRACKET},
{"lbrace", SDLK_LEFTBRACE},
{"rbrace", SDLK_RIGHTBRACE},
{"backslash", SDLK_BACKSLASH},
{"dash", SDLK_SLASH},
{"enter", SDLK_RETURN},
{"space", SDLK_SPACE},
{"tab", SDLK_TAB},
{"backspace", SDLK_BACKSPACE},
{"escape", SDLK_ESCAPE},
{"left", SDLK_LEFT},
{"right", SDLK_RIGHT},
{"up", SDLK_UP},
{"down", SDLK_DOWN},
{"lctrl", SDLK_LCTRL},
{"rctrl", SDLK_RCTRL},
{"lshift", SDLK_LSHIFT},
{"rshift", SDLK_RSHIFT},
{"lalt", SDLK_LALT},
{"ralt", SDLK_RALT},
{"lmeta", SDLK_LGUI},
{"rmeta", SDLK_RGUI},
{"lwin", SDLK_LGUI},
{"rwin", SDLK_RGUI},
{"home", SDLK_HOME},
{"end", SDLK_END},
{"pgup", SDLK_PAGEUP},
{"pgdown", SDLK_PAGEDOWN},
{"leftbutton", SDL_BUTTON_LEFT},
{"rightbutton", SDL_BUTTON_RIGHT},
{"middlebutton", SDL_BUTTON_MIDDLE},
{"sidebuttonback", SDL_BUTTON_X1},
{"sidebuttonforward", SDL_BUTTON_X2},
{"mousewheelup", SDL_MOUSE_WHEEL_UP},
{"mousewheeldown", SDL_MOUSE_WHEEL_DOWN},
{"mousewheelleft", SDL_MOUSE_WHEEL_LEFT},
{"mousewheelright", SDL_MOUSE_WHEEL_RIGHT},
{"kpperiod", SDLK_KP_PERIOD},
{"kpcomma", SDLK_KP_COMMA},
{"kpdivide", SDLK_KP_DIVIDE},
{"kpmultiply", SDLK_KP_MULTIPLY},
{"kpminus", SDLK_KP_MINUS},
{"kpplus", SDLK_KP_PLUS},
{"kpenter", SDLK_KP_ENTER},
{"kpequals", SDLK_KP_EQUALS},
{"capslock", SDLK_CAPSLOCK},
};
void ParseInputConfig(const std::string game_id);
class InputBinding {
public:
InputID keys[3];
InputBinding(InputID k1 = InputID(), InputID k2 = InputID(), InputID k3 = InputID()) {
// we format the keys so comparing them will be very fast, because we will only have to
// compare 3 sorted elements, where the only possible duplicate item is 0
// duplicate entries get changed to one original, one null
if (k1 == k2 && k1 != InputID()) {
k2 = InputID();
}
if (k1 == k3 && k1 != InputID()) {
k3 = InputID();
}
if (k3 == k2 && k2 != InputID()) {
k2 = InputID();
}
// this sorts them
if (k1 <= k2 && k1 <= k3) {
keys[0] = k1;
if (k2 <= k3) {
keys[1] = k2;
keys[2] = k3;
} else {
keys[1] = k3;
keys[2] = k2;
}
} else if (k2 <= k1 && k2 <= k3) {
keys[0] = k2;
if (k1 <= k3) {
keys[1] = k1;
keys[2] = k3;
} else {
keys[1] = k3;
keys[2] = k1;
}
} else {
keys[0] = k3;
if (k1 <= k2) {
keys[1] = k1;
keys[2] = k2;
} else {
keys[1] = k2;
keys[3] = k1;
}
}
}
// copy ctor
InputBinding(const InputBinding& o) {
keys[0] = o.keys[0];
keys[1] = o.keys[1];
keys[2] = o.keys[2];
}
inline bool operator==(const InputBinding& o) {
// InputID() signifies an unused slot
return (keys[0] == o.keys[0] || keys[0] == InputID() || o.keys[0] == InputID()) &&
(keys[1] == o.keys[1] || keys[1] == InputID() || o.keys[1] == InputID()) &&
(keys[2] == o.keys[2] || keys[2] == InputID() || o.keys[2] == InputID());
// it is already very fast,
// but reverse order makes it check the actual keys first instead of possible 0-s,
// potenially skipping the later expressions of the three-way AND
}
inline int KeyCount() const {
return (keys[0].IsValid() ? 1 : 0) + (keys[1].IsValid() ? 1 : 0) +
(keys[2].IsValid() ? 1 : 0);
}
// Sorts by the amount of non zero keys - left side is 'bigger' here
bool operator<(const InputBinding& other) const {
return KeyCount() > other.KeyCount();
}
inline bool IsEmpty() {
return !(keys[0].IsValid() || keys[1].IsValid() || keys[2].IsValid());
}
std::string ToString() { // todo add device type
switch (KeyCount()) {
case 1:
return fmt::format("({})", keys[0].ToString());
case 2:
return fmt::format("({}, {})", keys[0].ToString(), keys[1].ToString());
case 3:
return fmt::format("({}, {}, {})", keys[0].ToString(), keys[1].ToString(),
keys[2].ToString());
default:
return "Empty";
}
}
// returns an InputEvent based on the event type (keyboard, mouse buttons/wheel, or controller)
static InputEvent GetInputEventFromSDLEvent(const SDL_Event& e);
};
class ControllerOutput {
static GameController* controller;
public:
static void SetControllerOutputController(GameController* c);
static void LinkJoystickAxes();
u32 button;
u32 axis;
// these are only used as s8,
// but I added some padding to avoid overflow if it's activated by multiple inputs
// axis_plus and axis_minus pairs share a common new_param, the other outputs have their own
s16 old_param;
s16* new_param;
bool old_button_state, new_button_state, state_changed, positive_axis;
ControllerOutput(const u32 b, u32 a = SDL_GAMEPAD_AXIS_INVALID, bool p = true) {
button = b;
axis = a;
new_param = new s16(0);
old_param = 0;
positive_axis = p;
}
ControllerOutput(const ControllerOutput& o) : button(o.button), axis(o.axis) {
new_param = new s16(*o.new_param);
}
~ControllerOutput() {
delete new_param;
}
inline bool operator==(const ControllerOutput& o) const { // fucking consts everywhere
return button == o.button && axis == o.axis;
}
inline bool operator!=(const ControllerOutput& o) const {
return button != o.button || axis != o.axis;
}
std::string ToString() const {
return fmt::format("({}, {}, {})", (s32)button, (int)axis, old_param);
}
inline bool IsButton() const {
return axis == SDL_GAMEPAD_AXIS_INVALID && button != SDL_GAMEPAD_BUTTON_INVALID;
}
inline bool IsAxis() const {
return axis != SDL_GAMEPAD_AXIS_INVALID && button == SDL_GAMEPAD_BUTTON_INVALID;
}
void ResetUpdate();
void AddUpdate(InputEvent event);
void FinalizeUpdate();
};
class BindingConnection {
public:
InputBinding binding;
ControllerOutput* output;
u32 axis_param;
InputID toggle;
BindingConnection(InputBinding b, ControllerOutput* out, u32 param = 0, InputID t = InputID()) {
binding = b;
axis_param = param;
output = out;
toggle = t;
}
bool operator<(const BindingConnection& other) const {
// a button is a higher priority than an axis, as buttons can influence axes
// (e.g. joystick_halfmode)
if (output->IsButton() &&
(other.output->IsAxis() && (other.output->axis != SDL_GAMEPAD_AXIS_LEFT_TRIGGER &&
other.output->axis != SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))) {
return true;
}
if (binding < other.binding) {
return true;
}
return false;
}
InputEvent ProcessBinding();
};
// Updates the list of pressed keys with the given input.
// Returns whether the list was updated or not.
bool UpdatePressedKeys(InputEvent event);
void ActivateOutputsFromInputs();
} // namespace Input

74
src/input/input_mouse.cpp Normal file
View file

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cmath>
#include "common/types.h"
#include "input/controller.h"
#include "input_mouse.h"
#include "SDL3/SDL.h"
namespace Input {
int mouse_joystick_binding = 0;
float mouse_deadzone_offset = 0.5, mouse_speed = 1, mouse_speed_offset = 0.1250;
Uint32 mouse_polling_id = 0;
bool mouse_enabled = false;
// We had to go through 3 files of indirection just to update a flag
void ToggleMouseEnabled() {
mouse_enabled = !mouse_enabled;
}
void SetMouseToJoystick(int joystick) {
mouse_joystick_binding = joystick;
}
void SetMouseParams(float mdo, float ms, float mso) {
mouse_deadzone_offset = mdo;
mouse_speed = ms;
mouse_speed_offset = mso;
}
Uint32 MousePolling(void* param, Uint32 id, Uint32 interval) {
auto* controller = (GameController*)param;
if (!mouse_enabled)
return interval;
Axis axis_x, axis_y;
switch (mouse_joystick_binding) {
case 1:
axis_x = Axis::LeftX;
axis_y = Axis::LeftY;
break;
case 2:
axis_x = Axis::RightX;
axis_y = Axis::RightY;
break;
default:
return interval; // no update needed
}
float d_x = 0, d_y = 0;
SDL_GetRelativeMouseState(&d_x, &d_y);
float output_speed =
SDL_clamp((sqrt(d_x * d_x + d_y * d_y) + mouse_speed_offset * 128) * mouse_speed,
mouse_deadzone_offset * 128, 128.0);
float angle = atan2(d_y, d_x);
float a_x = cos(angle) * output_speed, a_y = sin(angle) * output_speed;
if (d_x != 0 && d_y != 0) {
controller->Axis(0, axis_x, GetAxis(-0x80, 0x80, a_x));
controller->Axis(0, axis_y, GetAxis(-0x80, 0x80, a_y));
} else {
controller->Axis(0, axis_x, GetAxis(-0x80, 0x80, 0));
controller->Axis(0, axis_y, GetAxis(-0x80, 0x80, 0));
}
return interval;
}
} // namespace Input

18
src/input/input_mouse.h Normal file
View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "SDL3/SDL.h"
#include "common/types.h"
namespace Input {
void ToggleMouseEnabled();
void SetMouseToJoystick(int joystick);
void SetMouseParams(float mouse_deadzone_offset, float mouse_speed, float mouse_speed_offset);
// Polls the mouse for changes, and simulates joystick movement from it.
Uint32 MousePolling(void* param, Uint32 id, Uint32 interval);
} // namespace Input

View file

@ -0,0 +1,237 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "kbm_config_dialog.h"
#include "kbm_help_dialog.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include "common/config.h"
#include "common/path_util.h"
#include "game_info.h"
#include "src/sdl_window.h"
#include <QCloseEvent>
#include <QComboBox>
#include <QFile>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPushButton>
#include <QTextStream>
#include <QVBoxLayout>
QString previous_game = "default";
bool isHelpOpen = false;
HelpDialog* helpDialog;
EditorDialog::EditorDialog(QWidget* parent) : QDialog(parent) {
setWindowTitle("Edit Keyboard + Mouse and Controller input bindings");
resize(600, 400);
// Create the editor widget
editor = new QPlainTextEdit(this);
editorFont.setPointSize(10); // Set default text size
editor->setFont(editorFont); // Apply font to the editor
// Create the game selection combo box
gameComboBox = new QComboBox(this);
gameComboBox->addItem("default"); // Add default option
/*
gameComboBox = new QComboBox(this);
layout->addWidget(gameComboBox); // Add the combobox for selecting game configurations
// Populate the combo box with game configurations
QStringList gameConfigs = GameInfoClass::GetGameInfo(this);
gameComboBox->addItems(gameConfigs);
gameComboBox->setCurrentText("default.ini"); // Set the default selection
*/
// Load all installed games
loadInstalledGames();
// Create Save, Cancel, and Help buttons
QPushButton* saveButton = new QPushButton("Save", this);
QPushButton* cancelButton = new QPushButton("Cancel", this);
QPushButton* helpButton = new QPushButton("Help", this);
QPushButton* defaultButton = new QPushButton("Default", this);
// Layout for the game selection and buttons
QHBoxLayout* topLayout = new QHBoxLayout();
topLayout->addWidget(gameComboBox);
topLayout->addStretch();
topLayout->addWidget(saveButton);
topLayout->addWidget(cancelButton);
topLayout->addWidget(defaultButton);
topLayout->addWidget(helpButton);
// Main layout with editor and buttons
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addLayout(topLayout);
layout->addWidget(editor);
// Load the default config file content into the editor
loadFile(gameComboBox->currentText());
// Connect button and combo box signals
connect(saveButton, &QPushButton::clicked, this, &EditorDialog::onSaveClicked);
connect(cancelButton, &QPushButton::clicked, this, &EditorDialog::onCancelClicked);
connect(helpButton, &QPushButton::clicked, this, &EditorDialog::onHelpClicked);
connect(defaultButton, &QPushButton::clicked, this, &EditorDialog::onResetToDefaultClicked);
connect(gameComboBox, &QComboBox::currentTextChanged, this,
&EditorDialog::onGameSelectionChanged);
}
void EditorDialog::loadFile(QString game) {
const auto config_file = Config::GetFoolproofKbmConfigFile(game.toStdString());
QFile file(config_file);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&file);
editor->setPlainText(in.readAll());
originalConfig = editor->toPlainText();
file.close();
} else {
QMessageBox::warning(this, "Error", "Could not open the file for reading");
}
}
void EditorDialog::saveFile(QString game) {
const auto config_file = Config::GetFoolproofKbmConfigFile(game.toStdString());
QFile file(config_file);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);
out << editor->toPlainText();
file.close();
} else {
QMessageBox::warning(this, "Error", "Could not open the file for writing");
}
}
// Override the close event to show the save confirmation dialog only if changes were made
void EditorDialog::closeEvent(QCloseEvent* event) {
if (isHelpOpen) {
helpDialog->close();
isHelpOpen = false;
// at this point I might have to add this flag and the help dialog to the class itself
}
if (hasUnsavedChanges()) {
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, "Save Changes", "Do you want to save changes?",
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (reply == QMessageBox::Yes) {
saveFile(gameComboBox->currentText());
event->accept(); // Close the dialog
} else if (reply == QMessageBox::No) {
event->accept(); // Close the dialog without saving
} else {
event->ignore(); // Cancel the close event
}
} else {
event->accept(); // No changes, close the dialog without prompting
}
}
void EditorDialog::keyPressEvent(QKeyEvent* event) {
if (event->key() == Qt::Key_Escape) {
if (isHelpOpen) {
helpDialog->close();
isHelpOpen = false;
}
close(); // Trigger the close action, same as pressing the close button
} else {
QDialog::keyPressEvent(event); // Call the base class implementation for other keys
}
}
void EditorDialog::onSaveClicked() {
if (isHelpOpen) {
helpDialog->close();
isHelpOpen = false;
}
saveFile(gameComboBox->currentText());
reject(); // Close the dialog
}
void EditorDialog::onCancelClicked() {
if (isHelpOpen) {
helpDialog->close();
isHelpOpen = false;
}
reject(); // Close the dialog
}
void EditorDialog::onHelpClicked() {
if (!isHelpOpen) {
helpDialog = new HelpDialog(&isHelpOpen, this);
helpDialog->setWindowTitle("Help");
helpDialog->setAttribute(Qt::WA_DeleteOnClose); // Clean up on close
// Get the position and size of the Config window
QRect configGeometry = this->geometry();
int helpX = configGeometry.x() + configGeometry.width() + 10; // 10 pixels offset
int helpY = configGeometry.y();
// Move the Help dialog to the right side of the Config window
helpDialog->move(helpX, helpY);
helpDialog->show();
isHelpOpen = true;
} else {
helpDialog->close();
isHelpOpen = false;
}
}
void EditorDialog::onResetToDefaultClicked() {
bool default_default = gameComboBox->currentText() == "default";
QString prompt =
default_default
? "Do you want to reset your custom default config to the original default config?"
: "Do you want to reset this config to your custom default config?";
QMessageBox::StandardButton reply =
QMessageBox::question(this, "Reset to Default", prompt, QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
if (default_default) {
const auto default_file = Config::GetFoolproofKbmConfigFile("default");
std::filesystem::remove(default_file);
}
const auto config_file = Config::GetFoolproofKbmConfigFile("default");
QFile file(config_file);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&file);
editor->setPlainText(in.readAll());
file.close();
} else {
QMessageBox::warning(this, "Error", "Could not open the file for reading");
}
// saveFile(gameComboBox->currentText());
}
}
bool EditorDialog::hasUnsavedChanges() {
// Compare the current content with the original content to check if there are unsaved changes
return editor->toPlainText() != originalConfig;
}
void EditorDialog::loadInstalledGames() {
previous_game = "default";
QStringList filePaths;
for (const auto& installLoc : Config::getGameInstallDirs()) {
QString installDir;
Common::FS::PathToQString(installDir, installLoc);
QDir parentFolder(installDir);
QFileInfoList fileList = parentFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const auto& fileInfo : fileList) {
if (fileInfo.isDir() && !fileInfo.filePath().endsWith("-UPDATE")) {
gameComboBox->addItem(fileInfo.fileName()); // Add game name to combo box
}
}
}
}
void EditorDialog::onGameSelectionChanged(const QString& game) {
saveFile(previous_game);
loadFile(gameComboBox->currentText()); // Reload file based on the selected game
previous_game = gameComboBox->currentText();
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QComboBox>
#include <QDialog>
#include <QPlainTextEdit>
#include "string"
class EditorDialog : public QDialog {
Q_OBJECT // Necessary for using Qt's meta-object system (signals/slots)
public : explicit EditorDialog(QWidget* parent = nullptr); // Constructor
protected:
void closeEvent(QCloseEvent* event) override; // Override close event
void keyPressEvent(QKeyEvent* event) override;
private:
QPlainTextEdit* editor; // Editor widget for the config file
QFont editorFont; // To handle the text size
QString originalConfig; // Starting config string
std::string gameId;
QComboBox* gameComboBox; // Combo box for selecting game configurations
void loadFile(QString game); // Function to load the config file
void saveFile(QString game); // Function to save the config file
void loadInstalledGames(); // Helper to populate gameComboBox
bool hasUnsavedChanges(); // Checks for unsaved changes
private slots:
void onSaveClicked(); // Save button slot
void onCancelClicked(); // Slot for handling cancel button
void onHelpClicked(); // Slot for handling help button
void onResetToDefaultClicked();
void onGameSelectionChanged(const QString& game); // Slot for game selection changes
};

View file

@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "kbm_help_dialog.h"
#include <QApplication>
#include <QDialog>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QWidget>
ExpandableSection::ExpandableSection(const QString& title, const QString& content,
QWidget* parent = nullptr)
: QWidget(parent) {
QVBoxLayout* layout = new QVBoxLayout(this);
// Button to toggle visibility of content
toggleButton = new QPushButton(title);
layout->addWidget(toggleButton);
// QTextBrowser for content (initially hidden)
contentBrowser = new QTextBrowser();
contentBrowser->setPlainText(content);
contentBrowser->setVisible(false);
// Remove scrollbars from QTextBrowser
contentBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
contentBrowser->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// Set size policy to allow vertical stretching only
contentBrowser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
// Calculate and set initial height based on content
updateContentHeight();
layout->addWidget(contentBrowser);
// Connect button click to toggle visibility
connect(toggleButton, &QPushButton::clicked, [this]() {
contentBrowser->setVisible(!contentBrowser->isVisible());
if (contentBrowser->isVisible()) {
updateContentHeight(); // Update height when expanding
}
emit expandedChanged(); // Notify for layout adjustments
});
// Connect to update height if content changes
connect(contentBrowser->document(), &QTextDocument::contentsChanged, this,
&ExpandableSection::updateContentHeight);
// Minimal layout settings for spacing
layout->setSpacing(2);
layout->setContentsMargins(0, 0, 0, 0);
}
void HelpDialog::closeEvent(QCloseEvent* event) {
*help_open_ptr = false;
close();
}
void HelpDialog::reject() {
*help_open_ptr = false;
close();
}
HelpDialog::HelpDialog(bool* open_flag, QWidget* parent) : QDialog(parent) {
help_open_ptr = open_flag;
// Main layout for the help dialog
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Container widget for the scroll area
QWidget* containerWidget = new QWidget;
QVBoxLayout* containerLayout = new QVBoxLayout(containerWidget);
// Add expandable sections to container layout
auto* quickstartSection = new ExpandableSection("Quickstart", quickstart());
auto* faqSection = new ExpandableSection("FAQ", faq());
auto* syntaxSection = new ExpandableSection("Syntax", syntax());
auto* specialSection = new ExpandableSection("Special Bindings", special());
auto* bindingsSection = new ExpandableSection("Keybindings", bindings());
containerLayout->addWidget(quickstartSection);
containerLayout->addWidget(faqSection);
containerLayout->addWidget(syntaxSection);
containerLayout->addWidget(specialSection);
containerLayout->addWidget(bindingsSection);
containerLayout->addStretch(1);
// Scroll area wrapping the container
QScrollArea* scrollArea = new QScrollArea;
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
scrollArea->setWidgetResizable(true);
scrollArea->setWidget(containerWidget);
// Add the scroll area to the main dialog layout
mainLayout->addWidget(scrollArea);
setLayout(mainLayout);
// Minimum size for the dialog
setMinimumSize(500, 400);
// Re-adjust dialog layout when any section expands/collapses
connect(quickstartSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
connect(faqSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
connect(syntaxSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
connect(specialSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
connect(bindingsSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
}

View file

@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QApplication>
#include <QDialog>
#include <QGroupBox>
#include <QLabel>
#include <QPropertyAnimation>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <QWidget>
class ExpandableSection : public QWidget {
Q_OBJECT
public:
explicit ExpandableSection(const QString& title, const QString& content, QWidget* parent);
signals:
void expandedChanged(); // Signal to indicate layout size change
private:
QPushButton* toggleButton;
QTextBrowser* contentBrowser; // Changed from QLabel to QTextBrowser
QPropertyAnimation* animation;
int contentHeight;
void updateContentHeight() {
int contentHeight = contentBrowser->document()->size().height();
contentBrowser->setMinimumHeight(contentHeight + 5);
contentBrowser->setMaximumHeight(contentHeight + 50);
}
};
class HelpDialog : public QDialog {
Q_OBJECT
public:
explicit HelpDialog(bool* open_flag = nullptr, QWidget* parent = nullptr);
protected:
void closeEvent(QCloseEvent* event) override;
void reject() override;
private:
bool* help_open_ptr;
QString quickstart() {
return
R"(The keyboard and controller remapping backend, GUI and documentation have been written by kalaposfos
In this section, you will find information about the project, its features and help on setting up your ideal setup.
To view the config file's syntax, check out the Syntax tab, for keybind names, visit Normal Keybinds and Special Bindings, and if you are here to view emulator-wide keybinds, you can find it in the FAQ section.
This project started out because I didn't like the original unchangeable keybinds, but rather than waiting for someone else to do it, I implemented this myself. From the default keybinds, you can clearly tell this was a project built for Bloodborne, but ovbiously you can make adjustments however you like.
)";
}
QString faq() {
return
R"(Q: What are the emulator-wide keybinds?
A: -F12: Triggers Renderdoc capture
-F11: Toggles fullscreen
-F10: Toggles FPS counter
-Ctrl F10: Open the debug menu
-F9: Pauses emultor, if the debug menu is open
-F8: Reparses the config file while in-game
-F7: Toggles mouse capture and mouse input
Q: How do I change between mouse and controller joystick input, and why is it even required?
A: You can switch between them with F7, and it is required, because mouse input is done with polling, which means mouse movement is checked every frame, and if it didn't move, the code manually sets the emulator's virtual controller to 0 (back to the center), even if other input devices would update it.
Q: What happens if I accidentally make a typo in the config?
A: The code recognises the line as wrong, and skip it, so the rest of the file will get parsed, but that line in question will be treated like a comment line. You can find these lines in the log, if you search for 'input_handler'.
Q: I want to bind <input> to <output>, but your code doesn't support <input>!
A: Some keys are intentionally omitted, but if you read the bindings through, and you're sure it is not there and isn't one of the intentionally disabled ones, open an issue on https://github.com/shadps4-emu/shadPS4.
)";
}
QString syntax() {
return
R"(This is the full list of currently supported mouse, keyboard and controller inputs, and how to use them.
Emulator-reserved keys: F1 through F12
Syntax (aka how a line can look like):
#Comment line
<controller_button> = <input>, <input>, <input>;
<controller_button> = <input>, <input>;
<controller_button> = <input>;
Examples:
#Interact
cross = e;
#Heavy attack (in BB)
r2 = leftbutton, lshift;
#Move forward
axis_left_y_minus = w;
You can make a comment line by putting # as the first character.
Whitespace doesn't matter, <output>=<input>; is just as valid as <output> = <input>;
';' at the ends of lines is also optional.
)";
}
QString bindings() {
return
R"(The following names should be interpreted without the '' around them, and for inputs that have left and right versions, only the left one is shown, but the right can be inferred from that.
Example: 'lshift', 'rshift'
Keyboard:
Alphabet: 'a', 'b', ..., 'z'
Numbers: '0', '1', ..., '9'
Keypad: 'kp0', kp1', ..., 'kp9', 'kpperiod', 'kpcomma',
'kpdivide', 'kpmultiply', 'kpdivide', 'kpplus', 'kpminus', 'kpenter'
Punctuation and misc:
'space', 'comma', 'period', 'question', 'semicolon', 'minus', 'plus', 'lparenthesis', 'lbracket', 'lbrace', 'backslash', 'dash',
'enter', 'tab', backspace', 'escape'
Arrow keys: 'up', 'down', 'left', 'right'
Modifier keys:
'lctrl', 'lshift', 'lalt', 'lwin' = 'lmeta' (same input, different names, so if you are not on Windows and don't like calling this the Windows key, there is an alternative)
Mouse:
'leftbutton', 'rightbutton', 'middlebutton', 'sidebuttonforward', 'sidebuttonback'
The following wheel inputs cannot be bound to axis input, only button:
'mousewheelup', 'mousewheeldown', 'mousewheelleft', 'mousewheelright'
Controller:
The touchpad currently can't be rebound to anything else, but you can bind buttons to it.
If you have a controller that has different names for buttons, it will still work, just look up what are the equivalent names for that controller
The same left-right rule still applies here.
Buttons:
'triangle', 'circle', 'cross', 'square', 'l1', 'l3',
'options', touchpad', 'up', 'down', 'left', 'right'
Axes if you bind them to a button input:
'axis_left_x_plus', 'axis_left_x_minus', 'axis_left_y_plus', 'axis_left_y_minus',
'axis_right_x_plus', ..., 'axis_right_y_minus',
'l2'
Axes if you bind them to another axis input:
'axis_left_x' 'axis_left_y' 'axis_right_x' 'axis_right_y',
'l2'
)";
}
QString special() {
return
R"(There are some extra bindings you can put into the config file, that don't correspond to a controller input, but rather something else.
You can find these here, with detailed comments, examples and suggestions for most of them.
'leftjoystick_halfmode' and 'rightjoystick_halfmode' = <key>;
These are a pair of input modifiers, that change the way keyboard button bound axes work. By default, those push the joystick to the max in their respective direction, but if their respective joystick_halfmode modifier value is true, they only push it... halfway. With this, you can change from run to walk in games like Bloodborne.
'mouse_to_joystick' = 'none', 'left' or 'right';
This binds the mouse movement to either joystick. If it recieves a value that is not 'left' or 'right', it defaults to 'none'.
'mouse_movement_params' = float, float, float;
(If you don't know what a float is, it is a data type that stores non-whole numbers.)
Default values: 0.5, 1, 0.125
Let's break each parameter down:
1st: mouse_deadzone_offset: this value should have a value between 0 and 1 (It gets clamped to that range anyway), with 0 being no offset and 1 being pushing the joystick to the max in the direction the mouse moved.
This controls the minimum distance the joystick gets moved, when moving the mouse. If set to 0, it will emulate raw mouse input, which doesn't work very well due to deadzones preventing input if the movement is not large enough.
2nd: mouse_speed: It's just a standard multiplier to the mouse input speed.
If you input a negative number, the axis directions get reversed (Keep in mind that the offset can still push it back to positive, if it's big enough)
3rd: mouse_speed_offset: This also should be in the 0 to 1 range, with 0 being no offset and 1 being offsetting to the max possible value.
This is best explained through an example: Let's set mouse_deadzone to 0.5, and this to 0: This means that if we move the mousevery slowly, it still inputs a half-strength joystick input, and if we increase the speed, it would stay that way until we move faster than half the max speed. If we instead set this to 0.25, we now only need to move the mouse faster than the 0.5-0.25=0.25=quarter of the max speed, to get an increase in joystick speed. If we set it to 0.5, then even moving the mouse at 1 pixel per frame will result in a faster-than-minimum speed.
'key_toggle' = <key>, <key_to_toggle>;
This assigns a key to another key, and if pressed, toggles that key's virtual value. If it's on, then it doesn't matter if the key is pressed or not, the input handler will treat it as if it's pressed.
You can make an input toggleable with this, for example: Let's say we want to be able to toggle l1 with t. You can then bind l1 to a key you won't use, like kpenter, then bind t to toggle that, so you will end up with this:
l1 = kpenter;
key_toggle = t, kpenter;
'analog_deadzone' = <device>, <value>;
value goes from 1 to 127 (no deadzone to max deadzone)
devices: leftjoystick, rightjoystick, l2, r2
)";
}
};

View file

@ -3,6 +3,7 @@
#include <QDockWidget>
#include <QKeyEvent>
#include <QPlainTextEdit>
#include <QProgressDialog>
#include "about_dialog.h"
@ -21,6 +22,9 @@
#include "install_dir_select.h"
#include "main_window.h"
#include "settings_dialog.h"
#include "kbm_config_dialog.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#ifdef ENABLE_DISCORD_RPC
#include "common/discord_rpc_handler.h"
@ -291,6 +295,12 @@ void MainWindow::CreateConnects() {
settingsDialog->exec();
});
// this is the editor for kbm keybinds
connect(ui->controllerButton, &QPushButton::clicked, this, [this]() {
EditorDialog* editorWindow = new EditorDialog(this);
editorWindow->exec(); // Show the editor window modally
});
#ifdef ENABLE_UPDATER
connect(ui->updaterAct, &QAction::triggered, this, [this]() {
auto checkUpdate = new CheckUpdate(true);

View file

@ -1,24 +1,27 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_properties.h>
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL_video.h>
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_hints.h"
#include "SDL3/SDL_init.h"
#include "SDL3/SDL_properties.h"
#include "SDL3/SDL_timer.h"
#include "SDL3/SDL_video.h"
#include "common/assert.h"
#include "common/config.h"
#include "common/elf_info.h"
#include "common/version.h"
#include "core/libraries/kernel/time.h"
#include "core/libraries/pad/pad.h"
#include "imgui/renderer/imgui_core.h"
#include "input/controller.h"
#include "input/input_handler.h"
#include "input/input_mouse.h"
#include "sdl_window.h"
#include "video_core/renderdoc.h"
#ifdef __APPLE__
#include <SDL3/SDL_metal.h>
#include "SDL3/SDL_metal.h"
#endif
namespace Input {
@ -290,6 +293,10 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_
window_info.type = WindowSystemType::Metal;
window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window));
#endif
// input handler init-s
Input::ControllerOutput::SetControllerOutputController(controller);
Input::ControllerOutput::LinkJoystickAxes();
Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
}
WindowSDL::~WindowSDL() = default;
@ -317,18 +324,28 @@ void WindowSDL::WaitEvent() {
is_shown = event.type == SDL_EVENT_WINDOW_EXPOSED;
OnResize();
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_MOUSE_WHEEL_OFF:
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
OnKeyPress(&event);
OnKeyboardMouseInput(&event);
break;
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
controller->SetEngine(std::make_unique<Input::SDLInputEngine>());
break;
case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
controller->SetTouchpadState(event.gtouchpad.finger,
event.type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event.gtouchpad.x,
event.gtouchpad.y);
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
OnGamepadEvent(&event);
break;
// i really would have appreciated ANY KIND OF DOCUMENTATION ON THIS
@ -355,6 +372,7 @@ void WindowSDL::WaitEvent() {
void WindowSDL::InitTimers() {
SDL_AddTimer(100, &PollController, controller);
SDL_AddTimer(33, Input::MousePolling, (void*)controller);
}
void WindowSDL::RequestKeyboard() {
@ -381,239 +399,87 @@ void WindowSDL::OnResize() {
ImGui::Core::OnResize();
}
void WindowSDL::OnKeyPress(const SDL_Event* event) {
auto button = OrbisPadButtonDataOffset::None;
Input::Axis axis = Input::Axis::AxisMax;
int axisvalue = 0;
int ax = 0;
std::string backButtonBehavior = Config::getBackButtonBehavior();
switch (event->key.key) {
case SDLK_UP:
button = OrbisPadButtonDataOffset::Up;
break;
case SDLK_DOWN:
button = OrbisPadButtonDataOffset::Down;
break;
case SDLK_LEFT:
button = OrbisPadButtonDataOffset::Left;
break;
case SDLK_RIGHT:
button = OrbisPadButtonDataOffset::Right;
break;
// Provide alternatives for face buttons for users without a numpad.
case SDLK_KP_8:
case SDLK_C:
button = OrbisPadButtonDataOffset::Triangle;
break;
case SDLK_KP_6:
case SDLK_B:
button = OrbisPadButtonDataOffset::Circle;
break;
case SDLK_KP_2:
case SDLK_N:
button = OrbisPadButtonDataOffset::Cross;
break;
case SDLK_KP_4:
case SDLK_V:
button = OrbisPadButtonDataOffset::Square;
break;
case SDLK_RETURN:
button = OrbisPadButtonDataOffset::Options;
break;
case SDLK_A:
axis = Input::Axis::LeftX;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += -127;
} else {
axisvalue = 0;
Uint32 wheelOffCallback(void* og_event, Uint32 timer_id, Uint32 interval) {
SDL_Event off_event = *(SDL_Event*)og_event;
off_event.type = SDL_EVENT_MOUSE_WHEEL_OFF;
SDL_PushEvent(&off_event);
delete (SDL_Event*)og_event;
return 0;
}
void WindowSDL::OnKeyboardMouseInput(const SDL_Event* event) {
using Libraries::Pad::OrbisPadButtonDataOffset;
// get the event's id, if it's keyup or keydown
const bool input_down = event->type == SDL_EVENT_KEY_DOWN ||
event->type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event->type == SDL_EVENT_MOUSE_WHEEL;
Input::InputEvent input_event = Input::InputBinding::GetInputEventFromSDLEvent(*event);
// Handle window controls outside of the input maps
if (event->type == SDL_EVENT_KEY_DOWN) {
u32 input_id = input_event.input.sdl_id;
// Reparse kbm inputs
if (input_id == SDLK_F8) {
Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
return;
}
ax = Input::GetAxis(-0x80, 0x80, axisvalue);
break;
case SDLK_D:
axis = Input::Axis::LeftX;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += 127;
} else {
axisvalue = 0;
// Toggle mouse capture and movement input
else if (input_id == SDLK_F7) {
Input::ToggleMouseEnabled();
SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(),
!SDL_GetWindowRelativeMouseMode(this->GetSDLWindow()));
return;
}
ax = Input::GetAxis(-0x80, 0x80, axisvalue);
break;
case SDLK_W:
axis = Input::Axis::LeftY;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += -127;
} else {
axisvalue = 0;
// Toggle fullscreen
else if (input_id == SDLK_F11) {
SDL_WindowFlags flag = SDL_GetWindowFlags(window);
bool is_fullscreen = flag & SDL_WINDOW_FULLSCREEN;
SDL_SetWindowFullscreen(window, !is_fullscreen);
return;
}
ax = Input::GetAxis(-0x80, 0x80, axisvalue);
break;
case SDLK_S:
axis = Input::Axis::LeftY;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += 127;
} else {
axisvalue = 0;
}
ax = Input::GetAxis(-0x80, 0x80, axisvalue);
break;
case SDLK_J:
axis = Input::Axis::RightX;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += -127;
} else {
axisvalue = 0;
}
ax = Input::GetAxis(-0x80, 0x80, axisvalue);
break;
case SDLK_L:
axis = Input::Axis::RightX;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += 127;
} else {
axisvalue = 0;
}
ax = Input::GetAxis(-0x80, 0x80, axisvalue);
break;
case SDLK_I:
axis = Input::Axis::RightY;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += -127;
} else {
axisvalue = 0;
}
ax = Input::GetAxis(-0x80, 0x80, axisvalue);
break;
case SDLK_K:
axis = Input::Axis::RightY;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += 127;
} else {
axisvalue = 0;
}
ax = Input::GetAxis(-0x80, 0x80, axisvalue);
break;
case SDLK_X:
button = OrbisPadButtonDataOffset::L3;
break;
case SDLK_M:
button = OrbisPadButtonDataOffset::R3;
break;
case SDLK_Q:
button = OrbisPadButtonDataOffset::L1;
break;
case SDLK_U:
button = OrbisPadButtonDataOffset::R1;
break;
case SDLK_E:
button = OrbisPadButtonDataOffset::L2;
axis = Input::Axis::TriggerLeft;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += 255;
} else {
axisvalue = 0;
}
ax = Input::GetAxis(0, 0x80, axisvalue);
break;
case SDLK_O:
button = OrbisPadButtonDataOffset::R2;
axis = Input::Axis::TriggerRight;
if (event->type == SDL_EVENT_KEY_DOWN) {
axisvalue += 255;
} else {
axisvalue = 0;
}
ax = Input::GetAxis(0, 0x80, axisvalue);
break;
case SDLK_SPACE:
if (backButtonBehavior != "none") {
float x = backButtonBehavior == "left" ? 0.25f
: (backButtonBehavior == "right" ? 0.75f : 0.5f);
// trigger a touchpad event so that the touchpad emulation for back button works
controller->SetTouchpadState(0, true, x, 0.5f);
button = OrbisPadButtonDataOffset::TouchPad;
} else {
button = {};
}
break;
case SDLK_F11:
if (event->type == SDL_EVENT_KEY_DOWN) {
{
SDL_WindowFlags flag = SDL_GetWindowFlags(window);
bool is_fullscreen = flag & SDL_WINDOW_FULLSCREEN;
SDL_SetWindowFullscreen(window, !is_fullscreen);
}
}
break;
case SDLK_F12:
if (event->type == SDL_EVENT_KEY_DOWN) {
// Trigger rdoc capture
// Trigger rdoc capture
else if (input_id == SDLK_F12) {
VideoCore::TriggerCapture();
return;
}
break;
default:
break;
}
if (button != OrbisPadButtonDataOffset::None) {
controller->CheckButton(0, button, event->type == SDL_EVENT_KEY_DOWN);
// if it's a wheel event, make a timer that turns it off after a set time
if (event->type == SDL_EVENT_MOUSE_WHEEL) {
const SDL_Event* copy = new SDL_Event(*event);
SDL_AddTimer(33, wheelOffCallback, (void*)copy);
}
if (axis != Input::Axis::AxisMax) {
controller->Axis(0, axis, ax);
// add/remove it from the list
bool inputs_changed = Input::UpdatePressedKeys(input_event);
// update bindings
if (inputs_changed) {
Input::ActivateOutputsFromInputs();
}
}
void WindowSDL::OnGamepadEvent(const SDL_Event* event) {
auto button = OrbisPadButtonDataOffset::None;
Input::Axis axis = Input::Axis::AxisMax;
switch (event->type) {
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
controller->SetEngine(std::make_unique<Input::SDLInputEngine>());
break;
case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
controller->SetTouchpadState(event->gtouchpad.finger,
event->type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP,
event->gtouchpad.x, event->gtouchpad.y);
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP: {
button = Input::SDLGamepadToOrbisButton(event->gbutton.button);
if (button == OrbisPadButtonDataOffset::None) {
break;
}
if (event->gbutton.button != SDL_GAMEPAD_BUTTON_BACK) {
controller->CheckButton(0, button, event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN);
break;
}
const auto backButtonBehavior = Config::getBackButtonBehavior();
if (backButtonBehavior != "none") {
float x = backButtonBehavior == "left" ? 0.25f
: (backButtonBehavior == "right" ? 0.75f : 0.5f);
// trigger a touchpad event so that the touchpad emulation for back button works
controller->SetTouchpadState(0, true, x, 0.5f);
controller->CheckButton(0, button, event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN);
}
break;
}
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
axis = event->gaxis.axis == SDL_GAMEPAD_AXIS_LEFTX ? Input::Axis::LeftX
: event->gaxis.axis == SDL_GAMEPAD_AXIS_LEFTY ? Input::Axis::LeftY
: event->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHTX ? Input::Axis::RightX
: event->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHTY ? Input::Axis::RightY
: event->gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER ? Input::Axis::TriggerLeft
: event->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER ? Input::Axis::TriggerRight
: Input::Axis::AxisMax;
if (axis != Input::Axis::AxisMax) {
if (event->gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER ||
event->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
controller->Axis(0, axis, Input::GetAxis(0, 0x8000, event->gaxis.value));
} else {
controller->Axis(0, axis, Input::GetAxis(-0x8000, 0x8000, event->gaxis.value));
}
}
break;
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);
// the touchpad button shouldn't be rebound to anything else,
// as it would break the entire touchpad handling
// You can still bind other things to it though
if (event->gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) {
controller->CheckButton(0, OrbisPadButtonDataOffset::TouchPad, input_down);
return;
}
// add/remove it from the list
bool inputs_changed = Input::UpdatePressedKeys(input_event);
// update bindings
if (inputs_changed) {
Input::ActivateOutputsFromInputs();
}
}

View file

@ -3,9 +3,10 @@
#pragma once
#include <string>
#include "common/types.h"
#include "core/libraries/pad/pad.h"
#include "input/controller.h"
#include "string"
struct SDL_Window;
struct SDL_Gamepad;
@ -94,7 +95,7 @@ public:
private:
void OnResize();
void OnKeyPress(const SDL_Event* event);
void OnKeyboardMouseInput(const SDL_Event* event);
void OnGamepadEvent(const SDL_Event* event);
private: