mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Controller rumble and phone rumble have both been dead since the move to a single shared core on the 8th of July, so 2.4.1, 2.5.0 and 2.5.1 all shipped without either. ARMSX2_iOSUpdatePadVibration is the only thing that ever writes the iOS rumble queue, and nothing has called it since that move. It used to be hooked into InputManager::SetPadVibrationIntensity as a patch on the iOS tree's own copy of the core, and when we adopted the shared one the patch did not come along. One dead producer starves all three consumers, which is why SDL rumble, the CoreHaptics pulse and the phone's taptic fallback went silent together rather than one at a time. Android hit exactly this from exactly this migration and was fixed three days later. That fix is still sitting in the same function saying so in its comment. The iOS block goes right beside it so the two read as a pair. Note InputManager.cpp had no TargetConditionals.h, so TARGET_OS_IPHONE was undefined and the guard would have compiled the whole thing back out while the build stayed green. The include is guarded the same way Host.cpp does it. Three more things sat behind the dead call site: The per frame pump skipped past the phone fallback before reaching it. It ran the rumble step only after confirming a gamepad was in the slot, and the taptic fallback exists for the case where there is no gamepad in the slot. Hoisted above the check, which is what makes phone rumble work rather than just controller rumble. Emulation Only Mode set a flag that turned phone haptics off for the rest of the session. It is only ever cleared on a branch that returning to a stripped VM does not take. Dropped it: the same call also releases the cached generators, trigger builds them again on demand, so the release was already self healing and the flag was only blocking it. The per slot pulse engine was an autoreleased object living in a static in a file built without ARC, then messaged again a third of a second later from a delayed stop. Retained now, and released at both places the slot is cleared. The @try around it never helped, since messaging freed memory does not raise. The three other unretained statics in that file have the same defect but their assigning function has no callers, so nothing can reach them.
1909 lines
63 KiB
C++
1909 lines
63 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "ImGui/ImGuiManager.h"
|
|
#include "Input/InputManager.h"
|
|
#include "Input/InputSource.h"
|
|
#include "SIO/Pad/Pad.h"
|
|
#include "SIO/Sio.h"
|
|
#include "USB/USB.h"
|
|
#include "VMManager.h"
|
|
#include "LayeredSettingsInterface.h"
|
|
|
|
#include "common/Assertions.h"
|
|
#include "common/Console.h"
|
|
#include "common/StringUtil.h"
|
|
#include "common/Timer.h"
|
|
|
|
#include "IconsPromptFont.h"
|
|
|
|
#include "fmt/format.h"
|
|
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <unordered_map>
|
|
#include <variant>
|
|
#include <vector>
|
|
|
|
#if defined(__APPLE__)
|
|
#include <TargetConditionals.h>
|
|
#endif
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Constants
|
|
// ------------------------------------------------------------------------
|
|
|
|
enum : u32
|
|
{
|
|
MAX_KEYS_PER_BINDING = 4,
|
|
MAX_MOTORS_PER_PAD = 2,
|
|
FIRST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Pointer) + 1u,
|
|
LAST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Count),
|
|
};
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Event Handler Type
|
|
// ------------------------------------------------------------------------
|
|
// This class acts as an adapter to convert from normalized values to
|
|
// binary values when the callback is a binary/button handler. That way
|
|
// you don't need to convert float->bool in your callbacks.
|
|
using InputEventHandler = std::variant<InputAxisEventHandler, InputButtonEventHandler>;
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Binding Type
|
|
// ------------------------------------------------------------------------
|
|
// This class tracks both the keys which make it up (for chords), as well
|
|
// as the state of all buttons. For button callbacks, it's fired when
|
|
// all keys go active, and for axis callbacks, when all are active and
|
|
// the value changes.
|
|
|
|
struct InputBinding
|
|
{
|
|
InputBindingKey keys[MAX_KEYS_PER_BINDING] = {};
|
|
InputEventHandler handler;
|
|
u8 num_keys = 0;
|
|
u8 full_mask = 0;
|
|
u8 current_mask = 0;
|
|
};
|
|
|
|
struct PadVibrationBinding
|
|
{
|
|
struct Motor
|
|
{
|
|
InputBindingKey binding;
|
|
u64 last_update_time;
|
|
InputSource* source;
|
|
float last_intensity;
|
|
};
|
|
|
|
u32 pad_index = 0;
|
|
Motor motors[MAX_MOTORS_PER_PAD] = {};
|
|
|
|
/// Returns true if the two motors are bound to the same host motor.
|
|
__fi bool AreMotorsCombined() const { return motors[0].binding == motors[1].binding; }
|
|
|
|
/// Returns the intensity when both motors are combined.
|
|
__fi float GetCombinedIntensity() const { return std::max(motors[0].last_intensity, motors[1].last_intensity); }
|
|
};
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Forward Declarations (for static qualifier)
|
|
// ------------------------------------------------------------------------
|
|
namespace InputManager
|
|
{
|
|
static std::optional<InputBindingKey> ParseHostKeyboardKey(const std::string_view source, const std::string_view sub_binding);
|
|
static std::optional<InputBindingKey> ParsePointerKey(const std::string_view source, const std::string_view sub_binding);
|
|
|
|
static TinyString ConvertKeyboardKeyToString(InputBindingKey key, bool display = false);
|
|
static TinyString ConvertPointerKeyToString(InputBindingKey key, bool display = false);
|
|
|
|
static bool SplitBinding(const std::string_view binding, std::string_view* source, std::string_view* sub_binding);
|
|
static void PrettifyInputBindingPart(const std::string_view binding, SmallString& ret, bool& changed, bool use_icons);
|
|
static std::shared_ptr<InputBinding> AddBinding(const std::string_view binding, const InputEventHandler& handler);
|
|
// Will also apply SDL2-SDL3 migrations and update the provided section & key
|
|
static void AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler,
|
|
InputBindingInfo::Type binding_type, SettingsInterface& si, const char* section, const char* key, bool is_profile);
|
|
static bool ParseBindingAndGetSource(const std::string_view binding, InputBindingKey* key, InputSource** source);
|
|
|
|
static bool IsAxisHandler(const InputEventHandler& handler);
|
|
static float ApplySingleBindingScale(float sensitivity, float deadzone, float value);
|
|
|
|
static void AddHotkeyBindings(SettingsInterface& si, bool is_profile);
|
|
static void AddPadBindings(SettingsInterface& si, u32 pad, bool is_profile);
|
|
static void AddUSBBindings(SettingsInterface& si, u32 port, bool is_profile);
|
|
static void UpdateContinuedVibration();
|
|
static void GenerateRelativeMouseEvents();
|
|
|
|
static bool DoEventHook(InputBindingKey key, float value);
|
|
static bool PreprocessEvent(InputBindingKey key, float value, GenericInputBinding generic_key,
|
|
GenericInputBinding axis_neg_key, GenericInputBinding axis_pos_key);
|
|
static bool ProcessEvent(InputBindingKey key, float value, bool skip_button_handlers);
|
|
|
|
template <typename T>
|
|
static void UpdateInputSourceState(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock, InputSourceType type);
|
|
} // namespace InputManager
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Local Variables
|
|
// ------------------------------------------------------------------------
|
|
|
|
// This is a multimap containing any binds related to the specified key.
|
|
using BindingMap = std::unordered_multimap<InputBindingKey, std::shared_ptr<InputBinding>, InputBindingKeyHash>;
|
|
using VibrationBindingArray = std::vector<PadVibrationBinding>;
|
|
static BindingMap s_binding_map;
|
|
static VibrationBindingArray s_pad_vibration_array;
|
|
static std::mutex s_binding_map_write_lock;
|
|
|
|
// Reverse lookups for controller navigation; user bindings take priority over static defaults.
|
|
using ControllerButtonGenericMap = std::unordered_map<InputBindingKey, GenericInputBinding, InputBindingKeyHash>;
|
|
static ControllerButtonGenericMap s_controller_button_generic_map;
|
|
|
|
using ControllerAxisGenericMap = std::unordered_map<InputBindingKey, std::array<GenericInputBinding, 2>, InputBindingKeyHash>;
|
|
static ControllerAxisGenericMap s_controller_axis_generic_map;
|
|
|
|
// Hooks/intercepting (for setting bindings)
|
|
static std::mutex m_event_intercept_mutex;
|
|
static InputInterceptHook::Callback m_event_intercept_callback;
|
|
|
|
// Input sources. Keyboard/mouse don't exist here.
|
|
static std::array<std::unique_ptr<InputSource>, static_cast<u32>(InputSourceType::Count)> s_input_sources;
|
|
|
|
// Layout preference for gamepad controller glyphs.
|
|
static std::atomic<InputLayout> s_gamepad_icon_preference = InputLayout::Unknown;
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Hotkeys
|
|
// ------------------------------------------------------------------------
|
|
static const HotkeyInfo* const s_hotkey_list[] = {g_common_hotkeys, g_gs_hotkeys, g_host_hotkeys};
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Tracking host mouse movement and turning into relative events
|
|
// 4 axes: pointer left/right, wheel vertical/horizontal. Last/Next/Normalized.
|
|
// ------------------------------------------------------------------------
|
|
static constexpr const std::array<const char*, static_cast<u8>(InputPointerAxis::Count)> s_pointer_axis_setting_names = {
|
|
{"X", "Y", "WheelX", "WheelY"}};
|
|
static constexpr const std::array<const char*, static_cast<u8>(InputPointerAxis::Count)> s_pointer_axis_names = {
|
|
{"X", "Y", "Wheel X", "Wheel Y"}};
|
|
static constexpr const std::array<const char*, 3> s_pointer_button_setting_names = {{"LeftButton", "RightButton", "MiddleButton"}};
|
|
static constexpr const std::array<const char*, 3> s_pointer_button_names = {{"Left Button", "Right Button", "Middle Button"}};
|
|
|
|
struct PointerAxisState
|
|
{
|
|
std::atomic<s32> delta;
|
|
float last_value;
|
|
};
|
|
static std::array<std::array<float, static_cast<u8>(InputPointerAxis::Count)>, InputManager::MAX_POINTER_DEVICES> s_host_pointer_positions;
|
|
static std::array<std::array<PointerAxisState, static_cast<u8>(InputPointerAxis::Count)>, InputManager::MAX_POINTER_DEVICES>
|
|
s_pointer_state;
|
|
static std::array<float, 2> s_pointer_axis_speed;
|
|
static std::array<float, 2> s_pointer_axis_dead_zone;
|
|
static std::array<float, 2> s_pointer_axis_range;
|
|
static std::array<float, 2> s_pointer_pos = {0.0f, 0.0f};
|
|
static float s_pointer_inertia = 0.0f;
|
|
|
|
using PointerMoveCallback = std::function<void(InputBindingKey key, float value)>;
|
|
using KeyboardEventCallback = std::function<void(InputBindingKey key, float value)>;
|
|
static std::vector<KeyboardEventCallback> s_keyboard_event_callbacks;
|
|
static std::vector<std::pair<u32, PointerMoveCallback>> s_pointer_move_callbacks;
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Binding Parsing
|
|
// ------------------------------------------------------------------------
|
|
|
|
std::vector<std::string_view> InputManager::SplitChord(const std::string_view binding)
|
|
{
|
|
std::vector<std::string_view> parts;
|
|
|
|
// under an if for RVO
|
|
if (!binding.empty())
|
|
{
|
|
std::string_view::size_type last = 0;
|
|
std::string_view::size_type next;
|
|
while ((next = binding.find('&', last)) != std::string_view::npos)
|
|
{
|
|
if (last != next)
|
|
{
|
|
std::string_view part(StringUtil::StripWhitespace(binding.substr(last, next - last)));
|
|
if (!part.empty())
|
|
parts.push_back(std::move(part));
|
|
}
|
|
last = next + 1;
|
|
}
|
|
if (last < (binding.size() - 1))
|
|
{
|
|
std::string_view part(StringUtil::StripWhitespace(binding.substr(last)));
|
|
if (!part.empty())
|
|
parts.push_back(std::move(part));
|
|
}
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
bool InputManager::SplitBinding(const std::string_view binding, std::string_view* source, std::string_view* sub_binding)
|
|
{
|
|
const std::string_view::size_type slash_pos = binding.find('/');
|
|
if (slash_pos == std::string_view::npos)
|
|
{
|
|
Console.Warning("Malformed binding: '%.*s'", static_cast<int>(binding.size()), binding.data());
|
|
return false;
|
|
}
|
|
|
|
*source = std::string_view(binding).substr(0, slash_pos);
|
|
*sub_binding = std::string_view(binding).substr(slash_pos + 1);
|
|
return true;
|
|
}
|
|
|
|
std::optional<InputBindingKey> InputManager::ParseInputBindingKey(const std::string_view binding)
|
|
{
|
|
std::string_view source, sub_binding;
|
|
if (!SplitBinding(binding, &source, &sub_binding))
|
|
return std::nullopt;
|
|
|
|
// lameee, string matching
|
|
if (source.starts_with("Keyboard"))
|
|
{
|
|
return ParseHostKeyboardKey(source, sub_binding);
|
|
}
|
|
else if (source.starts_with("Pointer"))
|
|
{
|
|
return ParsePointerKey(source, sub_binding);
|
|
}
|
|
else
|
|
{
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
if (s_input_sources[i]->IsInitialized())
|
|
{
|
|
std::optional<InputBindingKey> key = s_input_sources[i]->ParseKeyString(source, sub_binding);
|
|
if (key.has_value())
|
|
return key;
|
|
}
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool InputManager::ParseBindingAndGetSource(const std::string_view binding, InputBindingKey* key, InputSource** source)
|
|
{
|
|
std::string_view source_string, sub_binding;
|
|
if (!SplitBinding(binding, &source_string, &sub_binding))
|
|
return false;
|
|
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
if (s_input_sources[i]->IsInitialized())
|
|
{
|
|
std::optional<InputBindingKey> parsed_key = s_input_sources[i]->ParseKeyString(source_string, sub_binding);
|
|
if (parsed_key.has_value())
|
|
{
|
|
*key = parsed_key.value();
|
|
*source = s_input_sources[i].get();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
TinyString InputManager::ConvertKeyboardKeyToString(InputBindingKey key, bool display)
|
|
{
|
|
TinyString ret;
|
|
|
|
if (key.source_type == InputSourceType::Keyboard)
|
|
{
|
|
const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data));
|
|
if (str.has_value() && !str->empty())
|
|
{
|
|
if (display)
|
|
// Keyboard keys arn't spaced out for display yet
|
|
ret.format("Keyboard {}", str->c_str());
|
|
else
|
|
ret.format("Keyboard/{}", str->c_str());
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
TinyString InputManager::ConvertPointerKeyToString(InputBindingKey key, bool display)
|
|
{
|
|
TinyString ret;
|
|
|
|
if (key.source_type == InputSourceType::Pointer)
|
|
{
|
|
if (key.source_subtype == InputSubclass::PointerButton)
|
|
{
|
|
if (display)
|
|
{
|
|
if (key.data < s_pointer_button_setting_names.size())
|
|
ret.format("Pointer-{} {}", u32{key.source_index}, s_pointer_button_names[key.data]);
|
|
else
|
|
ret.format("Pointer-{} Button{}", u32{key.source_index}, key.data + 1);
|
|
}
|
|
else
|
|
{
|
|
if (key.data < s_pointer_button_setting_names.size())
|
|
ret.format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_setting_names[key.data]);
|
|
else
|
|
ret.format("Pointer-{}/Button{}", u32{key.source_index}, key.data);
|
|
}
|
|
}
|
|
else if (key.source_subtype == InputSubclass::PointerAxis)
|
|
{
|
|
if (display)
|
|
ret.format("Pointer-{} {}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data],
|
|
key.modifier == InputModifier::Negate ? '-' : '+');
|
|
else
|
|
ret.format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_setting_names[key.data],
|
|
key.modifier == InputModifier::Negate ? '-' : '+');
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::string InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key, bool migration)
|
|
{
|
|
if (binding_type == InputBindingInfo::Type::Pointer || binding_type == InputBindingInfo::Type::Device)
|
|
{
|
|
// pointer and device bindings don't have a data part
|
|
if (key.source_type == InputSourceType::Keyboard)
|
|
{
|
|
return "Keyboard";
|
|
}
|
|
else if (key.source_type == InputSourceType::Pointer)
|
|
{
|
|
return GetPointerDeviceName(key.data);
|
|
}
|
|
else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)])
|
|
{
|
|
// This assumes that it always follows the Type/Binding form.
|
|
std::string keystr(s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key));
|
|
std::string::size_type pos = keystr.find('/');
|
|
if (pos != std::string::npos)
|
|
keystr.erase(pos);
|
|
return keystr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (key.source_type == InputSourceType::Keyboard)
|
|
{
|
|
return std::string(ConvertKeyboardKeyToString(key));
|
|
}
|
|
else if (key.source_type == InputSourceType::Pointer)
|
|
{
|
|
return std::string(ConvertPointerKeyToString(key));
|
|
}
|
|
else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)])
|
|
{
|
|
return std::string(s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key, false, migration));
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
std::string InputManager::ConvertInputBindingKeysToString(InputBindingInfo::Type binding_type, const InputBindingKey* keys, size_t num_keys, bool migration)
|
|
{
|
|
// can't have a chord of devices/pointers
|
|
if (binding_type == InputBindingInfo::Type::Pointer || binding_type == InputBindingInfo::Type::Device)
|
|
{
|
|
// so only take the first
|
|
if (num_keys > 0)
|
|
return ConvertInputBindingKeyToString(binding_type, keys[0], migration);
|
|
}
|
|
|
|
std::stringstream ss;
|
|
for (size_t i = 0; i < num_keys; i++)
|
|
{
|
|
const std::string keystr(ConvertInputBindingKeyToString(binding_type, keys[i], migration));
|
|
if (keystr.empty())
|
|
return std::string();
|
|
|
|
if (i > 0)
|
|
ss << " & ";
|
|
|
|
ss << keystr;
|
|
}
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
bool InputManager::PrettifyInputBinding(SmallStringBase& binding, bool use_icons)
|
|
{
|
|
if (binding.empty())
|
|
return false;
|
|
|
|
const std::string_view binding_view = binding.view();
|
|
|
|
SmallString ret;
|
|
bool changed = false;
|
|
|
|
std::string_view::size_type last = 0;
|
|
std::string_view::size_type next;
|
|
while ((next = binding_view.find('&', last)) != std::string_view::npos)
|
|
{
|
|
if (last != next)
|
|
{
|
|
const std::string_view part = StringUtil::StripWhitespace(binding_view.substr(last, next - last));
|
|
if (!part.empty())
|
|
{
|
|
if (!ret.empty())
|
|
ret.append(" + ");
|
|
PrettifyInputBindingPart(part, ret, changed, use_icons);
|
|
}
|
|
}
|
|
last = next + 1;
|
|
}
|
|
if (last < (binding_view.size() - 1))
|
|
{
|
|
const std::string_view part = StringUtil::StripWhitespace(binding_view.substr(last));
|
|
if (!part.empty())
|
|
{
|
|
if (!ret.empty())
|
|
ret.append(" + ");
|
|
PrettifyInputBindingPart(part, ret, changed, use_icons);
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
binding = ret;
|
|
|
|
return changed;
|
|
}
|
|
|
|
void InputManager::SetGamepadIconPreference(InputLayout layout)
|
|
{
|
|
s_gamepad_icon_preference.store(layout, std::memory_order_relaxed);
|
|
}
|
|
|
|
InputLayout InputManager::GetGamepadIconPreference()
|
|
{
|
|
return s_gamepad_icon_preference.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
void InputManager::PrettifyInputBindingPart(const std::string_view binding, SmallString& ret, bool& changed, bool use_icons)
|
|
{
|
|
std::string_view source, sub_binding;
|
|
if (!SplitBinding(binding, &source, &sub_binding))
|
|
return;
|
|
|
|
// lameee, string matching
|
|
if (source.starts_with("Keyboard"))
|
|
{
|
|
std::optional<InputBindingKey> key = ParseHostKeyboardKey(source, sub_binding);
|
|
if (key.has_value())
|
|
{
|
|
if (use_icons)
|
|
{
|
|
const char* icon = ConvertHostKeyboardCodeToIcon(key->data);
|
|
if (icon)
|
|
{
|
|
ret.append(icon);
|
|
changed = true;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
ret.append(ConvertKeyboardKeyToString(key.value(), true));
|
|
changed = true;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ret.append(ConvertKeyboardKeyToString(key.value(), true));
|
|
changed = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (source.starts_with("Pointer"))
|
|
{
|
|
const std::optional<InputBindingKey> key = ParsePointerKey(source, sub_binding);
|
|
if (key.has_value())
|
|
{
|
|
if (use_icons && key->source_subtype == InputSubclass::PointerButton)
|
|
{
|
|
static constexpr const char* button_icons[] = {
|
|
ICON_PF_MOUSE_BUTTON_1,
|
|
ICON_PF_MOUSE_BUTTON_2,
|
|
ICON_PF_MOUSE_BUTTON_3,
|
|
ICON_PF_MOUSE_BUTTON_4,
|
|
ICON_PF_MOUSE_BUTTON_5,
|
|
};
|
|
if (key->data < std::size(button_icons))
|
|
ret.append(button_icons[key->data]);
|
|
else
|
|
ret.append(ConvertPointerKeyToString(key.value(), true));
|
|
}
|
|
else
|
|
ret.append(ConvertPointerKeyToString(key.value(), true));
|
|
|
|
changed = true;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
// We call ConvertKeyToIcon/String() even on disabled sources
|
|
// This ensures consistant appearance between enabled and disabled sources
|
|
if (s_input_sources[i])
|
|
{
|
|
std::optional<InputBindingKey> key = s_input_sources[i]->ParseKeyString(source, sub_binding);
|
|
if (key.has_value())
|
|
{
|
|
if (use_icons)
|
|
{
|
|
const TinyString icon = s_input_sources[i]->ConvertKeyToIcon(key.value());
|
|
if (!icon.empty())
|
|
ret.append(icon);
|
|
else
|
|
ret.append(s_input_sources[i]->ConvertKeyToString(key.value(), true));
|
|
}
|
|
else
|
|
ret.append(s_input_sources[i]->ConvertKeyToString(key.value(), true));
|
|
|
|
changed = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ret.append(binding);
|
|
}
|
|
|
|
|
|
std::shared_ptr<InputBinding> InputManager::AddBinding(const std::string_view binding, const InputEventHandler& handler)
|
|
{
|
|
std::shared_ptr<InputBinding> ibinding;
|
|
const std::vector<std::string_view> chord_bindings(SplitChord(binding));
|
|
|
|
for (const std::string_view& chord_binding : chord_bindings)
|
|
{
|
|
std::optional<InputBindingKey> key = ParseInputBindingKey(chord_binding);
|
|
if (!key.has_value())
|
|
{
|
|
Console.WriteLn(fmt::format("Invalid binding: '{}'", binding));
|
|
ibinding.reset();
|
|
break;
|
|
}
|
|
|
|
if (!ibinding)
|
|
{
|
|
ibinding = std::make_shared<InputBinding>();
|
|
ibinding->handler = handler;
|
|
}
|
|
|
|
if (ibinding->num_keys == MAX_KEYS_PER_BINDING)
|
|
{
|
|
Console.WriteLn(fmt::format("Too many chord parts, max is {} ({})", static_cast<u32>(MAX_KEYS_PER_BINDING), binding));
|
|
ibinding.reset();
|
|
break;
|
|
}
|
|
|
|
ibinding->keys[ibinding->num_keys] = key.value();
|
|
ibinding->full_mask |= (static_cast<u8>(1) << ibinding->num_keys);
|
|
ibinding->num_keys++;
|
|
}
|
|
|
|
if (!ibinding)
|
|
return nullptr;
|
|
|
|
// plop it in the input map for all the keys
|
|
for (u32 i = 0; i < ibinding->num_keys; i++)
|
|
s_binding_map.emplace(ibinding->keys[i].MaskDirection(), ibinding);
|
|
|
|
return ibinding;
|
|
}
|
|
|
|
void InputManager::AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler,
|
|
InputBindingInfo::Type binding_type, SettingsInterface& si, const char* section, const char* key, bool is_profile)
|
|
{
|
|
std::vector<std::shared_ptr<InputBinding>> ibindings;
|
|
|
|
bool migrate = false;
|
|
for (const std::string& binding : bindings)
|
|
{
|
|
std::shared_ptr<InputBinding> ibinding = AddBinding(binding, handler);
|
|
ibindings.push_back(ibinding);
|
|
|
|
if (ibinding)
|
|
{
|
|
// Check for SDL2-3 migrations
|
|
for (u32 i = 0; i < ibinding->num_keys; i++)
|
|
{
|
|
if (ibinding->keys[i].needs_migration)
|
|
migrate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save migrations
|
|
if (migrate)
|
|
{
|
|
std::vector<std::string> new_bindings;
|
|
new_bindings.reserve(bindings.size());
|
|
|
|
for (size_t i = 0; i < bindings.size(); i++)
|
|
{
|
|
if (ibindings[i])
|
|
new_bindings.push_back(ConvertInputBindingKeysToString(binding_type, ibindings[i]->keys, ibindings[i]->num_keys, true));
|
|
else
|
|
// Retain invalid bindings as is
|
|
new_bindings.push_back(bindings[i]);
|
|
}
|
|
|
|
if (is_profile)
|
|
{
|
|
// INISettingsInterface, can just update directly
|
|
si.SetStringList(section, key, new_bindings);
|
|
si.Save();
|
|
}
|
|
else
|
|
{
|
|
// LayeredSettingsInterface, Need to find which layer our binding came from
|
|
LayeredSettingsInterface& lsi = static_cast<LayeredSettingsInterface&>(si);
|
|
for (u32 i = 0; i < LayeredSettingsInterface::NUM_LAYERS; i++)
|
|
{
|
|
SettingsInterface* layer = lsi.GetLayer(static_cast<LayeredSettingsInterface::Layer>(i));
|
|
if (layer && layer->GetStringList(section, key) == bindings)
|
|
{
|
|
// Layer found, update settings
|
|
layer->SetStringList(section, key, new_bindings);
|
|
layer->Save();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Key Decoders
|
|
// ------------------------------------------------------------------------
|
|
|
|
InputBindingKey InputManager::MakeHostKeyboardKey(u32 key_code)
|
|
{
|
|
InputBindingKey key = {};
|
|
key.source_type = InputSourceType::Keyboard;
|
|
key.data = key_code;
|
|
return key;
|
|
}
|
|
|
|
InputBindingKey InputManager::MakePointerButtonKey(u32 index, u32 button_index)
|
|
{
|
|
InputBindingKey key = {};
|
|
key.source_index = index;
|
|
key.source_type = InputSourceType::Pointer;
|
|
key.source_subtype = InputSubclass::PointerButton;
|
|
key.data = button_index;
|
|
return key;
|
|
}
|
|
|
|
InputBindingKey InputManager::MakePointerAxisKey(u32 index, InputPointerAxis axis)
|
|
{
|
|
InputBindingKey key = {};
|
|
key.data = static_cast<u32>(axis);
|
|
key.source_index = index;
|
|
key.source_type = InputSourceType::Pointer;
|
|
key.source_subtype = InputSubclass::PointerAxis;
|
|
return key;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Bind Encoders
|
|
// ------------------------------------------------------------------------
|
|
|
|
static std::array<const char*, static_cast<u32>(InputSourceType::Count)> s_input_class_names = {{
|
|
"Keyboard",
|
|
"Mouse",
|
|
"SDL",
|
|
#ifdef _WIN32
|
|
"DInput",
|
|
"XInput",
|
|
#endif
|
|
}};
|
|
|
|
InputSource* InputManager::GetInputSourceInterface(InputSourceType type)
|
|
{
|
|
return s_input_sources[static_cast<u32>(type)].get();
|
|
}
|
|
|
|
const char* InputManager::InputSourceToString(InputSourceType clazz)
|
|
{
|
|
return s_input_class_names[static_cast<u32>(clazz)];
|
|
}
|
|
|
|
bool InputManager::GetInputSourceDefaultEnabled(InputSourceType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case InputSourceType::Keyboard:
|
|
case InputSourceType::Pointer:
|
|
case InputSourceType::SDL:
|
|
return true;
|
|
|
|
#ifdef _WIN32
|
|
case InputSourceType::DInput:
|
|
return false;
|
|
|
|
case InputSourceType::XInput:
|
|
return false;
|
|
#endif
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::optional<InputSourceType> InputManager::ParseInputSourceString(const std::string_view str)
|
|
{
|
|
for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)
|
|
{
|
|
if (str == s_input_class_names[i])
|
|
return static_cast<InputSourceType>(i);
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<InputBindingKey> InputManager::ParseHostKeyboardKey(const std::string_view source, const std::string_view sub_binding)
|
|
{
|
|
if (source != "Keyboard")
|
|
return std::nullopt;
|
|
|
|
const std::optional<s32> code = ConvertHostKeyboardStringToCode(sub_binding);
|
|
if (!code.has_value())
|
|
return std::nullopt;
|
|
|
|
InputBindingKey key = {};
|
|
key.source_type = InputSourceType::Keyboard;
|
|
key.data = static_cast<u32>(code.value());
|
|
return key;
|
|
}
|
|
|
|
std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_view source, const std::string_view sub_binding)
|
|
{
|
|
const std::optional<s32> pointer_index = StringUtil::FromChars<s32>(source.substr(8));
|
|
if (!pointer_index.has_value() || pointer_index.value() < 0)
|
|
return std::nullopt;
|
|
|
|
InputBindingKey key = {};
|
|
key.source_type = InputSourceType::Pointer;
|
|
key.source_index = static_cast<u32>(pointer_index.value());
|
|
|
|
if (sub_binding.starts_with("Button"))
|
|
{
|
|
const std::optional<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
|
|
if (!button_number.has_value() || button_number.value() < 0)
|
|
return std::nullopt;
|
|
|
|
key.source_subtype = InputSubclass::PointerButton;
|
|
key.data = static_cast<u32>(button_number.value());
|
|
return key;
|
|
}
|
|
|
|
for (u32 i = 0; i < s_pointer_axis_setting_names.size(); i++)
|
|
{
|
|
if (sub_binding.starts_with(s_pointer_axis_setting_names[i]))
|
|
{
|
|
key.source_subtype = InputSubclass::PointerAxis;
|
|
key.data = i;
|
|
|
|
const std::string_view dir_part(sub_binding.substr(std::strlen(s_pointer_axis_setting_names[i])));
|
|
if (dir_part == "+")
|
|
key.modifier = InputModifier::None;
|
|
else if (dir_part == "-")
|
|
key.modifier = InputModifier::Negate;
|
|
else
|
|
return std::nullopt;
|
|
|
|
return key;
|
|
}
|
|
}
|
|
|
|
for (u32 i = 0; i < s_pointer_button_setting_names.size(); i++)
|
|
{
|
|
if (sub_binding == s_pointer_button_setting_names[i])
|
|
{
|
|
key.source_subtype = InputSubclass::PointerButton;
|
|
key.data = i;
|
|
return key;
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<u32> InputManager::GetIndexFromPointerBinding(const std::string_view source)
|
|
{
|
|
if (!source.starts_with("Pointer-"))
|
|
return std::nullopt;
|
|
|
|
const std::optional<s32> pointer_index = StringUtil::FromChars<s32>(source.substr(8));
|
|
if (!pointer_index.has_value() || pointer_index.value() < 0)
|
|
return std::nullopt;
|
|
|
|
return static_cast<u32>(pointer_index.value());
|
|
}
|
|
|
|
std::string InputManager::GetPointerDeviceName(u32 pointer_index)
|
|
{
|
|
return fmt::format("Pointer-{}", pointer_index);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Binding Enumeration
|
|
// ------------------------------------------------------------------------
|
|
|
|
float InputManager::ApplySingleBindingScale(float scale, float deadzone, float value)
|
|
{
|
|
const float svalue = std::clamp(value * scale, 0.0f, 1.0f);
|
|
return (deadzone > 0.0f && svalue < deadzone) ? 0.0f : svalue;
|
|
}
|
|
|
|
std::vector<const HotkeyInfo*> InputManager::GetHotkeyList()
|
|
{
|
|
std::vector<const HotkeyInfo*> ret;
|
|
for (const HotkeyInfo* hotkey_list : s_hotkey_list)
|
|
{
|
|
for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
|
|
ret.push_back(hotkey);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void InputManager::AddHotkeyBindings(SettingsInterface& si, bool is_profile)
|
|
{
|
|
for (const HotkeyInfo* hotkey_list : s_hotkey_list)
|
|
{
|
|
for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
|
|
{
|
|
const std::vector<std::string> bindings(si.GetStringList("Hotkeys", hotkey->name));
|
|
if (bindings.empty())
|
|
continue;
|
|
|
|
AddBindings(bindings, InputButtonEventHandler{hotkey->handler}, InputBindingInfo::Type::Button, si, "Hotkeys", hotkey->name, is_profile);
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputManager::AddPadBindings(SettingsInterface& si, u32 pad_index, bool is_profile)
|
|
{
|
|
const Pad::ControllerType type = EmuConfig.Pad.Ports[pad_index].Type;
|
|
|
|
// Don't bother checking macros/vibration if it's not a connected type.
|
|
if (type == Pad::ControllerType::NotConnected)
|
|
return;
|
|
|
|
// Or if it's a multitap port, and this multitap isn't enabled.
|
|
if (sioPadIsMultitapSlot(pad_index))
|
|
{
|
|
const auto& [mt_port, mt_slot] = sioConvertPadToPortAndSlot(pad_index);
|
|
if (!EmuConfig.Pad.IsMultitapPortEnabled(mt_port))
|
|
return;
|
|
}
|
|
|
|
const std::string section = Pad::GetConfigSection(pad_index);
|
|
const Pad::ControllerInfo* cinfo = Pad::GetControllerInfo(type);
|
|
pxAssert(cinfo);
|
|
|
|
for (const InputBindingInfo& bi : cinfo->bindings)
|
|
{
|
|
switch (bi.bind_type)
|
|
{
|
|
case InputBindingInfo::Type::Button:
|
|
case InputBindingInfo::Type::Axis:
|
|
case InputBindingInfo::Type::HalfAxis:
|
|
{
|
|
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bi.name));
|
|
if (!bindings.empty())
|
|
{
|
|
// we use axes for all pad bindings to simplify things, and because they are pressure sensitive
|
|
const float sensitivity = si.GetFloatValue(section.c_str(), fmt::format("{}Scale", bi.name).c_str(), 1.0f);
|
|
const float deadzone = si.GetFloatValue(section.c_str(), fmt::format("{}Deadzone", bi.name).c_str(), 0.0f);
|
|
AddBindings(
|
|
bindings, InputAxisEventHandler{[pad_index, bind_index = bi.bind_index, sensitivity, deadzone](InputBindingKey key, float value) {
|
|
Pad::SetControllerState(pad_index, bind_index, ApplySingleBindingScale(sensitivity, deadzone, value));
|
|
}},
|
|
bi.bind_type, si, section.c_str(), bi.name, is_profile);
|
|
|
|
// Build reverse maps for controller navigation; user bindings take priority over static defaults.
|
|
if (bi.generic_mapping != GenericInputBinding::Unknown)
|
|
{
|
|
for (const std::string& binding_str : bindings)
|
|
{
|
|
InputBindingKey bkey;
|
|
InputSource* bsrc;
|
|
if (!ParseBindingAndGetSource(binding_str, &bkey, &bsrc))
|
|
continue;
|
|
|
|
if (bkey.source_subtype == InputSubclass::ControllerButton)
|
|
{
|
|
s_controller_button_generic_map.emplace(bkey.MaskDirection(), bi.generic_mapping);
|
|
}
|
|
else if (bkey.source_subtype == InputSubclass::ControllerAxis)
|
|
{
|
|
auto& entry = s_controller_axis_generic_map[bkey.MaskDirection()];
|
|
// Negate modifier = negative half of axis (e.g. "-Axis0" → LeftStickLeft)
|
|
if (bkey.modifier == InputModifier::Negate)
|
|
entry[0] = bi.generic_mapping;
|
|
else
|
|
entry[1] = bi.generic_mapping;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
// TODO: Move vibration motors in here.
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (u32 macro_button_index = 0; macro_button_index < Pad::NUM_MACRO_BUTTONS_PER_CONTROLLER; macro_button_index++)
|
|
{
|
|
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), fmt::format("Macro{}", macro_button_index + 1).c_str()));
|
|
if (!bindings.empty())
|
|
{
|
|
const float deadzone = si.GetFloatValue(section.c_str(), fmt::format("Macro{}Deadzone", macro_button_index + 1).c_str(), 0.0f);
|
|
AddBindings(
|
|
bindings, InputAxisEventHandler{[pad_index, macro_button_index, deadzone](InputBindingKey key, float value) {
|
|
const bool state = (value > deadzone);
|
|
Pad::SetMacroButtonState(key, pad_index, macro_button_index, state);
|
|
}},
|
|
InputBindingInfo::Type::Macro, si, section.c_str(), fmt::format("Macro{}", macro_button_index + 1).c_str(), is_profile);
|
|
}
|
|
}
|
|
|
|
if (cinfo->vibration_caps != Pad::VibrationCapabilities::NoVibration)
|
|
{
|
|
PadVibrationBinding vib;
|
|
vib.pad_index = pad_index;
|
|
|
|
bool has_any_bindings = false;
|
|
switch (cinfo->vibration_caps)
|
|
{
|
|
case Pad::VibrationCapabilities::LargeSmallMotors:
|
|
{
|
|
if (const std::string large_binding(si.GetStringValue(section.c_str(), "LargeMotor")); !large_binding.empty())
|
|
has_any_bindings |= ParseBindingAndGetSource(large_binding, &vib.motors[0].binding, &vib.motors[0].source);
|
|
if (const std::string small_binding(si.GetStringValue(section.c_str(), "SmallMotor")); !small_binding.empty())
|
|
has_any_bindings |= ParseBindingAndGetSource(small_binding, &vib.motors[1].binding, &vib.motors[1].source);
|
|
}
|
|
break;
|
|
|
|
case Pad::VibrationCapabilities::SingleMotor:
|
|
{
|
|
if (const std::string binding(si.GetStringValue(section.c_str(), "Motor")); !binding.empty())
|
|
has_any_bindings |= ParseBindingAndGetSource(binding, &vib.motors[0].binding, &vib.motors[0].source);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (has_any_bindings)
|
|
s_pad_vibration_array.push_back(std::move(vib));
|
|
}
|
|
}
|
|
|
|
void InputManager::AddUSBBindings(SettingsInterface& si, u32 port, bool is_profile)
|
|
{
|
|
const std::string device(USB::GetConfigDevice(si, port));
|
|
if (device.empty() || device == "None")
|
|
return;
|
|
|
|
const std::string section(USB::GetConfigSection(port));
|
|
const u32 subtype = USB::GetConfigSubType(si, port, device);
|
|
for (const InputBindingInfo& bi : USB::GetDeviceBindings(device, subtype))
|
|
{
|
|
const std::string bind_name(USB::GetConfigSubKey(device, bi.name));
|
|
|
|
switch (bi.bind_type)
|
|
{
|
|
case InputBindingInfo::Type::Button:
|
|
case InputBindingInfo::Type::Axis:
|
|
case InputBindingInfo::Type::HalfAxis:
|
|
{
|
|
// normal bindings
|
|
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bind_name.c_str()));
|
|
if (!bindings.empty())
|
|
{
|
|
const float sensitivity = si.GetFloatValue(section.c_str(), fmt::format("{}Scale", bi.name).c_str(), 1.0f);
|
|
const float deadzone = si.GetFloatValue(section.c_str(), fmt::format("{}Deadzone", bi.name).c_str(), 0.0f);
|
|
AddBindings(
|
|
bindings, InputAxisEventHandler{[port, bind_index = bi.bind_index, sensitivity, deadzone](InputBindingKey key, float value) {
|
|
USB::SetDeviceBindValue(port, bind_index, ApplySingleBindingScale(sensitivity, deadzone, value));
|
|
}},
|
|
bi.bind_type, si, section.c_str(), bind_name.c_str(), is_profile);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case InputBindingInfo::Type::Keyboard:
|
|
{
|
|
// set up to receive keyboard events
|
|
s_keyboard_event_callbacks.push_back([port, base = static_cast<u32>(bi.bind_index)](InputBindingKey key, float value) {
|
|
USB::SetDeviceBindValue(port, base + key.data, value);
|
|
});
|
|
}
|
|
break;
|
|
|
|
case InputBindingInfo::Type::Pointer:
|
|
{
|
|
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bind_name.c_str()));
|
|
for (const std::string& binding : bindings)
|
|
{
|
|
const std::optional<u32> key(GetIndexFromPointerBinding(binding));
|
|
if (!key.has_value())
|
|
continue;
|
|
|
|
s_pointer_move_callbacks.emplace_back(key.value(), [port, base = bi.bind_index](InputBindingKey key, float value) {
|
|
USB::SetDeviceBindValue(port, base + key.data, value);
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
|
|
case InputBindingInfo::Type::Motor:
|
|
{
|
|
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bind_name.c_str()));
|
|
for (const std::string& binding : bindings)
|
|
{
|
|
PadVibrationBinding vib;
|
|
vib.pad_index = Pad::NUM_CONTROLLER_PORTS + port;
|
|
ParseBindingAndGetSource(binding, &vib.motors[0].binding, &vib.motors[0].source);
|
|
s_pad_vibration_array.push_back(std::move(vib));
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Event Handling
|
|
// ------------------------------------------------------------------------
|
|
|
|
bool InputManager::HasAnyBindingsForKey(InputBindingKey key)
|
|
{
|
|
std::unique_lock lock(s_binding_map_write_lock);
|
|
return (s_binding_map.find(key.MaskDirection()) != s_binding_map.end());
|
|
}
|
|
|
|
bool InputManager::HasAnyBindingsForSource(InputBindingKey key)
|
|
{
|
|
std::unique_lock lock(s_binding_map_write_lock);
|
|
for (const auto& it : s_binding_map)
|
|
{
|
|
const InputBindingKey& okey = it.first;
|
|
if (okey.source_type == key.source_type && okey.source_index == key.source_index && okey.source_subtype == key.source_subtype)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool InputManager::IsAxisHandler(const InputEventHandler& handler)
|
|
{
|
|
return std::holds_alternative<InputAxisEventHandler>(handler);
|
|
}
|
|
|
|
bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key,
|
|
GenericInputBinding axis_neg_key, GenericInputBinding axis_pos_key)
|
|
{
|
|
if (DoEventHook(key, value))
|
|
return true;
|
|
|
|
// If imgui ate the event, don't fire our handlers.
|
|
const bool skip_button_handlers = PreprocessEvent(key, value, generic_key, axis_neg_key, axis_pos_key);
|
|
return ProcessEvent(key, value, skip_button_handlers);
|
|
}
|
|
|
|
bool InputManager::ProcessEvent(InputBindingKey key, float value, bool skip_button_handlers)
|
|
{
|
|
// find all the bindings associated with this key
|
|
const InputBindingKey masked_key = key.MaskDirection();
|
|
const auto range = s_binding_map.equal_range(masked_key);
|
|
if (range.first == s_binding_map.end())
|
|
return false;
|
|
|
|
// Now we can actually fire/activate bindings.
|
|
u32 min_num_keys = 0;
|
|
for (auto it = range.first; it != range.second; ++it)
|
|
{
|
|
InputBinding* binding = it->second.get();
|
|
|
|
// find the key which matches us
|
|
for (u32 i = 0; i < binding->num_keys; i++)
|
|
{
|
|
if (binding->keys[i].MaskDirection() != masked_key)
|
|
continue;
|
|
|
|
const u8 bit = static_cast<u8>(1) << i;
|
|
const bool negative = binding->keys[i].modifier == InputModifier::Negate;
|
|
const bool new_state = (negative ? (value < 0.0f) : (value > 0.0f));
|
|
|
|
float value_to_pass = 0.0f;
|
|
switch (binding->keys[i].modifier)
|
|
{
|
|
case InputModifier::None:
|
|
if (value > 0.0f)
|
|
value_to_pass = value;
|
|
break;
|
|
case InputModifier::Negate:
|
|
if (value < 0.0f)
|
|
value_to_pass = -value;
|
|
break;
|
|
case InputModifier::FullAxis:
|
|
value_to_pass = value * 0.5f + 0.5f;
|
|
break;
|
|
}
|
|
|
|
// handle inverting, needed for some wheels.
|
|
value_to_pass = binding->keys[i].invert ? (1.0f - value_to_pass) : value_to_pass;
|
|
|
|
// axes are fired regardless of a state change, unless they're zero
|
|
// (but going from not-zero to zero will still fire, because of the full state)
|
|
// for buttons, we can use the state of the last chord key, because it'll be 1 on press,
|
|
// and 0 on release (when the full state changes).
|
|
if (IsAxisHandler(binding->handler))
|
|
{
|
|
if (value_to_pass >= 0.0f && (!skip_button_handlers || value_to_pass == 0.0f))
|
|
std::get<InputAxisEventHandler>(binding->handler)(key, value_to_pass);
|
|
}
|
|
else if (binding->num_keys >= min_num_keys)
|
|
{
|
|
// update state based on whether the whole chord was activated
|
|
const u8 new_mask = (new_state ? (binding->current_mask | bit) : (binding->current_mask & ~bit));
|
|
const bool prev_full_state = (binding->current_mask == binding->full_mask);
|
|
const bool new_full_state = (new_mask == binding->full_mask);
|
|
binding->current_mask = new_mask;
|
|
|
|
// Workaround for multi-key bindings that share the same keys.
|
|
if (binding->num_keys > 1 && new_full_state && prev_full_state != new_full_state && range.first != range.second)
|
|
{
|
|
// Because the binding map isn't ordered, we could iterate in the order of Shift+F1 and then
|
|
// F1, which would mean that F1 wouldn't get cancelled and still activate. So, to handle this
|
|
// case, we skip activating any future bindings with a fewer number of keys.
|
|
min_num_keys = std::max<u32>(min_num_keys, binding->num_keys);
|
|
|
|
// Basically, if we bind say, F1 and Shift+F1, and press shift and then F1, we'll fire bindings
|
|
// for both F1 and Shift+F1, when we really only want to fire the binding for Shift+F1. So,
|
|
// when we activate a multi-key chord (key press), we go through the binding map for all the
|
|
// other keys in the chord, and cancel them if they have a shorter chord. If they're longer,
|
|
// they could still activate and take precedence over us, so we leave them alone.
|
|
for (u32 i = 0; i < binding->num_keys; i++)
|
|
{
|
|
const auto range = s_binding_map.equal_range(binding->keys[i].MaskDirection());
|
|
for (auto it = range.first; it != range.second; ++it)
|
|
{
|
|
InputBinding* other_binding = it->second.get();
|
|
if (other_binding == binding || IsAxisHandler(other_binding->handler) ||
|
|
other_binding->num_keys >= binding->num_keys)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// We only need to cancel the binding if it was fully active before. Which in the above
|
|
// case of Shift+F1 / F1, it will be.
|
|
if (other_binding->current_mask == other_binding->full_mask)
|
|
std::get<InputButtonEventHandler>(other_binding->handler)(-1);
|
|
|
|
// Zero out the current bits so that we don't release this binding, if the other part
|
|
// of the chord releases first.
|
|
other_binding->current_mask = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (prev_full_state != new_full_state && binding->num_keys >= min_num_keys)
|
|
{
|
|
const s32 pressed = skip_button_handlers ? -1 : static_cast<s32>(value_to_pass > 0.0f);
|
|
std::get<InputButtonEventHandler>(binding->handler)(pressed);
|
|
}
|
|
}
|
|
|
|
// bail out, since we shouldn't have the same key twice in the chord
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void InputManager::ClearBindStateFromSource(InputBindingKey key)
|
|
{
|
|
// Why are we doing it this way? Because any of the bindings could cause a reload and invalidate our iterators :(.
|
|
// Axis handlers should be fine, so we'll do those as a first pass.
|
|
for (const auto& [match_key, binding] : s_binding_map)
|
|
{
|
|
if (key.source_type != match_key.source_type || key.source_subtype != match_key.source_subtype ||
|
|
key.source_index != match_key.source_index || !IsAxisHandler(binding->handler))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (u32 i = 0; i < binding->num_keys; i++)
|
|
{
|
|
if (binding->keys[i].MaskDirection() != match_key)
|
|
continue;
|
|
|
|
std::get<InputAxisEventHandler>(binding->handler)(key, 0.0f);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Now go through the button handlers, and pick them off.
|
|
bool matched;
|
|
do
|
|
{
|
|
matched = false;
|
|
|
|
for (const auto& [match_key, binding] : s_binding_map)
|
|
{
|
|
if (key.source_type != match_key.source_type || key.source_subtype != match_key.source_subtype ||
|
|
key.source_index != match_key.source_index || IsAxisHandler(binding->handler))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (u32 i = 0; i < binding->num_keys; i++)
|
|
{
|
|
if (binding->keys[i].MaskDirection() != match_key)
|
|
continue;
|
|
|
|
// Skip if we weren't pressed.
|
|
const u8 bit = static_cast<u8>(1) << i;
|
|
if ((binding->current_mask & bit) == 0)
|
|
continue;
|
|
|
|
// Only fire handler if we're changing from active state.
|
|
const u8 current_mask = binding->current_mask;
|
|
binding->current_mask &= ~bit;
|
|
|
|
if (current_mask == binding->full_mask)
|
|
{
|
|
std::get<InputButtonEventHandler>(binding->handler)(0.0f);
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Need to start again, might've reloaded.
|
|
if (matched)
|
|
break;
|
|
}
|
|
} while (matched);
|
|
}
|
|
|
|
bool InputManager::PreprocessEvent(InputBindingKey key, float value, GenericInputBinding generic_key,
|
|
GenericInputBinding axis_neg_key, GenericInputBinding axis_pos_key)
|
|
{
|
|
// does imgui want the event?
|
|
if (key.source_type == InputSourceType::Keyboard)
|
|
{
|
|
if (ImGuiManager::ProcessHostKeyEvent(key, value))
|
|
return true;
|
|
|
|
for (const KeyboardEventCallback& kbc : s_keyboard_event_callbacks)
|
|
kbc(key, value);
|
|
}
|
|
else if (key.source_type == InputSourceType::Pointer && key.source_subtype == InputSubclass::PointerButton)
|
|
{
|
|
if (ImGuiManager::ProcessPointerButtonEvent(key, value))
|
|
return true;
|
|
}
|
|
else if (key.source_subtype == InputSubclass::ControllerButton)
|
|
{
|
|
// User binding takes priority; fall back to the generic_key passed by the source (static table).
|
|
const auto it = s_controller_button_generic_map.find(key.MaskDirection());
|
|
const GenericInputBinding resolved = (it != s_controller_button_generic_map.end()) ? it->second : generic_key;
|
|
if (resolved != GenericInputBinding::Unknown)
|
|
{
|
|
const u32 controller_id = (static_cast<u32>(key.source_type) << 8) | key.source_index;
|
|
const InputLayout layout = s_input_sources[static_cast<u32>(InputSourceType::SDL)]->GetControllerLayout(key.source_index);
|
|
if (ImGuiManager::ProcessGenericInputEvent(resolved, layout, value, controller_id) && value != 0.0f)
|
|
return true;
|
|
}
|
|
}
|
|
else if (key.source_subtype == InputSubclass::ControllerAxis)
|
|
{
|
|
// User binding takes priority; fall back to the neg/pos keys passed by the source (static table).
|
|
const auto it = s_controller_axis_generic_map.find(key.MaskDirection());
|
|
const GenericInputBinding neg = (it != s_controller_axis_generic_map.end()) ? it->second[0] : axis_neg_key;
|
|
const GenericInputBinding pos = (it != s_controller_axis_generic_map.end()) ? it->second[1] : axis_pos_key;
|
|
if (neg != GenericInputBinding::Unknown || pos != GenericInputBinding::Unknown)
|
|
{
|
|
const u32 controller_id = (static_cast<u32>(key.source_type) << 8) | key.source_index;
|
|
const InputLayout layout = s_input_sources[static_cast<u32>(InputSourceType::SDL)]->GetControllerLayout(key.source_index);
|
|
ImGuiManager::ProcessGenericAxisEvent(neg, pos, layout, value, controller_id);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void InputManager::GenerateRelativeMouseEvents()
|
|
{
|
|
for (u32 device = 0; device < MAX_POINTER_DEVICES; device++)
|
|
{
|
|
for (u32 axis = 0; axis < static_cast<u32>(static_cast<u8>(InputPointerAxis::Count)); axis++)
|
|
{
|
|
const InputBindingKey key(MakePointerAxisKey(device, static_cast<InputPointerAxis>(axis)));
|
|
|
|
PointerAxisState& state = s_pointer_state[device][axis];
|
|
const float delta = static_cast<float>(state.delta.exchange(0, std::memory_order_acquire)) / 65536.0f;
|
|
float value = 0.0f;
|
|
|
|
if (axis <= static_cast<u32>(InputPointerAxis::Y))
|
|
{
|
|
s_pointer_pos[axis] += delta * s_pointer_axis_speed[axis];
|
|
value = std::clamp(s_pointer_pos[axis], -1.0f, 1.0f);
|
|
s_pointer_pos[axis] -= value;
|
|
s_pointer_pos[axis] *= s_pointer_inertia;
|
|
|
|
value *= s_pointer_axis_range[axis];
|
|
if (value > 0.0f)
|
|
value += s_pointer_axis_dead_zone[axis];
|
|
else if (value < 0.0f)
|
|
value -= s_pointer_axis_dead_zone[axis];
|
|
}
|
|
else
|
|
{
|
|
// ImGui can consume mouse wheel events when the mouse is over a UI element.
|
|
if (delta != 0.0f && ImGuiManager::ProcessPointerAxisEvent(key, delta))
|
|
continue;
|
|
|
|
value = std::clamp(delta, -1.0f, 1.0f);
|
|
}
|
|
|
|
if (value != state.last_value)
|
|
{
|
|
state.last_value = value;
|
|
ProcessEvent(key, value, false);
|
|
}
|
|
|
|
if (delta != 0.0f)
|
|
{
|
|
for (const std::pair<u32, PointerMoveCallback>& pmc : s_pointer_move_callbacks)
|
|
{
|
|
if (pmc.first == device)
|
|
pmc.second(key, delta);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::pair<float, float> InputManager::GetPointerAbsolutePosition(u32 index)
|
|
{
|
|
return std::make_pair(s_host_pointer_positions[index][static_cast<u8>(InputPointerAxis::X)],
|
|
s_host_pointer_positions[index][static_cast<u8>(InputPointerAxis::Y)]);
|
|
}
|
|
|
|
void InputManager::UpdatePointerAbsolutePosition(u32 index, float x, float y)
|
|
{
|
|
const float dx = x - std::exchange(s_host_pointer_positions[index][static_cast<u8>(InputPointerAxis::X)], x);
|
|
const float dy = y - std::exchange(s_host_pointer_positions[index][static_cast<u8>(InputPointerAxis::Y)], y);
|
|
|
|
if (dx != 0.0f)
|
|
s_pointer_state[index][static_cast<u8>(InputPointerAxis::X)].delta.fetch_add(
|
|
static_cast<s32>(dx * 65536.0f), std::memory_order_release);
|
|
if (dy != 0.0f)
|
|
s_pointer_state[index][static_cast<u8>(InputPointerAxis::Y)].delta.fetch_add(
|
|
static_cast<s32>(dy * 65536.0f), std::memory_order_release);
|
|
|
|
if (index == 0)
|
|
ImGuiManager::UpdateMousePosition(x, y);
|
|
}
|
|
|
|
void InputManager::UpdatePointerRelativeDelta(u32 index, InputPointerAxis axis, float d, bool raw_input)
|
|
{
|
|
s_host_pointer_positions[index][static_cast<u8>(axis)] += d;
|
|
s_pointer_state[index][static_cast<u8>(axis)].delta.fetch_add(static_cast<s32>(d * 65536.0f), std::memory_order_release);
|
|
|
|
if (index == 0 && axis <= InputPointerAxis::Y)
|
|
ImGuiManager::UpdateMousePosition(s_host_pointer_positions[0][0], s_host_pointer_positions[0][1]);
|
|
}
|
|
|
|
void InputManager::OnInputDeviceConnected(const std::string_view identifier, const std::string_view device_name)
|
|
{
|
|
if (VMManager::HasValidVM())
|
|
USB::InputDeviceConnected(identifier);
|
|
|
|
Host::OnInputDeviceConnected(identifier, device_name);
|
|
}
|
|
|
|
void InputManager::OnInputDeviceDisconnected(const InputBindingKey key, const std::string_view identifier)
|
|
{
|
|
if (VMManager::HasValidVM())
|
|
USB::InputDeviceDisconnected(identifier);
|
|
|
|
Host::OnInputDeviceDisconnected(key, identifier);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Vibration
|
|
// ------------------------------------------------------------------------
|
|
|
|
void InputManager::SetUSBVibrationIntensity(u32 port, float large_or_single_motor_intensity, float small_motor_intensity)
|
|
{
|
|
SetPadVibrationIntensity(Pad::NUM_CONTROLLER_PORTS + port, large_or_single_motor_intensity, small_motor_intensity);
|
|
}
|
|
|
|
#ifdef __ANDROID__
|
|
// The Android pad is fed by the custom JNI input path, not an input source with motor
|
|
// bindings, so s_pad_vibration_array is empty and the loop below never drives a vibrator
|
|
// (this is why rumble did nothing after the mono migration). Forward intensity changes
|
|
// straight to the gamepad's Android vibrator via onPadRumble (NativeApp routes them to
|
|
// that player's controller, or the handheld's own haptic as a fallback). Deduped per pad
|
|
// to match the Java one-shot model (RUMBLE_MS re-issued only on change, cancelled on 0).
|
|
namespace Native { void onPadRumble(int pad, int largeMotor, int smallMotor); }
|
|
#endif
|
|
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
// Same story as Android above: iOS drives pads through its own SDL bridge and binds no
|
|
// motors, so s_pad_vibration_array is empty and the loop below never reaches a motor.
|
|
// Hand every intensity change to the iOS rumble queue, which the VM thread drains once a
|
|
// frame and turns into SDL rumble, a CoreHaptics pulse, or the phone's own taptic engine.
|
|
extern "C" void ARMSX2_iOSUpdatePadVibration(u32 pad_index, float large_intensity, float small_intensity);
|
|
#endif
|
|
|
|
void InputManager::SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity)
|
|
{
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
ARMSX2_iOSUpdatePadVibration(pad_index, large_or_single_motor_intensity, small_motor_intensity);
|
|
#endif
|
|
#ifdef __ANDROID__
|
|
if (pad_index < Pad::NUM_CONTROLLER_PORTS)
|
|
{
|
|
static float s_android_last[Pad::NUM_CONTROLLER_PORTS][2] = {};
|
|
if (s_android_last[pad_index][0] != large_or_single_motor_intensity ||
|
|
s_android_last[pad_index][1] != small_motor_intensity)
|
|
{
|
|
s_android_last[pad_index][0] = large_or_single_motor_intensity;
|
|
s_android_last[pad_index][1] = small_motor_intensity;
|
|
Native::onPadRumble(static_cast<int>(pad_index),
|
|
static_cast<int>(large_or_single_motor_intensity * 255.0f + 0.5f),
|
|
static_cast<int>(small_motor_intensity * 255.0f + 0.5f));
|
|
}
|
|
}
|
|
#endif
|
|
for (PadVibrationBinding& pad : s_pad_vibration_array)
|
|
{
|
|
if (pad.pad_index != pad_index)
|
|
continue;
|
|
|
|
PadVibrationBinding::Motor& large_motor = pad.motors[0];
|
|
PadVibrationBinding::Motor& small_motor = pad.motors[1];
|
|
if (large_motor.last_intensity == large_or_single_motor_intensity && small_motor.last_intensity == small_motor_intensity)
|
|
continue;
|
|
|
|
if (pad.AreMotorsCombined())
|
|
{
|
|
// if the motors are combined, we need to adjust to the maximum of both
|
|
const float report_intensity = std::max(large_or_single_motor_intensity, small_motor_intensity);
|
|
if (large_motor.source)
|
|
{
|
|
large_motor.last_update_time = Common::Timer::GetCurrentValue();
|
|
large_motor.source->UpdateMotorState(large_motor.binding, report_intensity);
|
|
}
|
|
}
|
|
else if (large_motor.source == small_motor.source)
|
|
{
|
|
// both motors are bound to the same source, do an optimal update
|
|
large_motor.last_update_time = Common::Timer::GetCurrentValue();
|
|
large_motor.source->UpdateMotorState(
|
|
large_motor.binding, small_motor.binding, large_or_single_motor_intensity, small_motor_intensity);
|
|
}
|
|
else
|
|
{
|
|
// update motors independently
|
|
if (large_motor.source && large_motor.last_intensity != large_or_single_motor_intensity)
|
|
{
|
|
large_motor.last_update_time = Common::Timer::GetCurrentValue();
|
|
large_motor.source->UpdateMotorState(large_motor.binding, large_or_single_motor_intensity);
|
|
}
|
|
if (small_motor.source && small_motor.last_intensity != small_motor_intensity)
|
|
{
|
|
small_motor.last_update_time = Common::Timer::GetCurrentValue();
|
|
small_motor.source->UpdateMotorState(small_motor.binding, small_motor_intensity);
|
|
}
|
|
}
|
|
|
|
large_motor.last_intensity = large_or_single_motor_intensity;
|
|
small_motor.last_intensity = small_motor_intensity;
|
|
}
|
|
}
|
|
|
|
void InputManager::PauseVibration()
|
|
{
|
|
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
// Nothing else stops the motor on the iOS path, so without this a pad that was
|
|
// rumbling when you opened the menu keeps rumbling behind it.
|
|
for (u32 pad_index = 0; pad_index < Pad::NUM_CONTROLLER_PORTS; pad_index++)
|
|
ARMSX2_iOSUpdatePadVibration(pad_index, 0.0f, 0.0f);
|
|
#endif
|
|
for (PadVibrationBinding& binding : s_pad_vibration_array)
|
|
{
|
|
for (u32 motor_index = 0; motor_index < MAX_MOTORS_PER_PAD; motor_index++)
|
|
{
|
|
PadVibrationBinding::Motor& motor = binding.motors[motor_index];
|
|
if (!motor.source || motor.last_intensity == 0.0f)
|
|
continue;
|
|
|
|
// we deliberately don't zero the intensity here, so it can resume later
|
|
motor.last_update_time = 0;
|
|
motor.source->UpdateMotorState(motor.binding, 0.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputManager::UpdateContinuedVibration()
|
|
{
|
|
// update vibration intensities, so if the game does a long effect, it continues
|
|
const u64 current_time = Common::Timer::GetCurrentValue();
|
|
for (PadVibrationBinding& pad : s_pad_vibration_array)
|
|
{
|
|
if (pad.AreMotorsCombined())
|
|
{
|
|
// motors are combined
|
|
PadVibrationBinding::Motor& large_motor = pad.motors[0];
|
|
if (!large_motor.source)
|
|
continue;
|
|
|
|
// so only check the first one
|
|
const double dt = Common::Timer::ConvertValueToSeconds(current_time - large_motor.last_update_time);
|
|
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
|
|
continue;
|
|
|
|
// but take max of both motors for the intensity
|
|
const float intensity = pad.GetCombinedIntensity();
|
|
if (intensity == 0.0f)
|
|
continue;
|
|
|
|
large_motor.last_update_time = current_time;
|
|
large_motor.source->UpdateMotorState(large_motor.binding, intensity);
|
|
}
|
|
else
|
|
{
|
|
// independent motor control
|
|
for (u32 i = 0; i < MAX_MOTORS_PER_PAD; i++)
|
|
{
|
|
PadVibrationBinding::Motor& motor = pad.motors[i];
|
|
if (!motor.source || motor.last_intensity == 0.0f)
|
|
continue;
|
|
|
|
const double dt = Common::Timer::ConvertValueToSeconds(current_time - motor.last_update_time);
|
|
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
|
|
continue;
|
|
|
|
// re-notify the source of the continued effect
|
|
motor.last_update_time = current_time;
|
|
motor.source->UpdateMotorState(motor.binding, motor.last_intensity);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Hooks/Event Intercepting
|
|
// ------------------------------------------------------------------------
|
|
|
|
void InputManager::SetHook(InputInterceptHook::Callback callback)
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
|
pxAssert(!m_event_intercept_callback);
|
|
m_event_intercept_callback = std::move(callback);
|
|
}
|
|
|
|
void InputManager::RemoveHook()
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
|
if (m_event_intercept_callback)
|
|
m_event_intercept_callback = {};
|
|
}
|
|
|
|
bool InputManager::HasHook()
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
|
return (bool)m_event_intercept_callback;
|
|
}
|
|
|
|
bool InputManager::DoEventHook(InputBindingKey key, float value)
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
|
if (!m_event_intercept_callback)
|
|
return false;
|
|
|
|
const InputInterceptHook::CallbackResult action = m_event_intercept_callback(key, value);
|
|
if (action >= InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent)
|
|
m_event_intercept_callback = {};
|
|
|
|
return (action == InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent ||
|
|
action == InputInterceptHook::CallbackResult::StopProcessingEvent);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Binding Updater
|
|
// ------------------------------------------------------------------------
|
|
|
|
void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& binding_si, SettingsInterface& hotkey_binding_si, bool is_binding_profile, bool is_hotkey_profile)
|
|
{
|
|
PauseVibration();
|
|
|
|
std::unique_lock lock(s_binding_map_write_lock);
|
|
|
|
s_binding_map.clear();
|
|
s_pad_vibration_array.clear();
|
|
s_keyboard_event_callbacks.clear();
|
|
s_pointer_move_callbacks.clear();
|
|
s_controller_button_generic_map.clear();
|
|
s_controller_axis_generic_map.clear();
|
|
|
|
// Hotkeys use the base configuration, except if the custom hotkeys option is enabled.
|
|
AddHotkeyBindings(hotkey_binding_si, is_hotkey_profile);
|
|
|
|
// If there's an input profile, we load pad bindings from it alone, rather than
|
|
// falling back to the base configuration.
|
|
for (u32 pad = 0; pad < Pad::NUM_CONTROLLER_PORTS; pad++)
|
|
AddPadBindings(binding_si, pad, is_binding_profile);
|
|
|
|
constexpr float ui_ctrl_range = 100.0f;
|
|
constexpr float pointer_sensitivity = 0.05f;
|
|
for (u32 axis = 0; axis <= static_cast<u32>(InputPointerAxis::Y); axis++)
|
|
{
|
|
s_pointer_axis_speed[axis] = si.GetFloatValue("Pad", fmt::format("Pointer{}Speed", s_pointer_axis_setting_names[axis]).c_str(), 40.0f) /
|
|
ui_ctrl_range * pointer_sensitivity;
|
|
s_pointer_axis_dead_zone[axis] = std::min(
|
|
si.GetFloatValue("Pad", fmt::format("Pointer{}DeadZone", s_pointer_axis_setting_names[axis]).c_str(), 20.0f) / ui_ctrl_range, 1.0f);
|
|
s_pointer_axis_range[axis] = 1.0f - s_pointer_axis_dead_zone[axis];
|
|
}
|
|
s_pointer_inertia = si.GetFloatValue("Pad", "PointerInertia", 10.0f) / ui_ctrl_range;
|
|
s_pointer_pos = {};
|
|
|
|
for (u32 port = 0; port < USB::NUM_PORTS; port++)
|
|
AddUSBBindings(binding_si, port, is_binding_profile);
|
|
|
|
UpdateHostMouseMode();
|
|
}
|
|
|
|
void InputManager::UpdateHostMouseMode()
|
|
{
|
|
// Check for relative mode bindings, and enable if there's anything using it.
|
|
bool has_relative_mode_bindings = !s_pointer_move_callbacks.empty();
|
|
if (!has_relative_mode_bindings)
|
|
{
|
|
for (const auto& it : s_binding_map)
|
|
{
|
|
const InputBindingKey& key = it.first;
|
|
if (key.source_type == InputSourceType::Pointer && key.source_subtype == InputSubclass::PointerAxis &&
|
|
key.data >= static_cast<u32>(InputPointerAxis::X) && key.data <= static_cast<u32>(InputPointerAxis::Y))
|
|
{
|
|
has_relative_mode_bindings = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool has_software_cursor = ImGuiManager::HasSoftwareCursor(0);
|
|
Host::SetMouseMode(has_relative_mode_bindings, has_relative_mode_bindings || has_software_cursor);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Source Management
|
|
// ------------------------------------------------------------------------
|
|
|
|
bool InputManager::ReloadDevices()
|
|
{
|
|
bool changed = false;
|
|
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
if (s_input_sources[i]->IsInitialized())
|
|
changed |= s_input_sources[i]->ReloadDevices();
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
void InputManager::CloseSources()
|
|
{
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
if (s_input_sources[i] && s_input_sources[i]->IsInitialized())
|
|
{
|
|
s_input_sources[i]->Shutdown();
|
|
}
|
|
s_input_sources[i].reset();
|
|
}
|
|
}
|
|
|
|
void InputManager::PollSources()
|
|
{
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
if (s_input_sources[i]->IsInitialized())
|
|
s_input_sources[i]->PollEvents();
|
|
}
|
|
|
|
GenerateRelativeMouseEvents();
|
|
|
|
if (VMManager::GetState() == VMState::Running && !s_pad_vibration_array.empty())
|
|
UpdateContinuedVibration();
|
|
}
|
|
|
|
|
|
std::vector<std::pair<std::string, std::string>> InputManager::EnumerateDevices()
|
|
{
|
|
std::vector<std::pair<std::string, std::string>> ret;
|
|
|
|
ret.emplace_back("Keyboard", "Keyboard");
|
|
ret.emplace_back("Mouse", "Mouse");
|
|
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
if (s_input_sources[i]->IsInitialized())
|
|
{
|
|
std::vector<std::pair<std::string, std::string>> devs(s_input_sources[i]->EnumerateDevices());
|
|
if (ret.empty())
|
|
ret = std::move(devs);
|
|
else
|
|
std::move(devs.begin(), devs.end(), std::back_inserter(ret));
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::vector<InputBindingKey> InputManager::EnumerateMotors()
|
|
{
|
|
std::vector<InputBindingKey> ret;
|
|
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
if (s_input_sources[i]->IsInitialized())
|
|
{
|
|
std::vector<InputBindingKey> devs(s_input_sources[i]->EnumerateMotors());
|
|
if (ret.empty())
|
|
ret = std::move(devs);
|
|
else
|
|
std::move(devs.begin(), devs.end(), std::back_inserter(ret));
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void GetKeyboardGenericBindingMapping(std::vector<std::pair<GenericInputBinding, std::string>>* mapping)
|
|
{
|
|
mapping->emplace_back(GenericInputBinding::DPadUp, "Keyboard/Up");
|
|
mapping->emplace_back(GenericInputBinding::DPadRight, "Keyboard/Right");
|
|
mapping->emplace_back(GenericInputBinding::DPadDown, "Keyboard/Down");
|
|
mapping->emplace_back(GenericInputBinding::DPadLeft, "Keyboard/Left");
|
|
mapping->emplace_back(GenericInputBinding::LeftStickUp, "Keyboard/W");
|
|
mapping->emplace_back(GenericInputBinding::LeftStickRight, "Keyboard/D");
|
|
mapping->emplace_back(GenericInputBinding::LeftStickDown, "Keyboard/S");
|
|
mapping->emplace_back(GenericInputBinding::LeftStickLeft, "Keyboard/A");
|
|
mapping->emplace_back(GenericInputBinding::RightStickUp, "Keyboard/T");
|
|
mapping->emplace_back(GenericInputBinding::RightStickRight, "Keyboard/H");
|
|
mapping->emplace_back(GenericInputBinding::RightStickDown, "Keyboard/G");
|
|
mapping->emplace_back(GenericInputBinding::RightStickLeft, "Keyboard/F");
|
|
mapping->emplace_back(GenericInputBinding::Start, "Keyboard/Return");
|
|
mapping->emplace_back(GenericInputBinding::Select, "Keyboard/Backspace");
|
|
mapping->emplace_back(GenericInputBinding::Triangle, "Keyboard/I");
|
|
mapping->emplace_back(GenericInputBinding::Circle, "Keyboard/L");
|
|
mapping->emplace_back(GenericInputBinding::Cross, "Keyboard/K");
|
|
mapping->emplace_back(GenericInputBinding::Square, "Keyboard/J");
|
|
mapping->emplace_back(GenericInputBinding::L1, "Keyboard/Q");
|
|
mapping->emplace_back(GenericInputBinding::L2, "Keyboard/1");
|
|
mapping->emplace_back(GenericInputBinding::L3, "Keyboard/2");
|
|
mapping->emplace_back(GenericInputBinding::R1, "Keyboard/E");
|
|
mapping->emplace_back(GenericInputBinding::R2, "Keyboard/3");
|
|
mapping->emplace_back(GenericInputBinding::R3, "Keyboard/4");
|
|
}
|
|
|
|
static bool GetInternalGenericBindingMapping(const std::string_view device, InputManager::GenericInputBindingMapping* mapping)
|
|
{
|
|
if (device == "Keyboard")
|
|
{
|
|
GetKeyboardGenericBindingMapping(mapping);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
InputManager::GenericInputBindingMapping InputManager::GetGenericBindingMapping(const std::string_view device)
|
|
{
|
|
GenericInputBindingMapping mapping;
|
|
|
|
if (!GetInternalGenericBindingMapping(device, &mapping))
|
|
{
|
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
|
{
|
|
if (s_input_sources[i]->IsInitialized() && s_input_sources[i]->GetGenericBindingMapping(device, &mapping))
|
|
break;
|
|
}
|
|
}
|
|
|
|
return mapping;
|
|
}
|
|
|
|
bool InputManager::IsInputSourceEnabled(SettingsInterface& si, InputSourceType type)
|
|
{
|
|
return si.GetBoolValue("InputSources", InputManager::InputSourceToString(type), InputManager::GetInputSourceDefaultEnabled(type));
|
|
}
|
|
|
|
template <typename T>
|
|
void InputManager::UpdateInputSourceState(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock, InputSourceType type)
|
|
{
|
|
if (!s_input_sources[static_cast<u32>(type)])
|
|
{
|
|
std::unique_ptr<InputSource> source = std::make_unique<T>();
|
|
if (!source->Initialize(si, settings_lock))
|
|
Console.Error("(InputManager) Source '%s' failed to initialize.", InputSourceToString(type));
|
|
|
|
s_input_sources[static_cast<u32>(type)] = std::move(source);
|
|
}
|
|
|
|
const bool enabled = IsInputSourceEnabled(si, type);
|
|
if (enabled)
|
|
{
|
|
if (s_input_sources[static_cast<u32>(type)]->IsInitialized())
|
|
{
|
|
s_input_sources[static_cast<u32>(type)]->UpdateSettings(si, settings_lock);
|
|
}
|
|
else if (!s_input_sources[static_cast<u32>(type)]->Initialize(si, settings_lock))
|
|
{
|
|
Console.Error("(InputManager) Source '%s' failed to initialize.", InputSourceToString(type));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (s_input_sources[static_cast<u32>(type)]->IsInitialized())
|
|
{
|
|
settings_lock.unlock();
|
|
s_input_sources[static_cast<u32>(type)]->Shutdown();
|
|
settings_lock.lock();
|
|
}
|
|
}
|
|
}
|
|
|
|
#include "Input/SDLInputSource.h"
|
|
|
|
#ifdef _WIN32
|
|
#include "Input/DInputSource.h"
|
|
#include "Input/XInputSource.h"
|
|
#endif
|
|
|
|
void InputManager::ReloadSources(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
|
{
|
|
UpdateInputSourceState<SDLInputSource>(si, settings_lock, InputSourceType::SDL);
|
|
#ifdef _WIN32
|
|
UpdateInputSourceState<DInputSource>(si, settings_lock, InputSourceType::DInput);
|
|
UpdateInputSourceState<XInputSource>(si, settings_lock, InputSourceType::XInput);
|
|
#endif
|
|
}
|