diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ec04dad576..cad5407de13 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -796,6 +796,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 @@ -845,6 +847,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 diff --git a/README.md b/README.md index fd40d2d63dc..7c3657065a4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/common/config.cpp b/src/common/config.cpp index b46ab8d6ebc..ea46a549274 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -852,4 +852,109 @@ 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 + +#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) / "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 diff --git a/src/common/config.h b/src/common/config.h index 6e6a5d9609b..2a86eaa89d6 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -140,6 +140,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 diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp new file mode 100644 index 00000000000..015ab600a94 --- /dev/null +++ b/src/input/input_handler.cpp @@ -0,0 +1,708 @@ +// 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> pressed_keys; +std::list toggled_keys; +std::list connections = std::list(); + +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 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.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") { + 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& 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& 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) { + if (pressed_keys.empty()) { + active = false; + return; + } + InputBinding i = connection.binding; + // Extract keys from InputBinding and ignore unused (0) or toggled keys + std::list 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 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 \ No newline at end of file diff --git a/src/input/input_handler.h b/src/input/input_handler.h new file mode 100644 index 00000000000..aa4b6cb5336 --- /dev/null +++ b/src/input/input_handler.h @@ -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 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 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 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 \ No newline at end of file diff --git a/src/qt_gui/kbm_config_dialog.cpp b/src/qt_gui/kbm_config_dialog.cpp new file mode 100644 index 00000000000..af198f79d12 --- /dev/null +++ b/src/qt_gui/kbm_config_dialog.cpp @@ -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 +#include +#include +#include "common/config.h" +#include "common/path_util.h" +#include "game_info.h" +#include "src/sdl_window.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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(); +} diff --git a/src/qt_gui/kbm_config_dialog.h b/src/qt_gui/kbm_config_dialog.h new file mode 100644 index 00000000000..f436b4a7179 --- /dev/null +++ b/src/qt_gui/kbm_config_dialog.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#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 +}; diff --git a/src/qt_gui/kbm_help_dialog.cpp b/src/qt_gui/kbm_help_dialog.cpp new file mode 100644 index 00000000000..44f75f6f873 --- /dev/null +++ b/src/qt_gui/kbm_help_dialog.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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); +} \ No newline at end of file diff --git a/src/qt_gui/kbm_help_dialog.h b/src/qt_gui/kbm_help_dialog.h new file mode 100644 index 00000000000..52c47ce9911 --- /dev/null +++ b/src/qt_gui/kbm_help_dialog.h @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include + +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 to , but your code doesn't support ! +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 + = , , ; + = , ; + = ; + +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, =; is just as valid as = ; +';' 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' = ; + 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' = , ; + 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; +)"; + } +}; \ No newline at end of file diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index bd3c27809e3..0dd2a58a887 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #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); diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 318b3349bf7..e7746f5cb25 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -1,66 +1,31 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include -#include -#include -#include -#include - +#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/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 +#include "SDL3/SDL_metal.h" #endif namespace Frontend { using namespace Libraries::Pad; -static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { - switch (button) { - case SDL_GAMEPAD_BUTTON_DPAD_DOWN: - return OrbisPadButtonDataOffset::Down; - case SDL_GAMEPAD_BUTTON_DPAD_UP: - return OrbisPadButtonDataOffset::Up; - case SDL_GAMEPAD_BUTTON_DPAD_LEFT: - return OrbisPadButtonDataOffset::Left; - case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: - return OrbisPadButtonDataOffset::Right; - case SDL_GAMEPAD_BUTTON_SOUTH: - return OrbisPadButtonDataOffset::Cross; - case SDL_GAMEPAD_BUTTON_NORTH: - return OrbisPadButtonDataOffset::Triangle; - case SDL_GAMEPAD_BUTTON_WEST: - return OrbisPadButtonDataOffset::Square; - case SDL_GAMEPAD_BUTTON_EAST: - return OrbisPadButtonDataOffset::Circle; - case SDL_GAMEPAD_BUTTON_START: - return OrbisPadButtonDataOffset::Options; - case SDL_GAMEPAD_BUTTON_TOUCHPAD: - return OrbisPadButtonDataOffset::TouchPad; - case SDL_GAMEPAD_BUTTON_BACK: - return OrbisPadButtonDataOffset::TouchPad; - case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: - return OrbisPadButtonDataOffset::L1; - case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: - return OrbisPadButtonDataOffset::R1; - case SDL_GAMEPAD_BUTTON_LEFT_STICK: - return OrbisPadButtonDataOffset::L3; - case SDL_GAMEPAD_BUTTON_RIGHT_STICK: - return OrbisPadButtonDataOffset::R3; - default: - return OrbisPadButtonDataOffset::None; - } -} - static Uint32 SDLCALL PollController(void* userdata, SDL_TimerID timer_id, Uint32 interval) { auto* controller = reinterpret_cast(userdata); return controller->Poll(); @@ -136,6 +101,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; @@ -163,18 +131,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_BUTTON_DOWN: - case SDL_EVENT_GAMEPAD_BUTTON_UP: - case SDL_EVENT_GAMEPAD_AXIS_MOTION: case SDL_EVENT_GAMEPAD_ADDED: case SDL_EVENT_GAMEPAD_REMOVED: + controller->TryOpenSDLController(); + 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: OnGamepadEvent(&event); break; // i really would have appreciated ANY KIND OF DOCUMENTATION ON THIS @@ -201,6 +179,7 @@ void WindowSDL::WaitEvent() { void WindowSDL::InitTimers() { SDL_AddTimer(100, &PollController, controller); + SDL_AddTimer(33, Input::MousePolling, (void*)controller); } void WindowSDL::RequestKeyboard() { @@ -223,248 +202,84 @@ 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; - } - 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; - } - 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; - } - 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; +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(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 = {}; + // Toggle mouse capture and movement input + else if (input_id == SDLK_F7) { + Input::ToggleMouseEnabled(); + SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), + !SDL_GetWindowRelativeMouseMode(this->GetSDLWindow())); + return; } - 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); - } + // 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; } - 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(); } } 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->TryOpenSDLController(); - 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 = 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; + + bool input_down = event->type == SDL_EVENT_GAMEPAD_AXIS_MOTION || + event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN; + u32 input_id = Input::InputBinding::GetInputIDFromEvent(*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; } - 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 inputs_changed = Input::UpdatePressedKeys(input_id, input_down); + + if (inputs_changed) { + Input::ActivateOutputsFromInputs(); } } diff --git a/src/sdl_window.h b/src/sdl_window.h index 78d4bbc39f9..1ac97dfe019 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -3,8 +3,9 @@ #pragma once -#include #include "common/types.h" +#include "core/libraries/pad/pad.h" +#include "string" struct SDL_Window; struct SDL_Gamepad; @@ -76,7 +77,7 @@ class WindowSDL { private: void OnResize(); - void OnKeyPress(const SDL_Event* event); + void OnKeyboardMouseInput(const SDL_Event* event); void OnGamepadEvent(const SDL_Event* event); private: