Squashed commits

This commit is contained in:
kalaposfos13 2024-12-15 10:32:32 +01:00
parent 40211642ca
commit 6f7bf0b2a3
13 changed files with 1786 additions and 214 deletions

View File

@ -793,6 +793,8 @@ 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
)
set(EMULATOR src/emulator.cpp
@ -842,6 +844,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

@ -74,7 +74,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
| Button | Function |
|-------------|-------------|
@ -86,32 +86,34 @@ F12 | Trigger RenderDoc Capture
> [!NOTE]
> Xbox and DualShock controllers work out of the box.
| Controller button | Keyboard equivelant | Mac alternative |
|-------------|-------------|--------------|
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 | C |
CIRCLE | Numpad 6 | B |
CROSS | Numpad 2 | N |
SQUARE | Numpad 4 | 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

@ -830,4 +830,81 @@ void setDefaultValues() {
checkCompatibilityOnStartup = false;
}
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
)";
}
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) / "inputConfig";
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

@ -137,6 +137,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

694
src/input/input_handler.cpp Normal file
View File

@ -0,0 +1,694 @@
// 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 "sstream"
#include "string"
#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"
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
*/
// Flags and values for varying purposes
// todo: can we change these?
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, leftjoystick_halfmode = false, rightjoystick_halfmode = false;
std::list<std::pair<u32, bool>> pressed_keys;
std::list<u32> toggled_keys;
std::list<BindingConnection> connections = std::list<BindingConnection>();
ControllerOutput output_array[] = {
// Important: these have to be the first, or else they will update in the wrong order
ControllerOutput((OrbisPadButtonDataOffset)LEFTJOYSTICK_HALFMODE),
ControllerOutput((OrbisPadButtonDataOffset)RIGHTJOYSTICK_HALFMODE),
ControllerOutput((OrbisPadButtonDataOffset)KEY_TOGGLE),
// Button mappings
ControllerOutput(OrbisPadButtonDataOffset::Triangle),
ControllerOutput(OrbisPadButtonDataOffset::Circle),
ControllerOutput(OrbisPadButtonDataOffset::Cross),
ControllerOutput(OrbisPadButtonDataOffset::Square),
ControllerOutput(OrbisPadButtonDataOffset::L1),
ControllerOutput(OrbisPadButtonDataOffset::L3),
ControllerOutput(OrbisPadButtonDataOffset::R1),
ControllerOutput(OrbisPadButtonDataOffset::R3),
ControllerOutput(OrbisPadButtonDataOffset::Options),
ControllerOutput(OrbisPadButtonDataOffset::TouchPad),
ControllerOutput(OrbisPadButtonDataOffset::Up),
ControllerOutput(OrbisPadButtonDataOffset::Down),
ControllerOutput(OrbisPadButtonDataOffset::Left),
ControllerOutput(OrbisPadButtonDataOffset::Right),
// Axis mappings
ControllerOutput(OrbisPadButtonDataOffset::None, Axis::LeftX),
ControllerOutput(OrbisPadButtonDataOffset::None, Axis::LeftY),
ControllerOutput(OrbisPadButtonDataOffset::None, Axis::RightX),
ControllerOutput(OrbisPadButtonDataOffset::None, Axis::RightY),
ControllerOutput(OrbisPadButtonDataOffset::None, Axis::TriggerLeft),
ControllerOutput(OrbisPadButtonDataOffset::None, Axis::TriggerRight),
// the "sorry, you gave an incorrect value, now we bind it to nothing" output
ControllerOutput(OrbisPadButtonDataOffset::None, Axis::AxisMax),
};
// We had to go through 3 files of indirection just to update a flag
void ToggleMouseEnabled() {
mouse_enabled ^= true;
}
// parsing related functions
u32 GetAxisInputId(AxisMapping a) {
// LOG_INFO(Input, "Parsing an axis...");
if (a.axis == Axis::AxisMax || a.value != 0) {
LOG_ERROR(Input, "Invalid axis given!");
return 0;
}
u32 value = (u32)a.axis + 0x80000000;
return value;
}
u32 GetOrbisToSdlButtonKeycode(OrbisPadButtonDataOffset cbutton) {
switch (cbutton) {
case OrbisPadButtonDataOffset::Circle:
return SDL_GAMEPAD_BUTTON_EAST;
case OrbisPadButtonDataOffset::Triangle:
return SDL_GAMEPAD_BUTTON_NORTH;
case OrbisPadButtonDataOffset::Square:
return SDL_GAMEPAD_BUTTON_WEST;
case OrbisPadButtonDataOffset::Cross:
return SDL_GAMEPAD_BUTTON_SOUTH;
case OrbisPadButtonDataOffset::L1:
return SDL_GAMEPAD_BUTTON_LEFT_SHOULDER;
case OrbisPadButtonDataOffset::R1:
return SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER;
case OrbisPadButtonDataOffset::L3:
return SDL_GAMEPAD_BUTTON_LEFT_STICK;
case OrbisPadButtonDataOffset::R3:
return SDL_GAMEPAD_BUTTON_RIGHT_STICK;
case OrbisPadButtonDataOffset::Up:
return SDL_GAMEPAD_BUTTON_DPAD_UP;
case OrbisPadButtonDataOffset::Down:
return SDL_GAMEPAD_BUTTON_DPAD_DOWN;
case OrbisPadButtonDataOffset::Left:
return SDL_GAMEPAD_BUTTON_DPAD_LEFT;
case OrbisPadButtonDataOffset::Right:
return SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
case OrbisPadButtonDataOffset::Options:
return SDL_GAMEPAD_BUTTON_START;
case (OrbisPadButtonDataOffset)BACK_BUTTON:
return SDL_GAMEPAD_BUTTON_BACK;
default:
return ((u32)-1) - 0x10000000;
}
}
u32 GetControllerButtonInputId(u32 cbutton) {
if ((cbutton & ((u32)OrbisPadButtonDataOffset::TouchPad | LEFTJOYSTICK_HALFMODE |
RIGHTJOYSTICK_HALFMODE)) != 0) {
return (u32)-1;
}
return GetOrbisToSdlButtonKeycode((OrbisPadButtonDataOffset)cbutton) + 0x10000000;
}
// syntax: 'name, name,name' or 'name,name' or 'name'
InputBinding GetBindingFromString(std::string& line) {
u32 key1 = 0, key2 = 0, key3 = 0;
// Split the string by commas
std::vector<std::string> tokens;
std::stringstream ss(line);
std::string token;
while (std::getline(ss, token, ',')) {
tokens.push_back(token);
}
// Check and process tokens
for (const auto& t : tokens) {
if (string_to_keyboard_key_map.find(t) != string_to_keyboard_key_map.end()) {
// Map to keyboard key
u32 key_id = string_to_keyboard_key_map.at(t);
if (!key1)
key1 = key_id;
else if (!key2)
key2 = key_id;
else if (!key3)
key3 = key_id;
} else if (string_to_axis_map.find(t) != string_to_axis_map.end()) {
// Map to axis input ID
u32 axis_id = GetAxisInputId(string_to_axis_map.at(t));
if (axis_id == (u32)-1) {
return InputBinding(0, 0, 0);
}
if (!key1)
key1 = axis_id;
else if (!key2)
key2 = axis_id;
else if (!key3)
key3 = axis_id;
} else if (string_to_cbutton_map.find(t) != string_to_cbutton_map.end()) {
// Map to controller button input ID
u32 cbutton_id = GetControllerButtonInputId((u32)string_to_cbutton_map.at(t));
if (cbutton_id == (u32)-1) {
return InputBinding(0, 0, 0);
}
if (!key1)
key1 = cbutton_id;
else if (!key2)
key2 = cbutton_id;
else if (!key3)
key3 = cbutton_id;
} else {
// Invalid token found; return default binding
return InputBinding(0, 0, 0);
}
}
return InputBinding(key1, key2, key3);
}
// function that takes a controlleroutput, and returns the array's corresponding element's pointer
ControllerOutput* GetOutputPointer(const ControllerOutput& parsed) {
// i wonder how long until someone notices this or I get rid of it
int i = 0;
for (; i[output_array] != ControllerOutput(OrbisPadButtonDataOffset::None, Axis::AxisMax);
i++) {
if (i[output_array] == parsed) {
return &output_array[i];
}
}
return &output_array[i];
}
void ParseInputConfig(const std::string game_id = "") {
const auto config_file = Config::GetFoolproofKbmConfigFile(game_id);
// todo: change usages of this to GetFoolproofKbmConfigFile (in the gui)
if (game_id == "") {
return;
}
// we reset these here so in case the user fucks up or doesn't include this,
// we can fall back to default
connections.clear();
mouse_deadzone_offset = 0.5;
mouse_speed = 1;
mouse_speed_offset = 0.125;
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(line.begin(), line.end(), ' '), line.end());
if (line[line.length() - 1] == ';') {
line = line.substr(0, line.length() - 1);
}
// Ignore comment lines
if (line.empty() || line[0] == '#') {
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") {
mouse_joystick_binding = 1;
} else if (input_string == "right") {
mouse_joystick_binding = 2;
} else {
mouse_joystick_binding = 0; // default to 'none' or invalid
}
continue;
}
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 =
GetOutputPointer(ControllerOutput((OrbisPadButtonDataOffset)KEY_TOGGLE));
BindingConnection toggle_connection =
BindingConnection(InputBinding(toggle_keys.key2), toggle_out, toggle_keys.key3);
connections.insert(connections.end(), toggle_connection);
continue;
}
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
}
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;
}
// normal cases
InputBinding binding = GetBindingFromString(input_string);
BindingConnection connection(0, 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, GetOutputPointer(ControllerOutput(button_it->second)));
connections.insert(connections.end(), connection);
} else if (axis_it != string_to_axis_map.end()) {
int value_to_set = (binding.key3 & 0x80000000) != 0 ? 0
: (axis_it->second.axis == Axis::TriggerLeft ||
axis_it->second.axis == Axis::TriggerRight)
? 127
: axis_it->second.value;
connection =
BindingConnection(binding,
GetOutputPointer(ControllerOutput(OrbisPadButtonDataOffset::None,
axis_it->second.axis)),
value_to_set);
connections.insert(connections.end(), connection);
} else {
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
}
// LOG_INFO(Input, "Succesfully parsed line {}", lineCount);
}
file.close();
connections.sort();
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_DEBUG(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;
}
u32 InputBinding::GetInputIDFromEvent(const SDL_Event& e) {
int value_mask;
switch (e.type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
return e.key.key;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
return (u32)e.button.button;
case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_MOUSE_WHEEL_OFF:
return GetMouseWheelEvent(e);
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
return (u32)e.gbutton.button + 0x10000000; // I believe this range is unused
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
// todo: somehow put this value into the correct connection
// solution 1: add it to the keycode as a 0x0FF00000 (a bit hacky but works I guess?)
// I guess in software developement, there really is nothing more permanent than a temporary
// solution
value_mask = (u32)((e.gaxis.value / 256 + 128) << 20); // +-32000 to +-128 to 0-255
return (u32)e.gaxis.axis + 0x80000000 +
value_mask; // they are pushed to the end of the sorted array
default:
return (u32)-1;
}
}
GameController* ControllerOutput::controller = nullptr;
void ControllerOutput::SetControllerOutputController(GameController* c) {
ControllerOutput::controller = c;
}
void ToggleKeyInList(u32 key) {
if ((key & 0x80000000) != 0) {
LOG_ERROR(Input, "Toggling analog inputs is not supported!");
return;
}
auto it = std::find(toggled_keys.begin(), toggled_keys.end(), key);
if (it == toggled_keys.end()) {
toggled_keys.insert(toggled_keys.end(), key);
LOG_DEBUG(Input, "Added {} to toggled keys", key);
} else {
toggled_keys.erase(it);
LOG_DEBUG(Input, "Removed {} from toggled keys", key);
}
}
void ControllerOutput::ResetUpdate() {
state_changed = false;
new_button_state = false;
new_param = 0;
}
void ControllerOutput::AddUpdate(bool pressed, bool analog, u32 param) {
state_changed = true;
if (button != OrbisPadButtonDataOffset::None) {
if (analog) {
new_button_state |= abs((s32)param) > 0x40;
} else {
new_button_state |= pressed;
new_param = param;
}
} else if (axis != Axis::AxisMax) {
switch (axis) {
case Axis::TriggerLeft:
case Axis::TriggerRight:
// 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
// warning: doesn't work yet
new_param = SDL_clamp((pressed ? (s32)param : 0) + new_param, 0, 127);
break;
default:
// todo: do the same as above
new_param = SDL_clamp((pressed ? (s32)param : 0) + new_param, -127, 127);
break;
}
}
}
void ControllerOutput::FinalizeUpdate() {
old_button_state = new_button_state;
old_param = new_param;
float touchpad_x = 0;
if (button != OrbisPadButtonDataOffset::None) {
switch ((u32)button) {
case (u32)OrbisPadButtonDataOffset::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, button, new_button_state);
break;
case LEFTJOYSTICK_HALFMODE:
leftjoystick_halfmode = new_button_state;
break;
case RIGHTJOYSTICK_HALFMODE:
rightjoystick_halfmode = new_button_state;
break;
case KEY_TOGGLE:
if (new_button_state) {
// LOG_DEBUG(Input, "Toggling a button...");
ToggleKeyInList(new_param);
}
break;
default: // is a normal key (hopefully)
controller->CheckButton(0, (OrbisPadButtonDataOffset)button, new_button_state);
break;
}
} else if (axis != Axis::AxisMax) {
float multiplier = 1.0;
switch (axis) {
case Axis::LeftX:
case Axis::LeftY:
multiplier = leftjoystick_halfmode ? 0.5 : 1.0;
break;
case Axis::RightX:
case Axis::RightY:
multiplier = rightjoystick_halfmode ? 0.5 : 1.0;
break;
case Axis::TriggerLeft:
case Axis::TriggerRight:
controller->Axis(0, axis, GetAxis(0x0, 0x80, new_param));
// Also handle button counterpart for TriggerLeft and TriggerRight
controller->CheckButton(0,
axis == Axis::TriggerLeft ? OrbisPadButtonDataOffset::L2
: OrbisPadButtonDataOffset::R2,
new_param > 0x20);
return;
default:
break;
}
controller->Axis(0, 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(u32 value, bool is_pressed) {
// Skip invalid inputs
if (value == (u32)-1) {
return false;
}
if ((value & 0x80000000) != 0) {
// analog input, it gets added when it first sends an event,
// and from there, it only changes the parameter
// reverse iterate until we get out of the 0x8000000 range, if found,
// update the parameter, if not, add it to the end
u32 value_to_search = value & 0xF00FFFFF;
for (auto& it = --pressed_keys.end(); (it->first & 0x80000000) != 0; it--) {
if ((it->first & 0xF00FFFFF) == value_to_search) {
it->first = value;
// LOG_DEBUG(Input, "New value for {:X}: {:x}", value, value);
return true;
}
}
// LOG_DEBUG(Input, "Input activated for the first time, adding it to the list");
pressed_keys.insert(pressed_keys.end(), {value, false});
return true;
}
if (is_pressed) {
// Find the correct position for insertion to maintain order
auto it =
std::lower_bound(pressed_keys.begin(), pressed_keys.end(), value,
[](const std::pair<u32, bool>& pk, u32 v) { return pk.first < v; });
// Insert only if 'value' is not already in the list
if (it == pressed_keys.end() || it->first != value) {
pressed_keys.insert(it, {value, 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(),
[value](const std::pair<u32, bool>& pk) { return pk.first == value; });
if (it != pressed_keys.end()) {
pressed_keys.erase(it);
return true;
}
}
return false;
}
// Check if a given binding's all keys are currently active.
// For now it also extracts the analog inputs' parameters.
void IsInputActive(BindingConnection& connection, bool& active, bool& analog) {
InputBinding i = connection.binding;
// Extract keys from InputBinding and ignore unused (0) or toggled keys
std::list<u32> input_keys = {i.key1, i.key2, i.key3};
input_keys.remove(0);
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");
active = true;
return;
}
// 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 (u32 key : input_keys) {
bool key_found = false;
// Search for the current key in pressed_keys starting from the last checked position
while (pressed_it != pressed_keys.end() && (pressed_it->first & 0x80000000) == 0) {
if (pressed_it->first == key) {
key_found = true;
flags_to_set.push_back(&pressed_it->second);
++pressed_it; // Move to the next key in pressed_keys
break;
}
++pressed_it;
}
if (!key_found && (key & 0x80000000) != 0) {
// reverse iterate over the analog inputs, as they can't be sorted
auto& rev_it = --pressed_keys.end();
for (auto rev_it = --pressed_keys.end(); (rev_it->first & 0x80000000) != 0; rev_it--) {
if ((rev_it->first & 0xF00FFFFF) == (key & 0xF00FFFFF)) {
connection.parameter = (u32)((s32)((rev_it->first & 0x0FF00000) >> 20) - 128);
// LOG_DEBUG(Input, "Extracted the following param: {:X} from {:X}",
// (s32)connection.parameter, rev_it->first);
key_found = true;
analog = true;
flags_to_set.push_back(&rev_it->second);
break;
}
}
}
if (!key_found) {
return;
}
}
bool is_fully_blocked = true;
for (bool* flag : flags_to_set) {
is_fully_blocked &= *flag;
}
if (is_fully_blocked) {
return;
}
for (bool* flag : flags_to_set) {
*flag = true;
}
LOG_DEBUG(Input, "Input found: {}", i.ToString());
active = true;
return; // All keys are active
}
void ActivateOutputsFromInputs() {
// LOG_DEBUG(Input, "Start of an input frame...");
for (auto& it : pressed_keys) {
it.second = false;
}
// this is the cleanest looking code I've ever written, too bad it is not working
// (i left the last part in by accident, then it turnd out to still be true even after I thought
// everything is good) (but now it really should be fine)
for (auto& it : output_array) {
it.ResetUpdate();
}
for (auto& it : connections) {
bool active = false, analog_input_detected = false;
IsInputActive(it, active, analog_input_detected);
it.output->AddUpdate(active, analog_input_detected, it.parameter);
}
for (auto& it : output_array) {
it.FinalizeUpdate();
}
}
void UpdateMouse(GameController* controller) {
if (!mouse_enabled)
return;
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;
case 0:
default:
return; // 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));
}
}
Uint32 MousePolling(void* param, Uint32 id, Uint32 interval) {
auto* data = (GameController*)param;
UpdateMouse(data);
return interval;
}
} // namespace Input

