From 5ad25d1b9a4b92fb82f0457e5cb192e4ef595fdb Mon Sep 17 00:00:00 2001 From: nickci2002 <58965309+nickci2002@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:33:52 -0400 Subject: [PATCH] KBM Input Bug Fixes / Added Binds Fixed input issues where some inputs would not bind when pressing (side mouse buttons, some symbols, etc). Also, fixed up code formatting in altered files (removed C-style casts and replaced with C++ , added a few macros and one member functions). --- input_handler.cpp | 718 ++++++++++++++++++++++++++++++ input_handler.h | 439 +++++++++++++++++++ kbm_gui.cpp | 1067 +++++++++++++++++++++++++++++++++++++++++++++ kbm_gui.h | 71 +++ 4 files changed, 2295 insertions(+) create mode 100644 input_handler.cpp create mode 100644 input_handler.h create mode 100644 kbm_gui.cpp create mode 100644 kbm_gui.h diff --git a/input_handler.cpp b/input_handler.cpp new file mode 100644 index 000000000..785e96d41 --- /dev/null +++ b/input_handler.cpp @@ -0,0 +1,718 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "input_handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "input/controller.h" +#include "input/input_mouse.h" + +namespace Input { +/* +Project structure: +n to m connection between inputs and outputs +Keyup and keydown events update a dynamic list* of u32 'flags' (what is currently in the list is +'pressed') On every event, after flag updates, we check for every input binding -> controller output +pair if all their flags are 'on' If not, disable; if so, enable them. For axes, we gather their data +into a struct cumulatively from all inputs, then after we checked all of those, we update them all +at once. Wheel inputs generate a timer that doesn't turn off their outputs automatically, but push a +userevent to do so. + +What structs are needed? +InputBinding(key1, key2, key3) +ControllerOutput(button, axis) - we only need a const array of these, and one of the attr-s is +always 0 BindingConnection(inputBinding (member), controllerOutput (ref to the array element)) + +Things to always test before pushing like a dumbass: +Button outputs +Axis outputs +Input hierarchy +Multi key inputs +Mouse to joystick +Key toggle +Joystick halfmode + +Don't be an idiot and test only the changed part expecting everything else to not be broken +*/ + +bool leftjoystick_halfmode = false, rightjoystick_halfmode = false; +std::pair leftjoystick_deadzone, rightjoystick_deadzone, lefttrigger_deadzone, + righttrigger_deadzone; + +std::list> pressed_keys; +std::list toggled_keys; +static std::vector connections; + +auto output_array = std::array{ + // Important: these have to be the first, or else they will update in the wrong order + ControllerOutput(LEFTJOYSTICK_HALFMODE), + ControllerOutput(RIGHTJOYSTICK_HALFMODE), + ControllerOutput(KEY_TOGGLE), + + // Button mappings + ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle + ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle + ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross + ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square + ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1 + ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3 + ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1 + ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3 + ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options + ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD), // TouchPad + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right + + // Axis mappings + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY), + + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER), + + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID), +}; + +void ControllerOutput::LinkJoystickAxes() { + // for (int i = 17; i < 23; i += 2) { + // delete output_array[i].new_param; + // output_array[i].new_param = output_array[i + 1].new_param; + // } +} + +static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { + using OPBDO = OrbisPadButtonDataOffset; + + switch (button) { + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + return OPBDO::Down; + case SDL_GAMEPAD_BUTTON_DPAD_UP: + return OPBDO::Up; + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + return OPBDO::Left; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + return OPBDO::Right; + case SDL_GAMEPAD_BUTTON_SOUTH: + return OPBDO::Cross; + case SDL_GAMEPAD_BUTTON_NORTH: + return OPBDO::Triangle; + case SDL_GAMEPAD_BUTTON_WEST: + return OPBDO::Square; + case SDL_GAMEPAD_BUTTON_EAST: + return OPBDO::Circle; + case SDL_GAMEPAD_BUTTON_START: + return OPBDO::Options; + case SDL_GAMEPAD_BUTTON_TOUCHPAD: + return OPBDO::TouchPad; + case SDL_GAMEPAD_BUTTON_BACK: + return OPBDO::TouchPad; + case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: + return OPBDO::L1; + case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: + return OPBDO::R1; + case SDL_GAMEPAD_BUTTON_LEFT_STICK: + return OPBDO::L3; + case SDL_GAMEPAD_BUTTON_RIGHT_STICK: + return OPBDO::R3; + default: + return OPBDO::None; + } +} + +Axis GetAxisFromSDLAxis(u8 sdl_axis) { + switch (sdl_axis) { + case SDL_GAMEPAD_AXIS_LEFTX: + return Axis::LeftX; + case SDL_GAMEPAD_AXIS_LEFTY: + return Axis::LeftY; + case SDL_GAMEPAD_AXIS_RIGHTX: + return Axis::RightX; + case SDL_GAMEPAD_AXIS_RIGHTY: + return Axis::RightY; + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: + return Axis::TriggerLeft; + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: + return Axis::TriggerRight; + default: + return Axis::AxisMax; + } +} + +// syntax: 'name, name,name' or 'name,name' or 'name' +InputBinding GetBindingFromString(std::string& line) { + std::array keys = {InputID(), InputID(), InputID()}; + + // Check and process tokens + for (const auto token : std::views::split(line, ',')) { // Split by comma + const std::string t(token.begin(), token.end()); + InputID input; + + if (string_to_keyboard_key_map.find(t) != string_to_keyboard_key_map.end()) { + input = InputID(InputType::KeyboardMouse, string_to_keyboard_key_map.at(t)); + } else if (string_to_axis_map.find(t) != string_to_axis_map.end()) { + input = InputID(InputType::Axis, string_to_axis_map.at(t).axis); + } else if (string_to_cbutton_map.find(t) != string_to_cbutton_map.end()) { + input = InputID(InputType::Controller, string_to_cbutton_map.at(t)); + } else { + // Invalid token found; return default binding + LOG_DEBUG(Input, "Invalid token found: {}", t); + return InputBinding(); + } + + // Assign to the first available slot + for (auto& key : keys) { + if (!key.IsValid()) { + key = input; + break; + } + } + } + LOG_DEBUG(Input, "Parsed line: {}", InputBinding(keys[0], keys[1], keys[2]).ToString()); + return InputBinding(keys[0], keys[1], keys[2]); +} + +void ParseInputConfig(const std::string game_id = "") { + std::string config_type = Config::GetUseUnifiedInputConfig() ? "default" : game_id; + const auto config_file = Config::GetFoolproofKbmConfigFile(config_type); + + // we reset these here so in case the user fucks up or doesn't include some of these, + // we can fall back to default + connections.clear(); + float mouse_deadzone_offset = 0.5; + float mouse_speed = 1; + float mouse_speed_offset = 0.125; + + leftjoystick_deadzone = {1, 127}; + rightjoystick_deadzone = {1, 127}; + lefttrigger_deadzone = {1, 127}; + righttrigger_deadzone = {1, 127}; + + Config::SetOverrideControllerColor(false); + Config::SetControllerCustomColor(0, 0, 255); + + int lineCount = 0; + + std::ifstream file(config_file); + std::string line = ""; + while (std::getline(file, line)) { + lineCount++; + + // Strip the ; and whitespace + line.erase(std::remove_if(line.begin(), line.end(), + [](unsigned char c) { return std::isspace(c); }), + line.end()); + + if (line.empty()) { + continue; + } + // Truncate lines starting at # + std::size_t comment_pos = line.find('#'); + if (comment_pos != std::string::npos) { + line = line.substr(0, comment_pos); + } + // Remove trailing semicolon + if (!line.empty() && line[line.length() - 1] == ';') { + line = line.substr(0, line.length() - 1); + } + if (line.empty()) { + continue; + } + + // Split the line by '=' + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) { + LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.", + lineCount, line); + continue; + } + + std::string output_string = line.substr(0, equal_pos); + std::string input_string = line.substr(equal_pos + 1); + std::size_t comma_pos = input_string.find(','); + + auto parseInt = [](const std::string& s) -> std::optional { + try { + return std::stoi(s); + } catch (...) { + return std::nullopt; + } + }; + + if (output_string == "mouse_to_joystick") { + if (input_string == "left") { + SetMouseToJoystick(1); + } else if (input_string == "right") { + SetMouseToJoystick(2); + } else { + LOG_WARNING(Input, "Invalid argument for mouse-to-joystick binding"); + SetMouseToJoystick(0); + } + continue; + } else if (output_string == "key_toggle") { + if (comma_pos != std::string::npos) { + // handle key-to-key toggling (separate list?) + InputBinding toggle_keys = GetBindingFromString(input_string); + if (toggle_keys.KeyCount() != 2) { + LOG_WARNING(Input, + "Syntax error: Please provide exactly 2 keys: " + "first is the toggler, the second is the key to toggle: {}", + line); + continue; + } + ControllerOutput* toggle_out = + &*std::ranges::find(output_array, ControllerOutput(KEY_TOGGLE)); + BindingConnection toggle_connection = BindingConnection( + InputBinding(toggle_keys.keys[0]), toggle_out, 0, toggle_keys.keys[1]); + connections.insert(connections.end(), toggle_connection); + continue; + } + LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.", + lineCount, line); + continue; + } else if (output_string == "mouse_movement_params") { + std::stringstream ss(input_string); + char comma; // To hold the comma separators between the floats + ss >> mouse_deadzone_offset >> comma >> mouse_speed >> comma >> mouse_speed_offset; + + // Check for invalid input (in case there's an unexpected format) + if (ss.fail()) { + LOG_WARNING(Input, "Failed to parse mouse movement parameters from line: {}", line); + continue; + } + SetMouseParams(mouse_deadzone_offset, mouse_speed, mouse_speed_offset); + continue; + } else if (output_string == "analog_deadzone") { + std::stringstream ss(input_string); + std::string device, inner_deadzone_str, outer_deadzone_str; + + if (!std::getline(ss, device, ',') || !std::getline(ss, inner_deadzone_str, ',') || + !std::getline(ss, outer_deadzone_str)) { + LOG_WARNING(Input, "Malformed deadzone config at line {}: \"{}\"", lineCount, line); + continue; + } + + auto inner_deadzone = parseInt(inner_deadzone_str); + auto outer_deadzone = parseInt(outer_deadzone_str); + + if (!inner_deadzone || !outer_deadzone) { + LOG_WARNING(Input, "Invalid deadzone values at line {}: \"{}\"", lineCount, line); + continue; + } + + std::pair deadzone = {*inner_deadzone, *outer_deadzone}; + + static std::unordered_map&> deadzone_map = { + {"leftjoystick", leftjoystick_deadzone}, + {"rightjoystick", rightjoystick_deadzone}, + {"l2", lefttrigger_deadzone}, + {"r2", righttrigger_deadzone}, + }; + + if (auto it = deadzone_map.find(device); it != deadzone_map.end()) { + it->second = deadzone; + LOG_DEBUG(Input, "Parsed deadzone: {} {} {}", device, inner_deadzone_str, + outer_deadzone_str); + } else { + LOG_WARNING(Input, "Invalid axis name at line {}: \"{}\", skipping line.", + lineCount, line); + } + continue; + } else if (output_string == "override_controller_color") { + std::stringstream ss(input_string); + std::string enable, r_s, g_s, b_s; + std::optional r, g, b; + if (!std::getline(ss, enable, ',') || !std::getline(ss, r_s, ',') || + !std::getline(ss, g_s, ',') || !std::getline(ss, b_s)) { + LOG_WARNING(Input, "Malformed controller color config at line {}: \"{}\"", + lineCount, line); + continue; + } + r = parseInt(r_s); + g = parseInt(g_s); + b = parseInt(b_s); + if (!r || !g || !b) { + LOG_WARNING(Input, "Invalid RGB values at line {}: \"{}\", skipping line.", + lineCount, line); + continue; + } + Config::SetOverrideControllerColor(enable == "true"); + Config::SetControllerCustomColor(*r, *g, *b); + LOG_DEBUG(Input, "Parsed color settings: {} {} {} {}", + enable == "true" ? "override" : "no override", *r, *b, *g); + continue; + } + + // normal cases + InputBinding binding = GetBindingFromString(input_string); + BindingConnection connection(InputID(), nullptr); + auto button_it = string_to_cbutton_map.find(output_string); + auto axis_it = string_to_axis_map.find(output_string); + + if (binding.IsEmpty()) { + LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.", + lineCount, line); + continue; + } + if (button_it != string_to_cbutton_map.end()) { + connection = BindingConnection( + binding, &*std::ranges::find(output_array, ControllerOutput(button_it->second))); + connections.insert(connections.end(), connection); + + } else if (axis_it != string_to_axis_map.end()) { + int value_to_set = binding.keys[2].type == InputType::Axis ? 0 : axis_it->second.value; + connection = BindingConnection( + binding, + &*std::ranges::find(output_array, ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, + axis_it->second.axis, + axis_it->second.value >= 0)), + value_to_set); + connections.insert(connections.end(), connection); + } else { + LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.", + lineCount, line); + continue; + } + LOG_DEBUG(Input, "Succesfully parsed line {}", lineCount); + } + file.close(); + std::sort(connections.begin(), connections.end()); + for (auto& c : connections) { + LOG_DEBUG(Input, "Binding: {} : {}", c.output->ToString(), c.binding.ToString()); + } + LOG_DEBUG(Input, "Done parsing the input config!"); +} + +u32 GetMouseWheelEvent(const SDL_Event& event) { + if (event.type != SDL_EVENT_MOUSE_WHEEL && event.type != SDL_EVENT_MOUSE_WHEEL_OFF) { + LOG_WARNING(Input, "Something went wrong with wheel input parsing!"); + return UINT32_MAX; + } + 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 UINT32_MAX; +} + +InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) { + switch (e.type) { + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + return InputEvent(InputType::KeyboardMouse, e.key.key, e.key.down, 0); + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + return InputEvent(InputType::KeyboardMouse, static_cast(e.button.button), + e.button.down, 0); + case SDL_EVENT_MOUSE_WHEEL: + case SDL_EVENT_MOUSE_WHEEL_OFF: + return InputEvent(InputType::KeyboardMouse, GetMouseWheelEvent(e), + e.type == SDL_EVENT_MOUSE_WHEEL, 0); + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + return InputEvent(InputType::Controller, static_cast(e.gbutton.button), + e.gbutton.down, 0); + case SDL_EVENT_GAMEPAD_AXIS_MOTION: + return InputEvent(InputType::Axis, static_cast(e.gaxis.axis), + true, e.gaxis.value / 256); + default: + return InputEvent(); + } +} + +GameController* ControllerOutput::controller = nullptr; +void ControllerOutput::SetControllerOutputController(GameController* c) { + ControllerOutput::controller = c; +} + +void ToggleKeyInList(InputID input) { + if (input.type == InputType::Axis) { + LOG_ERROR(Input, "Toggling analog inputs is not supported!"); + return; + } + auto it = std::find(toggled_keys.begin(), toggled_keys.end(), input); + if (it == toggled_keys.end()) { + toggled_keys.insert(toggled_keys.end(), input); + LOG_DEBUG(Input, "Added {} to toggled keys", input.ToString()); + } else { + toggled_keys.erase(it); + LOG_DEBUG(Input, "Removed {} from toggled keys", input.ToString()); + } +} + +void ControllerOutput::ResetUpdate() { + state_changed = false; + new_button_state = false; + *new_param = 0; // bruh +} +void ControllerOutput::AddUpdate(InputEvent event) { + if (button == KEY_TOGGLE) { + if (event.active) { + ToggleKeyInList(event.input); + } + } else if (button != SDL_GAMEPAD_BUTTON_INVALID) { + if (event.input.type == InputType::Axis) { + bool temp = event.axis_value * (positive_axis ? 1 : -1) > 0x40; + new_button_state |= event.active && event.axis_value * (positive_axis ? 1 : -1) > 0x40; + if (temp) { + LOG_DEBUG(Input, "Toggled a button from an axis"); + } + } else { + new_button_state |= event.active; + } + + } else if (axis != SDL_GAMEPAD_AXIS_INVALID) { + *new_param = (event.active ? event.axis_value : 0) + *new_param; + } +} +void ControllerOutput::FinalizeUpdate() { + state_changed = old_button_state != new_button_state || old_param != *new_param; + if (!state_changed) { + return; + } + old_button_state = new_button_state; + old_param = *new_param; + float touchpad_x = 0; + if (button != SDL_GAMEPAD_BUTTON_INVALID) { + switch (button) { + case SDL_GAMEPAD_BUTTON_TOUCHPAD: + touchpad_x = Config::getBackButtonBehavior() == "left" ? 0.25f + : Config::getBackButtonBehavior() == "right" ? 0.75f + : 0.5f; + controller->SetTouchpadState(0, new_button_state, touchpad_x, 0.5f); + controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state); + break; + case LEFTJOYSTICK_HALFMODE: + leftjoystick_halfmode = new_button_state; + break; + case RIGHTJOYSTICK_HALFMODE: + rightjoystick_halfmode = new_button_state; + break; + // KEY_TOGGLE isn't handled here anymore, as this function doesn't have the necessary data + // to do it, and it would be inconvenient to force it here, when AddUpdate does the job just + // fine, and a toggle doesn't have to checked against every input that's bound to it, it's + // enough that one is pressed + default: // is a normal key (hopefully) + controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state); + break; + } + } else if (axis != SDL_GAMEPAD_AXIS_INVALID && positive_axis) { + // avoid double-updating axes, but don't skip directional button bindings + auto ApplyDeadzone = [](s16* value, std::pair deadzone) { + if (std::abs(*value) <= deadzone.first || deadzone.first == deadzone.second) { + *value = 0; + } else { + *value = (*value >= 0 ? 1 : -1) * + std::clamp((int)((128.0 * (std::abs(*value) - deadzone.first)) / + (float)(deadzone.second - deadzone.first)), + 0, 128); + } + }; + float multiplier = 1.0; + Axis c_axis = GetAxisFromSDLAxis(axis); + switch (c_axis) { + case Axis::LeftX: + case Axis::LeftY: + ApplyDeadzone(new_param, leftjoystick_deadzone); + multiplier = leftjoystick_halfmode ? 0.5 : 1.0; + break; + case Axis::RightX: + case Axis::RightY: + ApplyDeadzone(new_param, rightjoystick_deadzone); + multiplier = rightjoystick_halfmode ? 0.5 : 1.0; + break; + case Axis::TriggerLeft: + ApplyDeadzone(new_param, lefttrigger_deadzone); + controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param)); + controller->CheckButton(0, OrbisPadButtonDataOffset::L2, *new_param > 0x20); + return; + case Axis::TriggerRight: + ApplyDeadzone(new_param, righttrigger_deadzone); + controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param)); + controller->CheckButton(0, OrbisPadButtonDataOffset::R2, *new_param > 0x20); + return; + default: + break; + } + controller->Axis(0, c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier)); + } +} + +// Updates the list of pressed keys with the given input. +// Returns whether the list was updated or not. +bool UpdatePressedKeys(InputEvent event) { + // Skip invalid inputs + InputID input = event.input; + if (input.sdl_id == UINT32_MAX) { + return false; + } + if (input.type == InputType::Axis) { + // analog input, it gets added when it first sends an event, + // and from there, it only changes the parameter + auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input, + [](const std::pair& e, InputID i) { + return std::tie(e.first.input.type, e.first.input.sdl_id) < + std::tie(i.type, i.sdl_id); + }); + if (it == pressed_keys.end() || it->first.input != input) { + pressed_keys.insert(it, {event, false}); + LOG_DEBUG(Input, "Added axis {} to the input list", event.input.sdl_id); + } else { + // noise filter + if (std::abs(it->first.axis_value - event.axis_value) <= 1) { + return false; + } + it->first.axis_value = event.axis_value; + } + return true; + } else if (event.active) { + // Find the correct position for insertion to maintain order + auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input, + [](const std::pair& e, InputID i) { + return std::tie(e.first.input.type, e.first.input.sdl_id) < + std::tie(i.type, i.sdl_id); + }); + + // Insert only if 'value' is not already in the list + if (it == pressed_keys.end() || it->first.input != input) { + pressed_keys.insert(it, {event, false}); + return true; + } + } else { + // Remove 'value' from the list if it's not pressed + auto it = std::find_if( + pressed_keys.begin(), pressed_keys.end(), + [input](const std::pair& e) { return e.first.input == input; }); + if (it != pressed_keys.end()) { + pressed_keys.erase(it); + return true; + } + } + LOG_DEBUG(Input, "No change was made!"); + return false; +} +// Check if the binding's all keys are currently active. +// It also extracts the analog inputs' parameters, and updates the input hierarchy flags. +InputEvent BindingConnection::ProcessBinding() { + // the last key is always set (if the connection isn't empty), + // and the analog inputs are always the last one due to how they are sorted, + // so this signifies whether or not the input is analog + InputEvent event = InputEvent(binding.keys[0]); + if (pressed_keys.empty()) { + return event; + } + if (event.input.type != InputType::Axis) { + // for button inputs + event.axis_value = axis_param; + } + // it's a bit scuffed, but if the output is a toggle, then we put the key here + if (output->button == KEY_TOGGLE) { + event.input = toggle; + } + + // Extract keys from InputBinding and ignore unused or toggled keys + std::list input_keys = {binding.keys[0], binding.keys[1], binding.keys[2]}; + input_keys.remove(InputID()); + for (auto key = input_keys.begin(); key != input_keys.end();) { + if (std::find(toggled_keys.begin(), toggled_keys.end(), *key) != toggled_keys.end()) { + key = input_keys.erase(key); // Use the returned iterator + } else { + ++key; // Increment only if no erase happened + } + } + if (input_keys.empty()) { + LOG_DEBUG(Input, "No actual inputs to check, returning true"); + event.active = true; + return event; + } + + // Iterator for pressed_keys, starting from the beginning + auto pressed_it = pressed_keys.begin(); + + // Store pointers to flags in pressed_keys that need to be set if all keys are active + std::list flags_to_set; + + // Check if all keys in input_keys are active + for (InputID key : input_keys) { + bool key_found = false; + + while (pressed_it != pressed_keys.end()) { + if (pressed_it->first.input == key && (pressed_it->second == false)) { + key_found = true; + if (output->positive_axis) { + flags_to_set.push_back(&pressed_it->second); + } + if (pressed_it->first.input.type == InputType::Axis) { + event.axis_value = pressed_it->first.axis_value; + } + ++pressed_it; + break; + } + ++pressed_it; + } + if (!key_found) { + return event; + } + } + + for (bool* flag : flags_to_set) { + *flag = true; + } + if (binding.keys[0].type != InputType::Axis) { // the axes spam inputs, making this unreadable + LOG_DEBUG(Input, "Input found: {}", binding.ToString()); + } + event.active = true; + return event; // All keys are active +} + +void ActivateOutputsFromInputs() { + // Reset values and flags + for (auto& it : pressed_keys) { + it.second = false; + } + for (auto& it : output_array) { + it.ResetUpdate(); + } + + // Iterate over all inputs, and update their respecive outputs accordingly + for (auto& it : connections) { + it.output->AddUpdate(it.ProcessBinding()); + } + + // Update all outputs + for (auto& it : output_array) { + it.FinalizeUpdate(); + } +} + +} // namespace Input diff --git a/input_handler.h b/input_handler.h new file mode 100644 index 000000000..f369cb94b --- /dev/null +++ b/input_handler.h @@ -0,0 +1,439 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "SDL3/SDL_events.h" +#include "SDL3/SDL_timer.h" + +#include "common/logging/log.h" +#include "common/types.h" +#include "core/libraries/pad/pad.h" +#include "fmt/format.h" +#include "input/controller.h" + +// +1 and +2 is taken +#define SDL_MOUSE_WHEEL_UP SDL_EVENT_MOUSE_WHEEL + 3 +#define SDL_MOUSE_WHEEL_DOWN SDL_EVENT_MOUSE_WHEEL + 4 +#define SDL_MOUSE_WHEEL_LEFT SDL_EVENT_MOUSE_WHEEL + 5 +#define SDL_MOUSE_WHEEL_RIGHT SDL_EVENT_MOUSE_WHEEL + 7 + +// idk who already used what where so I just chose a big number +#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 10 + +#define LEFTJOYSTICK_HALFMODE 0x00010000 +#define RIGHTJOYSTICK_HALFMODE 0x00020000 +#define BACK_BUTTON 0x00040000 + +#define KEY_TOGGLE 0x00200000 + +namespace Input { +using Input::Axis; +using Libraries::Pad::OrbisPadButtonDataOffset; + +struct AxisMapping { + u32 axis; + s16 value; + AxisMapping(SDL_GamepadAxis a, s16 v) : axis(a), value(v) {} +}; + +enum class InputType { Axis, KeyboardMouse, Controller, Count }; +const std::array input_type_names = {"Axis", "KBM", "Controller", "Unknown"}; + +class InputID { +public: + InputType type; + u32 sdl_id; + InputID(InputType d = InputType::Count, u32 i = UINT32_MAX) : type(d), sdl_id(i) {} + bool operator==(const InputID& o) const { + return type == o.type && sdl_id == o.sdl_id; + } + bool operator!=(const InputID& o) const { + return type != o.type || sdl_id != o.sdl_id; + } + bool operator<=(const InputID& o) const { + return type <= o.type && sdl_id <= o.sdl_id; + } + bool IsValid() const { + return *this != InputID(); + } + std::string ToString() { + return fmt::format("({}: {:x})", input_type_names[static_cast(type)], sdl_id); + } +}; + +class InputEvent { +public: + InputID input; + bool active; + s8 axis_value; + + InputEvent(InputID i = InputID(), bool a = false, s8 v = 0) + : input(i), active(a), axis_value(v) {} + InputEvent(InputType d, u32 i, bool a = false, s8 v = 0) + : input(d, i), active(a), axis_value(v) {} +}; + +// i strongly suggest you collapse these maps +const std::map string_to_cbutton_map = { + {"triangle", SDL_GAMEPAD_BUTTON_NORTH}, + {"circle", SDL_GAMEPAD_BUTTON_EAST}, + {"cross", SDL_GAMEPAD_BUTTON_SOUTH}, + {"square", SDL_GAMEPAD_BUTTON_WEST}, + {"l1", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER}, + {"r1", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER}, + {"l3", SDL_GAMEPAD_BUTTON_LEFT_STICK}, + {"r3", SDL_GAMEPAD_BUTTON_RIGHT_STICK}, + {"pad_up", SDL_GAMEPAD_BUTTON_DPAD_UP}, + {"pad_down", SDL_GAMEPAD_BUTTON_DPAD_DOWN}, + {"pad_left", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, + {"pad_right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT}, + {"options", SDL_GAMEPAD_BUTTON_START}, + + // these are outputs only (touchpad can only be bound to itself) + {"touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD}, + {"leftjoystick_halfmode", LEFTJOYSTICK_HALFMODE}, + {"rightjoystick_halfmode", RIGHTJOYSTICK_HALFMODE}, + + // this is only for input + {"back", SDL_GAMEPAD_BUTTON_BACK}, +}; + +const std::map string_to_axis_map = { + {"axis_left_x_plus", {SDL_GAMEPAD_AXIS_LEFTX, 127}}, + {"axis_left_x_minus", {SDL_GAMEPAD_AXIS_LEFTX, -127}}, + {"axis_left_y_plus", {SDL_GAMEPAD_AXIS_LEFTY, 127}}, + {"axis_left_y_minus", {SDL_GAMEPAD_AXIS_LEFTY, -127}}, + {"axis_right_x_plus", {SDL_GAMEPAD_AXIS_RIGHTX, 127}}, + {"axis_right_x_minus", {SDL_GAMEPAD_AXIS_RIGHTX, -127}}, + {"axis_right_y_plus", {SDL_GAMEPAD_AXIS_RIGHTY, 127}}, + {"axis_right_y_minus", {SDL_GAMEPAD_AXIS_RIGHTY, -127}}, + + {"l2", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 127}}, + {"r2", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 127}}, + + // should only use these to bind analog inputs to analog outputs + {"axis_left_x", {SDL_GAMEPAD_AXIS_LEFTX, 127}}, + {"axis_left_y", {SDL_GAMEPAD_AXIS_LEFTY, 127}}, + {"axis_right_x", {SDL_GAMEPAD_AXIS_RIGHTX, 127}}, + {"axis_right_y", {SDL_GAMEPAD_AXIS_RIGHTY, 127}}, +}; +const std::map string_to_keyboard_key_map = { + // alphanumeric + {"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}, + + // symbols + {"`", SDLK_GRAVE}, + {"~", SDLK_TILDE}, + {"!", SDLK_EXCLAIM}, + {"@", SDLK_AT}, + {"#", SDLK_HASH}, + {"$", SDLK_DOLLAR}, + {"%", SDLK_PERCENT}, + {"^", SDLK_CARET}, + {"&", SDLK_AMPERSAND}, + {"*", SDLK_ASTERISK}, + {"(", SDLK_LEFTPAREN}, + {")", SDLK_RIGHTPAREN}, + {"-", SDLK_MINUS}, + {"_", SDLK_UNDERSCORE}, + {"=", SDLK_EQUALS}, + {"+", SDLK_PLUS}, + {"[", SDLK_LEFTBRACKET}, + {"]", SDLK_RIGHTBRACKET}, + {"{", SDLK_LEFTBRACE}, + {"}", SDLK_RIGHTBRACE}, + {"\\", SDLK_BACKSLASH}, + {"|", SDLK_PIPE}, + {";", SDLK_SEMICOLON}, + {":", SDLK_COLON}, + {"'", SDLK_APOSTROPHE}, + {"\"", SDLK_DBLAPOSTROPHE}, + {",", SDLK_COMMA}, + {"<", SDLK_LESS}, + {".", SDLK_PERIOD}, + {">", SDLK_GREATER}, + {"/", SDLK_SLASH}, + {"?", SDLK_QUESTION}, + + // special keys + {"printscreen", SDLK_PRINTSCREEN}, + {"scrolllock", SDLK_SCROLLLOCK}, + {"pausebreak", SDLK_PAUSE}, + {"backspace", SDLK_BACKSPACE}, + {"delete", SDLK_DELETE}, + {"insert", SDLK_INSERT}, + {"home", SDLK_HOME}, + {"end", SDLK_END}, + {"pgup", SDLK_PAGEUP}, + {"pgdown", SDLK_PAGEDOWN}, + {"tab", SDLK_TAB}, + {"capslock", SDLK_CAPSLOCK}, + {"enter", SDLK_RETURN}, + {"lshift", SDLK_LSHIFT}, + {"rshift", SDLK_RSHIFT}, + {"lctrl", SDLK_LCTRL}, + {"rctrl", SDLK_RCTRL}, + {"lalt", SDLK_LALT}, + {"ralt", SDLK_RALT}, + {"lmeta", SDLK_LGUI}, + {"rmeta", SDLK_RGUI}, + {"lwin", SDLK_LGUI}, + {"rwin", SDLK_RGUI}, + {"space", SDLK_SPACE}, + {"up", SDLK_UP}, + {"down", SDLK_DOWN}, + {"left", SDLK_LEFT}, + {"right", SDLK_RIGHT}, + + // keypad + {"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}, + {"kp .", SDLK_KP_PERIOD}, + {"kp ,", SDLK_KP_COMMA}, + {"kp /", SDLK_KP_DIVIDE}, + {"kp *", SDLK_KP_MULTIPLY}, + {"kp -", SDLK_KP_MINUS}, + {"kp +", SDLK_KP_PLUS}, + {"kp =", SDLK_KP_EQUALS}, + {"kp enter", SDLK_KP_ENTER}, + + // mouse + {"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}, +}; + +void ParseInputConfig(const std::string game_id); + +class InputBinding { +public: + InputID keys[3]; + InputBinding(InputID k1 = InputID(), InputID k2 = InputID(), InputID k3 = InputID()) { + // we format the keys so comparing them will be very fast, because we will only have to + // compare 3 sorted elements, where the only possible duplicate item is 0 + + // duplicate entries get changed to one original, one null + if (k1 == k2 && k1 != InputID()) { + k2 = InputID(); + } + if (k1 == k3 && k1 != InputID()) { + k3 = InputID(); + } + if (k3 == k2 && k2 != InputID()) { + k2 = InputID(); + } + // this sorts them + if (k1 <= k2 && k1 <= k3) { + keys[0] = k1; + if (k2 <= k3) { + keys[1] = k2; + keys[2] = k3; + } else { + keys[1] = k3; + keys[2] = k2; + } + } else if (k2 <= k1 && k2 <= k3) { + keys[0] = k2; + if (k1 <= k3) { + keys[1] = k1; + keys[2] = k3; + } else { + keys[1] = k3; + keys[2] = k1; + } + } else { + keys[0] = k3; + if (k1 <= k2) { + keys[1] = k1; + keys[2] = k2; + } else { + keys[1] = k2; + keys[3] = k1; + } + } + } + // copy ctor + InputBinding(const InputBinding& o) { + keys[0] = o.keys[0]; + keys[1] = o.keys[1]; + keys[2] = o.keys[2]; + } + + inline bool operator==(const InputBinding& o) { + // InputID() signifies an unused slot + return (keys[0] == o.keys[0] || keys[0] == InputID() || o.keys[0] == InputID()) && + (keys[1] == o.keys[1] || keys[1] == InputID() || o.keys[1] == InputID()) && + (keys[2] == o.keys[2] || keys[2] == InputID() || o.keys[2] == InputID()); + // it is already very fast, + // but reverse order makes it check the actual keys first instead of possible 0-s, + // potenially skipping the later expressions of the three-way AND + } + inline int KeyCount() const { + return (keys[0].IsValid() ? 1 : 0) + (keys[1].IsValid() ? 1 : 0) + + (keys[2].IsValid() ? 1 : 0); + } + // Sorts by the amount of non zero keys - left side is 'bigger' here + bool operator<(const InputBinding& other) const { + return KeyCount() > other.KeyCount(); + } + inline bool IsEmpty() { + return !(keys[0].IsValid() || keys[1].IsValid() || keys[2].IsValid()); + } + std::string ToString() { // todo add device type + switch (KeyCount()) { + case 1: + return fmt::format("({})", keys[0].ToString()); + case 2: + return fmt::format("({}, {})", keys[0].ToString(), keys[1].ToString()); + case 3: + return fmt::format("({}, {}, {})", keys[0].ToString(), keys[1].ToString(), + keys[2].ToString()); + default: + return "Empty"; + } + } + + // returns an InputEvent based on the event type (keyboard, mouse buttons/wheel, or controller) + static InputEvent GetInputEventFromSDLEvent(const SDL_Event& e); +}; + +class ControllerOutput { + static GameController* controller; + +public: + static void SetControllerOutputController(GameController* c); + static void LinkJoystickAxes(); + + u32 button; + u32 axis; + // these are only used as s8, + // but I added some padding to avoid overflow if it's activated by multiple inputs + // axis_plus and axis_minus pairs share a common new_param, the other outputs have their own + s16 old_param; + s16* new_param; + bool old_button_state, new_button_state, state_changed, positive_axis; + + ControllerOutput(const u32 b, u32 a = SDL_GAMEPAD_AXIS_INVALID, bool p = true) { + button = b; + axis = a; + new_param = new s16(0); + old_param = 0; + positive_axis = p; + } + ControllerOutput(const ControllerOutput& o) : button(o.button), axis(o.axis) { + new_param = new s16(*o.new_param); + } + ~ControllerOutput() { + delete new_param; + } + inline bool operator==(const ControllerOutput& o) const { // fucking consts everywhere + return button == o.button && axis == o.axis; + } + inline bool operator!=(const ControllerOutput& o) const { + return button != o.button || axis != o.axis; + } + std::string ToString() const { + return fmt::format("({}, {}, {})", (s32)button, (int)axis, old_param); + } + inline bool IsButton() const { + return axis == SDL_GAMEPAD_AXIS_INVALID && button != SDL_GAMEPAD_BUTTON_INVALID; + } + inline bool IsAxis() const { + return axis != SDL_GAMEPAD_AXIS_INVALID && button == SDL_GAMEPAD_BUTTON_INVALID; + } + + void ResetUpdate(); + void AddUpdate(InputEvent event); + void FinalizeUpdate(); +}; +class BindingConnection { +public: + InputBinding binding; + ControllerOutput* output; + u32 axis_param; + InputID toggle; + + BindingConnection(InputBinding b, ControllerOutput* out, u32 param = 0, InputID t = InputID()) { + binding = b; + axis_param = param; + output = out; + toggle = t; + } + bool operator<(const BindingConnection& other) const { + // a button is a higher priority than an axis, as buttons can influence axes + // (e.g. joystick_halfmode) + if (output->IsButton() && + (other.output->IsAxis() && (other.output->axis != SDL_GAMEPAD_AXIS_LEFT_TRIGGER && + other.output->axis != SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))) { + return true; + } + if (binding < other.binding) { + return true; + } + return false; + } + InputEvent ProcessBinding(); +}; + +// Updates the list of pressed keys with the given input. +// Returns whether the list was updated or not. +bool UpdatePressedKeys(InputEvent event); + +void ActivateOutputsFromInputs(); + +} // namespace Input diff --git a/kbm_gui.cpp b/kbm_gui.cpp new file mode 100644 index 000000000..d19ac13d0 --- /dev/null +++ b/kbm_gui.cpp @@ -0,0 +1,1067 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include "common/path_util.h" +#include "kbm_config_dialog.h" +#include "kbm_gui.h" +#include "kbm_help_dialog.h" +#include "ui_kbm_gui.h" + +HelpDialog* HelpWindow; +KBMSettings::KBMSettings(std::shared_ptr game_info_get, QWidget* parent) + : QDialog(parent), m_game_info(game_info_get), ui(new Ui::KBMSettings) { + + ui->setupUi(this); + ui->PerGameCheckBox->setChecked(!Config::GetUseUnifiedInputConfig()); + ui->TextEditorButton->setFocus(); + this->setFocusPolicy(Qt::StrongFocus); + + ui->MouseJoystickBox->addItem("none"); + ui->MouseJoystickBox->addItem("right"); + ui->MouseJoystickBox->addItem("left"); + + ui->ProfileComboBox->addItem("Common Config"); + for (int i = 0; i < m_game_info->m_games.size(); i++) { + ui->ProfileComboBox->addItem(QString::fromStdString(m_game_info->m_games[i].serial)); + } + + ButtonsList = { + ui->CrossButton, ui->CircleButton, ui->TriangleButton, ui->SquareButton, + ui->L1Button, ui->R1Button, ui->L2Button, ui->R2Button, + ui->L3Button, ui->R3Button, ui->TouchpadButton, ui->OptionsButton, + ui->TouchpadButton, ui->DpadUpButton, ui->DpadDownButton, ui->DpadLeftButton, + ui->DpadRightButton, ui->LStickUpButton, ui->LStickDownButton, ui->LStickLeftButton, + ui->LStickRightButton, ui->RStickUpButton, ui->RStickDownButton, ui->RStickLeftButton, + ui->RStickRightButton, ui->LHalfButton, ui->RHalfButton}; + + ButtonConnects(); + SetUIValuestoMappings("default"); + installEventFilter(this); + + ui->ProfileComboBox->setCurrentText("Common Config"); + ui->TitleLabel->setText("Common Config"); + config_id = "default"; + + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton* button) { + if (button == ui->buttonBox->button(QDialogButtonBox::Save)) { + if (HelpWindowOpen) { + HelpWindow->close(); + HelpWindowOpen = false; + } + SaveKBMConfig(true); + } else if (button == ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)) { + SetDefault(); + } else if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) { + SaveKBMConfig(false); + } + }); + + ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save")); + ui->buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Apply")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(tr("Restore Defaults")); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + + connect(ui->HelpButton, &QPushButton::clicked, this, &KBMSettings::onHelpClicked); + connect(ui->TextEditorButton, &QPushButton::clicked, this, [this]() { + auto kbmWindow = new EditorDialog(this); + kbmWindow->exec(); + SetUIValuestoMappings(config_id); + }); + + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, [this] { + QWidget::close(); + if (HelpWindowOpen) { + HelpWindow->close(); + HelpWindowOpen = false; + } + }); + + connect(ui->ProfileComboBox, &QComboBox::currentTextChanged, this, [this] { + GetGameTitle(); + SetUIValuestoMappings(config_id); + }); + + connect(ui->CopyCommonButton, &QPushButton::clicked, this, [this] { + if (ui->ProfileComboBox->currentText() == "Common Config") { + QMessageBox::information(this, tr("Common Config Selected"), + // clang-format off +tr("This button copies mappings from the Common Config to the currently selected profile, and cannot be used when the currently selected profile is the Common Config.")); + // clang-format on + } else { + QMessageBox::StandardButton reply = + QMessageBox::question(this, tr("Copy values from Common Config"), + // clang-format off +tr("Do you want to overwrite existing mappings with the mappings from the Common Config?"), + // clang-format on + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) { + SetUIValuestoMappings("default"); + } + } + }); + + connect(ui->DeadzoneOffsetSlider, &QSlider::valueChanged, this, [this](int value) { + QString DOSValue = QString::number(value / 100.0, 'f', 2); + QString DOSString = tr("Deadzone Offset (def 0.50):") + " " + DOSValue; + ui->DeadzoneOffsetLabel->setText(DOSString); + }); + + connect(ui->SpeedMultiplierSlider, &QSlider::valueChanged, this, [this](int value) { + QString SMSValue = QString::number(value / 10.0, 'f', 1); + QString SMSString = tr("Speed Multiplier (def 1.0):") + " " + SMSValue; + ui->SpeedMultiplierLabel->setText(SMSString); + }); + + connect(ui->SpeedOffsetSlider, &QSlider::valueChanged, this, [this](int value) { + QString SOSValue = QString::number(value / 1000.0, 'f', 3); + QString SOSString = tr("Speed Offset (def 0.125):") + " " + SOSValue; + ui->SpeedOffsetLabel->setText(SOSString); + }); +} + +void KBMSettings::ButtonConnects() { + for (auto& button : ButtonsList) { + connect(button, &QPushButton::clicked, this, [this, &button]() { StartTimer(button); }); + } +} + +void KBMSettings::DisableMappingButtons() { + for (const auto& i : ButtonsList) { + i->setEnabled(false); + } +} + +void KBMSettings::EnableMappingButtons() { + for (const auto& i : ButtonsList) { + i->setEnabled(true); + } +} + +void KBMSettings::SaveKBMConfig(bool CloseOnSave) { + std::string output_string = "", input_string = ""; + std::vector lines, inputs; + + lines.push_back("#Feeling lost? Check out the Help section!"); + lines.push_back(""); + lines.push_back("#Keyboard bindings"); + lines.push_back(""); + + input_string = ui->CrossButton->text().toStdString(); + output_string = "cross"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->CircleButton->text().toStdString(); + output_string = "circle"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->TriangleButton->text().toStdString(); + output_string = "triangle"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->SquareButton->text().toStdString(); + output_string = "square"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + lines.push_back(""); + + input_string = ui->DpadUpButton->text().toStdString(); + output_string = "pad_up"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->DpadDownButton->text().toStdString(); + output_string = "pad_down"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->DpadLeftButton->text().toStdString(); + output_string = "pad_left"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->DpadRightButton->text().toStdString(); + output_string = "pad_right"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + lines.push_back(""); + + input_string = ui->L1Button->text().toStdString(); + output_string = "l1"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->R1Button->text().toStdString(); + output_string = "r1"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->L2Button->text().toStdString(); + output_string = "l2"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->R2Button->text().toStdString(); + output_string = "r2"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->L3Button->text().toStdString(); + output_string = "l3"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->R3Button->text().toStdString(); + output_string = "r3"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + lines.push_back(""); + + input_string = ui->OptionsButton->text().toStdString(); + output_string = "options"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->TouchpadButton->text().toStdString(); + output_string = "touchpad"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + lines.push_back(""); + + input_string = ui->LStickUpButton->text().toStdString(); + output_string = "axis_left_y_minus"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->LStickDownButton->text().toStdString(); + output_string = "axis_left_y_plus"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->LStickLeftButton->text().toStdString(); + output_string = "axis_left_x_minus"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->LStickRightButton->text().toStdString(); + output_string = "axis_left_x_plus"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + lines.push_back(""); + + input_string = ui->RStickUpButton->text().toStdString(); + output_string = "axis_right_y_minus"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->RStickDownButton->text().toStdString(); + output_string = "axis_right_y_plus"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->RStickLeftButton->text().toStdString(); + output_string = "axis_right_x_minus"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->RStickRightButton->text().toStdString(); + output_string = "axis_right_x_plus"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + lines.push_back(""); + + input_string = ui->MouseJoystickBox->currentText().toStdString(); + output_string = "mouse_to_joystick"; + if (input_string != "unmapped") + lines.push_back(output_string + " = " + input_string); + + input_string = ui->LHalfButton->text().toStdString(); + output_string = "leftjoystick_halfmode"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + input_string = ui->RHalfButton->text().toStdString(); + output_string = "rightjoystick_halfmode"; + lines.push_back(output_string + " = " + input_string); + if (input_string != "unmapped") + inputs.push_back(input_string); + + std::string DOString = std::format("{:.2f}", (ui->DeadzoneOffsetSlider->value() / 100.f)); + std::string SMString = std::format("{:.1f}", (ui->SpeedMultiplierSlider->value() / 10.f)); + std::string SOString = std::format("{:.3f}", (ui->SpeedOffsetSlider->value() / 1000.f)); + input_string = DOString + ", " + SMString + ", " + SOString; + output_string = "mouse_movement_params"; + lines.push_back(output_string + " = " + input_string); + + lines.push_back(""); + const auto config_file = Config::GetFoolproofKbmConfigFile(config_id); + std::fstream file(config_file); + int lineCount = 0; + std::string line; + while (std::getline(file, line)) { + lineCount++; + + if (line.empty()) { + lines.push_back(line); + continue; + } + + std::size_t comment_pos = line.find('#'); + if (comment_pos != std::string::npos) { + if (!line.contains("Keyboard bindings") && !line.contains("Feeling lost") && + !line.contains("Alternatives for users")) + lines.push_back(line); + continue; + } + + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) { + lines.push_back(line); + continue; + } + + output_string = line.substr(0, equal_pos - 1); + input_string = line.substr(equal_pos + 2); + + if (std::find(ControllerInputs.begin(), ControllerInputs.end(), input_string) != + ControllerInputs.end() || + output_string == "analog_deadzone" || output_string == "override_controller_color") { + lines.push_back(line); + } + } + file.close(); + + // Prevent duplicate inputs for KBM as this breaks the engine + for (auto it = inputs.begin(); it != inputs.end(); ++it) { + if (std::find(it + 1, inputs.end(), *it) != inputs.end()) { + QMessageBox::information(this, tr("Unable to Save"), + tr("Cannot bind any unique input more than once")); + return; + } + } + + std::vector save; + bool CurrentLineEmpty = false, LastLineEmpty = false; + for (auto const& line : lines) { + LastLineEmpty = CurrentLineEmpty ? true : false; + CurrentLineEmpty = line.empty() ? true : false; + if (!CurrentLineEmpty || !LastLineEmpty) + save.push_back(line); + } + + std::ofstream output_file(config_file); + for (auto const& line : save) { + output_file << line << '\n'; + } + output_file.close(); + + Config::SetUseUnifiedInputConfig(!ui->PerGameCheckBox->isChecked()); + Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml"); + + if (CloseOnSave) + QWidget::close(); +} + +void KBMSettings::SetDefault() { + ui->CrossButton->setText("kp2"); + ui->CircleButton->setText("kp6"); + ui->TriangleButton->setText("kp8"); + ui->SquareButton->setText("kp4"); + + ui->L1Button->setText("q"); + ui->L2Button->setText("e"); + ui->L3Button->setText("x"); + ui->R1Button->setText("u"); + ui->R2Button->setText("o"); + ui->R3Button->setText("m"); + + ui->TouchpadButton->setText("space"); + ui->OptionsButton->setText("enter"); + + ui->DpadUpButton->setText("up"); + ui->DpadDownButton->setText("down"); + ui->DpadLeftButton->setText("left"); + ui->DpadRightButton->setText("right"); + + ui->LStickUpButton->setText("w"); + ui->LStickDownButton->setText("s"); + ui->LStickLeftButton->setText("a"); + ui->LStickRightButton->setText("d"); + + ui->RStickUpButton->setText("i"); + ui->RStickDownButton->setText("k"); + ui->RStickLeftButton->setText("j"); + ui->RStickRightButton->setText("l"); + + ui->LHalfButton->setText("unmapped"); + ui->RHalfButton->setText("unmapped"); + + ui->MouseJoystickBox->setCurrentText("none"); + ui->DeadzoneOffsetSlider->setValue(50); + ui->SpeedMultiplierSlider->setValue(10); + ui->SpeedOffsetSlider->setValue(125); +} + +void KBMSettings::SetUIValuestoMappings(std::string config_id) { + const auto config_file = Config::GetFoolproofKbmConfigFile(config_id); + std::ifstream file(config_file); + + int lineCount = 0; + std::string line = ""; + while (std::getline(file, line)) { + lineCount++; + + std::size_t comment_pos = line.find('#'); + if (comment_pos != std::string::npos) + line = line.substr(0, comment_pos); + + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) + continue; + + std::string output_string = line.substr(0, equal_pos - 1); + std::string input_string = line.substr(equal_pos + 2); + + if (std::find(ControllerInputs.begin(), ControllerInputs.end(), input_string) == + ControllerInputs.end()) { + + if (output_string == "cross") { + ui->CrossButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "circle") { + ui->CircleButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "square") { + ui->SquareButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "triangle") { + ui->TriangleButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "l1") { + ui->L1Button->setText(QString::fromStdString(input_string)); + } else if (output_string == "l2") { + ui->L2Button->setText(QString::fromStdString(input_string)); + } else if (output_string == "r1") { + ui->R1Button->setText(QString::fromStdString(input_string)); + } else if (output_string == "r2") { + ui->R2Button->setText(QString::fromStdString(input_string)); + } else if (output_string == "l3") { + ui->L3Button->setText(QString::fromStdString(input_string)); + } else if (output_string == "r3") { + ui->R3Button->setText(QString::fromStdString(input_string)); + } else if (output_string == "pad_up") { + ui->DpadUpButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "pad_down") { + ui->DpadDownButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "pad_left") { + ui->DpadLeftButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "pad_right") { + ui->DpadRightButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "options") { + ui->OptionsButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "touchpad") { + ui->TouchpadButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "axis_left_x_minus") { + ui->LStickLeftButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "axis_left_x_plus") { + ui->LStickRightButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "axis_left_y_minus") { + ui->LStickUpButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "axis_left_y_plus") { + ui->LStickDownButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "axis_right_x_minus") { + ui->RStickLeftButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "axis_right_x_plus") { + ui->RStickRightButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "axis_right_y_minus") { + ui->RStickUpButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "axis_right_y_plus") { + ui->RStickDownButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "mouse_to_joystick") { + ui->MouseJoystickBox->setCurrentText(QString::fromStdString(input_string)); + } else if (output_string == "leftjoystick_halfmode") { + ui->LHalfButton->setText(QString::fromStdString(input_string)); + } else if (output_string == "rightjoystick_halfmode") { + ui->RHalfButton->setText(QString::fromStdString(input_string)); + } else if (output_string.contains("mouse_movement_params")) { + std::size_t comma_pos = line.find(','); + if (comma_pos != std::string::npos) { + std::string DOstring = line.substr(equal_pos + 1, comma_pos); + float DOffsetValue = std::stof(DOstring) * 100.0; + int DOffsetInt = int(DOffsetValue); + ui->DeadzoneOffsetSlider->setValue(DOffsetInt); + QString LabelValue = QString::number(DOffsetInt / 100.0, 'f', 2); + QString LabelString = tr("Deadzone Offset (def 0.50):") + " " + LabelValue; + ui->DeadzoneOffsetLabel->setText(LabelString); + + std::string SMSOstring = line.substr(comma_pos + 1); + std::size_t comma_pos2 = SMSOstring.find(','); + if (comma_pos2 != std::string::npos) { + std::string SMstring = SMSOstring.substr(0, comma_pos2); + float SpeedMultValue = std::stof(SMstring) * 10.0; + int SpeedMultInt = int(SpeedMultValue); + if (SpeedMultInt < 1) + SpeedMultInt = 1; + if (SpeedMultInt > 50) + SpeedMultInt = 50; + ui->SpeedMultiplierSlider->setValue(SpeedMultInt); + LabelValue = QString::number(SpeedMultInt / 10.0, 'f', 1); + LabelString = tr("Speed Multiplier (def 1.0):") + " " + LabelValue; + ui->SpeedMultiplierLabel->setText(LabelString); + + std::string SOstring = SMSOstring.substr(comma_pos2 + 1); + float SOffsetValue = std::stof(SOstring) * 1000.0; + int SOffsetInt = int(SOffsetValue); + ui->SpeedOffsetSlider->setValue(SOffsetInt); + LabelValue = QString::number(SOffsetInt / 1000.0, 'f', 3); + LabelString = tr("Speed Offset (def 0.125):") + " " + LabelValue; + ui->SpeedOffsetLabel->setText(LabelString); + } + } + } + } + } + file.close(); +} + +void KBMSettings::GetGameTitle() { + if (ui->ProfileComboBox->currentText() == "Common Config") { + ui->TitleLabel->setText("Common Config"); + } else { + for (int i = 0; i < m_game_info->m_games.size(); i++) { + if (m_game_info->m_games[i].serial == + ui->ProfileComboBox->currentText().toStdString()) { + ui->TitleLabel->setText(QString::fromStdString(m_game_info->m_games[i].name)); + } + } + } + config_id = (ui->ProfileComboBox->currentText() == "Common Config") + ? "default" + : ui->ProfileComboBox->currentText().toStdString(); +} + +void KBMSettings::onHelpClicked() { + if (!HelpWindowOpen) { + HelpWindow = new HelpDialog(&HelpWindowOpen, this); + HelpWindow->setWindowTitle(tr("Help")); + HelpWindow->setAttribute(Qt::WA_DeleteOnClose); // Clean up on close + HelpWindow->show(); + HelpWindowOpen = true; + } else { + HelpWindow->close(); + HelpWindowOpen = false; + } +} + +void KBMSettings::StartTimer(QPushButton*& button) { + MappingTimer = 3; + EnableMapping = true; + MappingCompleted = false; + modifier = ""; + mapping = button->text(); + + DisableMappingButtons(); + button->setText(tr("Press a key") + " [" + QString::number(MappingTimer) + "]"); + timer = new QTimer(this); + MappingButton = button; + connect(timer, &QTimer::timeout, this, [this]() { CheckMapping(MappingButton); }); + timer->start(1000); +} + +void KBMSettings::CheckMapping(QPushButton*& button) { + MappingTimer -= 1; + button->setText(tr("Press a key") + " [" + QString::number(MappingTimer) + "]"); + + if (pressedKeys.size() > 0) { + QStringList keyStrings; + + for (const QString& buttonAction : pressedKeys) { + keyStrings << buttonAction; + } + + QString combo = keyStrings.join(","); + SetMapping(combo); + MappingCompleted = true; + EnableMapping = false; + + MappingButton->setText(combo); + pressedKeys.clear(); + timer->stop(); + } + if (MappingCompleted) { + EnableMapping = false; + EnableMappingButtons(); + timer->stop(); + + button->setText(mapping); + } + + if (MappingTimer <= 0) { + button->setText(mapping); + EnableMapping = false; + EnableMappingButtons(); + timer->stop(); + } +} + +void KBMSettings::SetMapping(QString input) { + mapping = input; + MappingCompleted = true; +} + +bool KBMSettings::eventFilter(QObject* obj, QEvent* event) { + if (event->type() == QEvent::Close) { + if (HelpWindowOpen) { + HelpWindow->close(); + HelpWindowOpen = false; + } + } + + if (EnableMapping) { + if (event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + + if (keyEvent->isAutoRepeat()) + return true; + + if (pressedKeys.size() >= 3) { + return true; + } + + switch (keyEvent->key()) { + // alphanumeric + case Qt::Key_A: + pressedKeys.insert("a"); + break; + case Qt::Key_B: + pressedKeys.insert("b"); + break; + case Qt::Key_C: + pressedKeys.insert("c"); + break; + case Qt::Key_D: + pressedKeys.insert("d"); + break; + case Qt::Key_E: + pressedKeys.insert("e"); + break; + case Qt::Key_F: + pressedKeys.insert("f"); + break; + case Qt::Key_G: + pressedKeys.insert("g"); + break; + case Qt::Key_H: + pressedKeys.insert("h"); + break; + case Qt::Key_I: + pressedKeys.insert("i"); + break; + case Qt::Key_J: + pressedKeys.insert("j"); + break; + case Qt::Key_K: + pressedKeys.insert("k"); + break; + case Qt::Key_L: + pressedKeys.insert("l"); + break; + case Qt::Key_M: + pressedKeys.insert("m"); + break; + case Qt::Key_N: + pressedKeys.insert("n"); + break; + case Qt::Key_O: + pressedKeys.insert("o"); + break; + case Qt::Key_P: + pressedKeys.insert("p"); + break; + case Qt::Key_Q: + pressedKeys.insert("q"); + break; + case Qt::Key_R: + pressedKeys.insert("r"); + break; + case Qt::Key_S: + pressedKeys.insert("s"); + break; + case Qt::Key_T: + pressedKeys.insert("t"); + break; + case Qt::Key_U: + pressedKeys.insert("u"); + break; + case Qt::Key_V: + pressedKeys.insert("v"); + break; + case Qt::Key_W: + pressedKeys.insert("w"); + break; + case Qt::Key_X: + pressedKeys.insert("x"); + break; + case Qt::Key_Y: + pressedKeys.insert("y"); + break; + case Qt::Key_Z: + pressedKeys.insert("z"); + break; + case Qt::Key_0: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp0", "0")); + break; + case Qt::Key_1: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp1", "1")); + break; + case Qt::Key_2: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp2", "2")); + break; + case Qt::Key_3: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp3", "3")); + break; + case Qt::Key_4: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp4", "4")); + break; + case Qt::Key_5: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp5", "5")); + break; + case Qt::Key_6: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp6", "6")); + break; + case Qt::Key_7: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp7", "7")); + break; + case Qt::Key_8: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp8", "8")); + break; + case Qt::Key_9: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp9", "9")); + break; + + // symbols + case Qt::Key_Exclam: + pressedKeys.insert("!"); + break; + case Qt::Key_At: + pressedKeys.insert("@"); + break; + case Qt::Key_NumberSign: + pressedKeys.insert("#"); + break; + case Qt::Key_Dollar: + pressedKeys.insert("$"); + break; + case Qt::Key_Percent: + pressedKeys.insert("%"); + break; + case Qt::Key_AsciiCircum: + pressedKeys.insert("^"); + break; + case Qt::Key_Ampersand: + pressedKeys.insert("&"); + break; + case Qt::Key_Asterisk: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp *", "*")); + break; + case Qt::Key_ParenLeft: + pressedKeys.insert("("); + break; + case Qt::Key_ParenRight: + pressedKeys.insert(")"); + break; + case Qt::Key_Minus: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp -", "-")); + break; + case Qt::Key_Underscore: + pressedKeys.insert("_"); + break; + case Qt::Key_Equal: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp =", "=")); + break; + case Qt::Key_Plus: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp +", "+")); + break; + case Qt::Key_BracketLeft: + pressedKeys.insert("["); + break; + case Qt::Key_BracketRight: + pressedKeys.insert("]"); + break; + case Qt::Key_BraceLeft: + pressedKeys.insert("{"); + break; + case Qt::Key_BraceRight: + pressedKeys.insert("}"); + break; + case Qt::Key_Backslash: + pressedKeys.insert("\\"); + break; + case Qt::Key_Bar: + pressedKeys.insert("|"); + break; + case Qt::Key_Semicolon: + pressedKeys.insert(";"); + break; + case Qt::Key_Colon: + pressedKeys.insert(":"); + break; + case Qt::Key_Apostrophe: + pressedKeys.insert("'"); + break; + case Qt::Key_QuoteDbl: + pressedKeys.insert("\""); + break; + case Qt::Key_Comma: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp ,", ",")); + break; + case Qt::Key_Less: + pressedKeys.insert("<"); + break; + case Qt::Key_Period: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp .", ".")); + break; + case Qt::Key_Greater: + pressedKeys.insert(">"); + break; + case Qt::Key_Slash: + pressedKeys.insert(GetModifiedButton(Qt::KeypadModifier, "kp /", "/")); + break; + case Qt::Key_Question: + pressedKeys.insert("question"); + break; + + // special keys + case Qt::Key_Print: + pressedKeys.insert("print"); + break; + case Qt::Key_ScrollLock: + pressedKeys.insert("scrolllock"); + break; + case Qt::Key_Pause: + pressedKeys.insert("pause"); + break; + case Qt::Key_Backspace: + pressedKeys.insert("backspace"); + break; + case Qt::Key_Insert: + pressedKeys.insert("insert"); + break; + case Qt::Key_Delete: + pressedKeys.insert("delete"); + break; + case Qt::Key_Home: + pressedKeys.insert("home"); + break; + case Qt::Key_End: + pressedKeys.insert("end"); + break; + case Qt::Key_PageUp: + pressedKeys.insert("pgup"); + break; + case Qt::Key_PageDown: + pressedKeys.insert("pgdown"); + break; + case Qt::Key_Tab: + pressedKeys.insert("tab"); + break; + case Qt::Key_CapsLock: + pressedKeys.insert("capslock"); + break; + case Qt::Key_Return: + pressedKeys.insert("enter"); + break; + case Qt::Key_Enter: + pressedKeys.insert(GetModifiedButton(Qt::ShiftModifier, "kp enter", "enter")); + break; + case Qt::Key_Shift: + if (keyEvent->nativeScanCode() == LSHIFT_KEY) { + pressedKeys.insert("lshift"); + } else { + pressedKeys.insert("rshift"); + } + break; + case Qt::Key_Alt: + if (keyEvent->nativeScanCode() == LALT_KEY) { + pressedKeys.insert("lalt"); + } else { + pressedKeys.insert("ralt"); + } + break; + case Qt::Key_Control: + if (keyEvent->nativeScanCode() == LCTRL_KEY) { + pressedKeys.insert("lctrl"); + } else { + pressedKeys.insert("rctrl"); + } + break; + case Qt::Key_Meta: + activateWindow(); +#ifdef _WIN32 + pressedKeys.insert("lwin"); +#else + pressedKeys.insert("lmeta"); +#endif + break; + case Qt::Key_Space: + pressedKeys.insert("space"); + break; + case Qt::Key_Up: + activateWindow(); + pressedKeys.insert("up"); + break; + case Qt::Key_Down: + pressedKeys.insert("down"); + break; + case Qt::Key_Left: + pressedKeys.insert("left"); + break; + case Qt::Key_Right: + pressedKeys.insert("right"); + break; + + // cancel mapping + case Qt::Key_Escape: + pressedKeys.insert("unmapped"); + break; + + // default case + default: + break; + // bottom text + } + return true; + } + } + + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent* mouseEvent = static_cast(event); + if (pressedKeys.size() < 3) { + switch (mouseEvent->button()) { + case Qt::LeftButton: + pressedKeys.insert("leftbutton"); + break; + case Qt::RightButton: + pressedKeys.insert("rightbutton"); + break; + case Qt::MiddleButton: + pressedKeys.insert("middlebutton"); + break; + case Qt::XButton1: + pressedKeys.insert("sidebuttonback"); + break; + case Qt::XButton2: + pressedKeys.insert("sidebuttonforward"); + break; + + // default case + default: + break; + // bottom text + } + return true; + } + } + + const QList AxisList = { + ui->LStickUpButton, ui->LStickDownButton, ui->LStickLeftButton, ui->LStickRightButton, + ui->RStickUpButton, ui->LStickDownButton, ui->LStickLeftButton, ui->RStickRightButton}; + + if (event->type() == QEvent::Wheel) { + QWheelEvent* wheelEvent = static_cast(event); + if (pressedKeys.size() < 3) { + if (wheelEvent->angleDelta().y() > 5) { + if (std::find(AxisList.begin(), AxisList.end(), MappingButton) == AxisList.end()) { + pressedKeys.insert("mousewheelup"); + } else { + QMessageBox::information(this, tr("Cannot set mapping"), + tr("Mousewheel cannot be mapped to stick outputs")); + } + } else if (wheelEvent->angleDelta().y() < -5) { + if (std::find(AxisList.begin(), AxisList.end(), MappingButton) == AxisList.end()) { + pressedKeys.insert("mousewheeldown"); + } else { + QMessageBox::information(this, tr("Cannot set mapping"), + tr("Mousewheel cannot be mapped to stick outputs")); + } + } + if (wheelEvent->angleDelta().x() > 5) { + if (std::find(AxisList.begin(), AxisList.end(), MappingButton) == AxisList.end()) { + // QT changes scrolling to horizontal for all widgets with the alt modifier + pressedKeys.insert(GetModifiedButton(Qt::AltModifier, + "mousewheelup", "mousewheelright")); + } else { + QMessageBox::information(this, tr("Cannot set mapping"), + tr("Mousewheel cannot be mapped to stick outputs")); + } + } else if (wheelEvent->angleDelta().x() < -5) { + if (std::find(AxisList.begin(), AxisList.end(), MappingButton) == AxisList.end()) { + pressedKeys.insert(GetModifiedButton(Qt::AltModifier, + "mousewheeldown", "mousewheelleft")); + } else { + QMessageBox::information(this, tr("Cannot set mapping"), + tr("Mousewheel cannot be mapped to stick outputs")); + } + } + } + } + + return QDialog::eventFilter(obj, event); +} + +// m_button is modified button name, n_button is normal button name +QString KBMSettings::GetModifiedButton(Qt::KeyboardModifiers modifier, + std::string m_button, std::string n_button) { + if (QApplication::keyboardModifiers() & modifier) { + return QString::fromStdString(m_button); + } else { + return QString::fromStdString(n_button); + } +} + +// // Lambda version of GetModifiedButton +// auto GetModifiedButton = [](Qt::KeyboardModifiers modifier, const std::string& m_button, const std::string& n_button) -> QString { +// if (QApplication::keyboardModifiers() & modifier) { +// return QString::fromStdString(m_button); +// } else { +// return QString::fromStdString(n_button); +// } +// }; + +KBMSettings::~KBMSettings() {} diff --git a/kbm_gui.h b/kbm_gui.h new file mode 100644 index 000000000..7115ea958 --- /dev/null +++ b/kbm_gui.h @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "game_info.h" + +// macros > declaring constants +// also, we were only using one counterpart +#ifdef _WIN32 +#define LCTRL_KEY 29 +#define LALT_KEY 56 +#define LSHIFT_KEY 42 +#else +#define LCTRL_KEY 37 +#define LALT_KEY 64 +#define LSHIFT_KEY 50 +#endif + +namespace Ui { +class KBMSettings; +} + +class KBMSettings : public QDialog { + Q_OBJECT +public: + explicit KBMSettings(std::shared_ptr game_info_get, QWidget* parent = nullptr); + ~KBMSettings(); + +private Q_SLOTS: + void SaveKBMConfig(bool CloseOnSave); + void SetDefault(); + void CheckMapping(QPushButton*& button); + void StartTimer(QPushButton*& button); + void onHelpClicked(); + +private: + std::unique_ptr ui; + std::shared_ptr m_game_info; + + bool eventFilter(QObject* obj, QEvent* event) override; + QString GetModifiedButton(Qt::KeyboardModifiers modifier, + std::string m_button, std::string n_button); + void ButtonConnects(); + void SetUIValuestoMappings(std::string config_id); + void GetGameTitle(); + void DisableMappingButtons(); + void EnableMappingButtons(); + void SetMapping(QString input); + + QSet pressedKeys; + bool EnableMapping = false; + bool MappingCompleted = false; + bool HelpWindowOpen = false; + QString mapping; + QString modifier; + int MappingTimer; + QTimer* timer; + QPushButton* MappingButton; + QList ButtonsList; + std::string config_id; + const std::vector ControllerInputs = { + "cross", "circle", "square", "triangle", "l1", + "r1", "l2", "r2", "l3", + + "r3", "options", "pad_up", + + "pad_down", + + "pad_left", "pad_right", "axis_left_x", "axis_left_y", "axis_right_x", + "axis_right_y", "back"}; +};