diff options
Diffstat (limited to 'xbmc/input/joysticks/generic')
-rw-r--r-- | xbmc/input/joysticks/generic/ButtonMapping.cpp | 592 | ||||
-rw-r--r-- | xbmc/input/joysticks/generic/ButtonMapping.h | 393 | ||||
-rw-r--r-- | xbmc/input/joysticks/generic/CMakeLists.txt | 11 | ||||
-rw-r--r-- | xbmc/input/joysticks/generic/DriverReceiving.cpp | 38 | ||||
-rw-r--r-- | xbmc/input/joysticks/generic/DriverReceiving.h | 46 | ||||
-rw-r--r-- | xbmc/input/joysticks/generic/FeatureHandling.cpp | 551 | ||||
-rw-r--r-- | xbmc/input/joysticks/generic/FeatureHandling.h | 282 | ||||
-rw-r--r-- | xbmc/input/joysticks/generic/InputHandling.cpp | 179 | ||||
-rw-r--r-- | xbmc/input/joysticks/generic/InputHandling.h | 69 |
9 files changed, 2161 insertions, 0 deletions
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 |