diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
commit | c04dcc2e7d834218ef2d4194331e383402495ae1 (patch) | |
tree | 7333e38d10d75386e60f336b80c2443c1166031d /xbmc/input/joysticks | |
parent | Initial commit. (diff) | |
download | kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip |
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
47 files changed, 6150 insertions, 0 deletions
diff --git a/xbmc/input/joysticks/CMakeLists.txt b/xbmc/input/joysticks/CMakeLists.txt new file mode 100644 index 0000000..0155e5e --- /dev/null +++ b/xbmc/input/joysticks/CMakeLists.txt @@ -0,0 +1,30 @@ +set(SOURCES DeadzoneFilter.cpp + DriverPrimitive.cpp + JoystickEasterEgg.cpp + JoystickMonitor.cpp + JoystickTranslator.cpp + JoystickUtils.cpp + RumbleGenerator.cpp) + +set(HEADERS interfaces/IButtonMap.h + interfaces/IButtonMapCallback.h + interfaces/IButtonMapper.h + interfaces/IButtonSequence.h + interfaces/IDriverHandler.h + interfaces/IDriverReceiver.h + interfaces/IInputHandler.h + interfaces/IInputProvider.h + interfaces/IInputReceiver.h + interfaces/IKeyHandler.h + interfaces/IKeymapHandler.h + DeadzoneFilter.h + DriverPrimitive.h + JoystickEasterEgg.h + JoystickIDs.h + JoystickMonitor.h + JoystickTranslator.h + JoystickTypes.h + JoystickUtils.h + RumbleGenerator.h) + +core_add_library(input_joystick) diff --git a/xbmc/input/joysticks/DeadzoneFilter.cpp b/xbmc/input/joysticks/DeadzoneFilter.cpp new file mode 100644 index 0000000..c437f30 --- /dev/null +++ b/xbmc/input/joysticks/DeadzoneFilter.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DeadzoneFilter.h" + +#include "JoystickIDs.h" +#include "games/controllers/ControllerIDs.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "peripherals/devices/Peripheral.h" +#include "utils/log.h" + +#include <cmath> +#include <vector> + +using namespace KODI; +using namespace JOYSTICK; + +//! Allowed noise for detecting discrete D-pads (value of 0.007 when centered +//! has been observed) +#define AXIS_EPSILON 0.01f + +// Settings for analog sticks +#define SETTING_LEFT_STICK_DEADZONE "left_stick_deadzone" +#define SETTING_RIGHT_STICK_DEADZONE "right_stick_deadzone" + +CDeadzoneFilter::CDeadzoneFilter(IButtonMap* buttonMap, PERIPHERALS::CPeripheral* peripheral) + : m_buttonMap(buttonMap), m_peripheral(peripheral) +{ + if (m_buttonMap->ControllerID() != DEFAULT_CONTROLLER_ID) + CLog::Log(LOGERROR, "ERROR: Must use default controller profile instead of {}", + m_buttonMap->ControllerID()); +} + +float CDeadzoneFilter::FilterAxis(unsigned int axisIndex, float axisValue) +{ + float deadzone = 0.0f; + + bool bSuccess = + GetDeadzone(axisIndex, deadzone, DEFAULT_LEFT_STICK_NAME, SETTING_LEFT_STICK_DEADZONE) || + GetDeadzone(axisIndex, deadzone, DEFAULT_RIGHT_STICK_NAME, SETTING_RIGHT_STICK_DEADZONE); + + if (bSuccess) + return ApplyDeadzone(axisValue, deadzone); + + // Always filter noise about the center + if (std::abs(axisValue) < AXIS_EPSILON) + axisValue = 0.0f; + + return axisValue; +} + +bool CDeadzoneFilter::GetDeadzone(unsigned int axisIndex, + float& deadzone, + const char* featureName, + const char* settingName) +{ + std::vector<ANALOG_STICK_DIRECTION> dirs = { + ANALOG_STICK_DIRECTION::UP, + ANALOG_STICK_DIRECTION::RIGHT, + ANALOG_STICK_DIRECTION::DOWN, + ANALOG_STICK_DIRECTION::LEFT, + }; + + CDriverPrimitive primitive; + for (auto dir : dirs) + { + if (m_buttonMap->GetAnalogStick(featureName, dir, primitive)) + { + if (primitive.Type() == PRIMITIVE_TYPE::SEMIAXIS && primitive.Index() == axisIndex) + { + deadzone = m_peripheral->GetSettingFloat(settingName); + return true; + } + } + } + + return false; +} + +float CDeadzoneFilter::ApplyDeadzone(float value, float deadzone) +{ + if (deadzone < 0.0f || deadzone >= 1.0f) + return 0.0f; + + if (value > deadzone) + return (value - deadzone) / (1.0f - deadzone); + else if (value < -deadzone) + return (value + deadzone) / (1.0f - deadzone); + + return 0.0f; +} diff --git a/xbmc/input/joysticks/DeadzoneFilter.h b/xbmc/input/joysticks/DeadzoneFilter.h new file mode 100644 index 0000000..4547050 --- /dev/null +++ b/xbmc/input/joysticks/DeadzoneFilter.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +namespace PERIPHERALS +{ +class CPeripheral; +} + +namespace KODI +{ +namespace JOYSTICK +{ +class IButtonMap; + +/*! + * \ingroup joystick + * \brief Analog axis deadzone filtering + * + * Axis is scaled appropriately, so position is continuous + * from -1.0 to 1.0: + * + * | / 1.0 + * | / + * __|__/ + * / | + * / |--| Deadzone + * -1.0 / | + * + * After deadzone filtering, the value will be: + * + * - Negative in the interval [-1.0, -deadzone) + * - Zero in the interval [-deadzone, deadzone] + * - Positive in the interval (deadzone, 1.0] + */ +class CDeadzoneFilter +{ +public: + CDeadzoneFilter(IButtonMap* buttonMap, PERIPHERALS::CPeripheral* peripheral); + + /*! + * \brief Apply deadzone filtering to an axis + * \param axisIndex The axis index + * \param axisValue The axis value + * \return The value after applying deadzone filtering + */ + float FilterAxis(unsigned int axisIndex, float axisValue); + +private: + /*! + * \brief Get the deadzone value from the peripheral's settings + * \param axisIndex The axis index + * \param[out] result The deadzone value + * \param featureName The feature that axisIndex is mapped to + * \param settingName The setting corresponding to the given feature + * \return True if the feature is an analog stick and the peripheral has the setting + */ + bool GetDeadzone(unsigned int axisIndex, + float& result, + const char* featureName, + const char* settingName); + + /*! + * \brief Utility function to calculate the deadzone + * \param value The value + * \param deadzone The deadzone + * \return The scaled deadzone + */ + static float ApplyDeadzone(float value, float deadzone); + + // Construction parameters + IButtonMap* const m_buttonMap; + PERIPHERALS::CPeripheral* const m_peripheral; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/DriverPrimitive.cpp b/xbmc/input/joysticks/DriverPrimitive.cpp new file mode 100644 index 0000000..166e926 --- /dev/null +++ b/xbmc/input/joysticks/DriverPrimitive.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DriverPrimitive.h" + +#include "games/controllers/ControllerTranslator.h" +#include "utils/StringUtils.h" + +#include <utility> + +using namespace KODI; +using namespace JOYSTICK; + +CDriverPrimitive::CDriverPrimitive(void) = default; + +CDriverPrimitive::CDriverPrimitive(PRIMITIVE_TYPE type, unsigned int index) + : m_type(type), m_driverIndex(index) +{ +} + +CDriverPrimitive::CDriverPrimitive(unsigned int hatIndex, HAT_DIRECTION direction) + : m_type(PRIMITIVE_TYPE::HAT), m_driverIndex(hatIndex), m_hatDirection(direction) +{ +} + +CDriverPrimitive::CDriverPrimitive(unsigned int axisIndex, + int center, + SEMIAXIS_DIRECTION direction, + unsigned int range) + : m_type(PRIMITIVE_TYPE::SEMIAXIS), + m_driverIndex(axisIndex), + m_center(center), + m_semiAxisDirection(direction), + m_range(range) +{ +} + +CDriverPrimitive::CDriverPrimitive(XBMCKey keycode) + : m_type(PRIMITIVE_TYPE::KEY), m_keycode(keycode) +{ +} + +CDriverPrimitive::CDriverPrimitive(MOUSE::BUTTON_ID index) + : m_type(PRIMITIVE_TYPE::MOUSE_BUTTON), m_driverIndex(static_cast<unsigned int>(index)) +{ +} + +CDriverPrimitive::CDriverPrimitive(RELATIVE_POINTER_DIRECTION direction) + : m_type(PRIMITIVE_TYPE::RELATIVE_POINTER), m_pointerDirection(direction) +{ +} + +bool CDriverPrimitive::operator==(const CDriverPrimitive& rhs) const +{ + if (m_type == rhs.m_type) + { + switch (m_type) + { + case PRIMITIVE_TYPE::BUTTON: + case PRIMITIVE_TYPE::MOTOR: + case PRIMITIVE_TYPE::MOUSE_BUTTON: + return m_driverIndex == rhs.m_driverIndex; + case PRIMITIVE_TYPE::HAT: + return m_driverIndex == rhs.m_driverIndex && m_hatDirection == rhs.m_hatDirection; + case PRIMITIVE_TYPE::SEMIAXIS: + return m_driverIndex == rhs.m_driverIndex && m_center == rhs.m_center && + m_semiAxisDirection == rhs.m_semiAxisDirection && m_range == rhs.m_range; + case PRIMITIVE_TYPE::KEY: + return m_keycode == rhs.m_keycode; + case PRIMITIVE_TYPE::RELATIVE_POINTER: + return m_pointerDirection == rhs.m_pointerDirection; + default: + return true; + } + } + return false; +} + +bool CDriverPrimitive::operator<(const CDriverPrimitive& rhs) const +{ + if (m_type < rhs.m_type) + return true; + if (m_type > rhs.m_type) + return false; + + if (m_type == PRIMITIVE_TYPE::BUTTON || m_type == PRIMITIVE_TYPE::HAT || + m_type == PRIMITIVE_TYPE::SEMIAXIS || m_type == PRIMITIVE_TYPE::MOTOR || + m_type == PRIMITIVE_TYPE::MOUSE_BUTTON) + { + if (m_driverIndex < rhs.m_driverIndex) + return true; + if (m_driverIndex > rhs.m_driverIndex) + return false; + } + + if (m_type == PRIMITIVE_TYPE::HAT) + { + if (m_hatDirection < rhs.m_hatDirection) + return true; + if (m_hatDirection > rhs.m_hatDirection) + return false; + } + + if (m_type == PRIMITIVE_TYPE::SEMIAXIS) + { + if (m_center < rhs.m_center) + return true; + if (m_center > rhs.m_center) + return false; + + if (m_semiAxisDirection < rhs.m_semiAxisDirection) + return true; + if (m_semiAxisDirection > rhs.m_semiAxisDirection) + return false; + + if (m_range < rhs.m_range) + return true; + if (m_range > rhs.m_range) + return false; + } + + if (m_type == PRIMITIVE_TYPE::KEY) + { + if (m_keycode < rhs.m_keycode) + return true; + if (m_keycode > rhs.m_keycode) + return false; + } + + if (m_type == PRIMITIVE_TYPE::RELATIVE_POINTER) + { + if (m_pointerDirection < rhs.m_pointerDirection) + return true; + if (m_pointerDirection > rhs.m_pointerDirection) + return false; + } + + return false; +} + +bool CDriverPrimitive::IsValid(void) const +{ + if (m_type == PRIMITIVE_TYPE::BUTTON || m_type == PRIMITIVE_TYPE::MOTOR || + m_type == PRIMITIVE_TYPE::MOUSE_BUTTON) + return true; + + if (m_type == PRIMITIVE_TYPE::HAT) + { + return m_hatDirection == HAT_DIRECTION::UP || m_hatDirection == HAT_DIRECTION::DOWN || + m_hatDirection == HAT_DIRECTION::RIGHT || m_hatDirection == HAT_DIRECTION::LEFT; + } + + if (m_type == PRIMITIVE_TYPE::SEMIAXIS) + { + unsigned int maxRange = 1; + + switch (m_center) + { + case -1: + { + if (m_semiAxisDirection != SEMIAXIS_DIRECTION::POSITIVE) + return false; + maxRange = 2; + break; + } + case 0: + { + if (m_semiAxisDirection != SEMIAXIS_DIRECTION::POSITIVE && + m_semiAxisDirection != SEMIAXIS_DIRECTION::NEGATIVE) + return false; + break; + } + case 1: + { + if (m_semiAxisDirection != SEMIAXIS_DIRECTION::POSITIVE) + return false; + maxRange = 2; + break; + } + default: + break; + } + + return 1 <= m_range && m_range <= maxRange; + } + + if (m_type == PRIMITIVE_TYPE::KEY) + return m_keycode != XBMCK_UNKNOWN; + + if (m_type == PRIMITIVE_TYPE::RELATIVE_POINTER) + { + return m_pointerDirection == RELATIVE_POINTER_DIRECTION::UP || + m_pointerDirection == RELATIVE_POINTER_DIRECTION::DOWN || + m_pointerDirection == RELATIVE_POINTER_DIRECTION::RIGHT || + m_pointerDirection == RELATIVE_POINTER_DIRECTION::LEFT; + } + + return false; +} + +std::string CDriverPrimitive::ToString() const +{ + switch (m_type) + { + case PRIMITIVE_TYPE::BUTTON: + return StringUtils::Format("button {}", m_driverIndex); + case PRIMITIVE_TYPE::MOTOR: + return StringUtils::Format("motor {}", m_driverIndex); + case PRIMITIVE_TYPE::MOUSE_BUTTON: + return StringUtils::Format("mouse button {}", m_driverIndex); + case PRIMITIVE_TYPE::HAT: + { + switch (m_hatDirection) + { + case HAT_DIRECTION::UP: + return StringUtils::Format("hat {} up", m_driverIndex); + case HAT_DIRECTION::DOWN: + return StringUtils::Format("hat {} down", m_driverIndex); + case HAT_DIRECTION::RIGHT: + return StringUtils::Format("hat {} right", m_driverIndex); + case HAT_DIRECTION::LEFT: + return StringUtils::Format("hat {} left", m_driverIndex); + default: + break; + } + break; + } + case PRIMITIVE_TYPE::SEMIAXIS: + { + switch (m_semiAxisDirection) + { + case SEMIAXIS_DIRECTION::POSITIVE: + return StringUtils::Format("semiaxis +{}", m_driverIndex); + case SEMIAXIS_DIRECTION::NEGATIVE: + return StringUtils::Format("semiaxis -{}", m_driverIndex); + default: + break; + } + break; + } + case PRIMITIVE_TYPE::KEY: + return StringUtils::Format("key {}", + GAME::CControllerTranslator::TranslateKeycode(m_keycode)); + case PRIMITIVE_TYPE::RELATIVE_POINTER: + { + switch (m_pointerDirection) + { + case RELATIVE_POINTER_DIRECTION::UP: + return StringUtils::Format("pointer {} up", m_driverIndex); + case RELATIVE_POINTER_DIRECTION::DOWN: + return StringUtils::Format("pointer {} down", m_driverIndex); + case RELATIVE_POINTER_DIRECTION::RIGHT: + return StringUtils::Format("pointer {} right", m_driverIndex); + case RELATIVE_POINTER_DIRECTION::LEFT: + return StringUtils::Format("pointer {} left", m_driverIndex); + default: + break; + } + break; + } + default: + break; + } + + return ""; +} diff --git a/xbmc/input/joysticks/DriverPrimitive.h b/xbmc/input/joysticks/DriverPrimitive.h new file mode 100644 index 0000000..11d391b --- /dev/null +++ b/xbmc/input/joysticks/DriverPrimitive.h @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "JoystickTypes.h" +#include "input/keyboard/KeyboardTypes.h" +#include "input/mouse/MouseTypes.h" + +#include <stdint.h> + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \ingroup joystick + * \brief Basic driver element associated with input events + * + * Driver input (bools, floats and enums) is split into primitives that better + * map to the physical features on a joystick. + * + * A bool obviously only maps to a single feature, so it is a driver + * primitive. Here, these are called "buttons". + * + * A hat enum encodes the state of the four hat directions. Each direction + * can map to a different feature, so a hat enum consists of four driver + * primitives called "hat directions". + * + * A float is a little trickier. Trivially, it can map to an analog stick or + * trigger. However, DirectInput combines two triggers onto a single axis. + * Therefore, the axis is split into two primitives called "semiaxes". + * + * The type determines the fields in use: + * + * Button: + * - driver index + * + * Hat direction: + * - driver index + * - hat direction (up/right/down/left) + * + * Semiaxis: + * - driver index + * - center (-1, 0 or 1) + * - semiaxis direction (positive/negative) + * - range (1 or 2) + * + * Motor: + * - driver index + * + * Key: + * - keycode + * + * Mouse button: + * - driver index + * + * Relative pointer: + * - pointer direction + * + * For more info, see "Chapter 2. Joystick drivers" in the documentation + * thread: http://forum.kodi.tv/showthread.php?tid=257764 + */ +class CDriverPrimitive +{ +public: + /*! + * \brief Construct an invalid driver primitive + */ + CDriverPrimitive(void); + + /*! + * \brief Construct a driver primitive representing a button or motor + */ + CDriverPrimitive(PRIMITIVE_TYPE type, unsigned int index); + + /*! + * \brief Construct a driver primitive representing one of the four + * direction arrows on a dpad + */ + CDriverPrimitive(unsigned int hatIndex, HAT_DIRECTION direction); + + /*! + * \brief Construct a driver primitive representing the positive or negative + * half of an axis + */ + CDriverPrimitive(unsigned int axisIndex, + int center, + SEMIAXIS_DIRECTION direction, + unsigned int range); + + /*! + * \brief Construct a driver primitive representing a key on a keyboard + */ + CDriverPrimitive(KEYBOARD::KeySymbol keycode); + + /*! + * \brief Construct a driver primitive representing a mouse button + */ + CDriverPrimitive(MOUSE::BUTTON_ID index); + + /*! + * \brief Construct a driver primitive representing a relative pointer + */ + CDriverPrimitive(RELATIVE_POINTER_DIRECTION direction); + + bool operator==(const CDriverPrimitive& rhs) const; + bool operator<(const CDriverPrimitive& rhs) const; + + bool operator!=(const CDriverPrimitive& rhs) const { return !operator==(rhs); } + bool operator>(const CDriverPrimitive& rhs) const { return !(operator<(rhs) || operator==(rhs)); } + bool operator<=(const CDriverPrimitive& rhs) const { return operator<(rhs) || operator==(rhs); } + bool operator>=(const CDriverPrimitive& rhs) const { return !operator<(rhs); } + + /*! + * \brief The type of driver primitive + */ + PRIMITIVE_TYPE Type(void) const { return m_type; } + + /*! + * \brief The index used by the joystick driver + * + * Valid for: + * - buttons + * - hats + * - semiaxes + * - motors + */ + unsigned int Index(void) const { return m_driverIndex; } + + /*! + * \brief The direction arrow (valid for hat directions) + */ + HAT_DIRECTION HatDirection(void) const { return m_hatDirection; } + + /*! + * \brief The location of the zero point of the semiaxis + */ + int Center() const { return m_center; } + + /*! + * \brief The semiaxis direction (valid for semiaxes) + */ + SEMIAXIS_DIRECTION SemiAxisDirection(void) const { return m_semiAxisDirection; } + + /*! + * \brief The distance between the center and the farthest valid value (valid for semiaxes) + */ + unsigned int Range() const { return m_range; } + + /*! + * \brief The keyboard symbol (valid for keys) + */ + KEYBOARD::KeySymbol Keycode() const { return m_keycode; } + + /*! + * \brief The mouse button ID (valid for mouse buttons) + */ + MOUSE::BUTTON_ID MouseButton() const { return static_cast<MOUSE::BUTTON_ID>(m_driverIndex); } + + /*! + * \brief The relative pointer direction (valid for relative pointers) + */ + RELATIVE_POINTER_DIRECTION PointerDirection() const { return m_pointerDirection; } + + /*! + * \brief Test if an driver primitive is valid + * + * A driver primitive is valid if it has a known type and: + * + * 1) for hats, it is a cardinal direction + * 2) for semi-axes, it is a positive or negative direction + * 3) for keys, the keycode is non-empty + */ + bool IsValid(void) const; + + /*! + * \brief Convert primitive to a string suitable for logging + * + * \return The primitive as described by a short string, or empty if invalid + */ + std::string ToString() const; + +private: + PRIMITIVE_TYPE m_type = PRIMITIVE_TYPE::UNKNOWN; + unsigned int m_driverIndex = 0; + HAT_DIRECTION m_hatDirection = HAT_DIRECTION::NONE; + int m_center = 0; + SEMIAXIS_DIRECTION m_semiAxisDirection = SEMIAXIS_DIRECTION::ZERO; + unsigned int m_range = 1; + KEYBOARD::KeySymbol m_keycode = XBMCK_UNKNOWN; + RELATIVE_POINTER_DIRECTION m_pointerDirection = RELATIVE_POINTER_DIRECTION::NONE; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/JoystickEasterEgg.cpp b/xbmc/input/joysticks/JoystickEasterEgg.cpp new file mode 100644 index 0000000..b0259a6 --- /dev/null +++ b/xbmc/input/joysticks/JoystickEasterEgg.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "JoystickEasterEgg.h" + +#include "ServiceBroker.h" +#include "games/GameServices.h" +#include "games/GameSettings.h" +#include "games/controllers/ControllerIDs.h" +#include "games/controllers/DefaultController.h" +#include "guilib/GUIAudioManager.h" +#include "guilib/WindowIDs.h" + +using namespace KODI; +using namespace JOYSTICK; + +const std::map<std::string, std::vector<FeatureName>> CJoystickEasterEgg::m_sequence = { + { + DEFAULT_CONTROLLER_ID, + { + GAME::CDefaultController::FEATURE_UP, + GAME::CDefaultController::FEATURE_UP, + GAME::CDefaultController::FEATURE_DOWN, + GAME::CDefaultController::FEATURE_DOWN, + GAME::CDefaultController::FEATURE_LEFT, + GAME::CDefaultController::FEATURE_RIGHT, + GAME::CDefaultController::FEATURE_LEFT, + GAME::CDefaultController::FEATURE_RIGHT, + GAME::CDefaultController::FEATURE_B, + GAME::CDefaultController::FEATURE_A, + }, + }, + { + DEFAULT_REMOTE_ID, + { + "up", + "up", + "down", + "down", + "left", + "right", + "left", + "right", + "back", + "ok", + }, + }, +}; + +CJoystickEasterEgg::CJoystickEasterEgg(const std::string& controllerId) + : m_controllerId(controllerId), m_state(0) +{ +} + +bool CJoystickEasterEgg::OnButtonPress(const FeatureName& feature) +{ + bool bHandled = false; + + auto it = m_sequence.find(m_controllerId); + if (it != m_sequence.end()) + { + const auto& sequence = it->second; + + // Reset state if it previously finished + if (m_state >= sequence.size()) + m_state = 0; + + if (feature == sequence[m_state]) + m_state++; + else + m_state = 0; + + if (IsCapturing()) + { + bHandled = true; + + if (m_state >= sequence.size()) + OnFinish(); + } + } + + return bHandled; +} + +bool CJoystickEasterEgg::IsCapturing() +{ + // Capture input when finished with arrows (2 x up/down/left/right) + return m_state > 8; +} + +void CJoystickEasterEgg::OnFinish(void) +{ + GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings(); + gameSettings.ToggleGames(); + + WINDOW_SOUND sound = gameSettings.GamesEnabled() ? SOUND_INIT : SOUND_DEINIT; + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui) + gui->GetAudioManager().PlayWindowSound(WINDOW_DIALOG_KAI_TOAST, sound); + + //! @todo Shake screen +} diff --git a/xbmc/input/joysticks/JoystickEasterEgg.h b/xbmc/input/joysticks/JoystickEasterEgg.h new file mode 100644 index 0000000..7059d13 --- /dev/null +++ b/xbmc/input/joysticks/JoystickEasterEgg.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/interfaces/IButtonSequence.h" + +#include <map> +#include <string> +#include <vector> + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \brief Hush!!! + */ +class CJoystickEasterEgg : public IButtonSequence +{ +public: + explicit CJoystickEasterEgg(const std::string& controllerId); + ~CJoystickEasterEgg() override = default; + + // implementation of IButtonSequence + bool OnButtonPress(const FeatureName& feature) override; + bool IsCapturing() override; + + static void OnFinish(void); + +private: + // Construction parameters + const std::string m_controllerId; + + static const std::map<std::string, std::vector<FeatureName>> m_sequence; + + unsigned int m_state; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/JoystickIDs.h b/xbmc/input/joysticks/JoystickIDs.h new file mode 100644 index 0000000..52ffcf4 --- /dev/null +++ b/xbmc/input/joysticks/JoystickIDs.h @@ -0,0 +1,13 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// Analog sticks on the default controller +#define DEFAULT_LEFT_STICK_NAME "leftstick" +#define DEFAULT_RIGHT_STICK_NAME "rightstick" diff --git a/xbmc/input/joysticks/JoystickMonitor.cpp b/xbmc/input/joysticks/JoystickMonitor.cpp new file mode 100644 index 0000000..6375a41 --- /dev/null +++ b/xbmc/input/joysticks/JoystickMonitor.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "JoystickMonitor.h" + +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPowerHandling.h" +#include "games/controllers/ControllerIDs.h" +#include "input/InputManager.h" + +#include <cmath> + +using namespace KODI; +using namespace JOYSTICK; + +#define AXIS_DEADZONE 0.05f + +std::string CJoystickMonitor::ControllerID() const +{ + return DEFAULT_CONTROLLER_ID; +} + +bool CJoystickMonitor::AcceptsInput(const FeatureName& feature) const +{ + // Only accept input when screen saver is active + auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + return appPower->IsInScreenSaver(); +} + +bool CJoystickMonitor::OnButtonPress(const FeatureName& feature, bool bPressed) +{ + if (bPressed) + { + CServiceBroker::GetInputManager().SetMouseActive(false); + return ResetTimers(); + } + + return false; +} + +bool CJoystickMonitor::OnButtonMotion(const FeatureName& feature, + float magnitude, + unsigned int motionTimeMs) +{ + if (std::fabs(magnitude) > AXIS_DEADZONE) + { + CServiceBroker::GetInputManager().SetMouseActive(false); + return ResetTimers(); + } + + return false; +} + +bool CJoystickMonitor::OnAnalogStickMotion(const FeatureName& feature, + float x, + float y, + unsigned int motionTimeMs) +{ + // Analog stick deadzone already processed + if (x != 0.0f || y != 0.0f) + { + CServiceBroker::GetInputManager().SetMouseActive(false); + return ResetTimers(); + } + + return false; +} + +bool CJoystickMonitor::OnWheelMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) +{ + if (std::fabs(position) > AXIS_DEADZONE) + { + CServiceBroker::GetInputManager().SetMouseActive(false); + return ResetTimers(); + } + + return false; +} + +bool CJoystickMonitor::OnThrottleMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) +{ + if (std::fabs(position) > AXIS_DEADZONE) + { + CServiceBroker::GetInputManager().SetMouseActive(false); + return ResetTimers(); + } + + return false; +} + +bool CJoystickMonitor::ResetTimers(void) +{ + auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + appPower->ResetSystemIdleTimer(); + appPower->ResetScreenSaver(); + return appPower->WakeUpScreenSaverAndDPMS(); + + return true; +} diff --git a/xbmc/input/joysticks/JoystickMonitor.h b/xbmc/input/joysticks/JoystickMonitor.h new file mode 100644 index 0000000..783fff1 --- /dev/null +++ b/xbmc/input/joysticks/JoystickMonitor.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/interfaces/IInputHandler.h" + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \ingroup joystick + * \brief Monitors joystick input and resets screensaver/shutdown timers + * whenever motion occurs. + */ +class CJoystickMonitor : public IInputHandler +{ +public: + // implementation of IInputHandler + std::string ControllerID() const override; + bool HasFeature(const FeatureName& feature) const override { return true; } + bool AcceptsInput(const FeatureName& feature) const override; + bool OnButtonPress(const FeatureName& feature, bool bPressed) override; + void OnButtonHold(const FeatureName& feature, unsigned int holdTimeMs) override {} + bool OnButtonMotion(const FeatureName& feature, + float magnitude, + unsigned int motionTimeMs) override; + bool OnAnalogStickMotion(const FeatureName& feature, + float x, + float y, + unsigned int motionTimeMs) override; + bool OnAccelerometerMotion(const FeatureName& feature, float x, float y, float z) override + { + return false; + } + bool OnWheelMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) override; + bool OnThrottleMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) override; + void OnInputFrame() override {} + +private: + /*! + * \brief Reset screensaver and shutdown timers + * \return True if the application was woken from screensaver + */ + bool ResetTimers(void); +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/JoystickTranslator.cpp b/xbmc/input/joysticks/JoystickTranslator.cpp new file mode 100644 index 0000000..7edfeaf --- /dev/null +++ b/xbmc/input/joysticks/JoystickTranslator.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "JoystickTranslator.h" + +#include "guilib/LocalizeStrings.h" +#include "input/joysticks/DriverPrimitive.h" +#include "utils/StringUtils.h" + +using namespace KODI; +using namespace JOYSTICK; + +const char* CJoystickTranslator::HatStateToString(HAT_STATE state) +{ + switch (state) + { + case HAT_STATE::UP: + return "UP"; + case HAT_STATE::DOWN: + return "DOWN"; + case HAT_STATE::RIGHT: + return "RIGHT"; + case HAT_STATE::LEFT: + return "LEFT"; + case HAT_STATE::RIGHTUP: + return "UP RIGHT"; + case HAT_STATE::RIGHTDOWN: + return "DOWN RIGHT"; + case HAT_STATE::LEFTUP: + return "UP LEFT"; + case HAT_STATE::LEFTDOWN: + return "DOWN LEFT"; + default: + break; + } + + return "RELEASED"; +} + +const char* CJoystickTranslator::TranslateAnalogStickDirection(ANALOG_STICK_DIRECTION dir) +{ + switch (dir) + { + case ANALOG_STICK_DIRECTION::UP: + return "up"; + case ANALOG_STICK_DIRECTION::DOWN: + return "down"; + case ANALOG_STICK_DIRECTION::RIGHT: + return "right"; + case ANALOG_STICK_DIRECTION::LEFT: + return "left"; + default: + break; + } + + return ""; +} + +ANALOG_STICK_DIRECTION CJoystickTranslator::TranslateAnalogStickDirection(const std::string& dir) +{ + if (dir == "up") + return ANALOG_STICK_DIRECTION::UP; + if (dir == "down") + return ANALOG_STICK_DIRECTION::DOWN; + if (dir == "right") + return ANALOG_STICK_DIRECTION::RIGHT; + if (dir == "left") + return ANALOG_STICK_DIRECTION::LEFT; + + return ANALOG_STICK_DIRECTION::NONE; +} + +const char* CJoystickTranslator::TranslateWheelDirection(WHEEL_DIRECTION dir) +{ + switch (dir) + { + case WHEEL_DIRECTION::RIGHT: + return "right"; + case WHEEL_DIRECTION::LEFT: + return "left"; + default: + break; + } + + return ""; +} + +WHEEL_DIRECTION CJoystickTranslator::TranslateWheelDirection(const std::string& dir) +{ + if (dir == "right") + return WHEEL_DIRECTION::RIGHT; + if (dir == "left") + return WHEEL_DIRECTION::LEFT; + + return WHEEL_DIRECTION::NONE; +} + +const char* CJoystickTranslator::TranslateThrottleDirection(THROTTLE_DIRECTION dir) +{ + switch (dir) + { + case THROTTLE_DIRECTION::UP: + return "up"; + case THROTTLE_DIRECTION::DOWN: + return "down"; + default: + break; + } + + return ""; +} + +THROTTLE_DIRECTION CJoystickTranslator::TranslateThrottleDirection(const std::string& dir) +{ + if (dir == "up") + return THROTTLE_DIRECTION::UP; + if (dir == "down") + return THROTTLE_DIRECTION::DOWN; + + return THROTTLE_DIRECTION::NONE; +} + +SEMIAXIS_DIRECTION CJoystickTranslator::PositionToSemiAxisDirection(float position) +{ + if (position > 0) + return SEMIAXIS_DIRECTION::POSITIVE; + else if (position < 0) + return SEMIAXIS_DIRECTION::NEGATIVE; + + return SEMIAXIS_DIRECTION::ZERO; +} + +WHEEL_DIRECTION CJoystickTranslator::PositionToWheelDirection(float position) +{ + if (position > 0.0f) + return WHEEL_DIRECTION::RIGHT; + else if (position < 0.0f) + return WHEEL_DIRECTION::LEFT; + + return WHEEL_DIRECTION::NONE; +} + +THROTTLE_DIRECTION CJoystickTranslator::PositionToThrottleDirection(float position) +{ + if (position > 0.0f) + return THROTTLE_DIRECTION::UP; + else if (position < 0.0f) + return THROTTLE_DIRECTION::DOWN; + + return THROTTLE_DIRECTION::NONE; +} + +std::string CJoystickTranslator::GetPrimitiveName(const CDriverPrimitive& primitive) +{ + std::string primitiveTemplate; + + switch (primitive.Type()) + { + case PRIMITIVE_TYPE::BUTTON: + primitiveTemplate = g_localizeStrings.Get(35015); // "Button %d" + break; + case PRIMITIVE_TYPE::SEMIAXIS: + primitiveTemplate = g_localizeStrings.Get(35016); // "Axis %d" + break; + default: + break; + } + + return StringUtils::Format(primitiveTemplate, primitive.Index()); +} diff --git a/xbmc/input/joysticks/JoystickTranslator.h b/xbmc/input/joysticks/JoystickTranslator.h new file mode 100644 index 0000000..2efeacc --- /dev/null +++ b/xbmc/input/joysticks/JoystickTranslator.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "JoystickTypes.h" + +namespace KODI +{ +namespace JOYSTICK +{ +class CDriverPrimitive; + +/*! + * \brief Joystick translation utilities + */ +class CJoystickTranslator +{ +public: + /*! + * \brief Translate a hat state to a string representation + * + * \param state The hat state + * + * \return A capitalized string representation, or "RELEASED" if the hat is centered. + */ + static const char* HatStateToString(HAT_STATE state); + + /*! + * \brief Translate an analog stick direction to a lower-case string + * + * \param dir The analog stick direction + * + * \return A lower-case string representation, or "" if the direction is invalid + */ + static const char* TranslateAnalogStickDirection(ANALOG_STICK_DIRECTION dir); + + /*! + * \brief Translate an analog stick direction string to an enum value + * + * \param dir The analog stick direction + * + * \return The translated direction, or ANALOG_STICK_DIRECTION::UNKNOWN if unknown + */ + static ANALOG_STICK_DIRECTION TranslateAnalogStickDirection(const std::string& dir); + + /*! + * \brief Translate a wheel direction to a lower-case string + * + * \param dir The wheel direction + * + * \return A lower-case string representation, or "" if the direction is invalid + */ + static const char* TranslateWheelDirection(WHEEL_DIRECTION dir); + + /*! + * \brief Translate a wheel direction string to an enum value + * + * \param dir The wheel direction + * + * \return The translated direction, or WHEEL_DIRECTION::UNKNOWN if unknown + */ + static WHEEL_DIRECTION TranslateWheelDirection(const std::string& dir); + + /*! + * \brief Translate a throttle direction to a lower-case string + * + * \param dir The analog stick direction + * + * \return A lower-case string representation, or "" if the direction is invalid + */ + static const char* TranslateThrottleDirection(THROTTLE_DIRECTION dir); + + /*! + * \brief Translate a throttle direction string to an enum value + * + * \param dir The throttle direction + * + * \return The translated direction, or THROTTLE_DIRECTION::UNKNOWN if unknown + */ + static THROTTLE_DIRECTION TranslateThrottleDirection(const std::string& dir); + + /*! + * \brief Get the semi-axis direction containing the specified position + * + * \param position The position of the axis + * + * \return POSITIVE, NEGATIVE, or UNKNOWN if position is 0 + */ + static SEMIAXIS_DIRECTION PositionToSemiAxisDirection(float position); + + /*! + * \brief Get the wheel direction containing the specified position + * + * \param position The position of the axis + * + * \return LEFT, RIGHT, or UNKNOWN if position is 0 + */ + static WHEEL_DIRECTION PositionToWheelDirection(float position); + + /*! + * \brief Get the throttle direction containing the specified position + * + * \param position The position of the axis + * + * \return UP, DOWN, or UNKNOWN if position is 0 + */ + static THROTTLE_DIRECTION PositionToThrottleDirection(float position); + + /*! + * \brief Get the localized name of the primitive + * + * \param primitive The primitive, currently only buttons and axes are supported + * + * \return A title for the primitive, e.g. "Button 0" or "Axis 1" + */ + static std::string GetPrimitiveName(const CDriverPrimitive& primitive); +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/JoystickTypes.h b/xbmc/input/joysticks/JoystickTypes.h new file mode 100644 index 0000000..5925793 --- /dev/null +++ b/xbmc/input/joysticks/JoystickTypes.h @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +/*! + \file + \ingroup joystick + */ + +#include "input/InputTypes.h" + +#include <set> +#include <string> + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \brief Name of a physical feature belonging to the joystick + */ +using FeatureName = std::string; + +/*! + * \brief Types of features used in the joystick library + * + * Available types: + * + * 1) scalar[*] + * 2) analog stick + * 3) accelerometer + * 4) rumble motor + * 5) relative pointer + * 6) absolute pointer + * 7) wheel + * 8) throttle + * 9) keyboard key + * + * [*] All three driver primitives (buttons, hats and axes) have a state that + * can be represented using a single scalar value. For this reason, + * features that map to a single primitive are called "scalar features". + */ +enum class FEATURE_TYPE +{ + UNKNOWN, + SCALAR, + ANALOG_STICK, + ACCELEROMETER, + MOTOR, + RELPOINTER, + ABSPOINTER, + WHEEL, + THROTTLE, + KEY, +}; + +/*! + * \brief Categories of features used in the joystick library + */ +enum class FEATURE_CATEGORY +{ + UNKNOWN, + FACE, + SHOULDER, + TRIGGER, + ANALOG_STICK, + ACCELEROMETER, + HAPTICS, + MOUSE_BUTTON, + POINTER, + LIGHTGUN, + OFFSCREEN, // Virtual button to shoot light gun offscreen + KEY, // A keyboard key + KEYPAD, // A key on a numeric keymap, including star and pound + HARDWARE, // A button or functionality on the console + WHEEL, + JOYSTICK, + PADDLE, +}; + +/*! + * \brief Direction arrows on the hat (directional pad) + */ +using HAT_DIRECTION = INPUT::CARDINAL_DIRECTION; + +/*! + * \brief States in which a hat can be + */ +using HAT_STATE = INPUT::INTERCARDINAL_DIRECTION; + +/*! + * \brief Typedef for analog stick directions + */ +using ANALOG_STICK_DIRECTION = INPUT::CARDINAL_DIRECTION; + +/*! + * \brief Directions of motion for a relative pointer + */ +using RELATIVE_POINTER_DIRECTION = INPUT::CARDINAL_DIRECTION; + +/*! + * \brief Directions in which a semiaxis can point + */ +enum class SEMIAXIS_DIRECTION +{ + NEGATIVE = -1, // semiaxis lies in the interval [-1.0, 0.0] + ZERO = 0, // semiaxis is unknown or invalid + POSITIVE = 1, // semiaxis lies in the interval [0.0, 1.0] +}; + +/*! + * \brief Directions on a wheel + */ +enum class WHEEL_DIRECTION +{ + NONE, + RIGHT, + LEFT, +}; + +/*! + * \brief Directions on a throttle + */ +enum class THROTTLE_DIRECTION +{ + NONE, + UP, + DOWN, +}; + +/*! + * \brief Types of input available for scalar features + */ +enum class INPUT_TYPE +{ + UNKNOWN, + DIGITAL, + ANALOG, +}; + +/*! + * \brief Type of driver primitive + */ +enum class PRIMITIVE_TYPE +{ + UNKNOWN = 0, // primitive has no type (invalid) + BUTTON, // a digital button + HAT, // one of the four direction arrows on a D-pad + SEMIAXIS, // the positive or negative half of an axis + MOTOR, // a rumble motor + KEY, // a keyboard key + MOUSE_BUTTON, // a mouse button + RELATIVE_POINTER, // a relative pointer, such as on a mouse +}; + +/*! + * \ingroup joystick + * \brief Action entry in joystick.xml + */ +struct KeymapAction +{ + unsigned int actionId; + std::string actionString; + unsigned int holdTimeMs; + std::set<std::string> hotkeys; + + bool operator<(const KeymapAction& rhs) const { return holdTimeMs < rhs.holdTimeMs; } +}; + +/*! + * \ingroup joystick + * \brief Container that sorts action entries by their holdtime + */ +struct KeymapActionGroup +{ + int windowId = -1; + std::set<KeymapAction> actions; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/JoystickUtils.cpp b/xbmc/input/joysticks/JoystickUtils.cpp new file mode 100644 index 0000000..3c25121 --- /dev/null +++ b/xbmc/input/joysticks/JoystickUtils.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "JoystickUtils.h" + +#include "JoystickTranslator.h" +#include "utils/StringUtils.h" + +using namespace KODI; +using namespace JOYSTICK; + +std::string CJoystickUtils::MakeKeyName(const FeatureName& feature) +{ + return feature; +} + +std::string CJoystickUtils::MakeKeyName(const FeatureName& feature, ANALOG_STICK_DIRECTION dir) +{ + std::string keyName = feature; + + if (dir != ANALOG_STICK_DIRECTION::NONE) + keyName += CJoystickTranslator::TranslateAnalogStickDirection(dir); + + return keyName; +} + +std::string CJoystickUtils::MakeKeyName(const FeatureName& feature, WHEEL_DIRECTION dir) +{ + ANALOG_STICK_DIRECTION stickDir = ANALOG_STICK_DIRECTION::NONE; + + switch (dir) + { + case WHEEL_DIRECTION::LEFT: + stickDir = ANALOG_STICK_DIRECTION::LEFT; + break; + case WHEEL_DIRECTION::RIGHT: + stickDir = ANALOG_STICK_DIRECTION::RIGHT; + break; + default: + break; + } + + return MakeKeyName(feature, stickDir); +} + +std::string CJoystickUtils::MakeKeyName(const FeatureName& feature, THROTTLE_DIRECTION dir) +{ + ANALOG_STICK_DIRECTION stickDir = ANALOG_STICK_DIRECTION::NONE; + + switch (dir) + { + case THROTTLE_DIRECTION::UP: + stickDir = ANALOG_STICK_DIRECTION::UP; + break; + case THROTTLE_DIRECTION::DOWN: + stickDir = ANALOG_STICK_DIRECTION::DOWN; + break; + default: + break; + } + + return MakeKeyName(feature, stickDir); +} + +const std::vector<ANALOG_STICK_DIRECTION>& CJoystickUtils::GetAnalogStickDirections() +{ + static std::vector<ANALOG_STICK_DIRECTION> directions; + if (directions.empty()) + { + directions.push_back(ANALOG_STICK_DIRECTION::UP); + directions.push_back(ANALOG_STICK_DIRECTION::DOWN); + directions.push_back(ANALOG_STICK_DIRECTION::RIGHT); + directions.push_back(ANALOG_STICK_DIRECTION::LEFT); + } + return directions; +} + +const std::vector<WHEEL_DIRECTION>& CJoystickUtils::GetWheelDirections() +{ + static std::vector<WHEEL_DIRECTION> directions; + if (directions.empty()) + { + directions.push_back(WHEEL_DIRECTION::RIGHT); + directions.push_back(WHEEL_DIRECTION::LEFT); + } + return directions; +} + +const std::vector<THROTTLE_DIRECTION>& CJoystickUtils::GetThrottleDirections() +{ + static std::vector<THROTTLE_DIRECTION> directions; + if (directions.empty()) + { + directions.push_back(THROTTLE_DIRECTION::UP); + directions.push_back(THROTTLE_DIRECTION::DOWN); + } + return directions; +} diff --git a/xbmc/input/joysticks/JoystickUtils.h b/xbmc/input/joysticks/JoystickUtils.h new file mode 100644 index 0000000..49095d2 --- /dev/null +++ b/xbmc/input/joysticks/JoystickUtils.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "JoystickTypes.h" + +#include <string> +#include <vector> + +/// \ingroup joystick +/// \{ + +namespace KODI +{ +namespace JOYSTICK +{ + +inline HAT_DIRECTION& operator|=(HAT_DIRECTION& lhs, HAT_DIRECTION rhs) +{ + return lhs = static_cast<HAT_DIRECTION>(static_cast<int>(lhs) | static_cast<int>(rhs)); +} + +inline HAT_STATE& operator|=(HAT_STATE& lhs, HAT_STATE rhs) +{ + return lhs = static_cast<HAT_STATE>(static_cast<int>(lhs) | static_cast<int>(rhs)); +} + +inline bool operator&(HAT_STATE lhs, HAT_DIRECTION rhs) +{ + return (static_cast<int>(lhs) & static_cast<int>(rhs)) ? true : false; +} + +inline SEMIAXIS_DIRECTION operator*(SEMIAXIS_DIRECTION lhs, int rhs) +{ + return static_cast<SEMIAXIS_DIRECTION>(static_cast<int>(lhs) * rhs); +} + +inline float operator*(float lhs, SEMIAXIS_DIRECTION rhs) +{ + return lhs * static_cast<int>(rhs); +} + +class CJoystickUtils +{ +public: + /*! + * \brief Create a key name used to index an action in the keymap + * + * \param feature The feature name + * + * \return A valid name for a key in the joystick keymap + */ + static std::string MakeKeyName(const FeatureName& feature); + + /*! + * \brief Create a key name used to index an action in the keymap + * + * \param feature The feature name + * \param dir The direction for analog sticks + * + * \return A valid name for a key in the joystick keymap + */ + static std::string MakeKeyName(const FeatureName& feature, ANALOG_STICK_DIRECTION dir); + + /*! + * \brief Create a key name used to index an action in the keymap + * + * \param feature The feature name + * \param dir The direction for a wheel to turn + * + * \return A valid name for a key in the joystick keymap + */ + static std::string MakeKeyName(const FeatureName& feature, WHEEL_DIRECTION dir); + + /*! + * \brief Create a key name used to index an action in the keymap + * + * \param feature The feature name + * \param dir The direction for a throttle to move + * + * \return A valid name for a key in the joystick keymap + */ + static std::string MakeKeyName(const FeatureName& feature, THROTTLE_DIRECTION dir); + + /*! + * \brief Return a vector of the four cardinal directions + */ + static const std::vector<ANALOG_STICK_DIRECTION>& GetAnalogStickDirections(); + + /*! + * \brief Return a vector of the two wheel directions + */ + static const std::vector<WHEEL_DIRECTION>& GetWheelDirections(); + + /*! + * \brief Return a vector of the two throttle directions + */ + static const std::vector<THROTTLE_DIRECTION>& GetThrottleDirections(); +}; + +} // namespace JOYSTICK +} // namespace KODI + +/// \} diff --git a/xbmc/input/joysticks/RumbleGenerator.cpp b/xbmc/input/joysticks/RumbleGenerator.cpp new file mode 100644 index 0000000..db60cfa --- /dev/null +++ b/xbmc/input/joysticks/RumbleGenerator.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "RumbleGenerator.h" + +#include "ServiceBroker.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerIDs.h" +#include "games/controllers/ControllerManager.h" +#include "input/joysticks/interfaces/IInputReceiver.h" + +#include <algorithm> + +using namespace std::chrono_literals; + +namespace +{ +constexpr auto RUMBLE_TEST_DURATION_MS = 1000ms; // Per motor +constexpr auto RUMBLE_NOTIFICATION_DURATION_MS = 300ms; + +// From game.controller.default profile +#define WEAK_MOTOR_NAME "rightmotor" +} // namespace + +using namespace KODI; +using namespace JOYSTICK; + +CRumbleGenerator::CRumbleGenerator() + : CThread("RumbleGenerator"), m_motors(GetMotors(ControllerID())) +{ +} + +std::string CRumbleGenerator::ControllerID() const +{ + return DEFAULT_CONTROLLER_ID; +} + +void CRumbleGenerator::NotifyUser(IInputReceiver* receiver) +{ + if (receiver && !m_motors.empty()) + { + if (IsRunning()) + StopThread(true); + + m_receiver = receiver; + m_type = RUMBLE_NOTIFICATION; + Create(); + } +} + +bool CRumbleGenerator::DoTest(IInputReceiver* receiver) +{ + if (receiver && !m_motors.empty()) + { + if (IsRunning()) + StopThread(true); + + m_receiver = receiver; + m_type = RUMBLE_TEST; + Create(); + + return true; + } + return false; +} + +void CRumbleGenerator::Process(void) +{ + switch (m_type) + { + case RUMBLE_NOTIFICATION: + { + std::vector<std::string> motors; + + if (std::find(m_motors.begin(), m_motors.end(), WEAK_MOTOR_NAME) != m_motors.end()) + motors.emplace_back(WEAK_MOTOR_NAME); + else + motors = m_motors; // Not using default profile? Just rumble all motors + + for (const std::string& motor : motors) + m_receiver->SetRumbleState(motor, 1.0f); + + CThread::Sleep(RUMBLE_NOTIFICATION_DURATION_MS); + + if (m_bStop) + break; + + for (const std::string& motor : motors) + m_receiver->SetRumbleState(motor, 0.0f); + + break; + } + case RUMBLE_TEST: + { + for (const std::string& motor : m_motors) + { + m_receiver->SetRumbleState(motor, 1.0f); + + CThread::Sleep(RUMBLE_TEST_DURATION_MS); + + if (m_bStop) + break; + + m_receiver->SetRumbleState(motor, 0.0f); + } + break; + } + default: + break; + } +} + +std::vector<std::string> CRumbleGenerator::GetMotors(const std::string& controllerId) +{ + using namespace GAME; + + std::vector<std::string> motors; + + CControllerManager& controllerManager = CServiceBroker::GetGameControllerManager(); + ControllerPtr controller = controllerManager.GetController(controllerId); + if (controller) + controller->GetFeatures(motors, FEATURE_TYPE::MOTOR); + + return motors; +} diff --git a/xbmc/input/joysticks/RumbleGenerator.h b/xbmc/input/joysticks/RumbleGenerator.h new file mode 100644 index 0000000..a6e9853 --- /dev/null +++ b/xbmc/input/joysticks/RumbleGenerator.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Thread.h" + +#include <string> +#include <vector> + +namespace KODI +{ +namespace JOYSTICK +{ +class IInputReceiver; + +class CRumbleGenerator : public CThread +{ +public: + CRumbleGenerator(); + + ~CRumbleGenerator() override { AbortRumble(); } + + std::string ControllerID() const; + + void NotifyUser(IInputReceiver* receiver); + bool DoTest(IInputReceiver* receiver); + + void AbortRumble(void) { StopThread(); } + +protected: + // implementation of CThread + void Process() override; + +private: + enum RUMBLE_TYPE + { + RUMBLE_UNKNOWN, + RUMBLE_NOTIFICATION, + RUMBLE_TEST, + }; + + static std::vector<std::string> GetMotors(const std::string& controllerId); + + // Construction param + const std::vector<std::string> m_motors; + + // Test param + IInputReceiver* m_receiver = nullptr; + RUMBLE_TYPE m_type = RUMBLE_UNKNOWN; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/dialogs/CMakeLists.txt b/xbmc/input/joysticks/dialogs/CMakeLists.txt new file mode 100644 index 0000000..73adcec --- /dev/null +++ b/xbmc/input/joysticks/dialogs/CMakeLists.txt @@ -0,0 +1,5 @@ +set(SOURCES GUIDialogNewJoystick.cpp) + +set(HEADERS GUIDialogNewJoystick.h) + +core_add_library(input_joystick_dialogs) diff --git a/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.cpp b/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.cpp new file mode 100644 index 0000000..b66ce89 --- /dev/null +++ b/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogNewJoystick.h" + +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogHelper.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +using namespace KODI; +using namespace JOYSTICK; + +CGUIDialogNewJoystick::CGUIDialogNewJoystick() : CThread("NewJoystickDlg") +{ +} + +void CGUIDialogNewJoystick::ShowAsync() +{ + bool bShow = true; + + if (IsRunning()) + bShow = false; + else if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_INPUT_ASKNEWCONTROLLERS)) + bShow = false; + else if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive( + WINDOW_DIALOG_GAME_CONTROLLERS, false)) + bShow = false; + + if (bShow) + Create(); +} + +void CGUIDialogNewJoystick::Process() +{ + using namespace MESSAGING::HELPERS; + + // "New controller detected" + // "A new controller has been detected. Configuration can be done at any time in "Settings -> + // System Settings -> Input". Would you like to configure it now?" + if (ShowYesNoDialogText(CVariant{35011}, CVariant{35012}) == DialogResponse::CHOICE_YES) + { + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_GAME_CONTROLLERS); + } + else + { + CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool( + CSettings::SETTING_INPUT_ASKNEWCONTROLLERS, false); + } +} diff --git a/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.h b/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.h new file mode 100644 index 0000000..334191f --- /dev/null +++ b/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Thread.h" + +namespace KODI +{ +namespace JOYSTICK +{ +class CGUIDialogNewJoystick : protected CThread +{ +public: + CGUIDialogNewJoystick(); + ~CGUIDialogNewJoystick() override = default; + + void ShowAsync(); + +protected: + // implementation of CThread + void Process() override; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/generic/ButtonMapping.cpp b/xbmc/input/joysticks/generic/ButtonMapping.cpp new file mode 100644 index 0000000..665a233 --- /dev/null +++ b/xbmc/input/joysticks/generic/ButtonMapping.cpp @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ButtonMapping.h" + +#include "ServiceBroker.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerManager.h" +#include "games/controllers/input/PhysicalFeature.h" +#include "input/IKeymap.h" +#include "input/InputTranslator.h" +#include "input/Key.h" +#include "input/joysticks/DriverPrimitive.h" +#include "input/joysticks/JoystickTranslator.h" +#include "input/joysticks/JoystickUtils.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "input/joysticks/interfaces/IButtonMapper.h" +#include "utils/log.h" + +#include <algorithm> +#include <assert.h> +#include <cmath> + +using namespace KODI; +using namespace JOYSTICK; + +#define MAPPING_COOLDOWN_MS 50 // Guard against repeated input +#define AXIS_THRESHOLD 0.75f // Axis must exceed this value to be mapped +#define TRIGGER_DELAY_MS \ + 200 // Delay trigger detection to handle anomalous triggers with non-zero center + +// --- CPrimitiveDetector ------------------------------------------------------ + +CPrimitiveDetector::CPrimitiveDetector(CButtonMapping* buttonMapping) + : m_buttonMapping(buttonMapping) +{ +} + +bool CPrimitiveDetector::MapPrimitive(const CDriverPrimitive& primitive) +{ + if (primitive.IsValid()) + return m_buttonMapping->MapPrimitive(primitive); + + return false; +} + +// --- CButtonDetector --------------------------------------------------------- + +CButtonDetector::CButtonDetector(CButtonMapping* buttonMapping, unsigned int buttonIndex) + : CPrimitiveDetector(buttonMapping), m_buttonIndex(buttonIndex) +{ +} + +bool CButtonDetector::OnMotion(bool bPressed) +{ + if (bPressed) + return MapPrimitive(CDriverPrimitive(PRIMITIVE_TYPE::BUTTON, m_buttonIndex)); + + return false; +} + +// --- CHatDetector ------------------------------------------------------------ + +CHatDetector::CHatDetector(CButtonMapping* buttonMapping, unsigned int hatIndex) + : CPrimitiveDetector(buttonMapping), m_hatIndex(hatIndex) +{ +} + +bool CHatDetector::OnMotion(HAT_STATE state) +{ + return MapPrimitive(CDriverPrimitive(m_hatIndex, static_cast<HAT_DIRECTION>(state))); +} + +// --- CAxisDetector ----------------------------------------------------------- + +CAxisDetector::CAxisDetector(CButtonMapping* buttonMapping, + unsigned int axisIndex, + const AxisConfiguration& config) + : CPrimitiveDetector(buttonMapping), + m_axisIndex(axisIndex), + m_config(config), + m_state(AXIS_STATE::INACTIVE), + m_type(AXIS_TYPE::UNKNOWN), + m_initialPositionKnown(false), + m_initialPosition(0.0f), + m_initialPositionChanged(false) +{ +} + +bool CAxisDetector::OnMotion(float position) +{ + DetectType(position); + + if (m_type != AXIS_TYPE::UNKNOWN) + { + // Update position if this axis is an anomalous trigger + if (m_type == AXIS_TYPE::OFFSET) + position = (position - m_config.center) / m_config.range; + + // Reset state if position crosses zero + if (m_state == AXIS_STATE::MAPPED) + { + SEMIAXIS_DIRECTION activatedDir = m_activatedPrimitive.SemiAxisDirection(); + SEMIAXIS_DIRECTION newDir = CJoystickTranslator::PositionToSemiAxisDirection(position); + + if (activatedDir != newDir) + m_state = AXIS_STATE::INACTIVE; + } + + // Check if axis has become activated + if (m_state == AXIS_STATE::INACTIVE) + { + if (std::abs(position) >= AXIS_THRESHOLD) + m_state = AXIS_STATE::ACTIVATED; + + if (m_state == AXIS_STATE::ACTIVATED) + { + // Range is set later for anomalous triggers + m_activatedPrimitive = + CDriverPrimitive(m_axisIndex, m_config.center, + CJoystickTranslator::PositionToSemiAxisDirection(position), 1); + m_activationTimeMs = std::chrono::steady_clock::now(); + } + } + } + + return true; +} + +void CAxisDetector::ProcessMotion() +{ + // Process newly-activated axis + if (m_state == AXIS_STATE::ACTIVATED) + { + // Ignore anomalous triggers for a bit so we can detect the full range + bool bIgnore = false; + if (m_type == AXIS_TYPE::OFFSET) + { + auto now = std::chrono::steady_clock::now(); + auto duration = + std::chrono::duration_cast<std::chrono::milliseconds>(now - m_activationTimeMs); + + if (duration.count() < TRIGGER_DELAY_MS) + bIgnore = true; + } + + if (!bIgnore) + { + // Update driver primitive's range if we're mapping an anomalous trigger + if (m_type == AXIS_TYPE::OFFSET) + { + m_activatedPrimitive = + CDriverPrimitive(m_activatedPrimitive.Index(), m_activatedPrimitive.Center(), + m_activatedPrimitive.SemiAxisDirection(), m_config.range); + } + + // Map primitive + if (!MapPrimitive(m_activatedPrimitive)) + { + if (m_type == AXIS_TYPE::OFFSET) + CLog::Log(LOGDEBUG, "Mapping offset axis {} failed", m_axisIndex); + else + CLog::Log(LOGDEBUG, "Mapping normal axis {} failed", m_axisIndex); + } + + m_state = AXIS_STATE::MAPPED; + } + } +} + +void CAxisDetector::SetEmitted(const CDriverPrimitive& activePrimitive) +{ + m_state = AXIS_STATE::MAPPED; + m_activatedPrimitive = activePrimitive; +} + +void CAxisDetector::DetectType(float position) +{ + // Some platforms don't report a value until the axis is first changed. + // Detection relies on an initial value, so this axis will be disabled until + // the user begins button mapping again. + if (m_config.bLateDiscovery) + return; + + // Update range if a range of > 1 is observed + if (std::abs(position - m_config.center) > 1.0f) + m_config.range = 2; + + if (m_type != AXIS_TYPE::UNKNOWN) + return; + + if (m_config.bKnown) + { + if (m_config.center == 0) + m_type = AXIS_TYPE::NORMAL; + else + m_type = AXIS_TYPE::OFFSET; + } + + if (m_type != AXIS_TYPE::UNKNOWN) + return; + + if (!m_initialPositionKnown) + { + m_initialPositionKnown = true; + m_initialPosition = position; + } + + if (position != m_initialPosition) + m_initialPositionChanged = true; + + if (m_initialPositionChanged) + { + // Calculate center based on initial position. + if (m_initialPosition < -0.5f) + { + m_config.center = -1; + m_type = AXIS_TYPE::OFFSET; + CLog::Log(LOGDEBUG, "Anomalous trigger detected on axis {} with center {}", m_axisIndex, + m_config.center); + } + else if (m_initialPosition > 0.5f) + { + m_config.center = 1; + m_type = AXIS_TYPE::OFFSET; + CLog::Log(LOGDEBUG, "Anomalous trigger detected on axis {} with center {}", m_axisIndex, + m_config.center); + } + else + { + m_type = AXIS_TYPE::NORMAL; + CLog::Log(LOGDEBUG, "Normal axis detected on axis {}", m_axisIndex); + } + } +} + +// --- CKeyDetector --------------------------------------------------------- + +CKeyDetector::CKeyDetector(CButtonMapping* buttonMapping, XBMCKey keycode) + : CPrimitiveDetector(buttonMapping), m_keycode(keycode) +{ +} + +bool CKeyDetector::OnMotion(bool bPressed) +{ + if (bPressed) + return MapPrimitive(CDriverPrimitive(m_keycode)); + + return false; +} + +// --- CMouseButtonDetector ---------------------------------------------------- + +CMouseButtonDetector::CMouseButtonDetector(CButtonMapping* buttonMapping, + MOUSE::BUTTON_ID buttonIndex) + : CPrimitiveDetector(buttonMapping), m_buttonIndex(buttonIndex) +{ +} + +bool CMouseButtonDetector::OnMotion(bool bPressed) +{ + if (bPressed) + return MapPrimitive(CDriverPrimitive(m_buttonIndex)); + + return false; +} + +// --- CPointerDetector -------------------------------------------------------- + +CPointerDetector::CPointerDetector(CButtonMapping* buttonMapping) + : CPrimitiveDetector(buttonMapping) +{ +} + +bool CPointerDetector::OnMotion(int x, int y) +{ + if (!m_bStarted) + { + m_bStarted = true; + m_startX = x; + m_startY = y; + m_frameCount = 0; + } + + if (m_frameCount++ >= MIN_FRAME_COUNT) + { + int dx = x - m_startX; + int dy = y - m_startY; + + INPUT::INTERCARDINAL_DIRECTION dir = GetPointerDirection(dx, dy); + + CDriverPrimitive primitive(static_cast<RELATIVE_POINTER_DIRECTION>(dir)); + if (primitive.IsValid()) + { + if (MapPrimitive(primitive)) + m_bStarted = false; + } + } + + return true; +} + +KODI::INPUT::INTERCARDINAL_DIRECTION CPointerDetector::GetPointerDirection(int x, int y) +{ + using namespace INPUT; + + // Translate from left-handed coordinate system to right-handed coordinate system + y *= -1; + + return CInputTranslator::VectorToIntercardinalDirection(static_cast<float>(x), + static_cast<float>(y)); +} + +// --- CButtonMapping ---------------------------------------------------------- + +CButtonMapping::CButtonMapping(IButtonMapper* buttonMapper, IButtonMap* buttonMap, IKeymap* keymap) + : m_buttonMapper(buttonMapper), m_buttonMap(buttonMap), m_keymap(keymap), m_frameCount(0) +{ + assert(m_buttonMapper != nullptr); + assert(m_buttonMap != nullptr); + + // Make sure axes mapped to Select are centered before they can be mapped. + // This ensures that they are not immediately mapped to the first button. + if (m_keymap) + { + using namespace GAME; + + CControllerManager& controllerManager = CServiceBroker::GetGameControllerManager(); + ControllerPtr controller = controllerManager.GetController(m_keymap->ControllerID()); + + const auto& features = controller->Features(); + for (const auto& feature : features) + { + bool bIsSelectAction = false; + + const auto& actions = + m_keymap->GetActions(CJoystickUtils::MakeKeyName(feature.Name())).actions; + if (!actions.empty() && actions.begin()->actionId == ACTION_SELECT_ITEM) + bIsSelectAction = true; + + if (!bIsSelectAction) + continue; + + CDriverPrimitive primitive; + if (!m_buttonMap->GetScalar(feature.Name(), primitive)) + continue; + + if (primitive.Type() != PRIMITIVE_TYPE::SEMIAXIS) + continue; + + // Set initial config, as detection will fail because axis is already activated + AxisConfiguration axisConfig; + axisConfig.bKnown = true; + axisConfig.center = primitive.Center(); + axisConfig.range = primitive.Range(); + + GetAxis(primitive.Index(), static_cast<float>(primitive.Center()), axisConfig) + .SetEmitted(primitive); + } + } +} + +bool CButtonMapping::OnButtonMotion(unsigned int buttonIndex, bool bPressed) +{ + if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::BUTTON)) + return false; + + return GetButton(buttonIndex).OnMotion(bPressed); +} + +bool CButtonMapping::OnHatMotion(unsigned int hatIndex, HAT_STATE state) +{ + if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::HAT)) + return false; + + return GetHat(hatIndex).OnMotion(state); +} + +bool CButtonMapping::OnAxisMotion(unsigned int axisIndex, + float position, + int center, + unsigned int range) +{ + if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::SEMIAXIS)) + return false; + + return GetAxis(axisIndex, position).OnMotion(position); +} + +void CButtonMapping::OnInputFrame(void) +{ + for (auto& axis : m_axes) + axis.second.ProcessMotion(); + + m_buttonMapper->OnEventFrame(m_buttonMap, IsMapping()); + + m_frameCount++; +} + +bool CButtonMapping::OnKeyPress(const CKey& key) +{ + if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::KEY)) + return false; + + return GetKey(static_cast<XBMCKey>(key.GetKeycode())).OnMotion(true); +} + +bool CButtonMapping::OnPosition(int x, int y) +{ + if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::RELATIVE_POINTER)) + return false; + + return GetPointer().OnMotion(x, y); +} + +bool CButtonMapping::OnButtonPress(MOUSE::BUTTON_ID button) +{ + if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::MOUSE_BUTTON)) + return false; + + return GetMouseButton(button).OnMotion(true); +} + +void CButtonMapping::OnButtonRelease(MOUSE::BUTTON_ID button) +{ + if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::MOUSE_BUTTON)) + return; + + GetMouseButton(button).OnMotion(false); +} + +void CButtonMapping::SaveButtonMap() +{ + m_buttonMap->SaveButtonMap(); +} + +void CButtonMapping::ResetIgnoredPrimitives() +{ + std::vector<CDriverPrimitive> empty; + m_buttonMap->SetIgnoredPrimitives(empty); +} + +void CButtonMapping::RevertButtonMap() +{ + m_buttonMap->RevertButtonMap(); +} + +bool CButtonMapping::MapPrimitive(const CDriverPrimitive& primitive) +{ + bool bHandled = false; + + if (m_buttonMap->IsIgnored(primitive)) + { + bHandled = true; + } + else + { + auto now = std::chrono::steady_clock::now(); + + bool bTimeoutElapsed = true; + + if (m_buttonMapper->NeedsCooldown()) + bTimeoutElapsed = (now >= m_lastAction + std::chrono::milliseconds(MAPPING_COOLDOWN_MS)); + + if (bTimeoutElapsed) + { + bHandled = m_buttonMapper->MapPrimitive(m_buttonMap, m_keymap, primitive); + + if (bHandled) + m_lastAction = std::chrono::steady_clock::now(); + } + else + { + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastAction); + + CLog::Log(LOGDEBUG, "Button mapping: rapid input after {}ms dropped for profile \"{}\"", + duration.count(), m_buttonMapper->ControllerID()); + bHandled = true; + } + } + + return bHandled; +} + +bool CButtonMapping::IsMapping() const +{ + for (auto itAxis : m_axes) + { + if (itAxis.second.IsMapping()) + return true; + } + + return false; +} + +CButtonDetector& CButtonMapping::GetButton(unsigned int buttonIndex) +{ + auto itButton = m_buttons.find(buttonIndex); + + if (itButton == m_buttons.end()) + { + m_buttons.insert(std::make_pair(buttonIndex, CButtonDetector(this, buttonIndex))); + itButton = m_buttons.find(buttonIndex); + } + + return itButton->second; +} + +CHatDetector& CButtonMapping::GetHat(unsigned int hatIndex) +{ + auto itHat = m_hats.find(hatIndex); + + if (itHat == m_hats.end()) + { + m_hats.insert(std::make_pair(hatIndex, CHatDetector(this, hatIndex))); + itHat = m_hats.find(hatIndex); + } + + return itHat->second; +} + +CAxisDetector& CButtonMapping::GetAxis( + unsigned int axisIndex, + float position, + const AxisConfiguration& initialConfig /* = AxisConfiguration() */) +{ + auto itAxis = m_axes.find(axisIndex); + + if (itAxis == m_axes.end()) + { + AxisConfiguration config(initialConfig); + + if (m_frameCount >= 2) + { + config.bLateDiscovery = true; + OnLateDiscovery(axisIndex); + } + + // Report axis + CLog::Log(LOGDEBUG, "Axis {} discovered at position {:.4f} after {} frames", axisIndex, + position, static_cast<unsigned long>(m_frameCount)); + + m_axes.insert(std::make_pair(axisIndex, CAxisDetector(this, axisIndex, config))); + itAxis = m_axes.find(axisIndex); + } + + return itAxis->second; +} + +CKeyDetector& CButtonMapping::GetKey(XBMCKey keycode) +{ + auto itKey = m_keys.find(keycode); + + if (itKey == m_keys.end()) + { + m_keys.insert(std::make_pair(keycode, CKeyDetector(this, keycode))); + itKey = m_keys.find(keycode); + } + + return itKey->second; +} + +CMouseButtonDetector& CButtonMapping::GetMouseButton(MOUSE::BUTTON_ID buttonIndex) +{ + auto itButton = m_mouseButtons.find(buttonIndex); + + if (itButton == m_mouseButtons.end()) + { + m_mouseButtons.insert(std::make_pair(buttonIndex, CMouseButtonDetector(this, buttonIndex))); + itButton = m_mouseButtons.find(buttonIndex); + } + + return itButton->second; +} + +CPointerDetector& CButtonMapping::GetPointer() +{ + if (!m_pointer) + m_pointer.reset(new CPointerDetector(this)); + + return *m_pointer; +} + +void CButtonMapping::OnLateDiscovery(unsigned int axisIndex) +{ + m_buttonMapper->OnLateAxis(m_buttonMap, axisIndex); +} diff --git a/xbmc/input/joysticks/generic/ButtonMapping.h b/xbmc/input/joysticks/generic/ButtonMapping.h new file mode 100644 index 0000000..3f76767 --- /dev/null +++ b/xbmc/input/joysticks/generic/ButtonMapping.h @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/DriverPrimitive.h" +#include "input/joysticks/interfaces/IButtonMapCallback.h" +#include "input/joysticks/interfaces/IDriverHandler.h" +#include "input/keyboard/interfaces/IKeyboardDriverHandler.h" +#include "input/mouse/MouseTypes.h" +#include "input/mouse/interfaces/IMouseDriverHandler.h" + +#include <chrono> +#include <map> +#include <memory> +#include <stdint.h> + +class IKeymap; + +namespace KODI +{ +namespace JOYSTICK +{ +class CButtonMapping; +class IButtonMap; +class IButtonMapper; + +/*! + * \brief Detects and dispatches mapping events + * + * A mapping event usually occurs when a driver primitive is pressed or + * exceeds a certain threshold. + * + * Detection can be quite complicated due to driver bugs, so each type of + * driver primitive is given its own detector class inheriting from this one. + */ +class CPrimitiveDetector +{ +protected: + CPrimitiveDetector(CButtonMapping* buttonMapping); + + /*! + * \brief Dispatch a mapping event + * + * \return True if the primitive was mapped, false otherwise + */ + bool MapPrimitive(const CDriverPrimitive& primitive); + +private: + CButtonMapping* const m_buttonMapping; +}; + +/*! + * \brief Detects when a button should be mapped + */ +class CButtonDetector : public CPrimitiveDetector +{ +public: + CButtonDetector(CButtonMapping* buttonMapping, unsigned int buttonIndex); + + /*! + * \brief Button state has been updated + * + * \param bPressed The new state + * + * \return True if this press was handled, false if it should fall through + * to the next driver handler + */ + bool OnMotion(bool bPressed); + +private: + // Construction parameters + const unsigned int m_buttonIndex; +}; + +/*! + * \brief Detects when a D-pad direction should be mapped + */ +class CHatDetector : public CPrimitiveDetector +{ +public: + CHatDetector(CButtonMapping* buttonMapping, unsigned int hatIndex); + + /*! + * \brief Hat state has been updated + * + * \param state The new state + * + * \return True if state is a cardinal direction, false otherwise + */ + bool OnMotion(HAT_STATE state); + +private: + // Construction parameters + const unsigned int m_hatIndex; +}; + +struct AxisConfiguration +{ + bool bKnown = false; + int center = 0; + unsigned int range = 1; + bool bLateDiscovery = false; +}; + +/*! + * \brief Detects when an axis should be mapped + */ +class CAxisDetector : public CPrimitiveDetector +{ +public: + CAxisDetector(CButtonMapping* buttonMapping, + unsigned int axisIndex, + const AxisConfiguration& config); + + /*! + * \brief Axis state has been updated + * + * \param position The new state + * + * \return Always true - axis motion events are always absorbed while button mapping + */ + bool OnMotion(float position); + + /*! + * \brief Called once per frame + * + * If an axis was activated, the button mapping command will be emitted + * here. + */ + void ProcessMotion(); + + /*! + * \brief Check if the axis was mapped and is still in motion + * + * \return True between when the axis is mapped and when it crosses zero + */ + bool IsMapping() const { return m_state == AXIS_STATE::MAPPED; } + + /*! + * \brief Set the state such that this axis has generated a mapping event + * + * If an axis is mapped to the Select action, it may be pressed when button + * mapping begins. This function is used to indicate that the axis shouldn't + * be mapped until after it crosses zero again. + */ + void SetEmitted(const CDriverPrimitive& activePrimitive); + +private: + enum class AXIS_STATE + { + /*! + * \brief Axis is inactive (position is less than threshold) + */ + INACTIVE, + + /*! + * \brief Axis is activated (position has exceeded threshold) + */ + ACTIVATED, + + /*! + * \brief Axis has generated a mapping event, but has not been centered yet + */ + MAPPED, + }; + + enum class AXIS_TYPE + { + /*! + * \brief Axis type is initially unknown + */ + UNKNOWN, + + /*! + * \brief Axis is centered about 0 + * + * - If the axis is an analog stick, it can travel to -1 or +1. + * - If the axis is a pressure-sensitive button or a normal trigger, + * it can travel to +1. + * - If the axis is a DirectInput trigger, then it is possible that two + * triggers can be on the same axis in opposite directions. + * - Normally, D-pads appear as a hat or four buttons. However, some + * D-pads are reported as two axes that can have the discrete values + * -1, 0 or 1. This is called a "discrete D-pad". + */ + NORMAL, + + /*! + * \brief Axis is centered about -1 or 1 + * + * - On OSX, with the cocoa driver, triggers are centered about -1 and + * travel to +1. In this case, the range is 2 and the direction is + * positive. + * - The author of SDL has observed triggers centered at +1 and travel + * to 0. In this case, the range is 1 and the direction is negative. + */ + OFFSET, + }; + + void DetectType(float position); + + // Construction parameters + const unsigned int m_axisIndex; + AxisConfiguration m_config; // mutable + + // State variables + AXIS_STATE m_state; + CDriverPrimitive m_activatedPrimitive; + AXIS_TYPE m_type; + bool m_initialPositionKnown; // set to true on first motion + float m_initialPosition; // set to position of first motion + bool m_initialPositionChanged; // set to true when position differs from the initial position + std::chrono::time_point<std::chrono::steady_clock> + m_activationTimeMs; // only used to delay anomalous trigger mapping to detect full range +}; + +/*! + * \brief Detects when a keyboard key should be mapped + */ +class CKeyDetector : public CPrimitiveDetector +{ +public: + CKeyDetector(CButtonMapping* buttonMapping, XBMCKey keycode); + + /*! + * \brief Key state has been updated + * + * \param bPressed The new state + * + * \return True if this press was handled, false if it should fall through + * to the next driver handler + */ + bool OnMotion(bool bPressed); + +private: + // Construction parameters + const XBMCKey m_keycode; +}; + +/*! + * \brief Detects when a mouse button should be mapped + */ +class CMouseButtonDetector : public CPrimitiveDetector +{ +public: + CMouseButtonDetector(CButtonMapping* buttonMapping, MOUSE::BUTTON_ID buttonIndex); + + /*! + * \brief Button state has been updated + * + * \param bPressed The new state + * + * \return True if this press was handled, false if it should fall through + * to the next driver handler + */ + bool OnMotion(bool bPressed); + +private: + // Construction parameters + const MOUSE::BUTTON_ID m_buttonIndex; +}; + +/*! + * \brief Detects when a mouse button should be mapped + */ +class CPointerDetector : public CPrimitiveDetector +{ +public: + CPointerDetector(CButtonMapping* buttonMapping); + + /*! + * \brief Pointer position has been updated + * + * \param x The new x coordinate + * \param y The new y coordinate + * + * \return Always true - pointer motion events are always absorbed while + * button mapping + */ + bool OnMotion(int x, int y); + +private: + // Utility function + static INPUT::INTERCARDINAL_DIRECTION GetPointerDirection(int x, int y); + + static const unsigned int MIN_FRAME_COUNT = 10; + + // State variables + bool m_bStarted = false; + int m_startX = 0; + int m_startY = 0; + unsigned int m_frameCount = 0; +}; + +/*! + * \ingroup joystick + * \brief Generic implementation of a class that provides button mapping by + * translating driver events to button mapping commands + * + * Button mapping commands are invoked instantly for buttons and hats. + * + * Button mapping commands are deferred for a short while after an axis is + * activated, and only one button mapping command will be invoked per + * activation. + */ +class CButtonMapping : public IDriverHandler, + public KEYBOARD::IKeyboardDriverHandler, + public MOUSE::IMouseDriverHandler, + public IButtonMapCallback +{ +public: + /*! + * \brief Constructor for CButtonMapping + * + * \param buttonMapper Carries out button-mapping commands using <buttonMap> + * \param buttonMap The button map given to <buttonMapper> on each command + */ + CButtonMapping(IButtonMapper* buttonMapper, IButtonMap* buttonMap, IKeymap* keymap); + + ~CButtonMapping() override = default; + + // implementation of IDriverHandler + bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) override; + bool OnHatMotion(unsigned int hatIndex, HAT_STATE state) override; + bool OnAxisMotion(unsigned int axisIndex, + float position, + int center, + unsigned int range) override; + void OnInputFrame() override; + + // implementation of IKeyboardDriverHandler + bool OnKeyPress(const CKey& key) override; + void OnKeyRelease(const CKey& key) override {} + + // implementation of IMouseDriverHandler + bool OnPosition(int x, int y) override; + bool OnButtonPress(MOUSE::BUTTON_ID button) override; + void OnButtonRelease(MOUSE::BUTTON_ID button) override; + + // implementation of IButtonMapCallback + void SaveButtonMap() override; + void ResetIgnoredPrimitives() override; + void RevertButtonMap() override; + + /*! + * \brief Process the primitive mapping command + * + * First, this function checks if the input should be dropped. This can + * happen if the input is ignored or the cooldown period is active. If the + * input is dropped, this returns true with no effect, effectively absorbing + * the input. Otherwise, the mapping command is sent to m_buttonMapper. + * + * \param primitive The primitive being mapped + * \return True if the mapping command was handled, false otherwise + */ + bool MapPrimitive(const CDriverPrimitive& primitive); + +private: + bool IsMapping() const; + + void OnLateDiscovery(unsigned int axisIndex); + + CButtonDetector& GetButton(unsigned int buttonIndex); + CHatDetector& GetHat(unsigned int hatIndex); + CAxisDetector& GetAxis(unsigned int axisIndex, + float position, + const AxisConfiguration& initialConfig = AxisConfiguration()); + CKeyDetector& GetKey(XBMCKey keycode); + CMouseButtonDetector& GetMouseButton(MOUSE::BUTTON_ID buttonIndex); + CPointerDetector& GetPointer(); + + // Construction parameters + IButtonMapper* const m_buttonMapper; + IButtonMap* const m_buttonMap; + IKeymap* const m_keymap; + + std::map<unsigned int, CButtonDetector> m_buttons; + std::map<unsigned int, CHatDetector> m_hats; + std::map<unsigned int, CAxisDetector> m_axes; + std::map<XBMCKey, CKeyDetector> m_keys; + std::map<MOUSE::BUTTON_ID, CMouseButtonDetector> m_mouseButtons; + std::unique_ptr<CPointerDetector> m_pointer; + std::chrono::time_point<std::chrono::steady_clock> m_lastAction; + uint64_t m_frameCount; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/generic/CMakeLists.txt b/xbmc/input/joysticks/generic/CMakeLists.txt new file mode 100644 index 0000000..f44258a --- /dev/null +++ b/xbmc/input/joysticks/generic/CMakeLists.txt @@ -0,0 +1,11 @@ +set(SOURCES ButtonMapping.cpp + DriverReceiving.cpp + FeatureHandling.cpp + InputHandling.cpp) + +set(HEADERS ButtonMapping.h + DriverReceiving.h + FeatureHandling.h + InputHandling.h) + +core_add_library(input_joystick_generic) diff --git a/xbmc/input/joysticks/generic/DriverReceiving.cpp b/xbmc/input/joysticks/generic/DriverReceiving.cpp new file mode 100644 index 0000000..bf52f4f --- /dev/null +++ b/xbmc/input/joysticks/generic/DriverReceiving.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DriverReceiving.h" + +#include "input/joysticks/DriverPrimitive.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "input/joysticks/interfaces/IDriverReceiver.h" + +using namespace KODI; +using namespace JOYSTICK; + +CDriverReceiving::CDriverReceiving(IDriverReceiver* receiver, IButtonMap* buttonMap) + : m_receiver(receiver), m_buttonMap(buttonMap) +{ +} + +bool CDriverReceiving::SetRumbleState(const FeatureName& feature, float magnitude) +{ + bool bHandled = false; + + if (m_receiver != nullptr && m_buttonMap != nullptr) + { + CDriverPrimitive primitive; + if (m_buttonMap->GetScalar(feature, primitive)) + { + if (primitive.Type() == PRIMITIVE_TYPE::MOTOR) + bHandled = m_receiver->SetMotorState(primitive.Index(), magnitude); + } + } + + return bHandled; +} diff --git a/xbmc/input/joysticks/generic/DriverReceiving.h b/xbmc/input/joysticks/generic/DriverReceiving.h new file mode 100644 index 0000000..758b560 --- /dev/null +++ b/xbmc/input/joysticks/generic/DriverReceiving.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/JoystickTypes.h" +#include "input/joysticks/interfaces/IInputReceiver.h" + +#include <map> + +namespace KODI +{ +namespace JOYSTICK +{ +class IDriverReceiver; +class IButtonMap; + +/*! + * \ingroup joystick + * \brief Class to translate input events from higher-level features to driver primitives + * + * A button map is used to translate controller features to driver primitives. + * The button map has been abstracted away behind the IButtonMap interface + * so that it can be provided by an add-on. + */ +class CDriverReceiving : public IInputReceiver +{ +public: + CDriverReceiving(IDriverReceiver* receiver, IButtonMap* buttonMap); + + ~CDriverReceiving() override = default; + + // implementation of IInputReceiver + bool SetRumbleState(const FeatureName& feature, float magnitude) override; + +private: + IDriverReceiver* const m_receiver; + IButtonMap* const m_buttonMap; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/generic/FeatureHandling.cpp b/xbmc/input/joysticks/generic/FeatureHandling.cpp new file mode 100644 index 0000000..639ae5b --- /dev/null +++ b/xbmc/input/joysticks/generic/FeatureHandling.cpp @@ -0,0 +1,551 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FeatureHandling.h" + +#include "ServiceBroker.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerManager.h" +#include "input/joysticks/DriverPrimitive.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "input/joysticks/interfaces/IInputHandler.h" +#include "utils/log.h" + +#include <vector> + +using namespace KODI; +using namespace JOYSTICK; + +#define ANALOG_DIGITAL_THRESHOLD 0.5f +#define DISCRETE_ANALOG_RAMPUP_TIME_MS 1500 +#define DISCRETE_ANALOG_START_VALUE 0.3f + +// --- CJoystickFeature -------------------------------------------------------- + +CJoystickFeature::CJoystickFeature(const FeatureName& name, + IInputHandler* handler, + IButtonMap* buttonMap) + : m_name(name), + m_handler(handler), + m_buttonMap(buttonMap), + m_bEnabled(m_handler->HasFeature(name)) +{ +} + +bool CJoystickFeature::AcceptsInput(bool bActivation) +{ + bool bAcceptsInput = false; + + if (m_bEnabled) + { + if (m_handler->AcceptsInput(m_name)) + bAcceptsInput = true; + } + + return bAcceptsInput; +} + +void CJoystickFeature::ResetMotion() +{ + m_motionStartTimeMs = {}; +} + +void CJoystickFeature::StartMotion() +{ + m_motionStartTimeMs = std::chrono::steady_clock::now(); +} + +bool CJoystickFeature::InMotion() const +{ + return m_motionStartTimeMs.time_since_epoch().count() > 0; +} + +unsigned int CJoystickFeature::MotionTimeMs() const +{ + if (!InMotion()) + return 0; + + auto now = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_motionStartTimeMs); + + return duration.count(); +} + +// --- CScalarFeature ---------------------------------------------------------- + +CScalarFeature::CScalarFeature(const FeatureName& name, + IInputHandler* handler, + IButtonMap* buttonMap) + : CJoystickFeature(name, handler, buttonMap), + m_bDigitalState(false), + m_analogState(0.0f), + m_bActivated(false), + m_bDiscrete(true) +{ + GAME::ControllerPtr controller = + CServiceBroker::GetGameControllerManager().GetController(handler->ControllerID()); + if (controller) + m_inputType = controller->GetInputType(name); +} + +bool CScalarFeature::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) +{ + // Feature must accept input to be considered handled + bool bHandled = AcceptsInput(bPressed); + + if (m_inputType == INPUT_TYPE::DIGITAL) + bHandled &= OnDigitalMotion(bPressed); + else if (m_inputType == INPUT_TYPE::ANALOG) + bHandled &= OnAnalogMotion(bPressed ? 1.0f : 0.0f); + + return bHandled; +} + +bool CScalarFeature::OnAnalogMotion(const CDriverPrimitive& source, float magnitude) +{ + // Update activated status + if (magnitude > 0.0f) + m_bActivated = true; + + // Update discrete status + if (magnitude != 0.0f && magnitude != 1.0f) + m_bDiscrete = false; + + // Feature must accept input to be considered handled + bool bHandled = AcceptsInput(magnitude > 0.0f); + + if (m_inputType == INPUT_TYPE::DIGITAL) + bHandled &= OnDigitalMotion(magnitude >= ANALOG_DIGITAL_THRESHOLD); + else if (m_inputType == INPUT_TYPE::ANALOG) + bHandled &= OnAnalogMotion(magnitude); + + return bHandled; +} + +void CScalarFeature::ProcessMotions(void) +{ + if (m_inputType == INPUT_TYPE::DIGITAL && m_bDigitalState) + ProcessDigitalMotion(); + else if (m_inputType == INPUT_TYPE::ANALOG) + ProcessAnalogMotion(); +} + +bool CScalarFeature::OnDigitalMotion(bool bPressed) +{ + bool bHandled = false; + + if (m_bDigitalState != bPressed) + { + m_bDigitalState = bPressed; + + // Motion is initiated in ProcessMotions() + ResetMotion(); + + bHandled = m_bInitialPressHandled = m_handler->OnButtonPress(m_name, bPressed); + + if (m_bDigitalState) + CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} pressed ({})", m_name, m_handler->ControllerID(), + bHandled ? "handled" : "ignored"); + else + CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} released", m_name, m_handler->ControllerID()); + } + else if (m_bDigitalState) + { + bHandled = m_bInitialPressHandled; + } + + return bHandled; +} + +bool CScalarFeature::OnAnalogMotion(float magnitude) +{ + const bool bActivated = (magnitude != 0.0f); + + // Update analog state + m_analogState = magnitude; + + // Update motion time + if (!bActivated) + ResetMotion(); + else if (!InMotion()) + StartMotion(); + + // Log activation/deactivation + if (m_bDigitalState != bActivated) + { + m_bDigitalState = bActivated; + CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} {}", m_name, m_handler->ControllerID(), + bActivated ? "activated" : "deactivated"); + } + + return true; +} + +void CScalarFeature::ProcessDigitalMotion() +{ + if (!InMotion()) + { + // Button was just pressed, record start time and exit (button press event + // was already sent this frame) + StartMotion(); + } + else + { + // Button has been pressed more than one event frame + const unsigned int elapsed = MotionTimeMs(); + m_handler->OnButtonHold(m_name, elapsed); + } +} + +void CScalarFeature::ProcessAnalogMotion() +{ + float magnitude = m_analogState; + + // Calculate time elapsed since motion began + unsigned int elapsed = MotionTimeMs(); + + // If analog value is discrete, ramp up magnitude + if (m_bActivated && m_bDiscrete) + { + if (elapsed < DISCRETE_ANALOG_RAMPUP_TIME_MS) + { + magnitude *= static_cast<float>(elapsed) / DISCRETE_ANALOG_RAMPUP_TIME_MS; + if (magnitude < DISCRETE_ANALOG_START_VALUE) + magnitude = DISCRETE_ANALOG_START_VALUE; + } + } + + m_handler->OnButtonMotion(m_name, magnitude, elapsed); +} + +// --- CAxisFeature ------------------------------------------------------------ + +CAxisFeature::CAxisFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap) + : CJoystickFeature(name, handler, buttonMap), m_state(0.0f) +{ +} + +bool CAxisFeature::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) +{ + return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f); +} + +void CAxisFeature::ProcessMotions(void) +{ + const float newState = m_axis.GetPosition(); + + const bool bActivated = (newState != 0.0f); + + if (!AcceptsInput(bActivated)) + return; + + const bool bWasActivated = (m_state != 0.0f); + + if (!bActivated && bWasActivated) + CLog::Log(LOGDEBUG, "Feature [ {} ] on {} deactivated", m_name, m_handler->ControllerID()); + else if (bActivated && !bWasActivated) + { + CLog::Log(LOGDEBUG, "Feature [ {} ] on {} activated {}", m_name, m_handler->ControllerID(), + newState > 0.0f ? "positive" : "negative"); + } + + if (bActivated || bWasActivated) + { + m_state = newState; + + unsigned int motionTimeMs = 0; + + if (bActivated) + { + if (!InMotion()) + StartMotion(); + else + motionTimeMs = MotionTimeMs(); + } + else + ResetMotion(); + + switch (m_buttonMap->GetFeatureType(m_name)) + { + case FEATURE_TYPE::WHEEL: + m_handler->OnWheelMotion(m_name, newState, motionTimeMs); + break; + case FEATURE_TYPE::THROTTLE: + m_handler->OnThrottleMotion(m_name, newState, motionTimeMs); + break; + default: + break; + } + } +} + +// --- CWheel ------------------------------------------------------------------ + +CWheel::CWheel(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap) + : CAxisFeature(name, handler, buttonMap) +{ +} + +bool CWheel::OnAnalogMotion(const CDriverPrimitive& source, float magnitude) +{ + WHEEL_DIRECTION direction = WHEEL_DIRECTION::NONE; + + std::vector<WHEEL_DIRECTION> dirs = { + WHEEL_DIRECTION::RIGHT, + WHEEL_DIRECTION::LEFT, + }; + + CDriverPrimitive primitive; + for (auto dir : dirs) + { + if (m_buttonMap->GetWheel(m_name, dir, primitive) && primitive == source) + { + direction = dir; + break; + } + } + + // Feature must accept input to be considered handled + bool bHandled = AcceptsInput(magnitude > 0.0f); + + switch (direction) + { + case WHEEL_DIRECTION::RIGHT: + m_axis.SetPositiveDistance(magnitude); + break; + case WHEEL_DIRECTION::LEFT: + m_axis.SetNegativeDistance(magnitude); + break; + default: + // Just in case, avoid sticking + m_axis.Reset(); + break; + } + + return bHandled; +} + +// --- CThrottle --------------------------------------------------------------- + +CThrottle::CThrottle(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap) + : CAxisFeature(name, handler, buttonMap) +{ +} + +bool CThrottle::OnAnalogMotion(const CDriverPrimitive& source, float magnitude) +{ + THROTTLE_DIRECTION direction = THROTTLE_DIRECTION::NONE; + + std::vector<THROTTLE_DIRECTION> dirs = { + THROTTLE_DIRECTION::UP, + THROTTLE_DIRECTION::DOWN, + }; + + CDriverPrimitive primitive; + for (auto dir : dirs) + { + if (m_buttonMap->GetThrottle(m_name, dir, primitive) && primitive == source) + { + direction = dir; + break; + } + } + + // Feature must accept input to be considered handled + bool bHandled = AcceptsInput(magnitude > 0.0f); + + switch (direction) + { + case THROTTLE_DIRECTION::UP: + m_axis.SetPositiveDistance(magnitude); + break; + case THROTTLE_DIRECTION::DOWN: + m_axis.SetNegativeDistance(magnitude); + break; + default: + // Just in case, avoid sticking + m_axis.Reset(); + break; + } + + return bHandled; +} + +// --- CAnalogStick ------------------------------------------------------------ + +CAnalogStick::CAnalogStick(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap) + : CJoystickFeature(name, handler, buttonMap), m_vertState(0.0f), m_horizState(0.0f) +{ +} + +bool CAnalogStick::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) +{ + return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f); +} + +bool CAnalogStick::OnAnalogMotion(const CDriverPrimitive& source, float magnitude) +{ + ANALOG_STICK_DIRECTION direction = ANALOG_STICK_DIRECTION::NONE; + + std::vector<ANALOG_STICK_DIRECTION> dirs = { + ANALOG_STICK_DIRECTION::UP, + ANALOG_STICK_DIRECTION::DOWN, + ANALOG_STICK_DIRECTION::RIGHT, + ANALOG_STICK_DIRECTION::LEFT, + }; + + CDriverPrimitive primitive; + for (auto dir : dirs) + { + if (m_buttonMap->GetAnalogStick(m_name, dir, primitive) && primitive == source) + { + direction = dir; + break; + } + } + + // Feature must accept input to be considered handled + bool bHandled = AcceptsInput(magnitude > 0.0f); + + switch (direction) + { + case ANALOG_STICK_DIRECTION::UP: + m_vertAxis.SetPositiveDistance(magnitude); + break; + case ANALOG_STICK_DIRECTION::DOWN: + m_vertAxis.SetNegativeDistance(magnitude); + break; + case ANALOG_STICK_DIRECTION::RIGHT: + m_horizAxis.SetPositiveDistance(magnitude); + break; + case ANALOG_STICK_DIRECTION::LEFT: + m_horizAxis.SetNegativeDistance(magnitude); + break; + default: + // Just in case, avoid sticking + m_vertAxis.Reset(); + m_horizAxis.Reset(); + break; + } + + return bHandled; +} + +void CAnalogStick::ProcessMotions(void) +{ + const float newVertState = m_vertAxis.GetPosition(); + const float newHorizState = m_horizAxis.GetPosition(); + + const bool bActivated = (newVertState != 0.0f || newHorizState != 0.0f); + + if (!AcceptsInput(bActivated)) + return; + + const bool bWasActivated = (m_vertState != 0.0f || m_horizState != 0.0f); + + if (bActivated ^ bWasActivated) + { + CLog::Log(LOGDEBUG, "Feature [ {} ] on {} {}", m_name, m_handler->ControllerID(), + bActivated ? "activated" : "deactivated"); + } + + if (bActivated || bWasActivated) + { + m_vertState = newVertState; + m_horizState = newHorizState; + + unsigned int motionTimeMs = 0; + + if (bActivated) + { + if (!InMotion()) + StartMotion(); + else + motionTimeMs = MotionTimeMs(); + } + else + { + ResetMotion(); + } + + m_handler->OnAnalogStickMotion(m_name, newHorizState, newVertState, motionTimeMs); + } +} + +// --- CAccelerometer ---------------------------------------------------------- + +CAccelerometer::CAccelerometer(const FeatureName& name, + IInputHandler* handler, + IButtonMap* buttonMap) + : CJoystickFeature(name, handler, buttonMap), + m_xAxisState(0.0f), + m_yAxisState(0.0f), + m_zAxisState(0.0f) +{ +} + +bool CAccelerometer::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) +{ + return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f); +} + +bool CAccelerometer::OnAnalogMotion(const CDriverPrimitive& source, float magnitude) +{ + // Feature must accept input to be considered handled + bool bHandled = AcceptsInput(true); + + CDriverPrimitive positiveX; + CDriverPrimitive positiveY; + CDriverPrimitive positiveZ; + + m_buttonMap->GetAccelerometer(m_name, positiveX, positiveY, positiveZ); + + if (source == positiveX) + { + m_xAxis.SetPositiveDistance(magnitude); + } + else if (source == positiveY) + { + m_yAxis.SetPositiveDistance(magnitude); + } + else if (source == positiveZ) + { + m_zAxis.SetPositiveDistance(magnitude); + } + else + { + // Just in case, avoid sticking + m_xAxis.Reset(); + m_xAxis.Reset(); + m_yAxis.Reset(); + } + + return bHandled; +} + +void CAccelerometer::ProcessMotions(void) +{ + const float newXAxis = m_xAxis.GetPosition(); + const float newYAxis = m_yAxis.GetPosition(); + const float newZAxis = m_zAxis.GetPosition(); + + const bool bActivated = (newXAxis != 0.0f || newYAxis != 0.0f || newZAxis != 0.0f); + + if (!AcceptsInput(bActivated)) + return; + + const bool bWasActivated = (m_xAxisState != 0.0f || m_yAxisState != 0.0f || m_zAxisState != 0.0f); + + if (bActivated || bWasActivated) + { + m_xAxisState = newXAxis; + m_yAxisState = newYAxis; + m_zAxisState = newZAxis; + m_handler->OnAccelerometerMotion(m_name, newXAxis, newYAxis, newZAxis); + } +} diff --git a/xbmc/input/joysticks/generic/FeatureHandling.h b/xbmc/input/joysticks/generic/FeatureHandling.h new file mode 100644 index 0000000..1a42d5c --- /dev/null +++ b/xbmc/input/joysticks/generic/FeatureHandling.h @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/JoystickTypes.h" + +#include <chrono> +#include <memory> + +namespace KODI +{ +namespace JOYSTICK +{ +class CDriverPrimitive; +class IInputHandler; +class IButtonMap; + +class CJoystickFeature; +using FeaturePtr = std::shared_ptr<CJoystickFeature>; + +/*! + * \ingroup joystick + * \brief Base class for joystick features + * + * See list of feature types in JoystickTypes.h. + */ +class CJoystickFeature +{ +public: + CJoystickFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap); + virtual ~CJoystickFeature() = default; + + /*! + * \brief A digital motion has occurred + * + * \param source The source of the motion. Must be digital (button or hat) + * \param bPressed True for press motion, false for release motion + * + * \return true if the motion was handled, false otherwise + */ + virtual bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) = 0; + + /*! + * \brief An analog motion has occurred + * + * \param source The source of the motion. Must be a semiaxis + * \param magnitude The magnitude of the press or motion in the interval [0.0, 1.0] + * + * For semiaxes, the magnitude is the force or travel distance in the + * direction of the semiaxis. If the value is in the opposite direction, + * the magnitude is 0.0. + * + * For example, if the analog stick goes left, the negative semiaxis will + * have a value of 1.0 and the positive semiaxis will have a value of 0.0. + */ + virtual bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) = 0; + + /*! + * \brief Process the motions that have occurred since the last invocation + * + * This allows features with motion on multiple driver primitives to call + * their handler once all driver primitives are accounted for. + */ + virtual void ProcessMotions(void) = 0; + + /*! + * \brief Check if the input handler is accepting input + * + * \param bActivation True if the motion is activating (true or positive), + * false if the motion is deactivating (false or zero) + * + * \return True if input should be sent to the input handler, false otherwise + */ + bool AcceptsInput(bool bActivation); + +protected: + /*! + * \brief Reset motion timer + */ + void ResetMotion(); + + /*! + * \brief Start the motion timer + */ + void StartMotion(); + + /*! + * \brief Check if the feature is in motion + */ + bool InMotion() const; + + /*! + * \brief Get the time for which the feature has been in motion + */ + unsigned int MotionTimeMs() const; + + const FeatureName m_name; + IInputHandler* const m_handler; + IButtonMap* const m_buttonMap; + const bool m_bEnabled; + +private: + std::chrono::time_point<std::chrono::steady_clock> m_motionStartTimeMs; +}; + +class CScalarFeature : public CJoystickFeature +{ +public: + CScalarFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap); + ~CScalarFeature() override = default; + + // implementation of CJoystickFeature + bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override; + bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override; + void ProcessMotions() override; + +private: + bool OnDigitalMotion(bool bPressed); + bool OnAnalogMotion(float magnitude); + + void ProcessDigitalMotion(); + void ProcessAnalogMotion(); + + // State variables + INPUT_TYPE m_inputType = INPUT_TYPE::UNKNOWN; + bool m_bDigitalState; + bool m_bInitialPressHandled = false; + + // Analog state variables + float m_analogState; // The current magnitude + float m_bActivated; // Set to true when first activated (magnitude > 0.0) + bool m_bDiscrete; // Set to false when a non-discrete axis is detected +}; + +/*! + * \ingroup joystick + * \brief Axis of a feature (analog stick, accelerometer, etc) + * + * Axes are composed of two driver primitives, one for the positive semiaxis + * and one for the negative semiaxis. + * + * This effectively means that an axis is two-dimensional, with each dimension + * either: + * + * - a digital value (0.0 or 1.0) + * - an analog value (continuous in the interval [0.0, 1.0]) + */ +class CFeatureAxis +{ +public: + CFeatureAxis(void) { Reset(); } + + /*! + * \brief Set value of positive axis + */ + void SetPositiveDistance(float distance) { m_positiveDistance = distance; } + + /*! + * \brief Set value of negative axis + */ + void SetNegativeDistance(float distance) { m_negativeDistance = distance; } + + /*! + * \brief Get the final value of this axis. + * + * This axis is two-dimensional, so we need to compress these into a single + * dimension. This is done by subtracting the negative from the positive. + * Some examples: + * + * Positive axis: 1.0 (User presses right or analog stick moves right) + * Negative axis: 0.0 + * ------------------- + * Pos - Neg: 1.0 (Emulated analog stick moves right) + * + * + * Positive axis: 0.0 + * Negative axis: 1.0 (User presses left or analog stick moves left) + * ------------------- + * Pos - Neg: -1.0 (Emulated analog stick moves left) + * + * + * Positive axis: 1.0 (User presses both buttons) + * Negative axis: 1.0 + * ------------------- + * Pos - Neg: 0.0 (Emulated analog stick is centered) + * + */ + float GetPosition(void) const { return m_positiveDistance - m_negativeDistance; } + + /*! + * \brief Reset both positive and negative values to zero + */ + void Reset(void) { m_positiveDistance = m_negativeDistance = 0.0f; } + +protected: + float m_positiveDistance; + float m_negativeDistance; +}; + +class CAxisFeature : public CJoystickFeature +{ +public: + CAxisFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap); + ~CAxisFeature() override = default; + + // partial implementation of CJoystickFeature + bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override; + void ProcessMotions() override; + +protected: + CFeatureAxis m_axis; + + float m_state; +}; + +class CWheel : public CAxisFeature +{ +public: + CWheel(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap); + ~CWheel() override = default; + + // partial implementation of CJoystickFeature + bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override; +}; + +class CThrottle : public CAxisFeature +{ +public: + CThrottle(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap); + ~CThrottle() override = default; + + // partial implementation of CJoystickFeature + bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override; +}; + +class CAnalogStick : public CJoystickFeature +{ +public: + CAnalogStick(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap); + ~CAnalogStick() override = default; + + // implementation of CJoystickFeature + bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override; + bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override; + void ProcessMotions() override; + +protected: + CFeatureAxis m_vertAxis; + CFeatureAxis m_horizAxis; + + float m_vertState; + float m_horizState; +}; + +class CAccelerometer : public CJoystickFeature +{ +public: + CAccelerometer(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap); + ~CAccelerometer() override = default; + + // implementation of CJoystickFeature + bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override; + bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override; + void ProcessMotions() override; + +protected: + CFeatureAxis m_xAxis; + CFeatureAxis m_yAxis; + CFeatureAxis m_zAxis; + + float m_xAxisState; + float m_yAxisState; + float m_zAxisState; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/generic/InputHandling.cpp b/xbmc/input/joysticks/generic/InputHandling.cpp new file mode 100644 index 0000000..f6cae6d --- /dev/null +++ b/xbmc/input/joysticks/generic/InputHandling.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "InputHandling.h" + +#include "input/joysticks/DriverPrimitive.h" +#include "input/joysticks/JoystickUtils.h" +#include "input/joysticks/dialogs/GUIDialogNewJoystick.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "input/joysticks/interfaces/IInputHandler.h" +#include "utils/log.h" + +#include <array> +#include <cmath> +#include <tuple> + +using namespace KODI; +using namespace JOYSTICK; + +CGUIDialogNewJoystick* const CInputHandling::m_dialog = new CGUIDialogNewJoystick; + +CInputHandling::CInputHandling(IInputHandler* handler, IButtonMap* buttonMap) + : m_handler(handler), m_buttonMap(buttonMap) +{ +} + +CInputHandling::~CInputHandling(void) = default; + +bool CInputHandling::OnButtonMotion(unsigned int buttonIndex, bool bPressed) +{ + return OnDigitalMotion(CDriverPrimitive(PRIMITIVE_TYPE::BUTTON, buttonIndex), bPressed); +} + +bool CInputHandling::OnHatMotion(unsigned int hatIndex, HAT_STATE state) +{ + bool bHandled = false; + + bHandled |= + OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::UP), state & HAT_DIRECTION::UP); + bHandled |= OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::RIGHT), + state & HAT_DIRECTION::RIGHT); + bHandled |= + OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::DOWN), state & HAT_DIRECTION::DOWN); + bHandled |= + OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::LEFT), state & HAT_DIRECTION::LEFT); + + return bHandled; +} + +bool CInputHandling::OnAxisMotion(unsigned int axisIndex, + float position, + int center, + unsigned int range) +{ + bool bHandled = false; + + if (center != 0) + { + float translatedPostion = std::min((position - center) / range, 1.0f); + + // Calculate the direction the trigger travels from the center point + SEMIAXIS_DIRECTION dir; + if (center > 0) + dir = SEMIAXIS_DIRECTION::NEGATIVE; + else + dir = SEMIAXIS_DIRECTION::POSITIVE; + + CDriverPrimitive offsetSemiaxis(axisIndex, center, dir, range); + + bHandled = OnAnalogMotion(offsetSemiaxis, translatedPostion); + } + else + { + CDriverPrimitive positiveSemiaxis(axisIndex, 0, SEMIAXIS_DIRECTION::POSITIVE, 1); + CDriverPrimitive negativeSemiaxis(axisIndex, 0, SEMIAXIS_DIRECTION::NEGATIVE, 1); + + bHandled |= OnAnalogMotion(positiveSemiaxis, position > 0.0f ? position : 0.0f); + bHandled |= OnAnalogMotion(negativeSemiaxis, position < 0.0f ? -position : 0.0f); + } + + return bHandled; +} + +void CInputHandling::OnInputFrame(void) +{ + // Handle driver input + for (auto& it : m_features) + it.second->ProcessMotions(); + + // Handle higher-level controller input + m_handler->OnInputFrame(); +} + +bool CInputHandling::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) +{ + bool bHandled = false; + + FeatureName featureName; + if (m_buttonMap->GetFeature(source, featureName)) + { + auto it = m_features.find(featureName); + if (it == m_features.end()) + { + FeaturePtr feature(CreateFeature(featureName)); + if (feature) + std::tie(it, std::ignore) = m_features.insert({featureName, std::move(feature)}); + } + + if (it != m_features.end()) + bHandled = it->second->OnDigitalMotion(source, bPressed); + } + else if (bPressed) + { + // If button didn't resolve to a feature, check if the button map is empty + // and ask the user if they would like to start mapping the controller + if (m_buttonMap->IsEmpty()) + { + CLog::Log(LOGDEBUG, "Empty button map detected for {}", m_buttonMap->ControllerID()); + m_dialog->ShowAsync(); + } + } + + return bHandled; +} + +bool CInputHandling::OnAnalogMotion(const CDriverPrimitive& source, float magnitude) +{ + bool bHandled = false; + + FeatureName featureName; + if (m_buttonMap->GetFeature(source, featureName)) + { + auto it = m_features.find(featureName); + if (it == m_features.end()) + { + FeaturePtr feature(CreateFeature(featureName)); + if (feature) + std::tie(it, std::ignore) = m_features.insert({featureName, std::move(feature)}); + } + + if (it != m_features.end()) + bHandled = it->second->OnAnalogMotion(source, magnitude); + } + + return bHandled; +} + +CJoystickFeature* CInputHandling::CreateFeature(const FeatureName& featureName) +{ + CJoystickFeature* feature = nullptr; + + switch (m_buttonMap->GetFeatureType(featureName)) + { + case FEATURE_TYPE::SCALAR: + { + feature = new CScalarFeature(featureName, m_handler, m_buttonMap); + break; + } + case FEATURE_TYPE::ANALOG_STICK: + { + feature = new CAnalogStick(featureName, m_handler, m_buttonMap); + break; + } + case FEATURE_TYPE::ACCELEROMETER: + { + feature = new CAccelerometer(featureName, m_handler, m_buttonMap); + break; + } + default: + break; + } + + return feature; +} diff --git a/xbmc/input/joysticks/generic/InputHandling.h b/xbmc/input/joysticks/generic/InputHandling.h new file mode 100644 index 0000000..772d9d1 --- /dev/null +++ b/xbmc/input/joysticks/generic/InputHandling.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "FeatureHandling.h" +#include "input/joysticks/JoystickTypes.h" +#include "input/joysticks/interfaces/IDriverHandler.h" + +#include <map> + +namespace KODI +{ +namespace JOYSTICK +{ +class CDriverPrimitive; +class CGUIDialogNewJoystick; +class IInputHandler; +class IButtonMap; + +/*! + * \ingroup joystick + * \brief Class to translate input from the driver into higher-level features + * + * Raw driver input arrives for three elements: buttons, hats and axes. When + * driver input is handled by this class, it translates the raw driver + * elements into physical joystick features, such as buttons, analog sticks, + * etc. + * + * A button map is used to translate driver primitives to controller features. + * The button map has been abstracted away behind the IButtonMap + * interface so that it can be provided by an add-on. + */ +class CInputHandling : public IDriverHandler +{ +public: + CInputHandling(IInputHandler* handler, IButtonMap* buttonMap); + + ~CInputHandling() override; + + // implementation of IDriverHandler + bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) override; + bool OnHatMotion(unsigned int hatIndex, HAT_STATE state) override; + bool OnAxisMotion(unsigned int axisIndex, + float position, + int center, + unsigned int range) override; + void OnInputFrame() override; + +private: + bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed); + bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude); + + CJoystickFeature* CreateFeature(const FeatureName& featureName); + + IInputHandler* const m_handler; + IButtonMap* const m_buttonMap; + + std::map<FeatureName, FeaturePtr> m_features; + + static CGUIDialogNewJoystick* const m_dialog; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IButtonMap.h b/xbmc/input/joysticks/interfaces/IButtonMap.h new file mode 100644 index 0000000..0b4b7d5 --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IButtonMap.h @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/DriverPrimitive.h" +#include "input/joysticks/JoystickTypes.h" + +#include <string> +#include <vector> + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \ingroup joystick + * \brief Button map interface to translate between the driver's raw + * button/hat/axis elements and physical joystick features. + * + * \sa IButtonMapper + */ +class IButtonMap +{ +public: + virtual ~IButtonMap() = default; + + /*! + * \brief The add-on ID of the game controller associated with this button map + * + * The controller ID provided by the implementation serves as the context + * for the feature names below. + * + * \return The ID of this button map's game controller add-on + */ + virtual std::string ControllerID(void) const = 0; + + /*! + * \brief The Location of the peripheral associated with this button map + * + * \return The peripheral's location + */ + virtual std::string Location(void) const = 0; + + /*! + * \brief Load the button map into memory + * + * \return True if button map is ready to start translating buttons, false otherwise + */ + virtual bool Load(void) = 0; + + /*! + * \brief Reset the button map to its defaults, or clear button map if no defaults + */ + virtual void Reset(void) = 0; + + /*! + * \brief Check if the button map is empty + * + * \return True if the button map is empty, false if it has features + */ + virtual bool IsEmpty(void) const = 0; + + /*! + * \brief Get the ID of the controller profile that best represents the + * appearance of the peripheral + * + * \return The controller ID, or empty if the appearance is unknown + */ + virtual std::string GetAppearance() const = 0; + + /*! + * \brief Set the ID of the controller that best represents the appearance + * of the peripheral + * + * \param controllerId The controller ID, or empty to unset the appearance + * + * \return True if the appearance was set, false on error + */ + virtual bool SetAppearance(const std::string& controllerId) const = 0; + + /*! + * \brief Get the feature associated with a driver primitive + * + * Multiple primitives can be mapped to the same feature. For example, + * analog sticks use one primitive for each direction. + * + * \param primitive The driver primitive + * \param feature The name of the resolved joystick feature, or + * invalid if false is returned + * + * \return True if the driver primitive is associated with a feature, false otherwise + */ + virtual bool GetFeature(const CDriverPrimitive& primitive, FeatureName& feature) = 0; + + /*! + * \brief Get the type of the feature for the given name + * + * \param feature The feature to look up + * + * \return The feature's type + */ + virtual FEATURE_TYPE GetFeatureType(const FeatureName& feature) = 0; + + /*! + * \brief Get the driver primitive for a scalar feature + * + * When a feature can be represented by a single driver primitive, it is + * called a scalar feature. + * + * - This includes buttons and triggers, because they can be mapped to a + * single button/hat/semiaxis + * + * - This does not include analog sticks, because they require two axes + * and four driver primitives (one for each semiaxis) + * + * \param feature Must be a scalar feature (a feature that only + * requires a single driver primitive) + * \param primitive The resolved driver primitive + * + * \return True if the feature resolved to a driver primitive, false if the + * feature didn't resolve or isn't a scalar feature + */ + virtual bool GetScalar(const FeatureName& feature, CDriverPrimitive& primitive) = 0; + + /*! + * \brief Add or update a scalar feature + * + * \param feature Must be a scalar feature + * \param primitive The feature's driver primitive + * + * \return True if the feature was updated, false if the feature is + * unchanged or failure occurs + */ + virtual void AddScalar(const FeatureName& feature, const CDriverPrimitive& primitive) = 0; + + /*! + * \brief Get an analog stick direction from the button map + * + * \param feature Must be an analog stick or this will return false + * \param direction The direction whose primitive is to be retrieved + * \param[out] primitive The primitive mapped to the specified direction + * + * \return True if the feature and direction resolved to a driver primitive + */ + virtual bool GetAnalogStick(const FeatureName& feature, + ANALOG_STICK_DIRECTION direction, + CDriverPrimitive& primitive) = 0; + + /*! + * \brief Add or update an analog stick direction + * + * \param feature Must be an analog stick or this will return false + * \param direction The direction being mapped + * \param primitive The driver primitive for the specified analog stick and direction + * + * \return True if the analog stick was updated, false otherwise + */ + virtual void AddAnalogStick(const FeatureName& feature, + ANALOG_STICK_DIRECTION direction, + const CDriverPrimitive& primitive) = 0; + + /*! + * \brief Get a relative pointer direction from the button map + * + * \param feature Must be a relative pointer stick or this will return false + * \param direction The direction whose primitive is to be retrieved + * \param[out] primitive The primitive mapped to the specified direction + * + * \return True if the feature and direction resolved to a driver primitive + */ + virtual bool GetRelativePointer(const FeatureName& feature, + RELATIVE_POINTER_DIRECTION direction, + CDriverPrimitive& primitive) = 0; + + /*! + * \brief Add or update a relative pointer direction + * + * \param feature Must be a relative pointer or this will return false + * \param direction The direction being mapped + * \param primitive The driver primitive for the specified analog stick and direction + * + * \return True if the analog stick was updated, false otherwise + */ + virtual void AddRelativePointer(const FeatureName& feature, + RELATIVE_POINTER_DIRECTION direction, + const CDriverPrimitive& primitive) = 0; + + /*! + * \brief Get an accelerometer from the button map + * + * \param feature Must be an accelerometer or this will return false + * \param positiveX The semiaxis mapped to the positive X direction (possibly unknown) + * \param positiveY The semiaxis mapped to the positive Y direction (possibly unknown) + * \param positiveZ The semiaxis mapped to the positive Z direction (possibly unknown) + * + * \return True if the feature resolved to an accelerometer with at least 1 known axis + */ + virtual bool GetAccelerometer(const FeatureName& feature, + CDriverPrimitive& positiveX, + CDriverPrimitive& positiveY, + CDriverPrimitive& positiveZ) = 0; + + /*! + * \brief Get or update an accelerometer + * + * \param feature Must be an accelerometer or this will return false + * \param positiveX The semiaxis corresponding to the positive X direction + * \param positiveY The semiaxis corresponding to the positive Y direction + * \param positiveZ The semiaxis corresponding to the positive Z direction + * + * The driver primitives must be mapped to a semiaxis or this function will fail. + * + * \return True if the accelerometer was updated, false if unchanged or failure occurred + */ + virtual void AddAccelerometer(const FeatureName& feature, + const CDriverPrimitive& positiveX, + const CDriverPrimitive& positiveY, + const CDriverPrimitive& positiveZ) = 0; + + /*! + * \brief Get a wheel direction from the button map + * + * \param feature Must be a wheel or this will return false + * \param direction The direction whose primitive is to be retrieved + * \param[out] primitive The primitive mapped to the specified direction + * + * \return True if the feature and direction resolved to a driver primitive + */ + virtual bool GetWheel(const FeatureName& feature, + WHEEL_DIRECTION direction, + CDriverPrimitive& primitive) = 0; + + /*! + * \brief Add or update a wheel direction + * + * \param feature Must be a wheel or this will return false + * \param direction The direction being mapped + * \param primitive The driver primitive for the specified analog stick and direction + * + * \return True if the analog stick was updated, false otherwise + */ + virtual void AddWheel(const FeatureName& feature, + WHEEL_DIRECTION direction, + const CDriverPrimitive& primitive) = 0; + + /*! + * \brief Get a throttle direction from the button map + * + * \param feature Must be a throttle or this will return false + * \param direction The direction whose primitive is to be retrieved + * \param[out] primitive The primitive mapped to the specified direction + * + * \return True if the feature and direction resolved to a driver primitive + */ + virtual bool GetThrottle(const FeatureName& feature, + THROTTLE_DIRECTION direction, + CDriverPrimitive& primitive) = 0; + + /*! + * \brief Add or update a throttle direction + * + * \param feature Must be a throttle or this will return false + * \param direction The direction being mapped + * \param primitive The driver primitive for the specified analog stick and direction + * + * \return True if the analog stick was updated, false otherwise + */ + virtual void AddThrottle(const FeatureName& feature, + THROTTLE_DIRECTION direction, + const CDriverPrimitive& primitive) = 0; + + /*! + * \brief Get the driver primitive for a keyboard key + * + * \param feature Must be a key + * \param primitive The resolved driver primitive + * + * \return True if the feature resolved to a driver primitive, false if the + * feature didn't resolve or isn't a scalar feature + */ + virtual bool GetKey(const FeatureName& feature, CDriverPrimitive& primitive) = 0; + + /*! + * \brief Add or update a key + * + * \param feature Must be a key + * \param primitive The feature's driver primitive + * + * \return True if the feature was updated, false if the feature is + * unchanged or failure occurs + */ + virtual void AddKey(const FeatureName& feature, const CDriverPrimitive& primitive) = 0; + + /*! + * \brief Set a list of driver primitives to be ignored + * + * This is necessary to prevent features from interfering with the button + * mapping process. This includes accelerometers, as well as PS4 triggers + * which send both a button press and an analog value. + * + * \param primitives The driver primitives to be ignored + */ + virtual void SetIgnoredPrimitives(const std::vector<CDriverPrimitive>& primitives) = 0; + + /*! + * \brief Check if a primitive is in the list of primitives to be ignored + * + * \param primitive The primitive to check + * + * \return True if the primitive should be ignored in the mapping process + */ + virtual bool IsIgnored(const CDriverPrimitive& primitive) = 0; + + /*! + * \brief Get the properties of an axis + * + * \param axisIndex The index of the axis to check + * \param center[out] The center, if known + * \param range[out] The range, if known + * + * \return True if the properties are known, false otherwise + */ + virtual bool GetAxisProperties(unsigned int axisIndex, int& center, unsigned int& range) = 0; + + /*! + * \brief Save the button map + */ + virtual void SaveButtonMap() = 0; + + /*! + * \brief Revert changes to the button map since the last time it was loaded + * or committed to disk + */ + virtual void RevertButtonMap() = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IButtonMapCallback.h b/xbmc/input/joysticks/interfaces/IButtonMapCallback.h new file mode 100644 index 0000000..40744f8 --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IButtonMapCallback.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \brief Interface for handling button maps + */ +class IButtonMapCallback +{ +public: + virtual ~IButtonMapCallback() = default; + + /*! + * \brief Save the button map + */ + virtual void SaveButtonMap() = 0; + + /*! + * \brief Clear the list of ignored driver primitives + * + * Called if the user begins capturing primitives to be ignored, and + * no primitives are captured before the dialog is accepted by the user. + * + * In this case, the button mapper won't have been given access to the + * button map, so a callback is needed to indicate that no primitives were + * captured and the user accepted this. + */ + virtual void ResetIgnoredPrimitives() = 0; + + /*! + * \brief Revert changes to the button map since the last time it was loaded + * or committed to disk + */ + virtual void RevertButtonMap() = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IButtonMapper.h b/xbmc/input/joysticks/interfaces/IButtonMapper.h new file mode 100644 index 0000000..d0784df --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IButtonMapper.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/JoystickTypes.h" + +#include <map> +#include <string> + +class IKeymap; + +namespace KODI +{ +namespace JOYSTICK +{ +class CDriverPrimitive; +class IButtonMap; +class IButtonMapCallback; + +/*! + * \ingroup joystick + * \brief Button mapper interface to assign the driver's raw button/hat/axis + * elements to physical joystick features using a provided button map. + * + * \sa IButtonMap + */ +class IButtonMapper +{ +public: + IButtonMapper() = default; + + virtual ~IButtonMapper() = default; + + /*! + * \brief The add-on ID of the game controller associated with this button mapper + * + * \return The ID of the add-on extending kodi.game.controller + */ + virtual std::string ControllerID(void) const = 0; + + /*! + * \brief Return true if the button mapper wants a cooldown between button + * mapping commands + * + * \return True to only send button mapping commands that occur after a small + * timeout from the previous command. + */ + virtual bool NeedsCooldown(void) const = 0; + + /*! + * \brief Return true if the button mapper accepts primitives of the given type + * + * \param type The primitive type + * + * \return True if the button mapper can map the primitive type, false otherwise + */ + virtual bool AcceptsPrimitive(PRIMITIVE_TYPE type) const = 0; + + /*! + * \brief Handle button/hat press or axis threshold + * + * \param buttonMap The button map being manipulated + * \param keymap An interface capable of translating features to Kodi actions + * \param primitive The driver primitive + * + * Called in the same thread as \ref IButtonMapper::OnFrame. + * + * \return True if driver primitive was mapped to a feature + */ + virtual bool MapPrimitive(IButtonMap* buttonMap, + IKeymap* keyMap, + const CDriverPrimitive& primitive) = 0; + + /*! + * \brief Called once per event frame to notify the implementation of motion status + * + * \param buttonMap The button map passed to MapPrimitive() (shall not be modified) + * \param bMotion True if a previously-mapped axis is still in motion + * + * This allows the implementer to wait for an axis to be centered before + * allowing it to be used as Kodi input. + * + * If mapping finishes on an axis, then the axis will still be pressed and + * sending input every frame when the mapping ends. For example, when the + * right analog stick is the last feature to be mapped, it is still pressed + * when mapping ends and immediately sends Volume Down actions. + * + * The fix is to allow implementers to wait until all axes are motionless + * before detaching themselves. + * + * Called in the same thread as \ref IButtonMapper::MapPrimitive. + */ + virtual void OnEventFrame(const IButtonMap* buttonMap, bool bMotion) = 0; + + /*! + * \brief Called when an axis has been detected after mapping began + * + * \param axisIndex The index of the axis being discovered + * + * Some joystick drivers don't report an initial value for analog axes. + * + * Called in the same thread as \ref IButtonMapper::MapPrimitive. + */ + virtual void OnLateAxis(const IButtonMap* buttonMap, unsigned int axisIndex) = 0; + + // Button map callback interface + void SetButtonMapCallback(const std::string& deviceLocation, IButtonMapCallback* callback) + { + m_callbacks[deviceLocation] = callback; + } + void ResetButtonMapCallbacks(void) { m_callbacks.clear(); } + std::map<std::string, IButtonMapCallback*>& ButtonMapCallbacks(void) { return m_callbacks; } + +private: + std::map<std::string, IButtonMapCallback*> m_callbacks; // Device location -> callback +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IButtonSequence.h b/xbmc/input/joysticks/interfaces/IButtonSequence.h new file mode 100644 index 0000000..5c642e6 --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IButtonSequence.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/JoystickTypes.h" + +namespace KODI +{ +namespace JOYSTICK +{ +class IButtonSequence +{ +public: + virtual ~IButtonSequence() = default; + + virtual bool OnButtonPress(const FeatureName& feature) = 0; + + /*! + * \brief Returns true if a sequence is being captured to prevent input + * from falling through to the application + */ + virtual bool IsCapturing() = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IDriverHandler.h b/xbmc/input/joysticks/interfaces/IDriverHandler.h new file mode 100644 index 0000000..48e4da5 --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IDriverHandler.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/JoystickTypes.h" + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \brief Interface defining methods to handle joystick events for raw driver + * elements (buttons, hats, axes) + */ +class IDriverHandler +{ +public: + virtual ~IDriverHandler() = default; + + /*! + * \brief Handle button motion + * + * \param buttonIndex The index of the button as reported by the driver + * \param bPressed true for press motion, false for release motion + * + * \return True if a press was handled, false otherwise + */ + virtual bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) = 0; + + /*! + * \brief Handle hat motion + * + * \param hatIndex The index of the hat as reported by the driver + * \param state The direction the hat is now being pressed + * + * \return True if the new direction was handled, false otherwise + */ + virtual bool OnHatMotion(unsigned int hatIndex, HAT_STATE state) = 0; + + /*! + * \brief Handle axis motion + * + * If a joystick feature requires multiple axes (analog sticks, accelerometers), + * they can be buffered for later processing. + * + * \param axisIndex The index of the axis as reported by the driver + * \param position The position of the axis in the closed interval [-1.0, 1.0] + * \param center The center point of the axis (either -1, 0 or 1) + * \param range The maximum distance the axis can move (either 1 or 2) + * + * \return True if the motion was handled, false otherwise + */ + virtual bool OnAxisMotion(unsigned int axisIndex, + float position, + int center, + unsigned int range) = 0; + + /*! + * \brief Handle buffered input motion for features that require multiple axes + * + * OnInputFrame() is called at the end of the frame when all axis motions + * have been reported. This has several uses, including: + * + * - Combining multiple axes into a single analog stick or accelerometer event + * - Imitating an analog feature with a digital button so that events can be + * dispatched every frame. + */ + virtual void OnInputFrame(void) = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IDriverReceiver.h b/xbmc/input/joysticks/interfaces/IDriverReceiver.h new file mode 100644 index 0000000..2a4ff6c --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IDriverReceiver.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \ingroup joystick + * \brief Interface for sending input events to joystick drivers + */ +class IDriverReceiver +{ +public: + virtual ~IDriverReceiver() = default; + + /*! + * \brief Set the value of a rumble motor + * + * \param motorIndex The driver index of the motor to rumble + * \param magnitude The motor's new magnitude of vibration in the closed interval [0, 1] + * + * \return True if the event was handled otherwise false + */ + virtual bool SetMotorState(unsigned int motorIndex, float magnitude) = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IInputHandler.h b/xbmc/input/joysticks/interfaces/IInputHandler.h new file mode 100644 index 0000000..2ddbf5c --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IInputHandler.h @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/JoystickTypes.h" + +#include <string> + +namespace KODI +{ +namespace JOYSTICK +{ +class IInputReceiver; + +/*! + * \ingroup joystick + * \brief Interface for handling input events for game controllers + */ +class IInputHandler +{ +public: + virtual ~IInputHandler() = default; + + /*! + * \brief The add-on ID of the game controller associated with this input handler + * + * \return The ID of the add-on extending kodi.game.controller + */ + virtual std::string ControllerID(void) const = 0; + + /*! + * \brief Return true if the input handler accepts the given feature + * + * \param feature A feature belonging to the controller specified by ControllerID() + * + * \return True if the feature is used for input, false otherwise + */ + virtual bool HasFeature(const FeatureName& feature) const = 0; + + /*! + * \brief Return true if the input handler is currently accepting input for the + * given feature + * + * \param feature A feature belonging to the controller specified by ControllerID() + * + * \return True if the feature is currently accepting input, false otherwise + * + * This does not prevent the input events from being called, but can return + * false to indicate that input wasn't handled for the specified feature. + */ + virtual bool AcceptsInput(const FeatureName& feature) const = 0; + + /*! + * \brief A digital button has been pressed or released + * + * \param feature The feature being pressed + * \param bPressed True if pressed, false if released + * + * \return True if the event was handled otherwise false + */ + virtual bool OnButtonPress(const FeatureName& feature, bool bPressed) = 0; + + /*! + * \brief A digital button has been pressed for more than one event frame + * + * \param feature The feature being held + * \param holdTimeMs The time elapsed since the initial press (ms) + * + * If OnButtonPress() returns true for the initial press, then this callback + * is invoked on subsequent frames until the button is released. + */ + virtual void OnButtonHold(const FeatureName& feature, unsigned int holdTimeMs) = 0; + + /*! + * \brief An analog button (trigger or a pressure-sensitive button) has changed state + * + * \param feature The feature changing state + * \param magnitude The button pressure or trigger travel distance in the + * closed interval [0, 1] + * \param motionTimeMs The time elapsed since the magnitude was 0 + * + * \return True if the event was handled otherwise false + */ + virtual bool OnButtonMotion(const FeatureName& feature, + float magnitude, + unsigned int motionTimeMs) = 0; + + /*! + * \brief An analog stick has moved + * + * \param feature The analog stick being moved + * \param x The x coordinate in the closed interval [-1, 1] + * \param y The y coordinate in the closed interval [-1, 1] + * \param motionTimeMs The time elapsed since this analog stick was centered, + * or 0 if the analog stick is centered + * + * \return True if the event was handled otherwise false + */ + virtual bool OnAnalogStickMotion(const FeatureName& feature, + float x, + float y, + unsigned int motionTimeMs) = 0; + + /*! + * \brief An accelerometer's state has changed + * + * \param feature The accelerometer being accelerated + * \param x The x coordinate in the closed interval [-1, 1] + * \param y The y coordinate in the closed interval [-1, 1] + * \param z The z coordinate in the closed interval [-1, 1] + * + * \return True if the event was handled otherwise false + */ + virtual bool OnAccelerometerMotion(const FeatureName& feature, float x, float y, float z) + { + return false; + } + + /*! + * \brief A wheel has changed state + * + * Left is negative position, right is positive position + * + * \param feature The wheel changing state + * \param position The position in the closed interval [-1, 1] + * \param motionTimeMs The time elapsed since the position was 0 + * + * \return True if the event was handled otherwise false + */ + virtual bool OnWheelMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) = 0; + + /*! + * \brief A throttle has changed state + * + * Up is positive position, down is negative position. + * + * \param feature The wheel changing state + * \param position The position in the closed interval [-1, 1] + * \param motionTimeMs The time elapsed since the position was 0 + * + * \return True if the event was handled otherwise false + */ + virtual bool OnThrottleMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) = 0; + + /*! + * \brief Called at the end of the frame that provided input + */ + virtual void OnInputFrame() = 0; + + // Input receiver interface + void SetInputReceiver(IInputReceiver* receiver) { m_receiver = receiver; } + void ResetInputReceiver(void) { m_receiver = nullptr; } + IInputReceiver* InputReceiver(void) { return m_receiver; } + +private: + IInputReceiver* m_receiver = nullptr; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IInputProvider.h b/xbmc/input/joysticks/interfaces/IInputProvider.h new file mode 100644 index 0000000..09a0366 --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IInputProvider.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +namespace KODI +{ +namespace JOYSTICK +{ +class IInputHandler; + +/*! + * \ingroup joystick + * \brief Interface for classes that can provide input + */ +class IInputProvider +{ +public: + virtual ~IInputProvider() = default; + + /*! + * \brief Register a handler for the provided input + * + * \param handler The handler to receive input provided by this class + * \param bPromiscuous If true, receives all input (including handled input) + * in the background + */ + virtual void RegisterInputHandler(IInputHandler* handler, bool bPromiscuous) = 0; + + /*! + * \brief Unregister a handler + * + * \param handler The handler that was receiving input + */ + virtual void UnregisterInputHandler(IInputHandler* handler) = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IInputReceiver.h b/xbmc/input/joysticks/interfaces/IInputReceiver.h new file mode 100644 index 0000000..400da3f --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IInputReceiver.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/JoystickTypes.h" + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \ingroup joystick + * \brief Interface for sending input events to game controllers + */ +class IInputReceiver +{ +public: + virtual ~IInputReceiver() = default; + + /*! + * \brief Set the value of a rumble motor + * + * \param feature The name of the motor to rumble + * \param magnitude The motor's new magnitude of vibration in the closed interval [0, 1] + * + * \return True if the event was handled otherwise false + */ + virtual bool SetRumbleState(const FeatureName& feature, float magnitude) = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IKeyHandler.h b/xbmc/input/joysticks/interfaces/IKeyHandler.h new file mode 100644 index 0000000..30f49c5 --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IKeyHandler.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \ingroup joystick + * \brief Interface for handling keymap keys + * + * Keys can be mapped to analog actions (e.g. "AnalogSeekForward") or digital + * actions (e.g. "Up"). + */ +class IKeyHandler +{ +public: + virtual ~IKeyHandler() = default; + + /*! + * \brief Return true if the key is "pressed" (has a magnitude greater + * than 0.5) + * + * \return True if the key is "pressed", false otherwise + */ + virtual bool IsPressed() const = 0; + + /*! + * \brief A key mapped to a digital feature has been pressed or released + * + * \param bPressed true if the key's button/axis is activated, false if deactivated + * \param holdTimeMs The held time in ms for pressed buttons, or 0 for released + * + * \return True if the key is mapped to an action, false otherwise + */ + virtual bool OnDigitalMotion(bool bPressed, unsigned int holdTimeMs) = 0; + + /*! + * \brief Callback for keys mapped to analog features + * + * \param magnitude The amount of the analog action + * \param motionTimeMs The time since the magnitude was 0 + * + * \return True if the key is mapped to an action, false otherwise + */ + virtual bool OnAnalogMotion(float magnitude, unsigned int motionTimeMs) = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/interfaces/IKeymapHandler.h b/xbmc/input/joysticks/interfaces/IKeymapHandler.h new file mode 100644 index 0000000..f0af427 --- /dev/null +++ b/xbmc/input/joysticks/interfaces/IKeymapHandler.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <set> +#include <string> + +namespace KODI +{ +namespace JOYSTICK +{ +/*! + * \ingroup joystick + * \brief Interface for a class working with a keymap + */ +class IKeymapHandler +{ +public: + virtual ~IKeymapHandler() = default; + + /*! + * \brief Get the pressed state of the given keys + * + * \param keyNames The key names + * + * \return True if all keys are pressed or no keys are given, false otherwise + */ + virtual bool HotkeysPressed(const std::set<std::string>& keyNames) const = 0; + + /*! + * \brief Get the key name of the last button pressed + * + * \return The key name of the last-pressed button, or empty if no button + * is pressed + */ + virtual std::string GetLastPressed() const = 0; + + /*! + * \brief Called when a key has emitted an action after bring pressed + * + * \param keyName the key name that emitted the action + */ + virtual void OnPress(const std::string& keyName) = 0; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/keymaps/CMakeLists.txt b/xbmc/input/joysticks/keymaps/CMakeLists.txt new file mode 100644 index 0000000..c854b36 --- /dev/null +++ b/xbmc/input/joysticks/keymaps/CMakeLists.txt @@ -0,0 +1,11 @@ +set(SOURCES KeyHandler.cpp + KeymapHandler.cpp + KeymapHandling.cpp +) + +set(HEADERS KeyHandler.h + KeymapHandler.h + KeymapHandling.h +) + +core_add_library(input_joystick_keymaps) diff --git a/xbmc/input/joysticks/keymaps/KeyHandler.cpp b/xbmc/input/joysticks/keymaps/KeyHandler.cpp new file mode 100644 index 0000000..cd0f30a --- /dev/null +++ b/xbmc/input/joysticks/keymaps/KeyHandler.cpp @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "KeyHandler.h" + +#include "input/IKeymap.h" +#include "input/actions/ActionIDs.h" +#include "input/actions/ActionTranslator.h" +#include "input/joysticks/JoystickUtils.h" +#include "input/joysticks/interfaces/IKeymapHandler.h" +#include "interfaces/IActionListener.h" + +#include <algorithm> +#include <assert.h> + +using namespace KODI; +using namespace JOYSTICK; + +#define DIGITAL_ANALOG_THRESHOLD 0.5f + +#define HOLD_TIMEOUT_MS 500 +#define REPEAT_TIMEOUT_MS 50 + +CKeyHandler::CKeyHandler(const std::string& keyName, + IActionListener* actionHandler, + const IKeymap* keymap, + IKeymapHandler* keymapHandler) + : m_keyName(keyName), + m_actionHandler(actionHandler), + m_keymap(keymap), + m_keymapHandler(keymapHandler) +{ + assert(m_actionHandler != nullptr); + assert(m_keymap != nullptr); + assert(m_keymapHandler != nullptr); + + Reset(); +} + +void CKeyHandler::Reset() +{ + m_bHeld = false; + m_magnitude = 0.0f; + m_holdStartTimeMs = 0; + m_lastHoldTimeMs = 0; + m_bActionSent = false; + m_lastActionMs = 0; + m_activeWindowId = -1; + m_lastAction = CAction(); +} + +bool CKeyHandler::OnDigitalMotion(bool bPressed, unsigned int holdTimeMs) +{ + return OnAnalogMotion(bPressed ? 1.0f : 0.0f, holdTimeMs); +} + +bool CKeyHandler::OnAnalogMotion(float magnitude, unsigned int motionTimeMs) +{ + // Don't send deactivation event more than once + if (m_magnitude == 0.0f && magnitude == 0.0f) + return false; + + // Get actions for the key + const auto& actionGroup = m_keymap->GetActions(m_keyName); + const int windowId = actionGroup.windowId; + const auto& actions = actionGroup.actions; + + // Calculate press state + const bool bPressed = IsPressed(magnitude); + const bool bJustPressed = bPressed && !m_bHeld; + + if (bJustPressed) + { + // Reset key if just pressed + Reset(); + + // Record hold start time if just pressed + m_holdStartTimeMs = motionTimeMs; + + // Record window ID + if (windowId >= 0) + m_activeWindowId = windowId; + } + + // Calculate holdtime relative to when magnitude crossed the threshold + unsigned int holdTimeMs = 0; + if (bPressed) + holdTimeMs = motionTimeMs - m_holdStartTimeMs; + + // Give priority to actions with hotkeys + std::vector<const KeymapAction*> actionsWithHotkeys; + + for (const auto& action : actions) + { + if (!action.hotkeys.empty()) + actionsWithHotkeys.emplace_back(&action); + } + + CAction dispatchAction = + ProcessActions(std::move(actionsWithHotkeys), windowId, magnitude, holdTimeMs); + + // If that failed, try again with all actions + if (dispatchAction.GetID() == ACTION_NONE) + { + std::vector<const KeymapAction*> allActions; + + allActions.reserve(actions.size()); + for (const auto& action : actions) + allActions.emplace_back(&action); + + dispatchAction = ProcessActions(std::move(allActions), windowId, magnitude, holdTimeMs); + } + + // If specific action was sent last frame but not this one, send a release event + if (dispatchAction.GetID() != m_lastAction.GetID()) + { + if (CActionTranslator::IsAnalog(m_lastAction.GetID()) && m_lastAction.GetAmount() > 0.0f) + { + m_lastAction.ClearAmount(); + m_actionHandler->OnAction(m_lastAction); + } + } + + // Dispatch action + bool bHandled = false; + if (dispatchAction.GetID() != ACTION_NONE) + { + m_actionHandler->OnAction(dispatchAction); + bHandled = true; + } + + m_bHeld = bPressed; + m_magnitude = magnitude; + m_lastHoldTimeMs = holdTimeMs; + m_lastAction = dispatchAction; + + return bHandled; +} + +CAction CKeyHandler::ProcessActions(std::vector<const KeymapAction*> actions, + int windowId, + float magnitude, + unsigned int holdTimeMs) +{ + CAction dispatchAction; + + // Filter out actions without pressed hotkeys + actions.erase(std::remove_if(actions.begin(), actions.end(), + [this](const KeymapAction* action) { + return !m_keymapHandler->HotkeysPressed(action->hotkeys); + }), + actions.end()); + + if (actions.empty()) + return false; + + // Actions are sorted by holdtime, so the final action is the one with the + // greatest holdtime + const KeymapAction& finalAction = **actions.rbegin(); + const unsigned int maxHoldTimeMs = finalAction.holdTimeMs; + + const bool bHasDelay = (maxHoldTimeMs > 0); + if (!bHasDelay) + { + dispatchAction = ProcessAction(finalAction, windowId, magnitude, holdTimeMs); + } + else + { + // If holdtime has exceeded the last action, execute it now + if (holdTimeMs >= finalAction.holdTimeMs) + { + // Force holdtime to zero for the initial press + if (!m_bActionSent) + holdTimeMs = 0; + else + holdTimeMs -= finalAction.holdTimeMs; + + dispatchAction = ProcessAction(finalAction, windowId, magnitude, holdTimeMs); + } + else + { + // Calculate press state + const bool bPressed = IsPressed(magnitude); + const bool bJustReleased = m_bHeld && !bPressed; + + // If button was just released, send a release action + if (bJustReleased) + dispatchAction = ProcessRelease(actions, windowId); + } + } + + return dispatchAction; +} + +CAction CKeyHandler::ProcessRelease(std::vector<const KeymapAction*> actions, int windowId) +{ + CAction dispatchAction; + + // Use previous holdtime from before button release + const unsigned int holdTimeMs = m_lastHoldTimeMs; + + // Send an action on release if one occurs before the holdtime + for (auto it = actions.begin(); it != actions.end();) + { + const KeymapAction& action = **it; + + unsigned int thisHoldTime = (*it)->holdTimeMs; + + ++it; + if (it == actions.end()) + break; + + unsigned int nextHoldTime = (*it)->holdTimeMs; + + if (thisHoldTime <= holdTimeMs && holdTimeMs < nextHoldTime) + { + dispatchAction = ProcessAction(action, windowId, 1.0f, 0); + break; + } + } + + return dispatchAction; +} + +CAction CKeyHandler::ProcessAction(const KeymapAction& action, + int windowId, + float magnitude, + unsigned int holdTimeMs) +{ + CAction dispatchAction; + + bool bSendAction = false; + + if (windowId != m_activeWindowId) + { + // Don't send actions if the window has changed since being pressed + } + else if (CActionTranslator::IsAnalog(action.actionId)) + { + bSendAction = true; + } + else if (IsPressed(magnitude)) + { + // Dispatch action if button was pressed this frame + if (holdTimeMs == 0) + bSendAction = true; + else + bSendAction = SendRepeatAction(holdTimeMs); + } + + if (bSendAction) + { + const CAction guiAction(action.actionId, magnitude, 0.0f, action.actionString, holdTimeMs); + m_keymapHandler->OnPress(m_keyName); + m_bActionSent = true; + m_lastActionMs = holdTimeMs; + dispatchAction = guiAction; + } + + return dispatchAction; +} + +bool CKeyHandler::SendRepeatAction(unsigned int holdTimeMs) +{ + bool bSendRepeat = true; + + // Don't send a repeat action if the last key has changed + if (m_keymapHandler->GetLastPressed() != m_keyName) + bSendRepeat = false; + + // Ensure initial timeout has elapsed + else if (holdTimeMs < HOLD_TIMEOUT_MS) + bSendRepeat = false; + + // Ensure repeat timeout has elapsed + else if (holdTimeMs < m_lastActionMs + REPEAT_TIMEOUT_MS) + bSendRepeat = false; + + return bSendRepeat; +} + +bool CKeyHandler::IsPressed(float magnitude) +{ + return magnitude >= DIGITAL_ANALOG_THRESHOLD; +} diff --git a/xbmc/input/joysticks/keymaps/KeyHandler.h b/xbmc/input/joysticks/keymaps/KeyHandler.h new file mode 100644 index 0000000..3b5a46c --- /dev/null +++ b/xbmc/input/joysticks/keymaps/KeyHandler.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/actions/Action.h" +#include "input/joysticks/JoystickTypes.h" +#include "input/joysticks/interfaces/IKeyHandler.h" + +#include <map> +#include <string> +#include <vector> + +class CAction; +class IActionListener; +class IKeymap; + +namespace KODI +{ +namespace JOYSTICK +{ +class IKeymapHandler; + +/*! + * \ingroup joystick + * \brief + */ +class CKeyHandler : public IKeyHandler +{ +public: + CKeyHandler(const std::string& keyName, + IActionListener* actionHandler, + const IKeymap* keymap, + IKeymapHandler* keymapHandler); + + ~CKeyHandler() override = default; + + // implementation of IKeyHandler + bool IsPressed() const override { return m_bHeld; } + bool OnDigitalMotion(bool bPressed, unsigned int holdTimeMs) override; + bool OnAnalogMotion(float magnitude, unsigned int motionTimeMs) override; + +private: + void Reset(); + + /*! + * \brief Process actions to see if an action should be dispatched + * + * \param actions All actions from the keymap defined for the current window + * \param windowId The current window ID + * \param magnitude The magnitude or distance of the feature being handled + * \param holdTimeMs The time which the feature has been past the hold threshold + * + * \return The action to dispatch, or action with ID ACTION_NONE if no action should be dispatched + */ + CAction ProcessActions(std::vector<const KeymapAction*> actions, + int windowId, + float magnitude, + unsigned int holdTimeMs); + + /*! + * \brief Process actions after release event to see if an action should be dispatched + * + * \param actions All actions from the keymap defined for the current window + * \param windowId The current window ID + * + * \return The action to dispatch, or action with ID ACTION_NONE if no action should be dispatched + */ + CAction ProcessRelease(std::vector<const KeymapAction*> actions, int windowId); + + /*! + * \brief Process an action to see if it should be dispatched + * + * \param action The action chosen to be dispatched + * \param windowId The current window ID + * \param magnitude The magnitude or distance of the feature being handled + * \param holdTimeMs The time which the feature has been past the hold threshold + * + * \return The action to dispatch, or action with ID ACTION_NONE if no action should be dispatched + */ + CAction ProcessAction(const KeymapAction& action, + int windowId, + float magnitude, + unsigned int holdTimeMs); + + // Check criteria for sending a repeat action + bool SendRepeatAction(unsigned int holdTimeMs); + + // Helper function + static bool IsPressed(float magnitude); + + // Construction parameters + const std::string m_keyName; + IActionListener* const m_actionHandler; + const IKeymap* const m_keymap; + IKeymapHandler* const m_keymapHandler; + + // State variables + bool m_bHeld; + float m_magnitude; + unsigned int m_holdStartTimeMs; + unsigned int m_lastHoldTimeMs; + bool m_bActionSent; + unsigned int m_lastActionMs; + int m_activeWindowId = -1; // Window that activated the key + CAction m_lastAction; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/keymaps/KeymapHandler.cpp b/xbmc/input/joysticks/keymaps/KeymapHandler.cpp new file mode 100644 index 0000000..24e2a0b --- /dev/null +++ b/xbmc/input/joysticks/keymaps/KeymapHandler.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "KeymapHandler.h" + +#include "KeyHandler.h" +#include "games/controllers/Controller.h" +#include "input/IKeymap.h" +#include "input/IKeymapEnvironment.h" +#include "input/InputTranslator.h" +#include "input/joysticks/JoystickEasterEgg.h" +#include "input/joysticks/JoystickTranslator.h" +#include "input/joysticks/JoystickUtils.h" +#include "input/joysticks/interfaces/IKeyHandler.h" + +#include <algorithm> +#include <assert.h> +#include <cmath> +#include <utility> + +using namespace KODI; +using namespace JOYSTICK; + +CKeymapHandler::CKeymapHandler(IActionListener* actionHandler, const IKeymap* keymap) + : m_actionHandler(actionHandler), m_keymap(keymap) +{ + assert(m_actionHandler != nullptr); + assert(m_keymap != nullptr); + + if (m_keymap->Environment()->UseEasterEgg()) + m_easterEgg.reset(new CJoystickEasterEgg(ControllerID())); +} + +bool CKeymapHandler::HotkeysPressed(const std::set<std::string>& keyNames) const +{ + bool bHotkeysPressed = true; + + for (const auto& hotkey : keyNames) + { + auto it = m_keyHandlers.find(hotkey); + if (it == m_keyHandlers.end() || !it->second->IsPressed()) + { + bHotkeysPressed = false; + break; + } + } + + return bHotkeysPressed; +} + +std::string CKeymapHandler::ControllerID() const +{ + return m_keymap->ControllerID(); +} + +bool CKeymapHandler::AcceptsInput(const FeatureName& feature) const +{ + if (HasAction(CJoystickUtils::MakeKeyName(feature))) + return true; + + for (auto dir : CJoystickUtils::GetAnalogStickDirections()) + { + if (HasAction(CJoystickUtils::MakeKeyName(feature, dir))) + return true; + } + + return false; +} + +bool CKeymapHandler::OnButtonPress(const FeatureName& feature, bool bPressed) +{ + if (bPressed && m_easterEgg && m_easterEgg->OnButtonPress(feature)) + return true; + + const std::string keyName = CJoystickUtils::MakeKeyName(feature); + + IKeyHandler* handler = GetKeyHandler(keyName); + return handler->OnDigitalMotion(bPressed, 0); +} + +void CKeymapHandler::OnButtonHold(const FeatureName& feature, unsigned int holdTimeMs) +{ + if (m_easterEgg && m_easterEgg->IsCapturing()) + return; + + const std::string keyName = CJoystickUtils::MakeKeyName(feature); + + IKeyHandler* handler = GetKeyHandler(keyName); + handler->OnDigitalMotion(true, holdTimeMs); +} + +bool CKeymapHandler::OnButtonMotion(const FeatureName& feature, + float magnitude, + unsigned int motionTimeMs) +{ + const std::string keyName = CJoystickUtils::MakeKeyName(feature); + + IKeyHandler* handler = GetKeyHandler(keyName); + return handler->OnAnalogMotion(magnitude, motionTimeMs); +} + +bool CKeymapHandler::OnAnalogStickMotion(const FeatureName& feature, + float x, + float y, + unsigned int motionTimeMs) +{ + using namespace INPUT; + + bool bHandled = false; + + // Calculate the direction of the stick's position + const ANALOG_STICK_DIRECTION analogStickDir = CInputTranslator::VectorToCardinalDirection(x, y); + + // Calculate the magnitude projected onto that direction + const float magnitude = std::max(std::fabs(x), std::fabs(y)); + + // Deactivate directions in which the stick is not pointing first + for (auto dir : CJoystickUtils::GetAnalogStickDirections()) + { + if (dir != analogStickDir) + DeactivateDirection(feature, dir); + } + + // Now activate direction the analog stick is pointing + if (analogStickDir != ANALOG_STICK_DIRECTION::NONE) + bHandled = ActivateDirection(feature, magnitude, analogStickDir, motionTimeMs); + + return bHandled; +} + +bool CKeymapHandler::OnWheelMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) +{ + bool bHandled = false; + + // Calculate the direction of the wheel's position + const WHEEL_DIRECTION direction = CJoystickTranslator::PositionToWheelDirection(position); + + // Calculate the magnitude projected onto that direction + const float magnitude = std::fabs(position); + + // Deactivate directions in which the wheel is not pointing first + for (auto dir : CJoystickUtils::GetWheelDirections()) + { + if (dir != direction) + DeactivateDirection(feature, dir); + } + + // Now activate direction in which the wheel is positioned + if (direction != WHEEL_DIRECTION::NONE) + bHandled = ActivateDirection(feature, magnitude, direction, motionTimeMs); + + return bHandled; +} + +bool CKeymapHandler::OnThrottleMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) +{ + bool bHandled = false; + + // Calculate the direction of the throttle's position + const THROTTLE_DIRECTION direction = CJoystickTranslator::PositionToThrottleDirection(position); + + // Calculate the magnitude projected onto that direction + const float magnitude = std::fabs(position); + + // Deactivate directions in which the throttle is not pointing first + for (auto dir : CJoystickUtils::GetThrottleDirections()) + { + if (dir != direction) + DeactivateDirection(feature, dir); + } + + // Now activate direction in which the throttle is positioned + if (direction != THROTTLE_DIRECTION::NONE) + bHandled = ActivateDirection(feature, magnitude, direction, motionTimeMs); + + return bHandled; +} + +bool CKeymapHandler::OnAccelerometerMotion(const FeatureName& feature, float x, float y, float z) +{ + return false; //! @todo implement +} + +bool CKeymapHandler::ActivateDirection(const FeatureName& feature, + float magnitude, + ANALOG_STICK_DIRECTION dir, + unsigned int motionTimeMs) +{ + const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir); + + IKeyHandler* handler = GetKeyHandler(keyName); + return handler->OnAnalogMotion(magnitude, motionTimeMs); +} + +void CKeymapHandler::DeactivateDirection(const FeatureName& feature, ANALOG_STICK_DIRECTION dir) +{ + const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir); + + IKeyHandler* handler = GetKeyHandler(keyName); + handler->OnAnalogMotion(0.0f, 0); +} + +bool CKeymapHandler::ActivateDirection(const FeatureName& feature, + float magnitude, + WHEEL_DIRECTION dir, + unsigned int motionTimeMs) +{ + const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir); + + IKeyHandler* handler = GetKeyHandler(keyName); + return handler->OnAnalogMotion(magnitude, motionTimeMs); +} + +void CKeymapHandler::DeactivateDirection(const FeatureName& feature, WHEEL_DIRECTION dir) +{ + const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir); + + IKeyHandler* handler = GetKeyHandler(keyName); + handler->OnAnalogMotion(0.0f, 0); +} + +bool CKeymapHandler::ActivateDirection(const FeatureName& feature, + float magnitude, + THROTTLE_DIRECTION dir, + unsigned int motionTimeMs) +{ + const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir); + + IKeyHandler* handler = GetKeyHandler(keyName); + return handler->OnAnalogMotion(magnitude, motionTimeMs); +} + +void CKeymapHandler::DeactivateDirection(const FeatureName& feature, THROTTLE_DIRECTION dir) +{ + const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir); + + IKeyHandler* handler = GetKeyHandler(keyName); + handler->OnAnalogMotion(0.0f, 0); +} + +IKeyHandler* CKeymapHandler::GetKeyHandler(const std::string& keyName) +{ + auto it = m_keyHandlers.find(keyName); + if (it == m_keyHandlers.end()) + { + std::unique_ptr<IKeyHandler> handler(new CKeyHandler(keyName, m_actionHandler, m_keymap, this)); + m_keyHandlers.insert(std::make_pair(keyName, std::move(handler))); + it = m_keyHandlers.find(keyName); + } + + return it->second.get(); +} + +bool CKeymapHandler::HasAction(const std::string& keyName) const +{ + bool bHasAction = false; + + const auto& actions = m_keymap->GetActions(keyName).actions; + for (const auto& action : actions) + { + if (HotkeysPressed(action.hotkeys)) + { + bHasAction = true; + break; + } + } + + return bHasAction; +} diff --git a/xbmc/input/joysticks/keymaps/KeymapHandler.h b/xbmc/input/joysticks/keymaps/KeymapHandler.h new file mode 100644 index 0000000..a2b1d4f --- /dev/null +++ b/xbmc/input/joysticks/keymaps/KeymapHandler.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/joysticks/JoystickTypes.h" +#include "input/joysticks/interfaces/IButtonSequence.h" +#include "input/joysticks/interfaces/IInputHandler.h" +#include "input/joysticks/interfaces/IKeymapHandler.h" + +#include <map> +#include <memory> +#include <string> + +class IActionListener; +class IKeymap; + +namespace KODI +{ +namespace JOYSTICK +{ +class IKeyHandler; + +/*! + * \ingroup joystick + * \brief + */ +class CKeymapHandler : public IKeymapHandler, public IInputHandler +{ +public: + CKeymapHandler(IActionListener* actionHandler, const IKeymap* keymap); + + ~CKeymapHandler() override = default; + + // implementation of IKeymapHandler + bool HotkeysPressed(const std::set<std::string>& keyNames) const override; + std::string GetLastPressed() const override { return m_lastPressed; } + void OnPress(const std::string& keyName) override { m_lastPressed = keyName; } + + // implementation of IInputHandler + std::string ControllerID() const override; + bool HasFeature(const FeatureName& feature) const override { return true; } + bool AcceptsInput(const FeatureName& feature) const override; + bool OnButtonPress(const FeatureName& feature, bool bPressed) override; + void OnButtonHold(const FeatureName& feature, unsigned int holdTimeMs) override; + bool OnButtonMotion(const FeatureName& feature, + float magnitude, + unsigned int motionTimeMs) override; + bool OnAnalogStickMotion(const FeatureName& feature, + float x, + float y, + unsigned int motionTimeMs) override; + bool OnAccelerometerMotion(const FeatureName& feature, float x, float y, float z) override; + bool OnWheelMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) override; + bool OnThrottleMotion(const FeatureName& feature, + float position, + unsigned int motionTimeMs) override; + void OnInputFrame() override {} + +protected: + // Keep track of cheat code presses + std::unique_ptr<IButtonSequence> m_easterEgg; + +private: + // Analog stick helper functions + bool ActivateDirection(const FeatureName& feature, + float magnitude, + ANALOG_STICK_DIRECTION dir, + unsigned int motionTimeMs); + void DeactivateDirection(const FeatureName& feature, ANALOG_STICK_DIRECTION dir); + + // Wheel helper functions + bool ActivateDirection(const FeatureName& feature, + float magnitude, + WHEEL_DIRECTION dir, + unsigned int motionTimeMs); + void DeactivateDirection(const FeatureName& feature, WHEEL_DIRECTION dir); + + // Throttle helper functions + bool ActivateDirection(const FeatureName& feature, + float magnitude, + THROTTLE_DIRECTION dir, + unsigned int motionTimeMs); + void DeactivateDirection(const FeatureName& feature, THROTTLE_DIRECTION dir); + + // Helper functions + IKeyHandler* GetKeyHandler(const std::string& keyName); + bool HasAction(const std::string& keyName) const; + + // Construction parameters + IActionListener* const m_actionHandler; + const IKeymap* const m_keymap; + + // Handlers for individual keys + std::map<std::string, std::unique_ptr<IKeyHandler>> m_keyHandlers; // Key name -> handler + + // Last pressed key + std::string m_lastPressed; +}; +} // namespace JOYSTICK +} // namespace KODI diff --git a/xbmc/input/joysticks/keymaps/KeymapHandling.cpp b/xbmc/input/joysticks/keymaps/KeymapHandling.cpp new file mode 100644 index 0000000..b87ea68 --- /dev/null +++ b/xbmc/input/joysticks/keymaps/KeymapHandling.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "KeymapHandling.h" + +#include "KeymapHandler.h" +#include "ServiceBroker.h" +#include "input/ButtonTranslator.h" +#include "input/InputManager.h" +#include "input/Keymap.h" +#include "input/joysticks/interfaces/IInputHandler.h" +#include "input/joysticks/interfaces/IInputProvider.h" + +#include <algorithm> +#include <utility> + +using namespace KODI; +using namespace JOYSTICK; + +CKeymapHandling::CKeymapHandling(IInputProvider* inputProvider, + bool pPromiscuous, + const IKeymapEnvironment* environment) + : m_inputProvider(inputProvider), m_pPromiscuous(pPromiscuous), m_environment(environment) +{ + LoadKeymaps(); + CServiceBroker::GetInputManager().RegisterObserver(this); +} + +CKeymapHandling::~CKeymapHandling() +{ + CServiceBroker::GetInputManager().UnregisterObserver(this); + UnloadKeymaps(); +} + +IInputReceiver* CKeymapHandling::GetInputReceiver(const std::string& controllerId) const +{ + auto it = std::find_if(m_inputHandlers.begin(), m_inputHandlers.end(), + [&controllerId](const std::unique_ptr<IInputHandler>& inputHandler) { + return inputHandler->ControllerID() == controllerId; + }); + + if (it != m_inputHandlers.end()) + return (*it)->InputReceiver(); + + return nullptr; +} + +IKeymap* CKeymapHandling::GetKeymap(const std::string& controllerId) const +{ + auto it = std::find_if(m_keymaps.begin(), m_keymaps.end(), + [&controllerId](const std::unique_ptr<IKeymap>& keymap) { + return keymap->ControllerID() == controllerId; + }); + + if (it != m_keymaps.end()) + return it->get(); + + return nullptr; +} + +void CKeymapHandling::Notify(const Observable& obs, const ObservableMessage msg) +{ + if (msg == ObservableMessageButtonMapsChanged) + LoadKeymaps(); +} + +void CKeymapHandling::LoadKeymaps() +{ + UnloadKeymaps(); + + auto& inputManager = CServiceBroker::GetInputManager(); + + for (auto& windowKeymap : inputManager.GetJoystickKeymaps()) + { + // Create keymap + std::unique_ptr<IKeymap> keymap(new CKeymap(std::move(windowKeymap), m_environment)); + + // Create keymap handler + std::unique_ptr<IInputHandler> inputHandler(new CKeymapHandler(&inputManager, keymap.get())); + + // Register the handler with the input provider + m_inputProvider->RegisterInputHandler(inputHandler.get(), m_pPromiscuous); + + // Save the keymap and handler + m_keymaps.emplace_back(std::move(keymap)); + m_inputHandlers.emplace_back(std::move(inputHandler)); + } +} + +void CKeymapHandling::UnloadKeymaps() +{ + if (m_inputProvider != nullptr) + { + for (auto it = m_inputHandlers.rbegin(); it != m_inputHandlers.rend(); ++it) + m_inputProvider->UnregisterInputHandler(it->get()); + } + m_inputHandlers.clear(); + m_keymaps.clear(); +} diff --git a/xbmc/input/joysticks/keymaps/KeymapHandling.h b/xbmc/input/joysticks/keymaps/KeymapHandling.h new file mode 100644 index 0000000..fdc64b7 --- /dev/null +++ b/xbmc/input/joysticks/keymaps/KeymapHandling.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/Observer.h" + +#include <memory> +#include <string> +#include <vector> + +class IKeymap; +class IKeymapEnvironment; + +namespace KODI +{ +namespace JOYSTICK +{ +class IInputHandler; +class IInputProvider; +class IInputReceiver; + +/*! + * \ingroup joystick + * \brief + */ +class CKeymapHandling : public Observer +{ +public: + CKeymapHandling(IInputProvider* inputProvider, + bool pPromiscuous, + const IKeymapEnvironment* environment); + + ~CKeymapHandling() override; + + /*! + * \brief Unregister the input provider + * + * Call this if the input provider is invalidated, such as if a user + * disconnects a controller. This prevents accessing the invalidated + * input provider when keymaps are unloaded upon destruction. + */ + void UnregisterInputProvider() { m_inputProvider = nullptr; } + + /*! + * \brief + */ + IInputReceiver* GetInputReceiver(const std::string& controllerId) const; + + /*! + * \brief + */ + IKeymap* GetKeymap(const std::string& controllerId) const; + + // implementation of Observer + void Notify(const Observable& obs, const ObservableMessage msg) override; + +private: + void LoadKeymaps(); + void UnloadKeymaps(); + + // Construction parameter + IInputProvider* m_inputProvider; + const bool m_pPromiscuous; + const IKeymapEnvironment* const m_environment; + + std::vector<std::unique_ptr<IKeymap>> m_keymaps; + std::vector<std::unique_ptr<IInputHandler>> m_inputHandlers; +}; +} // namespace JOYSTICK +} // namespace KODI |