345
src/input/input_handler.h Normal file
View File

@ -0,0 +1,345 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "array"
#include "common/logging/log.h"
#include "common/types.h"
#include "core/libraries/pad/pad.h"
#include "fmt/format.h"
#include "input/controller.h"
#include "map"
#include "string"
#include "unordered_set"
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.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 {
Axis axis;
int value; // Value to set for key press (+127 or -127 for movement)
};
// i strongly suggest you collapse these maps
const std::map<std::string, OrbisPadButtonDataOffset> string_to_cbutton_map = {
{"triangle", OrbisPadButtonDataOffset::Triangle},
{"circle", OrbisPadButtonDataOffset::Circle},
{"cross", OrbisPadButtonDataOffset::Cross},
{"square", OrbisPadButtonDataOffset::Square},
{"l1", OrbisPadButtonDataOffset::L1},
{"r1", OrbisPadButtonDataOffset::R1},
{"l3", OrbisPadButtonDataOffset::L3},
{"r3", OrbisPadButtonDataOffset::R3},
{"pad_up", OrbisPadButtonDataOffset::Up},
{"pad_down", OrbisPadButtonDataOffset::Down},
{"pad_left", OrbisPadButtonDataOffset::Left},
{"pad_right", OrbisPadButtonDataOffset::Right},
{"options", OrbisPadButtonDataOffset::Options},
// these are outputs only (touchpad can only be bound to itself)
{"touchpad", OrbisPadButtonDataOffset::TouchPad},
{"leftjoystick_halfmode", (OrbisPadButtonDataOffset)LEFTJOYSTICK_HALFMODE},
{"rightjoystick_halfmode", (OrbisPadButtonDataOffset)RIGHTJOYSTICK_HALFMODE},
// this is only for input
{"back", (OrbisPadButtonDataOffset)BACK_BUTTON},
};
const std::map<std::string, AxisMapping> string_to_axis_map = {
{"axis_left_x_plus", {Input::Axis::LeftX, 127}},
{"axis_left_x_minus", {Input::Axis::LeftX, -127}},
{"axis_left_y_plus", {Input::Axis::LeftY, 127}},
{"axis_left_y_minus", {Input::Axis::LeftY, -127}},
{"axis_right_x_plus", {Input::Axis::RightX, 127}},
{"axis_right_x_minus", {Input::Axis::RightX, -127}},
{"axis_right_y_plus", {Input::Axis::RightY, 127}},
{"axis_right_y_minus", {Input::Axis::RightY, -127}},
{"l2", {Axis::TriggerLeft, 0}},
{"r2", {Axis::TriggerRight, 0}},
// should only use these to bind analog inputs to analog outputs
{"axis_left_x", {Input::Axis::LeftX, 0}},
{"axis_left_y", {Input::Axis::LeftY, 0}},
{"axis_right_x", {Input::Axis::RightX, 0}},
{"axis_right_y", {Input::Axis::RightY, 0}},
};
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},
};
// literally the only flag that needs external access
void ToggleMouseEnabled();
void ParseInputConfig(const std::string game_id);
class InputBinding {
public:
u32 key1, key2, key3;
InputBinding(u32 k1 = SDLK_UNKNOWN, u32 k2 = SDLK_UNKNOWN, u32 k3 = SDLK_UNKNOWN) {
// 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 != SDLK_UNKNOWN) {
k2 = 0;
}
if (k1 == k3 && k1 != SDLK_UNKNOWN) {
k3 = 0;
}
if (k3 == k2 && k2 != SDLK_UNKNOWN) {
k2 = 0;
}
// this sorts them
if (k1 <= k2 && k1 <= k3) {
key1 = k1;
if (k2 <= k3) {
key2 = k2;
key3 = k3;
} else {
key2 = k3;
key3 = k2;
}
} else if (k2 <= k1 && k2 <= k3) {
key1 = k2;
if (k1 <= k3) {
key2 = k1;
key3 = k3;
} else {
key2 = k3;
key3 = k1;
}
} else {
key1 = k3;
if (k1 <= k2) {
key2 = k1;
key3 = k2;
} else {
key2 = k2;
key3 = k1;
}
}
}
// copy ctor
InputBinding(const InputBinding& o) : key1(o.key1), key2(o.key2), key3(o.key3) {}
inline bool operator==(const InputBinding& o) {
// 0 = SDLK_UNKNOWN aka unused slot
return (key3 == o.key3 || key3 == 0 || o.key3 == 0) &&
(key2 == o.key2 || key2 == 0 || o.key2 == 0) &&
(key1 == o.key1 || key1 == 0 || o.key1 == 0);
// 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 (key1 ? 1 : 0) + (key2 ? 1 : 0) + (key3 ? 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 key1 == 0 && key2 == 0 && key3 == 0;
}
std::string ToString() {
return fmt::format("({:X}, {:X}, {:X})", key1, key2, key3);
}
// returns a u32 based on the event type (keyboard, mouse buttons, or wheel)
static u32 GetInputIDFromEvent(const SDL_Event& e);
};
class ControllerOutput {
static GameController* controller;
public:
static void SetControllerOutputController(GameController* c);
OrbisPadButtonDataOffset button;
Axis axis;
s32 old_param, new_param;
bool old_button_state, new_button_state, state_changed;
ControllerOutput(const OrbisPadButtonDataOffset b, Axis a = Axis::AxisMax) {
button = b;
axis = a;
old_param = 0;
}
ControllerOutput(const ControllerOutput& o) : button(o.button), axis(o.axis) {}
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("({}, {}, {})", (u32)button, (int)axis, old_param);
}
inline bool IsButton() const {
return axis == Axis::AxisMax && button != OrbisPadButtonDataOffset::None;
}
inline bool IsAxis() const {
return axis != Axis::AxisMax && button == OrbisPadButtonDataOffset::None;
}
void ResetUpdate();
void AddUpdate(bool pressed, bool analog, u32 param = 0);
void FinalizeUpdate();
};
class BindingConnection {
public:
InputBinding binding;
ControllerOutput* output;
u32 parameter;
BindingConnection(InputBinding b, ControllerOutput* out, u32 param = 0) {
binding = b;
parameter = param;
output = out;
}
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 != Axis::TriggerLeft &&
other.output->axis != Axis::TriggerRight))) {
return true;
}
if (binding < other.binding) {
return true;
}
return false;
}
};
// Updates the list of pressed keys with the given input.
// Returns whether the list was updated or not.
bool UpdatePressedKeys(u32 button, bool is_pressed);
void ActivateOutputsFromInputs();
void UpdateMouse(GameController* controller);
// 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,165 @@
// 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 mouse 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.
Q: Can I rebind controller inputs?
A: Not yet.
)";
}
QString syntax() {
return
R"(This is the full list of currently supported mouse and keyboard 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 (for output only):
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:
'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'
)";
}
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;
)";
}
};

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"
@ -277,6 +281,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,23 +1,26 @@
// 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_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/pad/pad.h"
#include "imgui/renderer/imgui_core.h"
#include "input/controller.h"
#include "input/input_handler.h"
#include "sdl_window.h"
#include "video_core/renderdoc.h"
#ifdef __APPLE__
#include <SDL3/SDL_metal.h>
#include "SDL3/SDL_metal.h"
#endif
namespace Frontend {
@ -120,6 +123,9 @@ 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::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
}
WindowSDL::~WindowSDL() = default;
@ -147,9 +153,13 @@ 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_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
@ -185,6 +195,7 @@ void WindowSDL::WaitEvent() {
void WindowSDL::InitTimers() {
SDL_AddTimer(100, &PollController, controller);
SDL_AddTimer(33, Input::MousePolling, (void*)controller);
}
void WindowSDL::RequestKeyboard() {
@ -207,192 +218,63 @@ void WindowSDL::OnResize() {
ImGui::Core::OnResize();
}
void WindowSDL::OnKeyPress(const SDL_Event* event) {
#ifdef __APPLE__
// Use keys that are more friendly for keyboards without a keypad.
// Once there are key binding options this won't be necessary.
constexpr SDL_Keycode CrossKey = SDLK_N;
constexpr SDL_Keycode CircleKey = SDLK_B;
constexpr SDL_Keycode SquareKey = SDLK_V;
constexpr SDL_Keycode TriangleKey = SDLK_C;
#else
constexpr SDL_Keycode CrossKey = SDLK_KP_2;
constexpr SDL_Keycode CircleKey = SDLK_KP_6;
constexpr SDL_Keycode SquareKey = SDLK_KP_4;
constexpr SDL_Keycode TriangleKey = SDLK_KP_8;
#endif
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;
}
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;
case TriangleKey:
button = OrbisPadButtonDataOffset::Triangle;
break;
case CircleKey:
button = OrbisPadButtonDataOffset::Circle;
break;
case CrossKey:
button = OrbisPadButtonDataOffset::Cross;
break;
case SquareKey:
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;
void WindowSDL::OnKeyboardMouseInput(const SDL_Event* event) {
using Libraries::Pad::OrbisPadButtonDataOffset;
// get the event's id, if it's keyup or keydown
bool input_down = event->type == SDL_EVENT_KEY_DOWN ||
event->type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event->type == SDL_EVENT_MOUSE_WHEEL;
u32 input_id = Input::InputBinding::GetInputIDFromEvent(*event);
// Handle window controls outside of the input maps
if (event->type == SDL_EVENT_KEY_DOWN) {
// 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_id, input_down);
// update bindings
if (inputs_changed) {
Input::ActivateOutputsFromInputs();
}
}

View File

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