diff options
Diffstat (limited to 'xbmc/games/controllers')
75 files changed, 7258 insertions, 0 deletions
diff --git a/xbmc/games/controllers/CMakeLists.txt b/xbmc/games/controllers/CMakeLists.txt new file mode 100644 index 0000000..b54f5c0 --- /dev/null +++ b/xbmc/games/controllers/CMakeLists.txt @@ -0,0 +1,18 @@ +set(SOURCES Controller.cpp + ControllerLayout.cpp + ControllerManager.cpp + ControllerTranslator.cpp + DefaultController.cpp +) + +set(HEADERS Controller.h + ControllerDefinitions.h + ControllerIDs.h + ControllerLayout.h + ControllerManager.h + ControllerTranslator.h + ControllerTypes.h + DefaultController.h +) + +core_add_library(games_controller) diff --git a/xbmc/games/controllers/Controller.cpp b/xbmc/games/controllers/Controller.cpp new file mode 100644 index 0000000..e51af4e --- /dev/null +++ b/xbmc/games/controllers/Controller.cpp @@ -0,0 +1,157 @@ +/* + * 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 "Controller.h" + +#include "ControllerDefinitions.h" +#include "ControllerLayout.h" +#include "URL.h" +#include "addons/addoninfo/AddonType.h" +#include "games/controllers/input/PhysicalTopology.h" +#include "utils/XBMCTinyXML.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <algorithm> + +using namespace KODI; +using namespace GAME; + +// --- FeatureTypeEqual -------------------------------------------------------- + +struct FeatureTypeEqual +{ + FeatureTypeEqual(FEATURE_TYPE type, JOYSTICK::INPUT_TYPE inputType) + : type(type), inputType(inputType) + { + } + + bool operator()(const CPhysicalFeature& feature) const + { + if (type == FEATURE_TYPE::UNKNOWN) + return true; // Match all feature types + + if (type == FEATURE_TYPE::SCALAR && feature.Type() == FEATURE_TYPE::SCALAR) + { + if (inputType == JOYSTICK::INPUT_TYPE::UNKNOWN) + return true; // Match all input types + + return inputType == feature.InputType(); + } + + return type == feature.Type(); + } + + const FEATURE_TYPE type; + const JOYSTICK::INPUT_TYPE inputType; +}; + +// --- CController ------------------------------------------------------------- + +const ControllerPtr CController::EmptyPtr; + +CController::CController(const ADDON::AddonInfoPtr& addonInfo) + : CAddon(addonInfo, ADDON::AddonType::GAME_CONTROLLER), m_layout(new CControllerLayout) +{ +} + +CController::~CController() = default; + +const CPhysicalFeature& CController::GetFeature(const std::string& name) const +{ + auto it = + std::find_if(m_features.begin(), m_features.end(), + [&name](const CPhysicalFeature& feature) { return name == feature.Name(); }); + + if (it != m_features.end()) + return *it; + + static const CPhysicalFeature invalid{}; + return invalid; +} + +unsigned int CController::FeatureCount( + FEATURE_TYPE type /* = FEATURE_TYPE::UNKNOWN */, + JOYSTICK::INPUT_TYPE inputType /* = JOYSTICK::INPUT_TYPE::UNKNOWN */) const +{ + auto featureCount = + std::count_if(m_features.begin(), m_features.end(), FeatureTypeEqual(type, inputType)); + return static_cast<unsigned int>(featureCount); +} + +void CController::GetFeatures(std::vector<std::string>& features, + FEATURE_TYPE type /* = FEATURE_TYPE::UNKNOWN */) const +{ + for (const CPhysicalFeature& feature : m_features) + { + if (type == FEATURE_TYPE::UNKNOWN || type == feature.Type()) + features.push_back(feature.Name()); + } +} + +JOYSTICK::FEATURE_TYPE CController::FeatureType(const std::string& feature) const +{ + for (auto it = m_features.begin(); it != m_features.end(); ++it) + { + if (feature == it->Name()) + return it->Type(); + } + return JOYSTICK::FEATURE_TYPE::UNKNOWN; +} + +JOYSTICK::INPUT_TYPE CController::GetInputType(const std::string& feature) const +{ + for (auto it = m_features.begin(); it != m_features.end(); ++it) + { + if (feature == it->Name()) + return it->InputType(); + } + return JOYSTICK::INPUT_TYPE::UNKNOWN; +} + +bool CController::LoadLayout(void) +{ + if (!m_bLoaded) + { + std::string strLayoutXmlPath = LibPath(); + + CLog::Log(LOGINFO, "Loading controller layout: {}", CURL::GetRedacted(strLayoutXmlPath)); + + CXBMCTinyXML xmlDoc; + if (!xmlDoc.LoadFile(strLayoutXmlPath)) + { + CLog::Log(LOGDEBUG, "Unable to load file: {} at line {}", xmlDoc.ErrorDesc(), + xmlDoc.ErrorRow()); + return false; + } + + TiXmlElement* pRootElement = xmlDoc.RootElement(); + if (!pRootElement || pRootElement->NoChildren() || pRootElement->ValueStr() != LAYOUT_XML_ROOT) + { + CLog::Log(LOGERROR, "Can't find root <{}> tag", LAYOUT_XML_ROOT); + return false; + } + + m_layout->Deserialize(pRootElement, this, m_features); + if (m_layout->IsValid(true)) + { + m_bLoaded = true; + } + else + { + m_layout->Reset(); + } + } + + return m_bLoaded; +} + +const CPhysicalTopology& CController::Topology() const +{ + return m_layout->Topology(); +} diff --git a/xbmc/games/controllers/Controller.h b/xbmc/games/controllers/Controller.h new file mode 100644 index 0000000..2dd7c18 --- /dev/null +++ b/xbmc/games/controllers/Controller.h @@ -0,0 +1,120 @@ +/* + * 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 "ControllerTypes.h" +#include "addons/Addon.h" +#include "games/controllers/input/PhysicalFeature.h" +#include "input/joysticks/JoystickTypes.h" + +#include <map> +#include <memory> +#include <string> +#include <vector> + +namespace KODI +{ +namespace GAME +{ +class CControllerLayout; +class CPhysicalTopology; + +using JOYSTICK::FEATURE_TYPE; + +class CController : public ADDON::CAddon +{ +public: + explicit CController(const ADDON::AddonInfoPtr& addonInfo); + + ~CController() override; + + static const ControllerPtr EmptyPtr; + + /*! + * \brief Get all controller features + * + * \return The features + */ + const std::vector<CPhysicalFeature>& Features(void) const { return m_features; } + + /*! + * \brief Get a feature by its name + * + * \param name The feature name + * + * \return The feature, or a feature of type FEATURE_TYPE::UNKNOWN if the name is invalid + */ + const CPhysicalFeature& GetFeature(const std::string& name) const; + + /*! + * \brief Get the count of controller features matching the specified types + * + * \param type The feature type, or FEATURE_TYPE::UNKNOWN to match all feature types + * \param inputType The input type, or INPUT_TYPE::UNKNOWN to match all input types + * + * \return The feature count + */ + unsigned int FeatureCount(FEATURE_TYPE type = FEATURE_TYPE::UNKNOWN, + JOYSTICK::INPUT_TYPE inputType = JOYSTICK::INPUT_TYPE::UNKNOWN) const; + + /*! + * \brief Get the features matching the specified type + * + * \param type The feature type, or FEATURE_TYPE::UNKNOWN to get all features + */ + void GetFeatures(std::vector<std::string>& features, + FEATURE_TYPE type = FEATURE_TYPE::UNKNOWN) const; + + /*! + * \brief Get the type of the specified feature + * + * \param feature The feature name to look up + * + * \return The feature type, or FEATURE_TYPE::UNKNOWN if an invalid feature was specified + */ + FEATURE_TYPE FeatureType(const std::string& feature) const; + + /*! + * \brief Get the input type of the specified feature + * + * \param feature The feature name to look up + * + * \return The input type of the feature, or INPUT_TYPE::UNKNOWN if unknown + */ + JOYSTICK::INPUT_TYPE GetInputType(const std::string& feature) const; + + /*! + * \brief Load the controller layout + * + * \return true if the layout is loaded or was already loaded, false otherwise + */ + bool LoadLayout(void); + + /*! + * \brief Get the controller layout + */ + const CControllerLayout& Layout(void) const { return *m_layout; } + + /*! + * \brief Get the controller's physical topology + * + * This defines how controllers physically connect to each other. + * + * \return The physical topology of the controller + */ + const CPhysicalTopology& Topology() const; + +private: + std::unique_ptr<CControllerLayout> m_layout; + std::vector<CPhysicalFeature> m_features; + bool m_bLoaded = false; +}; + +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/ControllerDefinitions.h b/xbmc/games/controllers/ControllerDefinitions.h new file mode 100644 index 0000000..517eb50 --- /dev/null +++ b/xbmc/games/controllers/ControllerDefinitions.h @@ -0,0 +1,55 @@ +/* + * 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 + +// XML definitions +#define LAYOUT_XML_ROOT "layout" +#define LAYOUT_XML_ELM_CATEGORY "category" +#define LAYOUT_XML_ELM_BUTTON "button" +#define LAYOUT_XML_ELM_ANALOG_STICK "analogstick" +#define LAYOUT_XML_ELM_ACCELEROMETER "accelerometer" +#define LAYOUT_XML_ELM_MOTOR "motor" +#define LAYOUT_XML_ELM_RELPOINTER "relpointer" +#define LAYOUT_XML_ELM_ABSPOINTER "abspointer" +#define LAYOUT_XML_ELM_WHEEL "wheel" +#define LAYOUT_XML_ELM_THROTTLE "throttle" +#define LAYOUT_XML_ELM_KEY "key" +#define LAYOUT_XML_ELM_TOPOLOGY "physicaltopology" +#define LAYOUT_XML_ELM_PORT "port" +#define LAYOUT_XML_ELM_ACCEPTS "accepts" +#define LAYOUT_XML_ATTR_LAYOUT_LABEL "label" +#define LAYOUT_XML_ATTR_LAYOUT_ICON "icon" +#define LAYOUT_XML_ATTR_LAYOUT_IMAGE "image" +#define LAYOUT_XML_ATTR_CATEGORY_NAME "name" +#define LAYOUT_XML_ATTR_CATEGORY_LABEL "label" +#define LAYOUT_XML_ATTR_FEATURE_NAME "name" +#define LAYOUT_XML_ATTR_FEATURE_LABEL "label" +#define LAYOUT_XML_ATTR_INPUT_TYPE "type" +#define LAYOUT_XML_ATTR_KEY_SYMBOL "symbol" +#define LAYOUT_XML_ATTR_PROVIDES_INPUT "providesinput" +#define LAYOUT_XML_ATTR_PORT_ID "id" +#define LAYOUT_XML_ATTR_CONTROLLER "controller" + +// Controller definitions +#define FEATURE_CATEGORY_FACE "face" +#define FEATURE_CATEGORY_SHOULDER "shoulder" +#define FEATURE_CATEGORY_TRIGGER "triggers" +#define FEATURE_CATEGORY_ANALOG_STICK "analogsticks" +#define FEATURE_CATEGORY_ACCELEROMETER "accelerometer" +#define FEATURE_CATEGORY_HAPTICS "haptics" +#define FEATURE_CATEGORY_MOUSE_BUTTON "mouse" +#define FEATURE_CATEGORY_POINTER "pointer" +#define FEATURE_CATEGORY_LIGHTGUN "lightgun" +#define FEATURE_CATEGORY_OFFSCREEN "offscreen" +#define FEATURE_CATEGORY_KEY "keys" +#define FEATURE_CATEGORY_KEYPAD "keypad" +#define FEATURE_CATEGORY_HARDWARE "hardware" +#define FEATURE_CATEGORY_WHEEL "wheel" +#define FEATURE_CATEGORY_JOYSTICK "joysticks" +#define FEATURE_CATEGORY_PADDLE "paddles" diff --git a/xbmc/games/controllers/ControllerIDs.h b/xbmc/games/controllers/ControllerIDs.h new file mode 100644 index 0000000..a9254e0 --- /dev/null +++ b/xbmc/games/controllers/ControllerIDs.h @@ -0,0 +1,15 @@ +/* + * 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 + +// Default controller IDs +#define DEFAULT_CONTROLLER_ID "game.controller.default" +#define DEFAULT_KEYBOARD_ID "game.controller.keyboard" +#define DEFAULT_MOUSE_ID "game.controller.mouse" +#define DEFAULT_REMOTE_ID "game.controller.remote" diff --git a/xbmc/games/controllers/ControllerLayout.cpp b/xbmc/games/controllers/ControllerLayout.cpp new file mode 100644 index 0000000..85816c1 --- /dev/null +++ b/xbmc/games/controllers/ControllerLayout.cpp @@ -0,0 +1,159 @@ +/* + * 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 "ControllerLayout.h" + +#include "Controller.h" +#include "ControllerDefinitions.h" +#include "ControllerTranslator.h" +#include "games/controllers/input/PhysicalTopology.h" +#include "guilib/LocalizeStrings.h" +#include "utils/URIUtils.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <sstream> + +using namespace KODI; +using namespace GAME; + +CControllerLayout::CControllerLayout() : m_topology(new CPhysicalTopology) +{ +} + +CControllerLayout::CControllerLayout(const CControllerLayout& other) + : m_controller(other.m_controller), + m_labelId(other.m_labelId), + m_icon(other.m_icon), + m_strImage(other.m_strImage), + m_topology(new CPhysicalTopology(*other.m_topology)) +{ +} + +CControllerLayout::~CControllerLayout() = default; + +void CControllerLayout::Reset(void) +{ + m_controller = nullptr; + m_labelId = -1; + m_icon.clear(); + m_strImage.clear(); + m_topology->Reset(); +} + +bool CControllerLayout::IsValid(bool bLog) const +{ + if (m_labelId < 0) + { + if (bLog) + CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", LAYOUT_XML_ROOT, + LAYOUT_XML_ATTR_LAYOUT_LABEL); + return false; + } + + if (m_strImage.empty()) + { + if (bLog) + CLog::Log(LOGDEBUG, "<{}> tag has no \"{}\" attribute", LAYOUT_XML_ROOT, + LAYOUT_XML_ATTR_LAYOUT_IMAGE); + return false; + } + + return true; +} + +std::string CControllerLayout::Label(void) const +{ + std::string label; + + if (m_labelId >= 0 && m_controller != nullptr) + label = g_localizeStrings.GetAddonString(m_controller->ID(), m_labelId); + + return label; +} + +std::string CControllerLayout::ImagePath(void) const +{ + std::string path; + + if (!m_strImage.empty() && m_controller != nullptr) + return URIUtils::AddFileToFolder(URIUtils::GetDirectory(m_controller->LibPath()), m_strImage); + + return path; +} + +void CControllerLayout::Deserialize(const TiXmlElement* pElement, + const CController* controller, + std::vector<CPhysicalFeature>& features) +{ + if (pElement == nullptr || controller == nullptr) + return; + + // Controller (used for string lookup and path translation) + m_controller = controller; + + // Label + std::string strLabel = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_LABEL); + if (!strLabel.empty()) + std::istringstream(strLabel) >> m_labelId; + + // Icon + std::string icon = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_ICON); + if (!icon.empty()) + m_icon = icon; + + // Fallback icon, use add-on icon + if (m_icon.empty()) + m_icon = controller->Icon(); + + // Image + std::string image = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_IMAGE); + if (!image.empty()) + m_strImage = image; + + for (const TiXmlElement* pChild = pElement->FirstChildElement(); pChild != nullptr; + pChild = pChild->NextSiblingElement()) + { + if (pChild->ValueStr() == LAYOUT_XML_ELM_CATEGORY) + { + // Category + std::string strCategory = XMLUtils::GetAttribute(pChild, LAYOUT_XML_ATTR_CATEGORY_NAME); + JOYSTICK::FEATURE_CATEGORY category = + CControllerTranslator::TranslateFeatureCategory(strCategory); + + // Category label + int categoryLabelId = -1; + + std::string strCategoryLabelId = + XMLUtils::GetAttribute(pChild, LAYOUT_XML_ATTR_CATEGORY_LABEL); + if (!strCategoryLabelId.empty()) + std::istringstream(strCategoryLabelId) >> categoryLabelId; + + // Features + for (const TiXmlElement* pFeature = pChild->FirstChildElement(); pFeature != nullptr; + pFeature = pFeature->NextSiblingElement()) + { + CPhysicalFeature feature; + + if (feature.Deserialize(pFeature, controller, category, categoryLabelId)) + features.push_back(feature); + } + } + else if (pChild->ValueStr() == LAYOUT_XML_ELM_TOPOLOGY) + { + // Topology + CPhysicalTopology topology; + if (topology.Deserialize(pChild)) + *m_topology = std::move(topology); + } + else + { + CLog::Log(LOGDEBUG, "Ignoring <{}> tag", pChild->ValueStr()); + } + } +} diff --git a/xbmc/games/controllers/ControllerLayout.h b/xbmc/games/controllers/ControllerLayout.h new file mode 100644 index 0000000..1589837 --- /dev/null +++ b/xbmc/games/controllers/ControllerLayout.h @@ -0,0 +1,92 @@ +/* + * 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 <memory> +#include <string> +#include <vector> + +class TiXmlElement; + +namespace KODI +{ +namespace GAME +{ +class CController; +class CPhysicalFeature; +class CPhysicalTopology; + +class CControllerLayout +{ +public: + CControllerLayout(); + CControllerLayout(const CControllerLayout& other); + ~CControllerLayout(); + + void Reset(void); + + int LabelID(void) const { return m_labelId; } + const std::string& Icon(void) const { return m_icon; } + const std::string& Image(void) const { return m_strImage; } + + /*! + * \brief Ensures the layout was deserialized correctly, and optionally logs if not + * + * \param bLog If true, output the cause of invalidness to the log + * + * \return True if the layout is valid and can be used in the GUI, false otherwise + */ + bool IsValid(bool bLog) const; + + /*! + * \brief Get the label of the primary layout used when mapping the controller + * + * \return The label, or empty if unknown + */ + std::string Label(void) const; + + /*! + * \brief Get the image path of the primary layout used when mapping the controller + * + * \return The image path, or empty if unknown + */ + std::string ImagePath(void) const; + + /*! + * \brief Get the physical topology of this controller + * + * The topology of a controller defines its ports and which controllers can + * physically be connected to them. Also, the topology defines if the + * controller can provide player input, which is false in the case of hubs. + * + * \return The physical topology of the controller + */ + const CPhysicalTopology& Topology(void) const { return *m_topology; } + + /*! + * \brief Deserialize the specified XML element + * + * \param pLayoutElement The XML element + * \param controller The controller, used to obtain read-only properties + * \param features The deserialized features, if any + */ + void Deserialize(const TiXmlElement* pLayoutElement, + const CController* controller, + std::vector<CPhysicalFeature>& features); + +private: + const CController* m_controller = nullptr; + int m_labelId = -1; + std::string m_icon; + std::string m_strImage; + std::unique_ptr<CPhysicalTopology> m_topology; +}; + +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/ControllerManager.cpp b/xbmc/games/controllers/ControllerManager.cpp new file mode 100644 index 0000000..707733b --- /dev/null +++ b/xbmc/games/controllers/ControllerManager.cpp @@ -0,0 +1,122 @@ +/* + * 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 "ControllerManager.h" + +#include "Controller.h" +#include "ControllerIDs.h" +#include "addons/AddonEvents.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonType.h" + +#include <mutex> + +using namespace KODI; +using namespace GAME; + +CControllerManager::CControllerManager(ADDON::CAddonMgr& addonManager) + : m_addonManager(addonManager) +{ + m_addonManager.Events().Subscribe(this, &CControllerManager::OnEvent); +} + +CControllerManager::~CControllerManager() +{ + m_addonManager.Events().Unsubscribe(this); +} + +ControllerPtr CControllerManager::GetController(const std::string& controllerId) +{ + using namespace ADDON; + + std::lock_guard<CCriticalSection> lock(m_mutex); + + ControllerPtr& cachedController = m_cache[controllerId]; + + if (!cachedController && m_failedControllers.find(controllerId) == m_failedControllers.end()) + { + AddonPtr addon; + if (m_addonManager.GetAddon(controllerId, addon, AddonType::GAME_CONTROLLER, + OnlyEnabled::CHOICE_NO)) + cachedController = LoadController(addon); + } + + return cachedController; +} + +ControllerPtr CControllerManager::GetDefaultController() +{ + return GetController(DEFAULT_CONTROLLER_ID); +} + +ControllerPtr CControllerManager::GetDefaultKeyboard() +{ + return GetController(DEFAULT_KEYBOARD_ID); +} + +ControllerPtr CControllerManager::GetDefaultMouse() +{ + return GetController(DEFAULT_MOUSE_ID); +} + +ControllerVector CControllerManager::GetControllers() +{ + using namespace ADDON; + + ControllerVector controllers; + + std::lock_guard<CCriticalSection> lock(m_mutex); + + VECADDONS addons; + if (m_addonManager.GetInstalledAddons(addons, AddonType::GAME_CONTROLLER)) + { + for (auto& addon : addons) + { + ControllerPtr& cachedController = m_cache[addon->ID()]; + if (!cachedController && m_failedControllers.find(addon->ID()) == m_failedControllers.end()) + cachedController = LoadController(addon); + + if (cachedController) + controllers.emplace_back(cachedController); + } + } + + return controllers; +} + +void CControllerManager::OnEvent(const ADDON::AddonEvent& event) +{ + if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) || // Also called on install + typeid(event) == typeid(ADDON::AddonEvents::ReInstalled)) + { + std::lock_guard<CCriticalSection> lock(m_mutex); + + const std::string& addonId = event.addonId; + + // Clear caches for add-on + auto it = m_cache.find(addonId); + if (it != m_cache.end()) + m_cache.erase(it); + + auto it2 = m_failedControllers.find(addonId); + if (it2 != m_failedControllers.end()) + m_failedControllers.erase(it2); + } +} + +ControllerPtr CControllerManager::LoadController(const ADDON::AddonPtr& addon) +{ + ControllerPtr controller = std::static_pointer_cast<CController>(addon); + if (!controller->LoadLayout()) + { + m_failedControllers.insert(addon->ID()); + controller.reset(); + } + + return controller; +} diff --git a/xbmc/games/controllers/ControllerManager.h b/xbmc/games/controllers/ControllerManager.h new file mode 100644 index 0000000..25ffe0f --- /dev/null +++ b/xbmc/games/controllers/ControllerManager.h @@ -0,0 +1,93 @@ +/* + * 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 "ControllerTypes.h" +#include "addons/IAddon.h" +#include "threads/CriticalSection.h" + +#include <map> +#include <set> +#include <string> + +namespace ADDON +{ +struct AddonEvent; +} // namespace ADDON + +namespace KODI +{ +namespace GAME +{ +class CControllerManager +{ +public: + CControllerManager(ADDON::CAddonMgr& addonManager); + ~CControllerManager(); + + /*! + * \brief Get a controller + * + * A cache is used to avoid reloading controllers each time they are + * requested. + * + * \param controllerId The controller's ID + * + * \return The controller, or empty if the controller isn't installed or + * can't be loaded + */ + ControllerPtr GetController(const std::string& controllerId); + + /*! + * \brief Get the default controller + * + * \return The default controller, or empty if the controller failed to load + */ + ControllerPtr GetDefaultController(); + + /*! + * \brief Get the default keyboard + * + * \return The keyboard controller, or empty if the controller failed to load + */ + ControllerPtr GetDefaultKeyboard(); + + /*! + * \brief Get the default mouse + * + * \return The mouse controller, or empty if the controller failed to load + */ + ControllerPtr GetDefaultMouse(); + + /*! + * \brief Get installed controllers + * + * \return The installed controllers that loaded successfully + */ + ControllerVector GetControllers(); + +private: + // Add-on event handler + void OnEvent(const ADDON::AddonEvent& event); + + // Utility functions + ControllerPtr LoadController(const ADDON::AddonPtr& addon); + + // Construction parameters + ADDON::CAddonMgr& m_addonManager; + + // Controller state + std::map<std::string, ControllerPtr> m_cache; + std::set<std::string> m_failedControllers; // Controllers that failed to load + + // Synchronization parameters + CCriticalSection m_mutex; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/ControllerTranslator.cpp b/xbmc/games/controllers/ControllerTranslator.cpp new file mode 100644 index 0000000..df8641b --- /dev/null +++ b/xbmc/games/controllers/ControllerTranslator.cpp @@ -0,0 +1,744 @@ +/* + * 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 "ControllerTranslator.h" + +#include "ControllerDefinitions.h" + +using namespace KODI; +using namespace GAME; +using namespace JOYSTICK; + +const char* CControllerTranslator::TranslateFeatureType(FEATURE_TYPE type) +{ + switch (type) + { + case FEATURE_TYPE::SCALAR: + return LAYOUT_XML_ELM_BUTTON; + case FEATURE_TYPE::ANALOG_STICK: + return LAYOUT_XML_ELM_ANALOG_STICK; + case FEATURE_TYPE::ACCELEROMETER: + return LAYOUT_XML_ELM_ACCELEROMETER; + case FEATURE_TYPE::MOTOR: + return LAYOUT_XML_ELM_MOTOR; + case FEATURE_TYPE::RELPOINTER: + return LAYOUT_XML_ELM_RELPOINTER; + case FEATURE_TYPE::ABSPOINTER: + return LAYOUT_XML_ELM_ABSPOINTER; + case FEATURE_TYPE::WHEEL: + return LAYOUT_XML_ELM_WHEEL; + case FEATURE_TYPE::THROTTLE: + return LAYOUT_XML_ELM_THROTTLE; + case FEATURE_TYPE::KEY: + return LAYOUT_XML_ELM_KEY; + default: + break; + } + return ""; +} + +FEATURE_TYPE CControllerTranslator::TranslateFeatureType(const std::string& strType) +{ + if (strType == LAYOUT_XML_ELM_BUTTON) + return FEATURE_TYPE::SCALAR; + if (strType == LAYOUT_XML_ELM_ANALOG_STICK) + return FEATURE_TYPE::ANALOG_STICK; + if (strType == LAYOUT_XML_ELM_ACCELEROMETER) + return FEATURE_TYPE::ACCELEROMETER; + if (strType == LAYOUT_XML_ELM_MOTOR) + return FEATURE_TYPE::MOTOR; + if (strType == LAYOUT_XML_ELM_RELPOINTER) + return FEATURE_TYPE::RELPOINTER; + if (strType == LAYOUT_XML_ELM_ABSPOINTER) + return FEATURE_TYPE::ABSPOINTER; + if (strType == LAYOUT_XML_ELM_WHEEL) + return FEATURE_TYPE::WHEEL; + if (strType == LAYOUT_XML_ELM_THROTTLE) + return FEATURE_TYPE::THROTTLE; + if (strType == LAYOUT_XML_ELM_KEY) + return FEATURE_TYPE::KEY; + + return FEATURE_TYPE::UNKNOWN; +} + +const char* CControllerTranslator::TranslateFeatureCategory(FEATURE_CATEGORY category) +{ + switch (category) + { + case FEATURE_CATEGORY::FACE: + return FEATURE_CATEGORY_FACE; + case FEATURE_CATEGORY::SHOULDER: + return FEATURE_CATEGORY_SHOULDER; + case FEATURE_CATEGORY::TRIGGER: + return FEATURE_CATEGORY_TRIGGER; + case FEATURE_CATEGORY::ANALOG_STICK: + return FEATURE_CATEGORY_ANALOG_STICK; + case FEATURE_CATEGORY::ACCELEROMETER: + return FEATURE_CATEGORY_ACCELEROMETER; + case FEATURE_CATEGORY::HAPTICS: + return FEATURE_CATEGORY_HAPTICS; + case FEATURE_CATEGORY::MOUSE_BUTTON: + return FEATURE_CATEGORY_MOUSE_BUTTON; + case FEATURE_CATEGORY::POINTER: + return FEATURE_CATEGORY_POINTER; + case FEATURE_CATEGORY::LIGHTGUN: + return FEATURE_CATEGORY_LIGHTGUN; + case FEATURE_CATEGORY::OFFSCREEN: + return FEATURE_CATEGORY_OFFSCREEN; + case FEATURE_CATEGORY::KEY: + return FEATURE_CATEGORY_KEY; + case FEATURE_CATEGORY::KEYPAD: + return FEATURE_CATEGORY_KEYPAD; + case FEATURE_CATEGORY::HARDWARE: + return FEATURE_CATEGORY_HARDWARE; + case FEATURE_CATEGORY::WHEEL: + return FEATURE_CATEGORY_WHEEL; + case FEATURE_CATEGORY::JOYSTICK: + return FEATURE_CATEGORY_JOYSTICK; + case FEATURE_CATEGORY::PADDLE: + return FEATURE_CATEGORY_PADDLE; + default: + break; + } + return ""; +} + +FEATURE_CATEGORY CControllerTranslator::TranslateFeatureCategory(const std::string& strCategory) +{ + if (strCategory == FEATURE_CATEGORY_FACE) + return FEATURE_CATEGORY::FACE; + if (strCategory == FEATURE_CATEGORY_SHOULDER) + return FEATURE_CATEGORY::SHOULDER; + if (strCategory == FEATURE_CATEGORY_TRIGGER) + return FEATURE_CATEGORY::TRIGGER; + if (strCategory == FEATURE_CATEGORY_ANALOG_STICK) + return FEATURE_CATEGORY::ANALOG_STICK; + if (strCategory == FEATURE_CATEGORY_ACCELEROMETER) + return FEATURE_CATEGORY::ACCELEROMETER; + if (strCategory == FEATURE_CATEGORY_HAPTICS) + return FEATURE_CATEGORY::HAPTICS; + if (strCategory == FEATURE_CATEGORY_MOUSE_BUTTON) + return FEATURE_CATEGORY::MOUSE_BUTTON; + if (strCategory == FEATURE_CATEGORY_POINTER) + return FEATURE_CATEGORY::POINTER; + if (strCategory == FEATURE_CATEGORY_LIGHTGUN) + return FEATURE_CATEGORY::LIGHTGUN; + if (strCategory == FEATURE_CATEGORY_OFFSCREEN) + return FEATURE_CATEGORY::OFFSCREEN; + if (strCategory == FEATURE_CATEGORY_KEY) + return FEATURE_CATEGORY::KEY; + if (strCategory == FEATURE_CATEGORY_KEYPAD) + return FEATURE_CATEGORY::KEYPAD; + if (strCategory == FEATURE_CATEGORY_HARDWARE) + return FEATURE_CATEGORY::HARDWARE; + if (strCategory == FEATURE_CATEGORY_WHEEL) + return FEATURE_CATEGORY::WHEEL; + if (strCategory == FEATURE_CATEGORY_JOYSTICK) + return FEATURE_CATEGORY::JOYSTICK; + if (strCategory == FEATURE_CATEGORY_PADDLE) + return FEATURE_CATEGORY::PADDLE; + + return FEATURE_CATEGORY::UNKNOWN; +} + +const char* CControllerTranslator::TranslateInputType(INPUT_TYPE type) +{ + switch (type) + { + case INPUT_TYPE::DIGITAL: + return "digital"; + case INPUT_TYPE::ANALOG: + return "analog"; + default: + break; + } + return ""; +} + +INPUT_TYPE CControllerTranslator::TranslateInputType(const std::string& strType) +{ + if (strType == "digital") + return INPUT_TYPE::DIGITAL; + if (strType == "analog") + return INPUT_TYPE::ANALOG; + + return INPUT_TYPE::UNKNOWN; +} + +KEYBOARD::KeySymbol CControllerTranslator::TranslateKeysym(const std::string& symbol) +{ + if (symbol == "backspace") + return XBMCK_BACKSPACE; + if (symbol == "tab") + return XBMCK_TAB; + if (symbol == "clear") + return XBMCK_CLEAR; + if (symbol == "enter") + return XBMCK_RETURN; + if (symbol == "pause") + return XBMCK_PAUSE; + if (symbol == "escape") + return XBMCK_ESCAPE; + if (symbol == "space") + return XBMCK_SPACE; + if (symbol == "exclaim") + return XBMCK_EXCLAIM; + if (symbol == "doublequote") + return XBMCK_QUOTEDBL; + if (symbol == "hash") + return XBMCK_HASH; + if (symbol == "dollar") + return XBMCK_DOLLAR; + if (symbol == "ampersand") + return XBMCK_AMPERSAND; + if (symbol == "quote") + return XBMCK_QUOTE; + if (symbol == "leftparen") + return XBMCK_LEFTPAREN; + if (symbol == "rightparen") + return XBMCK_RIGHTPAREN; + if (symbol == "asterisk") + return XBMCK_ASTERISK; + if (symbol == "plus") + return XBMCK_PLUS; + if (symbol == "comma") + return XBMCK_COMMA; + if (symbol == "minus") + return XBMCK_MINUS; + if (symbol == "period") + return XBMCK_PERIOD; + if (symbol == "slash") + return XBMCK_SLASH; + if (symbol == "0") + return XBMCK_0; + if (symbol == "1") + return XBMCK_1; + if (symbol == "2") + return XBMCK_2; + if (symbol == "3") + return XBMCK_3; + if (symbol == "4") + return XBMCK_4; + if (symbol == "5") + return XBMCK_5; + if (symbol == "6") + return XBMCK_6; + if (symbol == "7") + return XBMCK_7; + if (symbol == "8") + return XBMCK_8; + if (symbol == "9") + return XBMCK_9; + if (symbol == "colon") + return XBMCK_COLON; + if (symbol == "semicolon") + return XBMCK_SEMICOLON; + if (symbol == "less") + return XBMCK_LESS; + if (symbol == "equals") + return XBMCK_EQUALS; + if (symbol == "greater") + return XBMCK_GREATER; + if (symbol == "question") + return XBMCK_QUESTION; + if (symbol == "at") + return XBMCK_AT; + if (symbol == "leftbracket") + return XBMCK_LEFTBRACKET; + if (symbol == "backslash") + return XBMCK_BACKSLASH; + if (symbol == "rightbracket") + return XBMCK_RIGHTBRACKET; + if (symbol == "caret") + return XBMCK_CARET; + if (symbol == "underscore") + return XBMCK_UNDERSCORE; + if (symbol == "grave") + return XBMCK_BACKQUOTE; + if (symbol == "a") + return XBMCK_a; + if (symbol == "b") + return XBMCK_b; + if (symbol == "c") + return XBMCK_c; + if (symbol == "d") + return XBMCK_d; + if (symbol == "e") + return XBMCK_e; + if (symbol == "f") + return XBMCK_f; + if (symbol == "g") + return XBMCK_g; + if (symbol == "h") + return XBMCK_h; + if (symbol == "i") + return XBMCK_i; + if (symbol == "j") + return XBMCK_j; + if (symbol == "k") + return XBMCK_k; + if (symbol == "l") + return XBMCK_l; + if (symbol == "m") + return XBMCK_m; + if (symbol == "n") + return XBMCK_n; + if (symbol == "o") + return XBMCK_o; + if (symbol == "p") + return XBMCK_p; + if (symbol == "q") + return XBMCK_q; + if (symbol == "r") + return XBMCK_r; + if (symbol == "s") + return XBMCK_s; + if (symbol == "t") + return XBMCK_t; + if (symbol == "u") + return XBMCK_u; + if (symbol == "v") + return XBMCK_v; + if (symbol == "w") + return XBMCK_w; + if (symbol == "x") + return XBMCK_x; + if (symbol == "y") + return XBMCK_y; + if (symbol == "z") + return XBMCK_z; + if (symbol == "leftbrace") + return XBMCK_LEFTBRACE; + if (symbol == "bar") + return XBMCK_PIPE; + if (symbol == "rightbrace") + return XBMCK_RIGHTBRACE; + if (symbol == "tilde") + return XBMCK_TILDE; + if (symbol == "delete") + return XBMCK_DELETE; + if (symbol == "kp0") + return XBMCK_KP0; + if (symbol == "kp1") + return XBMCK_KP1; + if (symbol == "kp2") + return XBMCK_KP2; + if (symbol == "kp3") + return XBMCK_KP3; + if (symbol == "kp4") + return XBMCK_KP4; + if (symbol == "kp5") + return XBMCK_KP5; + if (symbol == "kp6") + return XBMCK_KP6; + if (symbol == "kp7") + return XBMCK_KP7; + if (symbol == "kp8") + return XBMCK_KP8; + if (symbol == "kp9") + return XBMCK_KP9; + if (symbol == "kpperiod") + return XBMCK_KP_PERIOD; + if (symbol == "kpdivide") + return XBMCK_KP_DIVIDE; + if (symbol == "kpmultiply") + return XBMCK_KP_MULTIPLY; + if (symbol == "kpminus") + return XBMCK_KP_MINUS; + if (symbol == "kpplus") + return XBMCK_KP_PLUS; + if (symbol == "kpenter") + return XBMCK_KP_ENTER; + if (symbol == "kpequals") + return XBMCK_KP_EQUALS; + if (symbol == "up") + return XBMCK_UP; + if (symbol == "down") + return XBMCK_DOWN; + if (symbol == "right") + return XBMCK_RIGHT; + if (symbol == "left") + return XBMCK_LEFT; + if (symbol == "insert") + return XBMCK_INSERT; + if (symbol == "home") + return XBMCK_HOME; + if (symbol == "end") + return XBMCK_END; + if (symbol == "pageup") + return XBMCK_PAGEUP; + if (symbol == "pagedown") + return XBMCK_PAGEDOWN; + if (symbol == "f1") + return XBMCK_F1; + if (symbol == "f2") + return XBMCK_F2; + if (symbol == "f3") + return XBMCK_F3; + if (symbol == "f4") + return XBMCK_F4; + if (symbol == "f5") + return XBMCK_F5; + if (symbol == "f6") + return XBMCK_F6; + if (symbol == "f7") + return XBMCK_F7; + if (symbol == "f8") + return XBMCK_F8; + if (symbol == "f9") + return XBMCK_F9; + if (symbol == "f10") + return XBMCK_F10; + if (symbol == "f11") + return XBMCK_F11; + if (symbol == "f12") + return XBMCK_F12; + if (symbol == "f13") + return XBMCK_F13; + if (symbol == "f14") + return XBMCK_F14; + if (symbol == "f15") + return XBMCK_F15; + if (symbol == "numlock") + return XBMCK_NUMLOCK; + if (symbol == "capslock") + return XBMCK_CAPSLOCK; + if (symbol == "scrolllock") + return XBMCK_SCROLLOCK; + if (symbol == "leftshift") + return XBMCK_LSHIFT; + if (symbol == "rightshift") + return XBMCK_RSHIFT; + if (symbol == "leftctrl") + return XBMCK_LCTRL; + if (symbol == "rightctrl") + return XBMCK_RCTRL; + if (symbol == "leftalt") + return XBMCK_LALT; + if (symbol == "rightalt") + return XBMCK_RALT; + if (symbol == "leftmeta") + return XBMCK_LMETA; + if (symbol == "rightmeta") + return XBMCK_RMETA; + if (symbol == "leftsuper") + return XBMCK_LSUPER; + if (symbol == "rightsuper") + return XBMCK_RSUPER; + if (symbol == "mode") + return XBMCK_MODE; + if (symbol == "compose") + return XBMCK_COMPOSE; + if (symbol == "help") + return XBMCK_HELP; + if (symbol == "printscreen") + return XBMCK_PRINT; + if (symbol == "sysreq") + return XBMCK_SYSREQ; + if (symbol == "break") + return XBMCK_BREAK; + if (symbol == "menu") + return XBMCK_MENU; + if (symbol == "power") + return XBMCK_POWER; + if (symbol == "euro") + return XBMCK_EURO; + if (symbol == "undo") + return XBMCK_UNDO; + + return XBMCK_UNKNOWN; +} + +const char* CControllerTranslator::TranslateKeycode(KEYBOARD::KeySymbol keycode) +{ + switch (keycode) + { + case XBMCK_BACKSPACE: + return "backspace"; + case XBMCK_TAB: + return "tab"; + case XBMCK_CLEAR: + return "clear"; + case XBMCK_RETURN: + return "enter"; + case XBMCK_PAUSE: + return "pause"; + case XBMCK_ESCAPE: + return "escape"; + case XBMCK_SPACE: + return "space"; + case XBMCK_EXCLAIM: + return "exclaim"; + case XBMCK_QUOTEDBL: + return "doublequote"; + case XBMCK_HASH: + return "hash"; + case XBMCK_DOLLAR: + return "dollar"; + case XBMCK_AMPERSAND: + return "ampersand"; + case XBMCK_QUOTE: + return "quote"; + case XBMCK_LEFTPAREN: + return "leftparen"; + case XBMCK_RIGHTPAREN: + return "rightparen"; + case XBMCK_ASTERISK: + return "asterisk"; + case XBMCK_PLUS: + return "plus"; + case XBMCK_COMMA: + return "comma"; + case XBMCK_MINUS: + return "minus"; + case XBMCK_PERIOD: + return "period"; + case XBMCK_SLASH: + return "slash"; + case XBMCK_0: + return "0"; + case XBMCK_1: + return "1"; + case XBMCK_2: + return "2"; + case XBMCK_3: + return "3"; + case XBMCK_4: + return "4"; + case XBMCK_5: + return "5"; + case XBMCK_6: + return "6"; + case XBMCK_7: + return "7"; + case XBMCK_8: + return "8"; + case XBMCK_9: + return "9"; + case XBMCK_COLON: + return "colon"; + case XBMCK_SEMICOLON: + return "semicolon"; + case XBMCK_LESS: + return "less"; + case XBMCK_EQUALS: + return "equals"; + case XBMCK_GREATER: + return "greater"; + case XBMCK_QUESTION: + return "question"; + case XBMCK_AT: + return "at"; + case XBMCK_LEFTBRACKET: + return "leftbracket"; + case XBMCK_BACKSLASH: + return "backslash"; + case XBMCK_RIGHTBRACKET: + return "rightbracket"; + case XBMCK_CARET: + return "caret"; + case XBMCK_UNDERSCORE: + return "underscore"; + case XBMCK_BACKQUOTE: + return "grave"; + case XBMCK_a: + return "a"; + case XBMCK_b: + return "b"; + case XBMCK_c: + return "c"; + case XBMCK_d: + return "d"; + case XBMCK_e: + return "e"; + case XBMCK_f: + return "f"; + case XBMCK_g: + return "g"; + case XBMCK_h: + return "h"; + case XBMCK_i: + return "i"; + case XBMCK_j: + return "j"; + case XBMCK_k: + return "k"; + case XBMCK_l: + return "l"; + case XBMCK_m: + return "m"; + case XBMCK_n: + return "n"; + case XBMCK_o: + return "o"; + case XBMCK_p: + return "p"; + case XBMCK_q: + return "q"; + case XBMCK_r: + return "r"; + case XBMCK_s: + return "s"; + case XBMCK_t: + return "t"; + case XBMCK_u: + return "u"; + case XBMCK_v: + return "v"; + case XBMCK_w: + return "w"; + case XBMCK_x: + return "x"; + case XBMCK_y: + return "y"; + case XBMCK_z: + return "z"; + case XBMCK_LEFTBRACE: + return "leftbrace"; + case XBMCK_PIPE: + return "bar"; + case XBMCK_RIGHTBRACE: + return "rightbrace"; + case XBMCK_TILDE: + return "tilde"; + case XBMCK_DELETE: + return "delete"; + case XBMCK_KP0: + return "kp0"; + case XBMCK_KP1: + return "kp1"; + case XBMCK_KP2: + return "kp2"; + case XBMCK_KP3: + return "kp3"; + case XBMCK_KP4: + return "kp4"; + case XBMCK_KP5: + return "kp5"; + case XBMCK_KP6: + return "kp6"; + case XBMCK_KP7: + return "kp7"; + case XBMCK_KP8: + return "kp8"; + case XBMCK_KP9: + return "kp9"; + case XBMCK_KP_PERIOD: + return "kpperiod"; + case XBMCK_KP_DIVIDE: + return "kpdivide"; + case XBMCK_KP_MULTIPLY: + return "kpmultiply"; + case XBMCK_KP_MINUS: + return "kpminus"; + case XBMCK_KP_PLUS: + return "kpplus"; + case XBMCK_KP_ENTER: + return "kpenter"; + case XBMCK_KP_EQUALS: + return "kpequals"; + case XBMCK_UP: + return "up"; + case XBMCK_DOWN: + return "down"; + case XBMCK_RIGHT: + return "right"; + case XBMCK_LEFT: + return "left"; + case XBMCK_INSERT: + return "insert"; + case XBMCK_HOME: + return "home"; + case XBMCK_END: + return "end"; + case XBMCK_PAGEUP: + return "pageup"; + case XBMCK_PAGEDOWN: + return "pagedown"; + case XBMCK_F1: + return "f1"; + case XBMCK_F2: + return "f2"; + case XBMCK_F3: + return "f3"; + case XBMCK_F4: + return "f4"; + case XBMCK_F5: + return "f5"; + case XBMCK_F6: + return "f6"; + case XBMCK_F7: + return "f7"; + case XBMCK_F8: + return "f8"; + case XBMCK_F9: + return "f9"; + case XBMCK_F10: + return "f10"; + case XBMCK_F11: + return "f11"; + case XBMCK_F12: + return "f12"; + case XBMCK_F13: + return "f13"; + case XBMCK_F14: + return "f14"; + case XBMCK_F15: + return "f15"; + case XBMCK_NUMLOCK: + return "numlock"; + case XBMCK_CAPSLOCK: + return "capslock"; + case XBMCK_SCROLLOCK: + return "scrolllock"; + case XBMCK_LSHIFT: + return "leftshift"; + case XBMCK_RSHIFT: + return "rightshift"; + case XBMCK_LCTRL: + return "leftctrl"; + case XBMCK_RCTRL: + return "rightctrl"; + case XBMCK_LALT: + return "leftalt"; + case XBMCK_RALT: + return "rightalt"; + case XBMCK_LMETA: + return "leftmeta"; + case XBMCK_RMETA: + return "rightmeta"; + case XBMCK_LSUPER: + return "leftsuper"; + case XBMCK_RSUPER: + return "rightsuper"; + case XBMCK_MODE: + return "mode"; + case XBMCK_COMPOSE: + return "compose"; + case XBMCK_HELP: + return "help"; + case XBMCK_PRINT: + return "printscreen"; + case XBMCK_SYSREQ: + return "sysreq"; + case XBMCK_BREAK: + return "break"; + case XBMCK_MENU: + return "menu"; + case XBMCK_POWER: + return "power"; + case XBMCK_EURO: + return "euro"; + case XBMCK_UNDO: + return "undo"; + default: + break; + } + + return ""; +} diff --git a/xbmc/games/controllers/ControllerTranslator.h b/xbmc/games/controllers/ControllerTranslator.h new file mode 100644 index 0000000..0bf4bf1 --- /dev/null +++ b/xbmc/games/controllers/ControllerTranslator.h @@ -0,0 +1,53 @@ +/* + * 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/JoystickTypes.h" +#include "input/keyboard/KeyboardTypes.h" + +#include <string> + +namespace KODI +{ +namespace GAME +{ + +class CControllerTranslator +{ +public: + static const char* TranslateFeatureType(JOYSTICK::FEATURE_TYPE type); + static JOYSTICK::FEATURE_TYPE TranslateFeatureType(const std::string& strType); + + static const char* TranslateFeatureCategory(JOYSTICK::FEATURE_CATEGORY category); + static JOYSTICK::FEATURE_CATEGORY TranslateFeatureCategory(const std::string& strCategory); + + static const char* TranslateInputType(JOYSTICK::INPUT_TYPE type); + static JOYSTICK::INPUT_TYPE TranslateInputType(const std::string& strType); + + /*! + * \brief Translate a keyboard symbol to a Kodi key code + * + * \param symbol The key's symbol, defined in the kodi-game-controllers project + * + * \return The layout-independent keycode associated with the key + */ + static KEYBOARD::KeySymbol TranslateKeysym(const std::string& symbol); + + /*! + * \brief Translate a Kodi key code to a keyboard symbol + * + * \param keycode The Kodi key code + * + * \return The key's symbol, or an empty string if no symbol is defined for the keycode + */ + static const char* TranslateKeycode(KEYBOARD::KeySymbol keycode); +}; + +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/ControllerTypes.h b/xbmc/games/controllers/ControllerTypes.h new file mode 100644 index 0000000..838a07d --- /dev/null +++ b/xbmc/games/controllers/ControllerTypes.h @@ -0,0 +1,33 @@ +/* + * 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 <memory> +#include <vector> + +namespace KODI +{ +namespace GAME +{ +class CController; +using ControllerPtr = std::shared_ptr<CController>; +using ControllerVector = std::vector<ControllerPtr>; + +/*! + * \brief Type of input provided by a hardware or controller port + */ +enum class PORT_TYPE +{ + UNKNOWN, + KEYBOARD, + MOUSE, + CONTROLLER, +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/DefaultController.cpp b/xbmc/games/controllers/DefaultController.cpp new file mode 100644 index 0000000..58bb464 --- /dev/null +++ b/xbmc/games/controllers/DefaultController.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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 "DefaultController.h" + +using namespace KODI; +using namespace GAME; + +const char* CDefaultController::FEATURE_A = "a"; +const char* CDefaultController::FEATURE_B = "b"; +const char* CDefaultController::FEATURE_X = "x"; +const char* CDefaultController::FEATURE_Y = "y"; +const char* CDefaultController::FEATURE_START = "start"; +const char* CDefaultController::FEATURE_BACK = "back"; +const char* CDefaultController::FEATURE_GUIDE = "guide"; +const char* CDefaultController::FEATURE_UP = "up"; +const char* CDefaultController::FEATURE_RIGHT = "right"; +const char* CDefaultController::FEATURE_DOWN = "down"; +const char* CDefaultController::FEATURE_LEFT = "left"; +const char* CDefaultController::FEATURE_LEFT_THUMB = "leftthumb"; +const char* CDefaultController::FEATURE_RIGHT_THUMB = "rightthumb"; +const char* CDefaultController::FEATURE_LEFT_BUMPER = "leftbumper"; +const char* CDefaultController::FEATURE_RIGHT_BUMPER = "rightbumper"; +const char* CDefaultController::FEATURE_LEFT_TRIGGER = "lefttrigger"; +const char* CDefaultController::FEATURE_RIGHT_TRIGGER = "righttrigger"; +const char* CDefaultController::FEATURE_LEFT_STICK = "leftstick"; +const char* CDefaultController::FEATURE_RIGHT_STICK = "rightstick"; +const char* CDefaultController::FEATURE_LEFT_MOTOR = "leftmotor"; +const char* CDefaultController::FEATURE_RIGHT_MOTOR = "rightmotor"; diff --git a/xbmc/games/controllers/DefaultController.h b/xbmc/games/controllers/DefaultController.h new file mode 100644 index 0000000..f82ce49 --- /dev/null +++ b/xbmc/games/controllers/DefaultController.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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 GAME +{ +class CDefaultController +{ +public: + // Face buttons + static const char* FEATURE_A; + static const char* FEATURE_B; + static const char* FEATURE_X; + static const char* FEATURE_Y; + static const char* FEATURE_START; + static const char* FEATURE_BACK; + static const char* FEATURE_GUIDE; + static const char* FEATURE_UP; + static const char* FEATURE_RIGHT; + static const char* FEATURE_DOWN; + static const char* FEATURE_LEFT; + static const char* FEATURE_LEFT_THUMB; + static const char* FEATURE_RIGHT_THUMB; + + // Shoulder buttons + static const char* FEATURE_LEFT_BUMPER; + static const char* FEATURE_RIGHT_BUMPER; + + // Triggers + static const char* FEATURE_LEFT_TRIGGER; + static const char* FEATURE_RIGHT_TRIGGER; + + // Analog sticks + static const char* FEATURE_LEFT_STICK; + static const char* FEATURE_RIGHT_STICK; + + // Haptics + static const char* FEATURE_LEFT_MOTOR; + static const char* FEATURE_RIGHT_MOTOR; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/dialogs/CMakeLists.txt b/xbmc/games/controllers/dialogs/CMakeLists.txt new file mode 100644 index 0000000..e40e60e --- /dev/null +++ b/xbmc/games/controllers/dialogs/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES ControllerInstaller.cpp + ControllerSelect.cpp + GUIDialogAxisDetection.cpp + GUIDialogButtonCapture.cpp + GUIDialogIgnoreInput.cpp +) + +set(HEADERS ControllerInstaller.h + ControllerSelect.h + GUIDialogAxisDetection.h + GUIDialogButtonCapture.h + GUIDialogIgnoreInput.h +) + +core_add_library(games_controller_dialogs) diff --git a/xbmc/games/controllers/dialogs/ControllerInstaller.cpp b/xbmc/games/controllers/dialogs/ControllerInstaller.cpp new file mode 100644 index 0000000..9b0fe6f --- /dev/null +++ b/xbmc/games/controllers/dialogs/ControllerInstaller.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 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 "ControllerInstaller.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "addons/Addon.h" +#include "addons/AddonInstaller.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonType.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +using namespace KODI; +using namespace GAME; + +CControllerInstaller::CControllerInstaller() : CThread("ControllerInstaller") +{ +} + +void CControllerInstaller::Process() +{ + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui == nullptr) + return; + + CGUIWindowManager& windowManager = gui->GetWindowManager(); + + auto pSelectDialog = windowManager.GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (pSelectDialog == nullptr) + return; + + auto pProgressDialog = windowManager.GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (pProgressDialog == nullptr) + return; + + ADDON::VECADDONS installableAddons; + CServiceBroker::GetAddonMgr().GetInstallableAddons(installableAddons, + ADDON::AddonType::GAME_CONTROLLER); + if (installableAddons.empty()) + { + // "Controller profiles" + // "All available controller profiles are installed." + MESSAGING::HELPERS::ShowOKDialogText(CVariant{35050}, CVariant{35062}); + return; + } + + CLog::Log(LOGDEBUG, "Controller installer: Found {} controller add-ons", + installableAddons.size()); + + CFileItemList items; + for (const auto& addon : installableAddons) + { + CFileItemPtr item(new CFileItem(addon->Name())); + item->SetArt("icon", addon->Icon()); + items.Add(std::move(item)); + } + + pSelectDialog->Reset(); + pSelectDialog->SetHeading(39020); // "The following additional add-ons will be installed" + pSelectDialog->SetUseDetails(true); + pSelectDialog->EnableButton(true, 186); // "OK"" + for (const auto& it : items) + pSelectDialog->Add(*it); + pSelectDialog->Open(); + + if (!pSelectDialog->IsButtonPressed()) + { + CLog::Log(LOGDEBUG, "Controller installer: User cancelled installation dialog"); + return; + } + + CLog::Log(LOGDEBUG, "Controller installer: Installing {} controller add-ons", + installableAddons.size()); + + pProgressDialog->SetHeading(CVariant{24086}); // "Installing add-on..." + pProgressDialog->SetLine(0, CVariant{""}); + pProgressDialog->SetLine(1, CVariant{""}); + pProgressDialog->SetLine(2, CVariant{""}); + + pProgressDialog->Open(); + + unsigned int installedCount = 0; + while (installedCount < installableAddons.size()) + { + const auto& addon = installableAddons[installedCount]; + + // Set dialog text + const std::string& progressTemplate = g_localizeStrings.Get(24057); // "Installing {0:s}..." + const std::string progressText = StringUtils::Format(progressTemplate, addon->Name()); + pProgressDialog->SetLine(0, CVariant{progressText}); + + // Set dialog percentage + const unsigned int percentage = + 100 * (installedCount + 1) / static_cast<unsigned int>(installableAddons.size()); + pProgressDialog->SetPercentage(percentage); + + if (!ADDON::CAddonInstaller::GetInstance().InstallOrUpdate( + addon->ID(), ADDON::BackgroundJob::CHOICE_NO, ADDON::ModalJob::CHOICE_NO)) + { + CLog::Log(LOGERROR, "Controller installer: Failed to install {}", addon->ID()); + // "Error" + // "Failed to install add-on." + MESSAGING::HELPERS::ShowOKDialogText(257, 35256); + break; + } + + if (pProgressDialog->IsCanceled()) + { + CLog::Log(LOGDEBUG, "Controller installer: User cancelled add-on installation"); + break; + } + + if (windowManager.GetActiveWindowOrDialog() != WINDOW_DIALOG_PROGRESS) + { + CLog::Log(LOGDEBUG, "Controller installer: Progress dialog is hidden, cancelling"); + break; + } + + installedCount++; + } + + CLog::Log(LOGDEBUG, "Controller window: Installed {} controller add-ons", installedCount); + pProgressDialog->Close(); +} diff --git a/xbmc/games/controllers/dialogs/ControllerInstaller.h b/xbmc/games/controllers/dialogs/ControllerInstaller.h new file mode 100644 index 0000000..9863e27 --- /dev/null +++ b/xbmc/games/controllers/dialogs/ControllerInstaller.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 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 GAME +{ +class CControllerInstaller : public CThread +{ +public: + CControllerInstaller(); + ~CControllerInstaller() override = default; + +protected: + // implementation of CThread + void Process() override; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/dialogs/ControllerSelect.cpp b/xbmc/games/controllers/dialogs/ControllerSelect.cpp new file mode 100644 index 0000000..2ef9941 --- /dev/null +++ b/xbmc/games/controllers/dialogs/ControllerSelect.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2021 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 "ControllerSelect.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogSelect.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerLayout.h" +#include "games/controllers/ControllerTypes.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "utils/Variant.h" +#include "utils/log.h" + +using namespace KODI; +using namespace GAME; + +CControllerSelect::CControllerSelect() : CThread("ControllerSelect") +{ +} + +CControllerSelect::~CControllerSelect() = default; + +void CControllerSelect::Initialize(ControllerVector controllers, + ControllerPtr defaultController, + bool showDisconnect, + const std::function<void(ControllerPtr)>& callback) +{ + // Validate parameters + if (!callback) + return; + + // Stop thread and reset state + Deinitialize(); + + // Initialize state + m_controllers = std::move(controllers); + m_defaultController = std::move(defaultController); + m_showDisconnect = showDisconnect; + m_callback = callback; + + // Create thread + Create(false); +} + +void CControllerSelect::Deinitialize() +{ + // Stop thread + StopThread(true); + + // Reset state + m_controllers.clear(); + m_defaultController.reset(); + m_showDisconnect = true; + m_callback = nullptr; +} + +void CControllerSelect::Process() +{ + // Select first controller by default + unsigned int initialSelected = 0; + + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui == nullptr) + return; + + CGUIWindowManager& windowManager = gui->GetWindowManager(); + + auto pSelectDialog = windowManager.GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (pSelectDialog == nullptr) + return; + + CLog::Log(LOGDEBUG, "Controller select: Showing dialog for {} controllers", m_controllers.size()); + + CFileItemList items; + for (const ControllerPtr& controller : m_controllers) + { + CFileItemPtr item(new CFileItem(controller->Layout().Label())); + item->SetArt("icon", controller->Layout().ImagePath()); + items.Add(std::move(item)); + + // Check if a specified controller should be selected by default + if (m_defaultController && m_defaultController->ID() == controller->ID()) + initialSelected = items.Size() - 1; + } + + if (m_showDisconnect) + { + // Add a button to disconnect the port + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(13298))); // "Disconnected" + item->SetArt("icon", "DefaultAddonNone.png"); + items.Add(std::move(item)); + + // Check if the disconnect button should be selected by default + if (!m_defaultController) + initialSelected = items.Size() - 1; + } + + pSelectDialog->Reset(); + pSelectDialog->SetHeading(CVariant{35113}); // "Select a Controller" + pSelectDialog->SetUseDetails(true); + pSelectDialog->EnableButton(false, 186); // "OK"" + pSelectDialog->SetButtonFocus(false); + for (const auto& it : items) + pSelectDialog->Add(*it); + pSelectDialog->SetSelected(static_cast<int>(initialSelected)); + pSelectDialog->Open(); + + // If the thread was stopped, exit early + if (m_bStop) + return; + + if (pSelectDialog->IsConfirmed()) + { + ControllerPtr resultController; + + const int selected = pSelectDialog->GetSelectedItem(); + if (0 <= selected && selected < static_cast<int>(m_controllers.size())) + resultController = m_controllers.at(selected); + + // Fire a callback with the result + m_callback(resultController); + } +} diff --git a/xbmc/games/controllers/dialogs/ControllerSelect.h b/xbmc/games/controllers/dialogs/ControllerSelect.h new file mode 100644 index 0000000..83a1b41 --- /dev/null +++ b/xbmc/games/controllers/dialogs/ControllerSelect.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 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 "games/controllers/ControllerTypes.h" +#include "threads/Thread.h" + +#include <functional> + +namespace KODI +{ +namespace GAME +{ +class CControllerSelect : public CThread +{ +public: + CControllerSelect(); + ~CControllerSelect() override; + + void Initialize(ControllerVector controllers, + ControllerPtr defaultController, + bool showDisconnect, + const std::function<void(ControllerPtr)>& callback); + void Deinitialize(); + +protected: + // Implementation of CThread + void Process() override; + +private: + // State parameters + ControllerVector m_controllers; + ControllerPtr m_defaultController; + bool m_showDisconnect = true; + std::function<void(ControllerPtr)> m_callback; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp new file mode 100644 index 0000000..2052a4c --- /dev/null +++ b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp @@ -0,0 +1,84 @@ +/* + * 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 "GUIDialogAxisDetection.h" + +#include "guilib/LocalizeStrings.h" +#include "input/joysticks/DriverPrimitive.h" +#include "input/joysticks/JoystickTranslator.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "utils/StringUtils.h" + +#include <algorithm> + +using namespace KODI; +using namespace GAME; + +std::string CGUIDialogAxisDetection::GetDialogText() +{ + // "Press all analog buttons now to detect them:[CR][CR]%s" + const std::string& dialogText = g_localizeStrings.Get(35020); + + std::vector<std::string> primitives; + + for (const auto& axisEntry : m_detectedAxes) + { + JOYSTICK::CDriverPrimitive axis(axisEntry.second, 0, JOYSTICK::SEMIAXIS_DIRECTION::POSITIVE, 1); + primitives.emplace_back(JOYSTICK::CJoystickTranslator::GetPrimitiveName(axis)); + } + + return StringUtils::Format(dialogText, StringUtils::Join(primitives, " | ")); +} + +std::string CGUIDialogAxisDetection::GetDialogHeader() +{ + return g_localizeStrings.Get(35058); // "Controller Configuration" +} + +bool CGUIDialogAxisDetection::MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) +{ + if (primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS) + AddAxis(buttonMap->Location(), primitive.Index()); + + return true; +} + +bool CGUIDialogAxisDetection::AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const +{ + switch (type) + { + case JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS: + return true; + default: + break; + } + + return false; +} + +void CGUIDialogAxisDetection::OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, + unsigned int axisIndex) +{ + AddAxis(buttonMap->Location(), axisIndex); +} + +void CGUIDialogAxisDetection::AddAxis(const std::string& deviceLocation, unsigned int axisIndex) +{ + auto it = std::find_if(m_detectedAxes.begin(), m_detectedAxes.end(), + [&deviceLocation, axisIndex](const AxisEntry& axis) { + return axis.first == deviceLocation && axis.second == axisIndex; + }); + + if (it == m_detectedAxes.end()) + { + m_detectedAxes.emplace_back(std::make_pair(deviceLocation, axisIndex)); + m_captureEvent.Set(); + } +} diff --git a/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h new file mode 100644 index 0000000..4ca0d27 --- /dev/null +++ b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h @@ -0,0 +1,54 @@ +/* + * 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 "GUIDialogButtonCapture.h" + +#include <string> +#include <utility> +#include <vector> + +namespace KODI +{ +namespace GAME +{ +class CGUIDialogAxisDetection : public CGUIDialogButtonCapture +{ +public: + CGUIDialogAxisDetection() = default; + + ~CGUIDialogAxisDetection() override = default; + + // specialization of IButtonMapper via CGUIDialogButtonCapture + bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override; + void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override; + +protected: + // implementation of CGUIDialogButtonCapture + std::string GetDialogText() override; + std::string GetDialogHeader() override; + bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) override; + void OnClose(bool bAccepted) override {} + +private: + void AddAxis(const std::string& deviceLocation, unsigned int axisIndex); + + // Axis types + using DeviceName = std::string; + using AxisIndex = unsigned int; + using AxisEntry = std::pair<DeviceName, AxisIndex>; + using AxisVector = std::vector<AxisEntry>; + + // Axis detection + AxisVector m_detectedAxes; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp new file mode 100644 index 0000000..9bf8dbc --- /dev/null +++ b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.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 "GUIDialogButtonCapture.h" + +#include "ServiceBroker.h" +#include "games/controllers/ControllerIDs.h" +#include "input/IKeymap.h" +#include "input/actions/ActionIDs.h" +#include "input/joysticks/JoystickUtils.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "peripherals/Peripherals.h" +#include "utils/Variant.h" + +#include <algorithm> +#include <iterator> + +using namespace KODI; +using namespace GAME; +using namespace KODI::MESSAGING; + +CGUIDialogButtonCapture::CGUIDialogButtonCapture() : CThread("ButtonCaptureDlg") +{ +} + +std::string CGUIDialogButtonCapture::ControllerID(void) const +{ + return DEFAULT_CONTROLLER_ID; +} + +void CGUIDialogButtonCapture::Show() +{ + if (!IsRunning()) + { + InstallHooks(); + + Create(); + + bool bAccepted = + HELPERS::ShowOKDialogText(CVariant{GetDialogHeader()}, CVariant{GetDialogText()}); + + StopThread(false); + + m_captureEvent.Set(); + + OnClose(bAccepted); + + RemoveHooks(); + } +} + +void CGUIDialogButtonCapture::Process() +{ + while (!m_bStop) + { + m_captureEvent.Wait(); + + if (m_bStop) + break; + + //! @todo Move to rendering thread when there is a rendering thread + HELPERS::UpdateOKDialogText(CVariant{35013}, CVariant{GetDialogText()}); + } +} + +bool CGUIDialogButtonCapture::MapPrimitive(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) +{ + if (m_bStop) + return false; + + // First check to see if driver primitive closes the dialog + if (keymap && keymap->ControllerID() == buttonMap->ControllerID()) + { + std::string feature; + if (buttonMap->GetFeature(primitive, feature)) + { + const auto& actions = + keymap->GetActions(JOYSTICK::CJoystickUtils::MakeKeyName(feature)).actions; + if (!actions.empty()) + { + switch (actions.begin()->actionId) + { + case ACTION_SELECT_ITEM: + case ACTION_NAV_BACK: + case ACTION_PREVIOUS_MENU: + return false; + default: + break; + } + } + } + } + + return MapPrimitiveInternal(buttonMap, keymap, primitive); +} + +void CGUIDialogButtonCapture::InstallHooks(void) +{ + CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this); + CServiceBroker::GetPeripherals().RegisterObserver(this); +} + +void CGUIDialogButtonCapture::RemoveHooks(void) +{ + CServiceBroker::GetPeripherals().UnregisterObserver(this); + CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this); +} + +void CGUIDialogButtonCapture::Notify(const Observable& obs, const ObservableMessage msg) +{ + switch (msg) + { + case ObservableMessagePeripheralsChanged: + { + CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this); + CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this); + break; + } + default: + break; + } +} diff --git a/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h new file mode 100644 index 0000000..97795c6 --- /dev/null +++ b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h @@ -0,0 +1,65 @@ +/* + * 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/IButtonMapper.h" +#include "threads/Event.h" +#include "threads/Thread.h" +#include "utils/Observer.h" + +#include <string> +#include <vector> + +namespace KODI +{ +namespace GAME +{ +class CGUIDialogButtonCapture : public JOYSTICK::IButtonMapper, public Observer, protected CThread +{ +public: + CGUIDialogButtonCapture(); + + ~CGUIDialogButtonCapture() override = default; + + // implementation of IButtonMapper + std::string ControllerID() const override; + bool NeedsCooldown() const override { return false; } + bool MapPrimitive(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) override; + void OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion) override {} + void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override {} + + // implementation of Observer + void Notify(const Observable& obs, const ObservableMessage msg) override; + + /*! + * \brief Show the dialog + */ + void Show(); + +protected: + // implementation of CThread + void Process() override; + + virtual std::string GetDialogText() = 0; + virtual std::string GetDialogHeader() = 0; + virtual bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) = 0; + virtual void OnClose(bool bAccepted) = 0; + + CEvent m_captureEvent; + +private: + void InstallHooks(); + void RemoveHooks(); +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp new file mode 100644 index 0000000..b5f28d6 --- /dev/null +++ b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp @@ -0,0 +1,132 @@ +/* + * 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 "GUIDialogIgnoreInput.h" + +#include "guilib/LocalizeStrings.h" +#include "input/joysticks/JoystickTranslator.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "input/joysticks/interfaces/IButtonMapCallback.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> + +using namespace KODI; +using namespace GAME; + +bool CGUIDialogIgnoreInput::AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const +{ + switch (type) + { + case JOYSTICK::PRIMITIVE_TYPE::BUTTON: + case JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS: + return true; + default: + break; + } + + return false; +} + +std::string CGUIDialogIgnoreInput::GetDialogText() +{ + // "Some controllers have buttons and axes that interfere with mapping. Press + // these now to disable them:[CR]%s" + const std::string& dialogText = g_localizeStrings.Get(35014); + + std::vector<std::string> primitives; + + std::transform(m_capturedPrimitives.begin(), m_capturedPrimitives.end(), + std::back_inserter(primitives), [](const JOYSTICK::CDriverPrimitive& primitive) { + return JOYSTICK::CJoystickTranslator::GetPrimitiveName(primitive); + }); + + return StringUtils::Format(dialogText, StringUtils::Join(primitives, " | ")); +} + +std::string CGUIDialogIgnoreInput::GetDialogHeader() +{ + + return g_localizeStrings.Get(35019); // "Ignore input" +} + +bool CGUIDialogIgnoreInput::MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) +{ + // Check if we have already started capturing primitives for a device + const bool bHasDevice = !m_location.empty(); + + // If a primitive comes from a different device, ignore it + if (bHasDevice && m_location != buttonMap->Location()) + { + CLog::Log(LOGDEBUG, "{}: ignoring input from device {}", buttonMap->ControllerID(), + buttonMap->Location()); + return false; + } + + if (!bHasDevice) + { + CLog::Log(LOGDEBUG, "{}: capturing input for device {}", buttonMap->ControllerID(), + buttonMap->Location()); + m_location = buttonMap->Location(); + } + + if (AddPrimitive(primitive)) + { + buttonMap->SetIgnoredPrimitives(m_capturedPrimitives); + m_captureEvent.Set(); + } + + return true; +} + +void CGUIDialogIgnoreInput::OnClose(bool bAccepted) +{ + for (auto& callback : ButtonMapCallbacks()) + { + if (bAccepted) + { + // See documentation of IButtonMapCallback::ResetIgnoredPrimitives() + // for why this call is needed + if (m_location.empty()) + callback.second->ResetIgnoredPrimitives(); + + if (m_location.empty() || m_location == callback.first) + callback.second->SaveButtonMap(); + } + else + callback.second->RevertButtonMap(); + } +} + +bool CGUIDialogIgnoreInput::AddPrimitive(const JOYSTICK::CDriverPrimitive& primitive) +{ + bool bValid = false; + + if (primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::BUTTON || + primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS) + { + auto PrimitiveMatch = [&primitive](const JOYSTICK::CDriverPrimitive& other) { + return primitive.Type() == other.Type() && primitive.Index() == other.Index(); + }; + + bValid = std::find_if(m_capturedPrimitives.begin(), m_capturedPrimitives.end(), + PrimitiveMatch) == m_capturedPrimitives.end(); + } + + if (bValid) + { + m_capturedPrimitives.emplace_back(primitive); + return true; + } + + return false; +} diff --git a/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h new file mode 100644 index 0000000..e1d3922 --- /dev/null +++ b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h @@ -0,0 +1,47 @@ +/* + * 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 "GUIDialogButtonCapture.h" +#include "input/joysticks/DriverPrimitive.h" + +#include <string> +#include <vector> + +namespace KODI +{ +namespace GAME +{ +class CGUIDialogIgnoreInput : public CGUIDialogButtonCapture +{ +public: + CGUIDialogIgnoreInput() = default; + + ~CGUIDialogIgnoreInput() override = default; + + // specialization of IButtonMapper via CGUIDialogButtonCapture + bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override; + +protected: + // implementation of CGUIDialogButtonCapture + std::string GetDialogText() override; + std::string GetDialogHeader() override; + bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) override; + void OnClose(bool bAccepted) override; + +private: + bool AddPrimitive(const JOYSTICK::CDriverPrimitive& primitive); + + std::string m_location; + std::vector<JOYSTICK::CDriverPrimitive> m_capturedPrimitives; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/CMakeLists.txt b/xbmc/games/controllers/guicontrols/CMakeLists.txt new file mode 100644 index 0000000..e6babcc --- /dev/null +++ b/xbmc/games/controllers/guicontrols/CMakeLists.txt @@ -0,0 +1,28 @@ +set(SOURCES GUICardinalFeatureButton.cpp + GUIControllerButton.cpp + GUIFeatureButton.cpp + GUIFeatureControls.cpp + GUIFeatureFactory.cpp + GUIFeatureTranslator.cpp + GUIGameController.cpp + GUIScalarFeatureButton.cpp + GUISelectKeyButton.cpp + GUIThrottleButton.cpp + GUIWheelButton.cpp +) + +set(HEADERS GUICardinalFeatureButton.h + GUIControllerButton.h + GUIControlTypes.h + GUIFeatureButton.h + GUIFeatureControls.h + GUIFeatureFactory.h + GUIFeatureTranslator.h + GUIGameController.h + GUIScalarFeatureButton.h + GUISelectKeyButton.h + GUIThrottleButton.h + GUIWheelButton.h +) + +core_add_library(games_controller_guicontrols) diff --git a/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp new file mode 100644 index 0000000..3c8732c --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp @@ -0,0 +1,100 @@ +/* + * 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 "GUICardinalFeatureButton.h" + +#include "guilib/LocalizeStrings.h" + +#include <string> + +using namespace KODI; +using namespace GAME; + +CGUICardinalFeatureButton::CGUICardinalFeatureButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index) + : CGUIFeatureButton(buttonTemplate, wizard, feature, index) +{ + Reset(); +} + +bool CGUICardinalFeatureButton::PromptForInput(CEvent& waitEvent) +{ + using namespace JOYSTICK; + + bool bInterrupted = false; + + // Get the prompt for the current analog stick direction + std::string strPrompt; + std::string strWarn; + switch (m_state) + { + case STATE::CARDINAL_DIRECTION_UP: + strPrompt = g_localizeStrings.Get(35092); // "Move %s up" + strWarn = g_localizeStrings.Get(35093); // "Move %s up (%d)" + break; + case STATE::CARDINAL_DIRECTION_RIGHT: + strPrompt = g_localizeStrings.Get(35096); // "Move %s right" + strWarn = g_localizeStrings.Get(35097); // "Move %s right (%d)" + break; + case STATE::CARDINAL_DIRECTION_DOWN: + strPrompt = g_localizeStrings.Get(35094); // "Move %s down" + strWarn = g_localizeStrings.Get(35095); // "Move %s down (%d)" + break; + case STATE::CARDINAL_DIRECTION_LEFT: + strPrompt = g_localizeStrings.Get(35098); // "Move %s left" + strWarn = g_localizeStrings.Get(35099); // "Move %s left (%d)" + break; + default: + break; + } + + if (!strPrompt.empty()) + { + bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent); + + if (!bInterrupted) + m_state = STATE::FINISHED; // Not interrupted, must have timed out + else + m_state = GetNextState(m_state); // Interrupted by input, proceed + } + + return bInterrupted; +} + +bool CGUICardinalFeatureButton::IsFinished(void) const +{ + return m_state >= STATE::FINISHED; +} + +KODI::INPUT::CARDINAL_DIRECTION CGUICardinalFeatureButton::GetCardinalDirection(void) const +{ + using namespace INPUT; + + switch (m_state) + { + case STATE::CARDINAL_DIRECTION_UP: + return CARDINAL_DIRECTION::UP; + case STATE::CARDINAL_DIRECTION_RIGHT: + return CARDINAL_DIRECTION::RIGHT; + case STATE::CARDINAL_DIRECTION_DOWN: + return CARDINAL_DIRECTION::DOWN; + case STATE::CARDINAL_DIRECTION_LEFT: + return CARDINAL_DIRECTION::LEFT; + default: + break; + } + + return CARDINAL_DIRECTION::NONE; +} + +void CGUICardinalFeatureButton::Reset(void) +{ + m_state = STATE::CARDINAL_DIRECTION_UP; +} diff --git a/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h new file mode 100644 index 0000000..48eb0e8 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h @@ -0,0 +1,49 @@ +/* + * 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 "GUIFeatureButton.h" + +namespace KODI +{ +namespace GAME +{ +class CGUICardinalFeatureButton : public CGUIFeatureButton +{ +public: + CGUICardinalFeatureButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index); + + ~CGUICardinalFeatureButton() override = default; + + // implementation of IFeatureButton + bool PromptForInput(CEvent& waitEvent) override; + bool IsFinished() const override; + INPUT::CARDINAL_DIRECTION GetCardinalDirection() const override; + void Reset() override; + +private: + enum class STATE + { + CARDINAL_DIRECTION_UP, + CARDINAL_DIRECTION_RIGHT, + CARDINAL_DIRECTION_DOWN, + CARDINAL_DIRECTION_LEFT, + FINISHED, + }; + + STATE m_state; +}; + +using CGUIAnalogStickButton = CGUICardinalFeatureButton; +using CGUIRelativePointerButton = CGUICardinalFeatureButton; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIControlTypes.h b/xbmc/games/controllers/guicontrols/GUIControlTypes.h new file mode 100644 index 0000000..bf91b6e --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIControlTypes.h @@ -0,0 +1,29 @@ +/* + * 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 GAME +{ +/*! + * \brief Types of button controls that can populate the feature list + */ +enum class BUTTON_TYPE +{ + UNKNOWN, + BUTTON, + ANALOG_STICK, + RELATIVE_POINTER, + WHEEL, + THROTTLE, + SELECT_KEY, +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp b/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp new file mode 100644 index 0000000..c8156ce --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp @@ -0,0 +1,26 @@ +/* + * 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 "GUIControllerButton.h" + +#include "games/controllers/windows/GUIControllerDefines.h" + +using namespace KODI; +using namespace GAME; + +CGUIControllerButton::CGUIControllerButton(const CGUIButtonControl& buttonControl, + const std::string& label, + unsigned int index) + : CGUIButtonControl(buttonControl) +{ + // Initialize CGUIButtonControl + SetLabel(label); + SetID(CONTROL_CONTROLLER_BUTTONS_START + index); + SetVisible(true); + AllocResources(); +} diff --git a/xbmc/games/controllers/guicontrols/GUIControllerButton.h b/xbmc/games/controllers/guicontrols/GUIControllerButton.h new file mode 100644 index 0000000..ebdc924 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIControllerButton.h @@ -0,0 +1,29 @@ +/* + * 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 "guilib/GUIButtonControl.h" + +#include <string> + +namespace KODI +{ +namespace GAME +{ +class CGUIControllerButton : public CGUIButtonControl +{ +public: + CGUIControllerButton(const CGUIButtonControl& buttonControl, + const std::string& label, + unsigned int index); + + ~CGUIControllerButton() override = default; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp new file mode 100644 index 0000000..0f1bfd6 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp @@ -0,0 +1,86 @@ +/* + * 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 "GUIFeatureButton.h" + +#include "ServiceBroker.h" +#include "games/controllers/windows/GUIControllerDefines.h" +#include "guilib/GUIMessage.h" +#include "guilib/WindowIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "threads/Event.h" +#include "utils/StringUtils.h" + +using namespace KODI; +using namespace GAME; +using namespace std::chrono_literals; + +CGUIFeatureButton::CGUIFeatureButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index) + : CGUIButtonControl(buttonTemplate), m_feature(feature), m_wizard(wizard) +{ + // Initialize CGUIButtonControl + SetLabel(m_feature.Label()); + SetID(CONTROL_FEATURE_BUTTONS_START + index); + SetVisible(true); + AllocResources(); +} + +void CGUIFeatureButton::OnUnFocus(void) +{ + CGUIButtonControl::OnUnFocus(); + m_wizard->OnUnfocus(this); +} + +bool CGUIFeatureButton::DoPrompt(const std::string& strPrompt, + const std::string& strWarn, + const std::string& strFeature, + CEvent& waitEvent) +{ + bool bInterrupted = false; + + if (!HasFocus()) + { + CGUIMessage msgFocus(GUI_MSG_SETFOCUS, GetID(), GetID()); + CServiceBroker::GetAppMessenger()->SendGUIMessage(msgFocus, WINDOW_INVALID, false); + } + + CGUIMessage msgLabel(GUI_MSG_LABEL_SET, GetID(), GetID()); + + for (unsigned int i = 0; i < COUNTDOWN_DURATION_SEC; i++) + { + const unsigned int secondsElapsed = i; + const unsigned int secondsRemaining = COUNTDOWN_DURATION_SEC - i; + + const bool bWarn = secondsElapsed >= WAIT_TO_WARN_SEC; + + std::string strLabel; + + if (bWarn) + strLabel = StringUtils::Format(strWarn, strFeature, secondsRemaining); + else + strLabel = StringUtils::Format(strPrompt, strFeature, secondsRemaining); + + msgLabel.SetLabel(strLabel); + CServiceBroker::GetAppMessenger()->SendGUIMessage(msgLabel, WINDOW_INVALID, false); + + waitEvent.Reset(); + bInterrupted = waitEvent.Wait(1000ms); // Wait 1 second + + if (bInterrupted) + break; + } + + // Reset label + msgLabel.SetLabel(m_feature.Label()); + CServiceBroker::GetAppMessenger()->SendGUIMessage(msgLabel, WINDOW_INVALID, false); + + return bInterrupted; +} diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureButton.h b/xbmc/games/controllers/guicontrols/GUIFeatureButton.h new file mode 100644 index 0000000..b69f521 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIFeatureButton.h @@ -0,0 +1,68 @@ +/* + * 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 "games/controllers/input/PhysicalFeature.h" +#include "games/controllers/windows/IConfigurationWindow.h" +#include "guilib/GUIButtonControl.h" + +#include <string> + +namespace KODI +{ +namespace GAME +{ +class CGUIFeatureButton : public CGUIButtonControl, public IFeatureButton +{ +public: + CGUIFeatureButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index); + + ~CGUIFeatureButton() override = default; + + // implementation of CGUIControl via CGUIButtonControl + void OnUnFocus() override; + + // partial implementation of IFeatureButton + const CPhysicalFeature& Feature() const override { return m_feature; } + INPUT::CARDINAL_DIRECTION GetCardinalDirection() const override + { + return INPUT::CARDINAL_DIRECTION::NONE; + } + JOYSTICK::WHEEL_DIRECTION GetWheelDirection() const override + { + return JOYSTICK::WHEEL_DIRECTION::NONE; + } + JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection() const override + { + return JOYSTICK::THROTTLE_DIRECTION::NONE; + } + +protected: + bool DoPrompt(const std::string& strPrompt, + const std::string& strWarn, + const std::string& strFeature, + CEvent& waitEvent); + + // FSM helper + template<typename T> + T GetNextState(T state) + { + return static_cast<T>(static_cast<int>(state) + 1); + } + + const CPhysicalFeature m_feature; + +private: + IConfigurationWizard* const m_wizard; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp new file mode 100644 index 0000000..63d4757 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp @@ -0,0 +1,36 @@ +/* + * 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 "GUIFeatureControls.h" + +#include "games/controllers/windows/GUIControllerDefines.h" + +using namespace KODI; +using namespace GAME; + +CGUIFeatureGroupTitle::CGUIFeatureGroupTitle(const CGUILabelControl& groupTitleTemplate, + const std::string& groupName, + unsigned int buttonIndex) + : CGUILabelControl(groupTitleTemplate) +{ + // Initialize CGUILabelControl + SetLabel(groupName); + SetID(CONTROL_FEATURE_GROUPS_START + buttonIndex); + SetVisible(true); + AllocResources(); +} + +CGUIFeatureSeparator::CGUIFeatureSeparator(const CGUIImage& separatorTemplate, + unsigned int buttonIndex) + : CGUIImage(separatorTemplate) +{ + // Initialize CGUIImage + SetID(CONTROL_FEATURE_SEPARATORS_START + buttonIndex); + SetVisible(true); + AllocResources(); +} diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureControls.h b/xbmc/games/controllers/guicontrols/GUIFeatureControls.h new file mode 100644 index 0000000..c72a11a --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIFeatureControls.h @@ -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. + */ + +#pragma once + +#include "guilib/GUIImage.h" +#include "guilib/GUILabelControl.h" + +#include <string> + +namespace KODI +{ +namespace GAME +{ +class CGUIFeatureGroupTitle : public CGUILabelControl +{ +public: + CGUIFeatureGroupTitle(const CGUILabelControl& groupTitleTemplate, + const std::string& groupName, + unsigned int buttonIndex); + + ~CGUIFeatureGroupTitle() override = default; +}; + +class CGUIFeatureSeparator : public CGUIImage +{ +public: + CGUIFeatureSeparator(const CGUIImage& separatorTemplate, unsigned int buttonIndex); + + ~CGUIFeatureSeparator() override = default; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp new file mode 100644 index 0000000..2cce033 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp @@ -0,0 +1,51 @@ +/* + * 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 "GUIFeatureFactory.h" + +#include "GUICardinalFeatureButton.h" +#include "GUIScalarFeatureButton.h" +#include "GUISelectKeyButton.h" +#include "GUIThrottleButton.h" +#include "GUIWheelButton.h" + +using namespace KODI; +using namespace GAME; + +CGUIButtonControl* CGUIFeatureFactory::CreateButton(BUTTON_TYPE type, + const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index) +{ + switch (type) + { + case BUTTON_TYPE::BUTTON: + return new CGUIScalarFeatureButton(buttonTemplate, wizard, feature, index); + + case BUTTON_TYPE::ANALOG_STICK: + return new CGUIAnalogStickButton(buttonTemplate, wizard, feature, index); + + case BUTTON_TYPE::WHEEL: + return new CGUIWheelButton(buttonTemplate, wizard, feature, index); + + case BUTTON_TYPE::THROTTLE: + return new CGUIThrottleButton(buttonTemplate, wizard, feature, index); + + case BUTTON_TYPE::SELECT_KEY: + return new CGUISelectKeyButton(buttonTemplate, wizard, index); + + case BUTTON_TYPE::RELATIVE_POINTER: + return new CGUIRelativePointerButton(buttonTemplate, wizard, feature, index); + + default: + break; + } + + return nullptr; +} diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h new file mode 100644 index 0000000..e35070d --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h @@ -0,0 +1,37 @@ +/* + * 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 "GUIControlTypes.h" + +class CGUIButtonControl; + +namespace KODI +{ +namespace GAME +{ +class CPhysicalFeature; +class IConfigurationWizard; + +class CGUIFeatureFactory +{ +public: + /*! + * \brief Create a button of the specified type + * \param type The type of button control being created + * \return A button control, or nullptr if type is invalid + */ + static CGUIButtonControl* CreateButton(BUTTON_TYPE type, + const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index); +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp new file mode 100644 index 0000000..ab941c5 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp @@ -0,0 +1,39 @@ +/* + * 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 "GUIFeatureTranslator.h" + +using namespace KODI; +using namespace GAME; + +BUTTON_TYPE CGUIFeatureTranslator::GetButtonType(JOYSTICK::FEATURE_TYPE featureType) +{ + switch (featureType) + { + case JOYSTICK::FEATURE_TYPE::SCALAR: + case JOYSTICK::FEATURE_TYPE::KEY: + return BUTTON_TYPE::BUTTON; + + case JOYSTICK::FEATURE_TYPE::ANALOG_STICK: + return BUTTON_TYPE::ANALOG_STICK; + + case JOYSTICK::FEATURE_TYPE::RELPOINTER: + return BUTTON_TYPE::RELATIVE_POINTER; + + case JOYSTICK::FEATURE_TYPE::WHEEL: + return BUTTON_TYPE::WHEEL; + + case JOYSTICK::FEATURE_TYPE::THROTTLE: + return BUTTON_TYPE::THROTTLE; + + default: + break; + } + + return BUTTON_TYPE::UNKNOWN; +} diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h new file mode 100644 index 0000000..8e28d16 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h @@ -0,0 +1,27 @@ +/* + * 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 "GUIControlTypes.h" +#include "input/joysticks/JoystickTypes.h" + +namespace KODI +{ +namespace GAME +{ +class CGUIFeatureTranslator +{ +public: + /*! + * \brief Get the type of button control used to map the feature + */ + static BUTTON_TYPE GetButtonType(JOYSTICK::FEATURE_TYPE featureType); +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIGameController.cpp b/xbmc/games/controllers/guicontrols/GUIGameController.cpp new file mode 100644 index 0000000..8990cf2 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIGameController.cpp @@ -0,0 +1,64 @@ +/* + * 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 "GUIGameController.h" + +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerLayout.h" +#include "utils/log.h" + +#include <mutex> + +using namespace KODI; +using namespace GAME; + +CGUIGameController::CGUIGameController( + int parentID, int controlID, float posX, float posY, float width, float height) + : CGUIImage(parentID, controlID, posX, posY, width, height, CTextureInfo()) +{ + // Initialize CGUIControl + ControlType = GUICONTROL_GAMECONTROLLER; +} + +CGUIGameController::CGUIGameController(const CGUIGameController& from) : CGUIImage(from) +{ + // Initialize CGUIControl + ControlType = GUICONTROL_GAMECONTROLLER; +} + +CGUIGameController* CGUIGameController::Clone(void) const +{ + return new CGUIGameController(*this); +} + +void CGUIGameController::Render(void) +{ + CGUIImage::Render(); + + std::unique_lock<CCriticalSection> lock(m_mutex); + + if (m_currentController) + { + //! @todo Render pressed buttons + } +} + +void CGUIGameController::ActivateController(const ControllerPtr& controller) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + + if (controller && controller != m_currentController) + { + m_currentController = controller; + + lock.unlock(); + + //! @todo Sometimes this fails on window init + SetFileName(m_currentController->Layout().ImagePath()); + } +} diff --git a/xbmc/games/controllers/guicontrols/GUIGameController.h b/xbmc/games/controllers/guicontrols/GUIGameController.h new file mode 100644 index 0000000..d62819a --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIGameController.h @@ -0,0 +1,39 @@ +/* + * 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 "games/controllers/ControllerTypes.h" +#include "guilib/GUIImage.h" +#include "threads/CriticalSection.h" + +namespace KODI +{ +namespace GAME +{ +class CGUIGameController : public CGUIImage +{ +public: + CGUIGameController( + int parentID, int controlID, float posX, float posY, float width, float height); + CGUIGameController(const CGUIGameController& from); + + ~CGUIGameController() override = default; + + // implementation of CGUIControl via CGUIImage + CGUIGameController* Clone() const override; + void Render() override; + + void ActivateController(const ControllerPtr& controller); + +private: + ControllerPtr m_currentController; + CCriticalSection m_mutex; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp new file mode 100644 index 0000000..5358aea --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.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 "GUIScalarFeatureButton.h" + +#include "guilib/LocalizeStrings.h" + +#include <string> + +using namespace KODI; +using namespace GAME; + +CGUIScalarFeatureButton::CGUIScalarFeatureButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index) + : CGUIFeatureButton(buttonTemplate, wizard, feature, index) +{ + Reset(); +} + +bool CGUIScalarFeatureButton::PromptForInput(CEvent& waitEvent) +{ + bool bInterrupted = false; + + switch (m_state) + { + case STATE::NEED_INPUT: + { + const std::string& strPrompt = g_localizeStrings.Get(35090); // "Press %s" + const std::string& strWarn = g_localizeStrings.Get(35091); // "Press %s (%d)" + + bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent); + + m_state = GetNextState(m_state); + + break; + } + default: + break; + } + + return bInterrupted; +} + +bool CGUIScalarFeatureButton::IsFinished(void) const +{ + return m_state >= STATE::FINISHED; +} + +void CGUIScalarFeatureButton::Reset(void) +{ + m_state = STATE::NEED_INPUT; +} diff --git a/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h new file mode 100644 index 0000000..6505684 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h @@ -0,0 +1,42 @@ +/* + * 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 "GUIFeatureButton.h" + +namespace KODI +{ +namespace GAME +{ +class CGUIScalarFeatureButton : public CGUIFeatureButton +{ +public: + CGUIScalarFeatureButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index); + + ~CGUIScalarFeatureButton() override = default; + + // implementation of IFeatureButton + bool PromptForInput(CEvent& waitEvent) override; + bool IsFinished() const override; + void Reset() override; + +private: + enum class STATE + { + NEED_INPUT, + FINISHED, + }; + + STATE m_state; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp new file mode 100644 index 0000000..ea4a656 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp @@ -0,0 +1,87 @@ +/* + * 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 "GUISelectKeyButton.h" + +#include "guilib/LocalizeStrings.h" + +#include <string> + +using namespace KODI; +using namespace GAME; + +CGUISelectKeyButton::CGUISelectKeyButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + unsigned int index) + : CGUIFeatureButton(buttonTemplate, wizard, GetFeature(), index) +{ +} + +const CPhysicalFeature& CGUISelectKeyButton::Feature(void) const +{ + if (m_state == STATE::NEED_INPUT) + return m_selectedKey; + + return CGUIFeatureButton::Feature(); +} + +bool CGUISelectKeyButton::PromptForInput(CEvent& waitEvent) +{ + bool bInterrupted = false; + + switch (m_state) + { + case STATE::NEED_KEY: + { + const std::string& strPrompt = g_localizeStrings.Get(35169); // "Press a key" + const std::string& strWarn = g_localizeStrings.Get(35170); // "Press a key ({1:d})" + + bInterrupted = DoPrompt(strPrompt, strWarn, "", waitEvent); + + m_state = GetNextState(m_state); + + break; + } + case STATE::NEED_INPUT: + { + const std::string& strPrompt = g_localizeStrings.Get(35090); // "Press {0:s}" + const std::string& strWarn = g_localizeStrings.Get(35091); // "Press {0:s} ({1:d})" + + bInterrupted = DoPrompt(strPrompt, strWarn, m_selectedKey.Label(), waitEvent); + + m_state = GetNextState(m_state); + + break; + } + default: + break; + } + + return bInterrupted; +} + +bool CGUISelectKeyButton::IsFinished(void) const +{ + return m_state >= STATE::FINISHED; +} + +void CGUISelectKeyButton::SetKey(const CPhysicalFeature& key) +{ + m_selectedKey = key; +} + +void CGUISelectKeyButton::Reset(void) +{ + m_state = STATE::NEED_KEY; + m_selectedKey.Reset(); +} + +CPhysicalFeature CGUISelectKeyButton::GetFeature() +{ + return CPhysicalFeature(35168); // "Select key" +} diff --git a/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h new file mode 100644 index 0000000..e96f78b --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h @@ -0,0 +1,51 @@ +/* + * 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 "GUIFeatureButton.h" +#include "games/controllers/input/PhysicalFeature.h" + +namespace KODI +{ +namespace GAME +{ +class CGUISelectKeyButton : public CGUIFeatureButton +{ +public: + CGUISelectKeyButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + unsigned int index); + + ~CGUISelectKeyButton() override = default; + + // implementation of IFeatureButton + const CPhysicalFeature& Feature(void) const override; + bool AllowWizard() const override { return false; } + bool PromptForInput(CEvent& waitEvent) override; + bool IsFinished() const override; + bool NeedsKey() const override { return m_state == STATE::NEED_KEY; } + void SetKey(const CPhysicalFeature& key) override; + void Reset() override; + +private: + static CPhysicalFeature GetFeature(); + + enum class STATE + { + NEED_KEY, + NEED_INPUT, + FINISHED, + }; + + STATE m_state = STATE::NEED_KEY; + + CPhysicalFeature m_selectedKey; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp b/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp new file mode 100644 index 0000000..69f32a1 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp @@ -0,0 +1,88 @@ +/* + * 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 "GUIThrottleButton.h" + +#include "guilib/LocalizeStrings.h" + +#include <string> + +using namespace KODI; +using namespace GAME; + +CGUIThrottleButton::CGUIThrottleButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index) + : CGUIFeatureButton(buttonTemplate, wizard, feature, index) +{ + Reset(); +} + +bool CGUIThrottleButton::PromptForInput(CEvent& waitEvent) +{ + using namespace JOYSTICK; + + bool bInterrupted = false; + + // Get the prompt for the current analog stick direction + std::string strPrompt; + std::string strWarn; + switch (m_state) + { + case STATE::THROTTLE_UP: + strPrompt = g_localizeStrings.Get(35092); // "Move %s up" + strWarn = g_localizeStrings.Get(35093); // "Move %s up (%d)" + break; + case STATE::THROTTLE_DOWN: + strPrompt = g_localizeStrings.Get(35094); // "Move %s down" + strWarn = g_localizeStrings.Get(35095); // "Move %s down (%d)" + break; + default: + break; + } + + if (!strPrompt.empty()) + { + bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent); + + if (!bInterrupted) + m_state = STATE::FINISHED; // Not interrupted, must have timed out + else + m_state = GetNextState(m_state); // Interrupted by input, proceed + } + + return bInterrupted; +} + +bool CGUIThrottleButton::IsFinished(void) const +{ + return m_state >= STATE::FINISHED; +} + +JOYSTICK::THROTTLE_DIRECTION CGUIThrottleButton::GetThrottleDirection(void) const +{ + using namespace JOYSTICK; + + switch (m_state) + { + case STATE::THROTTLE_UP: + return THROTTLE_DIRECTION::UP; + case STATE::THROTTLE_DOWN: + return THROTTLE_DIRECTION::DOWN; + default: + break; + } + + return THROTTLE_DIRECTION::NONE; +} + +void CGUIThrottleButton::Reset(void) +{ + m_state = STATE::THROTTLE_UP; +} diff --git a/xbmc/games/controllers/guicontrols/GUIThrottleButton.h b/xbmc/games/controllers/guicontrols/GUIThrottleButton.h new file mode 100644 index 0000000..72b7ee3 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIThrottleButton.h @@ -0,0 +1,44 @@ +/* + * 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 "GUIFeatureButton.h" + +namespace KODI +{ +namespace GAME +{ +class CGUIThrottleButton : public CGUIFeatureButton +{ +public: + CGUIThrottleButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index); + + ~CGUIThrottleButton() override = default; + + // implementation of IFeatureButton + bool PromptForInput(CEvent& waitEvent) override; + bool IsFinished() const override; + JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection() const override; + void Reset() override; + +private: + enum class STATE + { + THROTTLE_UP, + THROTTLE_DOWN, + FINISHED, + }; + + STATE m_state; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp b/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp new file mode 100644 index 0000000..63c3819 --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp @@ -0,0 +1,88 @@ +/* + * 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 "GUIWheelButton.h" + +#include "guilib/LocalizeStrings.h" + +#include <string> + +using namespace KODI; +using namespace GAME; + +CGUIWheelButton::CGUIWheelButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index) + : CGUIFeatureButton(buttonTemplate, wizard, feature, index) +{ + Reset(); +} + +bool CGUIWheelButton::PromptForInput(CEvent& waitEvent) +{ + using namespace JOYSTICK; + + bool bInterrupted = false; + + // Get the prompt for the current analog stick direction + std::string strPrompt; + std::string strWarn; + switch (m_state) + { + case STATE::WHEEL_LEFT: + strPrompt = g_localizeStrings.Get(35098); // "Move %s left" + strWarn = g_localizeStrings.Get(35099); // "Move %s left (%d)" + break; + case STATE::WHEEL_RIGHT: + strPrompt = g_localizeStrings.Get(35096); // "Move %s right" + strWarn = g_localizeStrings.Get(35097); // "Move %s right (%d)" + break; + default: + break; + } + + if (!strPrompt.empty()) + { + bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent); + + if (!bInterrupted) + m_state = STATE::FINISHED; // Not interrupted, must have timed out + else + m_state = GetNextState(m_state); // Interrupted by input, proceed + } + + return bInterrupted; +} + +bool CGUIWheelButton::IsFinished(void) const +{ + return m_state >= STATE::FINISHED; +} + +JOYSTICK::WHEEL_DIRECTION CGUIWheelButton::GetWheelDirection(void) const +{ + using namespace JOYSTICK; + + switch (m_state) + { + case STATE::WHEEL_LEFT: + return WHEEL_DIRECTION::LEFT; + case STATE::WHEEL_RIGHT: + return WHEEL_DIRECTION::RIGHT; + default: + break; + } + + return WHEEL_DIRECTION::NONE; +} + +void CGUIWheelButton::Reset(void) +{ + m_state = STATE::WHEEL_LEFT; +} diff --git a/xbmc/games/controllers/guicontrols/GUIWheelButton.h b/xbmc/games/controllers/guicontrols/GUIWheelButton.h new file mode 100644 index 0000000..937703a --- /dev/null +++ b/xbmc/games/controllers/guicontrols/GUIWheelButton.h @@ -0,0 +1,44 @@ +/* + * 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 "GUIFeatureButton.h" + +namespace KODI +{ +namespace GAME +{ +class CGUIWheelButton : public CGUIFeatureButton +{ +public: + CGUIWheelButton(const CGUIButtonControl& buttonTemplate, + IConfigurationWizard* wizard, + const CPhysicalFeature& feature, + unsigned int index); + + ~CGUIWheelButton() override = default; + + // implementation of IFeatureButton + bool PromptForInput(CEvent& waitEvent) override; + bool IsFinished() const override; + JOYSTICK::WHEEL_DIRECTION GetWheelDirection() const override; + void Reset() override; + +private: + enum class STATE + { + WHEEL_LEFT, + WHEEL_RIGHT, + FINISHED, + }; + + STATE m_state; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/input/CMakeLists.txt b/xbmc/games/controllers/input/CMakeLists.txt new file mode 100644 index 0000000..511fa37 --- /dev/null +++ b/xbmc/games/controllers/input/CMakeLists.txt @@ -0,0 +1,11 @@ +set(SOURCES InputSink.cpp + PhysicalFeature.cpp + PhysicalTopology.cpp +) + +set(HEADERS InputSink.h + PhysicalFeature.h + PhysicalTopology.h +) + +core_add_library(games_controller_input) diff --git a/xbmc/games/controllers/input/InputSink.cpp b/xbmc/games/controllers/input/InputSink.cpp new file mode 100644 index 0000000..a48b4b9 --- /dev/null +++ b/xbmc/games/controllers/input/InputSink.cpp @@ -0,0 +1,67 @@ +/* + * 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 "InputSink.h" + +#include "games/controllers/ControllerIDs.h" + +using namespace KODI; +using namespace GAME; + +CInputSink::CInputSink(JOYSTICK::IInputHandler* gameInput) : m_gameInput(gameInput) +{ +} + +std::string CInputSink::ControllerID(void) const +{ + return DEFAULT_CONTROLLER_ID; +} + +bool CInputSink::AcceptsInput(const std::string& feature) const +{ + return m_gameInput->AcceptsInput(feature); +} + +bool CInputSink::OnButtonPress(const std::string& feature, bool bPressed) +{ + return true; +} + +bool CInputSink::OnButtonMotion(const std::string& feature, + float magnitude, + unsigned int motionTimeMs) +{ + return true; +} + +bool CInputSink::OnAnalogStickMotion(const std::string& feature, + float x, + float y, + unsigned int motionTimeMs) +{ + return true; +} + +bool CInputSink::OnAccelerometerMotion(const std::string& feature, float x, float y, float z) +{ + return true; +} + +bool CInputSink::OnWheelMotion(const std::string& feature, + float position, + unsigned int motionTimeMs) +{ + return true; +} + +bool CInputSink::OnThrottleMotion(const std::string& feature, + float position, + unsigned int motionTimeMs) +{ + return true; +} diff --git a/xbmc/games/controllers/input/InputSink.h b/xbmc/games/controllers/input/InputSink.h new file mode 100644 index 0000000..5dc331f --- /dev/null +++ b/xbmc/games/controllers/input/InputSink.h @@ -0,0 +1,53 @@ +/* + * 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/interfaces/IInputHandler.h" + +namespace KODI +{ +namespace GAME +{ +class CGameClient; + +class CInputSink : public JOYSTICK::IInputHandler +{ +public: + explicit CInputSink(JOYSTICK::IInputHandler* gameInput); + + ~CInputSink() override = default; + + // Implementation of IInputHandler + std::string ControllerID() const override; + bool HasFeature(const std::string& feature) const override { return true; } + bool AcceptsInput(const std::string& feature) const override; + bool OnButtonPress(const std::string& feature, bool bPressed) override; + void OnButtonHold(const std::string& feature, unsigned int holdTimeMs) override {} + bool OnButtonMotion(const std::string& feature, + float magnitude, + unsigned int motionTimeMs) override; + bool OnAnalogStickMotion(const std::string& feature, + float x, + float y, + unsigned int motionTimeMs) override; + bool OnAccelerometerMotion(const std::string& feature, float x, float y, float z) override; + bool OnWheelMotion(const std::string& feature, + float position, + unsigned int motionTimeMs) override; + bool OnThrottleMotion(const std::string& feature, + float position, + unsigned int motionTimeMs) override; + void OnInputFrame() override {} + +private: + // Construction parameters + JOYSTICK::IInputHandler* m_gameInput; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/input/PhysicalFeature.cpp b/xbmc/games/controllers/input/PhysicalFeature.cpp new file mode 100644 index 0000000..24e0c41 --- /dev/null +++ b/xbmc/games/controllers/input/PhysicalFeature.cpp @@ -0,0 +1,162 @@ +/* + * 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 "PhysicalFeature.h" + +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerDefinitions.h" +#include "games/controllers/ControllerTranslator.h" +#include "guilib/LocalizeStrings.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <sstream> + +using namespace KODI; +using namespace GAME; +using namespace JOYSTICK; + +CPhysicalFeature::CPhysicalFeature(int labelId) +{ + Reset(); + m_labelId = labelId; +} + +void CPhysicalFeature::Reset(void) +{ + *this = CPhysicalFeature(); +} + +CPhysicalFeature& CPhysicalFeature::operator=(const CPhysicalFeature& rhs) +{ + if (this != &rhs) + { + m_controller = rhs.m_controller; + m_type = rhs.m_type; + m_category = rhs.m_category; + m_categoryLabelId = rhs.m_categoryLabelId; + m_strName = rhs.m_strName; + m_labelId = rhs.m_labelId; + m_inputType = rhs.m_inputType; + m_keycode = rhs.m_keycode; + } + return *this; +} + +std::string CPhysicalFeature::CategoryLabel() const +{ + std::string categoryLabel; + + if (m_categoryLabelId >= 0 && m_controller != nullptr) + categoryLabel = g_localizeStrings.GetAddonString(m_controller->ID(), m_categoryLabelId); + + if (categoryLabel.empty()) + categoryLabel = g_localizeStrings.Get(m_categoryLabelId); + + return categoryLabel; +} + +std::string CPhysicalFeature::Label() const +{ + std::string label; + + if (m_labelId >= 0 && m_controller != nullptr) + label = g_localizeStrings.GetAddonString(m_controller->ID(), m_labelId); + + if (label.empty()) + label = g_localizeStrings.Get(m_labelId); + + return label; +} + +bool CPhysicalFeature::Deserialize(const TiXmlElement* pElement, + const CController* controller, + FEATURE_CATEGORY category, + int categoryLabelId) +{ + Reset(); + + if (!pElement) + return false; + + std::string strType(pElement->Value()); + + // Type + m_type = CControllerTranslator::TranslateFeatureType(strType); + if (m_type == FEATURE_TYPE::UNKNOWN) + { + CLog::Log(LOGDEBUG, "Invalid feature: <{}> ", pElement->Value()); + return false; + } + + // Cagegory was obtained from parent XML node + m_category = category; + m_categoryLabelId = categoryLabelId; + + // Name + m_strName = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_FEATURE_NAME); + if (m_strName.empty()) + { + CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_FEATURE_NAME); + return false; + } + + // Label ID + std::string strLabel = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_FEATURE_LABEL); + if (strLabel.empty()) + CLog::Log(LOGDEBUG, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_FEATURE_LABEL); + else + std::istringstream(strLabel) >> m_labelId; + + // Input type + if (m_type == FEATURE_TYPE::SCALAR) + { + std::string strInputType = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_INPUT_TYPE); + if (strInputType.empty()) + { + CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_INPUT_TYPE); + return false; + } + else + { + m_inputType = CControllerTranslator::TranslateInputType(strInputType); + if (m_inputType == INPUT_TYPE::UNKNOWN) + { + CLog::Log(LOGERROR, "<{}> tag - attribute \"{}\" is invalid: \"{}\"", strType, + LAYOUT_XML_ATTR_INPUT_TYPE, strInputType); + return false; + } + } + } + + // Keycode + if (m_type == FEATURE_TYPE::KEY) + { + std::string strSymbol = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_KEY_SYMBOL); + if (strSymbol.empty()) + { + CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_KEY_SYMBOL); + return false; + } + else + { + m_keycode = CControllerTranslator::TranslateKeysym(strSymbol); + if (m_keycode == XBMCK_UNKNOWN) + { + CLog::Log(LOGERROR, "<{}> tag - attribute \"{}\" is invalid: \"{}\"", strType, + LAYOUT_XML_ATTR_KEY_SYMBOL, strSymbol); + return false; + } + } + } + + // Save controller for string translation + m_controller = controller; + + return true; +} diff --git a/xbmc/games/controllers/input/PhysicalFeature.h b/xbmc/games/controllers/input/PhysicalFeature.h new file mode 100644 index 0000000..6319a63 --- /dev/null +++ b/xbmc/games/controllers/input/PhysicalFeature.h @@ -0,0 +1,65 @@ +/* + * 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 "games/controllers/ControllerTypes.h" +#include "input/joysticks/JoystickTypes.h" +#include "input/keyboard/KeyboardTypes.h" + +#include <string> + +class TiXmlElement; + +namespace KODI +{ +namespace GAME +{ + +class CPhysicalFeature +{ +public: + CPhysicalFeature() = default; + CPhysicalFeature(int labelId); + CPhysicalFeature(const CPhysicalFeature& other) { *this = other; } + + void Reset(void); + + CPhysicalFeature& operator=(const CPhysicalFeature& rhs); + + JOYSTICK::FEATURE_TYPE Type(void) const { return m_type; } + JOYSTICK::FEATURE_CATEGORY Category(void) const { return m_category; } + const std::string& Name(void) const { return m_strName; } + + // GUI properties + std::string Label(void) const; + int LabelID(void) const { return m_labelId; } + std::string CategoryLabel(void) const; + + // Input properties + JOYSTICK::INPUT_TYPE InputType(void) const { return m_inputType; } + KEYBOARD::KeySymbol Keycode() const { return m_keycode; } + + bool Deserialize(const TiXmlElement* pElement, + const CController* controller, + JOYSTICK::FEATURE_CATEGORY category, + int categoryLabelId); + +private: + const CController* m_controller = nullptr; // Used for translating addon-specific labels + JOYSTICK::FEATURE_TYPE m_type = JOYSTICK::FEATURE_TYPE::UNKNOWN; + JOYSTICK::FEATURE_CATEGORY m_category = JOYSTICK::FEATURE_CATEGORY::UNKNOWN; + int m_categoryLabelId = -1; + std::string m_strName; + int m_labelId = -1; + JOYSTICK::INPUT_TYPE m_inputType = JOYSTICK::INPUT_TYPE::UNKNOWN; + KEYBOARD::KeySymbol m_keycode = XBMCK_UNKNOWN; +}; + +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/input/PhysicalTopology.cpp b/xbmc/games/controllers/input/PhysicalTopology.cpp new file mode 100644 index 0000000..2fecb5a --- /dev/null +++ b/xbmc/games/controllers/input/PhysicalTopology.cpp @@ -0,0 +1,56 @@ +/* + * 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 "PhysicalTopology.h" + +#include "games/controllers/ControllerDefinitions.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <utility> + +using namespace KODI; +using namespace GAME; + +CPhysicalTopology::CPhysicalTopology(bool bProvidesInput, std::vector<CPhysicalPort> ports) + : m_bProvidesInput(bProvidesInput), m_ports(std::move(ports)) +{ +} + +void CPhysicalTopology::Reset() +{ + CPhysicalTopology defaultTopology; + *this = std::move(defaultTopology); +} + +bool CPhysicalTopology::Deserialize(const TiXmlElement* pElement) +{ + Reset(); + + if (pElement == nullptr) + return false; + + m_bProvidesInput = (XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_PROVIDES_INPUT) != "false"); + + for (const TiXmlElement* pChild = pElement->FirstChildElement(); pChild != nullptr; + pChild = pChild->NextSiblingElement()) + { + if (pChild->ValueStr() == LAYOUT_XML_ELM_PORT) + { + CPhysicalPort port; + if (port.Deserialize(pChild)) + m_ports.emplace_back(std::move(port)); + } + else + { + CLog::Log(LOGDEBUG, "Unknown physical topology tag: <{}>", pChild->ValueStr()); + } + } + + return true; +} diff --git a/xbmc/games/controllers/input/PhysicalTopology.h b/xbmc/games/controllers/input/PhysicalTopology.h new file mode 100644 index 0000000..c194de8 --- /dev/null +++ b/xbmc/games/controllers/input/PhysicalTopology.h @@ -0,0 +1,61 @@ +/* + * 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 "games/ports/input/PhysicalPort.h" + +#include <vector> + +class TiXmlElement; + +namespace KODI +{ +namespace GAME +{ + +/*! + * \brief Represents the physical topology of controller add-ons + * + * The physical topology of a controller defines how many ports it has and + * whether it can provide player input (hubs like the Super Multitap don't + * provide input). + */ +class CPhysicalTopology +{ +public: + CPhysicalTopology() = default; + CPhysicalTopology(bool bProvidesInput, std::vector<CPhysicalPort> ports); + + void Reset(); + + /*! + * \brief Check if the controller can provide player input + * + * This allows hubs to specify that they provide no input + * + * \return True if the controller can provide player input, false otherwise + */ + bool ProvidesInput() const { return m_bProvidesInput; } + + /*! + * \brief Get a list of ports provided by this controller + * + * \return The ports + */ + const std::vector<CPhysicalPort>& Ports() const { return m_ports; } + + bool Deserialize(const TiXmlElement* pElement); + +private: + bool m_bProvidesInput = true; + std::vector<CPhysicalPort> m_ports; +}; + +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/types/CMakeLists.txt b/xbmc/games/controllers/types/CMakeLists.txt new file mode 100644 index 0000000..503d254 --- /dev/null +++ b/xbmc/games/controllers/types/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SOURCES ControllerGrid.cpp + ControllerHub.cpp + ControllerNode.cpp +) + +set(HEADERS ControllerGrid.h + ControllerHub.h + ControllerNode.h + ControllerTree.h +) + +core_add_library(games_controller_types) diff --git a/xbmc/games/controllers/types/ControllerGrid.cpp b/xbmc/games/controllers/types/ControllerGrid.cpp new file mode 100644 index 0000000..f718d5a --- /dev/null +++ b/xbmc/games/controllers/types/ControllerGrid.cpp @@ -0,0 +1,242 @@ +/* + * 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 "ControllerGrid.h" + +#include "games/controllers/Controller.h" +#include "utils/log.h" + +#include <algorithm> +#include <utility> + +using namespace KODI; +using namespace GAME; + +CControllerGrid::~CControllerGrid() = default; + +void CControllerGrid::SetControllerTree(const CControllerTree& controllerTree) +{ + // Clear the result + m_grid.clear(); + + m_height = AddPorts(controllerTree.GetPorts(), m_grid); + SetHeight(m_height, m_grid); +} + +ControllerVector CControllerGrid::GetControllers(unsigned int playerIndex) const +{ + ControllerVector controllers; + + if (playerIndex < m_grid.size()) + { + for (const auto& controllerVertex : m_grid[playerIndex].vertices) + { + if (controllerVertex.controller) + controllers.emplace_back(controllerVertex.controller); + } + } + + return controllers; +} + +unsigned int CControllerGrid::AddPorts(const PortVec& ports, ControllerGrid& grid) +{ + unsigned int height = 0; + + auto itKeyboard = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) { + return port.GetPortType() == PORT_TYPE::KEYBOARD; + }); + + auto itMouse = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) { + return port.GetPortType() == PORT_TYPE::MOUSE; + }); + + auto itController = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) { + return port.GetPortType() == PORT_TYPE::CONTROLLER; + }); + + // Keyboard and mouse are not allowed to have ports because they might + // overlap with controllers + if (itKeyboard != ports.end() && itKeyboard->GetActiveController().GetHub().HasPorts()) + { + CLog::Log(LOGERROR, "Found keyboard with controller ports, skipping"); + itKeyboard = ports.end(); + } + if (itMouse != ports.end() && itMouse->GetActiveController().GetHub().HasPorts()) + { + CLog::Log(LOGERROR, "Found mouse with controller ports, skipping"); + itMouse = ports.end(); + } + + if (itController != ports.end()) + { + // Add controller ports + bool bFirstPlayer = true; + for (const CPortNode& port : ports) + { + ControllerColumn column; + + if (port.GetPortType() == PORT_TYPE::CONTROLLER) + { + // Add controller + height = + std::max(height, AddController(port, static_cast<unsigned int>(column.vertices.size()), + column.vertices, grid)); + + if (bFirstPlayer) + { + bFirstPlayer = false; + + // Keyboard and mouse are added below the first controller + if (itKeyboard != ports.end()) + height = + std::max(height, AddController(*itKeyboard, + static_cast<unsigned int>(column.vertices.size()), + column.vertices, grid)); + if (itMouse != ports.end()) + height = std::max( + height, AddController(*itMouse, static_cast<unsigned int>(column.vertices.size()), + column.vertices, grid)); + } + } + + if (!column.vertices.empty()) + grid.emplace_back(std::move(column)); + } + } + else + { + // No controllers, add keyboard and mouse + ControllerColumn column; + + if (itKeyboard != ports.end()) + height = std::max(height, AddController(*itKeyboard, + static_cast<unsigned int>(column.vertices.size()), + column.vertices, grid)); + if (itMouse != ports.end()) + height = std::max(height, + AddController(*itMouse, static_cast<unsigned int>(column.vertices.size()), + column.vertices, grid)); + + if (!column.vertices.empty()) + grid.emplace_back(std::move(column)); + } + + return height; +} + +unsigned int CControllerGrid::AddController(const CPortNode& port, + unsigned int height, + std::vector<ControllerVertex>& column, + ControllerGrid& grid) +{ + // Add spacers + while (column.size() < height) + AddInvisible(column); + + const CControllerNode& activeController = port.GetActiveController(); + + // Add vertex + ControllerVertex vertex; + vertex.bVisible = true; + vertex.bConnected = port.IsConnected(); + vertex.portType = port.GetPortType(); + vertex.controller = activeController.GetController(); + vertex.address = activeController.GetControllerAddress(); + for (const CControllerNode& node : port.GetCompatibleControllers()) + vertex.compatible.emplace_back(node.GetController()); + column.emplace_back(std::move(vertex)); + + height++; + + // Process ports + const PortVec& ports = activeController.GetHub().GetPorts(); + if (!ports.empty()) + { + switch (GetDirection(activeController)) + { + case GRID_DIRECTION::RIGHT: + { + height = std::max(height, AddHub(ports, height - 1, false, grid)); + break; + } + case GRID_DIRECTION::DOWN: + { + const unsigned int row = height; + + // Add the first controller to the column + const CPortNode firstController = ports.at(0); + height = std::max(height, AddController(firstController, row, column, grid)); + + // Add the remaining controllers on the same row + height = std::max(height, AddHub(ports, row, true, grid)); + + break; + } + } + } + + return height; +} + +unsigned int CControllerGrid::AddHub(const PortVec& ports, + unsigned int height, + bool bSkipFirst, + ControllerGrid& grid) +{ + const unsigned int row = height; + + unsigned int port = 0; + for (const auto& controllerPort : ports) + { + // If controller has no player, it has already added the hub's first controller + if (bSkipFirst && port == 0) + continue; + + // Add a column for this controller + grid.emplace_back(); + ControllerColumn& column = grid.back(); + + height = std::max(height, AddController(controllerPort, row, column.vertices, grid)); + + port++; + } + + return height; +} + +void CControllerGrid::AddInvisible(std::vector<ControllerVertex>& column) +{ + ControllerVertex vertex; + vertex.bVisible = false; + column.emplace_back(std::move(vertex)); +} + +void CControllerGrid::SetHeight(unsigned int height, ControllerGrid& grid) +{ + for (auto& column : grid) + { + while (static_cast<unsigned int>(column.vertices.size()) < height) + AddInvisible(column.vertices); + } +} + +CControllerGrid::GRID_DIRECTION CControllerGrid::GetDirection(const CControllerNode& node) +{ + // Hub controllers are added horizontally, one per row. + // + // If the current controller offers a player spot, the row starts to the + // right at the same height as the controller. + // + // Otherwise, to row starts below the current controller in the same + // column. + if (node.ProvidesInput()) + return GRID_DIRECTION::RIGHT; + else + return GRID_DIRECTION::DOWN; +} diff --git a/xbmc/games/controllers/types/ControllerGrid.h b/xbmc/games/controllers/types/ControllerGrid.h new file mode 100644 index 0000000..44ec1d3 --- /dev/null +++ b/xbmc/games/controllers/types/ControllerGrid.h @@ -0,0 +1,167 @@ +/* + * 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 "ControllerTree.h" +#include "games/controllers/ControllerTypes.h" + +#include <string> +#include <vector> + +namespace KODI +{ +namespace GAME +{ +/*! + * \brief Vertex in the grid of controllers + */ +struct ControllerVertex +{ + bool bVisible = true; + bool bConnected = false; + ControllerPtr controller; // Mandatory if connected + PORT_TYPE portType = PORT_TYPE::UNKNOWN; // Optional + std::string address; // Optional + ControllerVector compatible; // Compatible controllers +}; + +/*! + * \brief Column of controllers in the grid + */ +struct ControllerColumn +{ + std::vector<ControllerVertex> vertices; +}; + +/*! + * \brief Collection of controllers in a grid layout + */ +using ControllerGrid = std::vector<ControllerColumn>; + +/*! + * \brief Class to encapsulate grid operations + */ +class CControllerGrid +{ +public: + CControllerGrid() = default; + CControllerGrid(const CControllerGrid& other) = default; + ~CControllerGrid(); + + /*! + * \brief Create a grid from a controller tree + */ + void SetControllerTree(const CControllerTree& controllerTree); + + /*! + * \brief Get the width of the controller grid + */ + unsigned int GetWidth() const { return static_cast<unsigned int>(m_grid.size()); } + + /*! + * \brief Get the height (deepest controller) of the controller grid + * + * The height is cached when the controller grid is created to avoid + * iterating the grid + */ + unsigned int GetHeight() const { return m_height; } + + /*! + * \brief Access the controller grid + */ + const ControllerGrid& GetGrid() const { return m_grid; } + + /*! + * \brief Get the controllers in use by the specified player + * + * \param playerIndex The column in the grid to get controllers from + */ + ControllerVector GetControllers(unsigned int playerIndex) const; + +private: + /*! + * \brief Directions of vertex traversal + */ + enum class GRID_DIRECTION + { + RIGHT, + DOWN, + }; + + /*! + * \brief Add ports to the grid + * + * \param ports The ports on a console or controller + * \param[out] grid The controller grid being created + * + * \return The height of the grid determined by the maximum column height + */ + static unsigned int AddPorts(const PortVec& ports, ControllerGrid& grid); + + /*! + * \brief Draw a controller to the column at the specified height + * + * \param port The controller's port node + * \param height The height to draw the controller at + * \param column[in/out] The column to draw to + * \param grid[in/out] The grid to add additional columns to + * + * \return The height of the grid + */ + static unsigned int AddController(const CPortNode& port, + unsigned int height, + std::vector<ControllerVertex>& column, + ControllerGrid& grid); + + /*! + * \brief Draw a series of controllers to the grid at the specified height + * + * \param ports The ports of the controllers to draw + * \param height The height to start drawing the controllers at + * \param bSkipFirst True if the first controller has already been drawn to + * a column, false to start drawing at the first controller + * \param grid[in/out] The grid to add columns to + * + * \return The height of the grid + */ + static unsigned int AddHub(const PortVec& ports, + unsigned int height, + bool bSkipFirst, + ControllerGrid& grid); + + /*! + * \brief Draw an invisible vertex to the column + * + * \param[in/out] column The column in a controller grid + */ + static void AddInvisible(std::vector<ControllerVertex>& column); + + /*! + * \brief Fill all columns with invisible vertices until the specified height + * + * \param height The height to make all columns + * \param[in/out] grid The grid to update + */ + static void SetHeight(unsigned int height, ControllerGrid& grid); + + /*! + * \brief Get the direction of traversal for the next vertex + * + * \param node The node in the controller tree being visited + * + * \return The direction of the next vertex, or GRID_DIRECTION::UNKNOWN if + * unknown + */ + static GRID_DIRECTION GetDirection(const CControllerNode& node); + + ControllerGrid m_grid; + unsigned int m_height = 0; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/types/ControllerHub.cpp b/xbmc/games/controllers/types/ControllerHub.cpp new file mode 100644 index 0000000..5fd0ece --- /dev/null +++ b/xbmc/games/controllers/types/ControllerHub.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2017-2021 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 "ControllerHub.h" + +#include "ControllerTree.h" +#include "games/controllers/Controller.h" + +#include <algorithm> +#include <utility> + +using namespace KODI; +using namespace GAME; + +CControllerHub::~CControllerHub() = default; + +CControllerHub& CControllerHub::operator=(const CControllerHub& rhs) +{ + if (this != &rhs) + { + m_ports = rhs.m_ports; + } + + return *this; +} + +CControllerHub& CControllerHub::operator=(CControllerHub&& rhs) noexcept +{ + if (this != &rhs) + { + m_ports = std::move(rhs.m_ports); + } + + return *this; +} + +void CControllerHub::Clear() +{ + m_ports.clear(); +} + +void CControllerHub::SetPorts(PortVec ports) +{ + m_ports = std::move(ports); +} + +bool CControllerHub::IsControllerAccepted(const std::string& controllerId) const +{ + return std::any_of(m_ports.begin(), m_ports.end(), [controllerId](const CPortNode& port) { + return port.IsControllerAccepted(controllerId); + }); +} + +bool CControllerHub::IsControllerAccepted(const std::string& portAddress, + const std::string& controllerId) const +{ + return std::any_of(m_ports.begin(), m_ports.end(), + [portAddress, controllerId](const CPortNode& port) { + return port.IsControllerAccepted(portAddress, controllerId); + }); +} + +ControllerVector CControllerHub::GetControllers() const +{ + ControllerVector controllers; + GetControllers(controllers); + return controllers; +} + +void CControllerHub::GetControllers(ControllerVector& controllers) const +{ + for (const CPortNode& port : m_ports) + { + for (const CControllerNode& node : port.GetCompatibleControllers()) + node.GetControllers(controllers); + } +} + +const CPortNode& CControllerHub::GetPort(const std::string& address) const +{ + return GetPortInternal(m_ports, address); +} + +const CPortNode& CControllerHub::GetPortInternal(const PortVec& ports, const std::string& address) +{ + for (const CPortNode& port : ports) + { + // Base case + if (port.GetAddress() == address) + return port; + + // Check children + for (const CControllerNode& controller : port.GetCompatibleControllers()) + { + const CPortNode& port = GetPortInternal(controller.GetHub().GetPorts(), address); + if (port.GetAddress() == address) + return port; + } + } + + // Not found + static const CPortNode empty{}; + return empty; +} diff --git a/xbmc/games/controllers/types/ControllerHub.h b/xbmc/games/controllers/types/ControllerHub.h new file mode 100644 index 0000000..fc48a81 --- /dev/null +++ b/xbmc/games/controllers/types/ControllerHub.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017-2021 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 "games/controllers/ControllerTypes.h" +#include "games/ports/types/PortNode.h" + +#include <string> + +namespace KODI +{ +namespace GAME +{ +/*! + * \brief A branch in the controller tree + */ +class CControllerHub +{ +public: + CControllerHub() = default; + CControllerHub(const CControllerHub& other) { *this = other; } + CControllerHub(CControllerHub&& other) = default; + CControllerHub& operator=(const CControllerHub& rhs); + CControllerHub& operator=(CControllerHub&& rhs) noexcept; + ~CControllerHub(); + + void Clear(); + + bool HasPorts() const { return !m_ports.empty(); } + PortVec& GetPorts() { return m_ports; } + const PortVec& GetPorts() const { return m_ports; } + void SetPorts(PortVec ports); + + bool IsControllerAccepted(const std::string& controllerId) const; + bool IsControllerAccepted(const std::string& portAddress, const std::string& controllerId) const; + ControllerVector GetControllers() const; + void GetControllers(ControllerVector& controllers) const; + + const CPortNode& GetPort(const std::string& address) const; + +private: + static const CPortNode& GetPortInternal(const PortVec& ports, const std::string& address); + + PortVec m_ports; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/types/ControllerNode.cpp b/xbmc/games/controllers/types/ControllerNode.cpp new file mode 100644 index 0000000..4bfda73 --- /dev/null +++ b/xbmc/games/controllers/types/ControllerNode.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2017-2021 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 "ControllerNode.h" + +#include "ControllerHub.h" +#include "games/controllers/Controller.h" +#include "games/controllers/input/PhysicalTopology.h" +#include "games/ports/types/PortNode.h" + +#include <algorithm> +#include <utility> + +using namespace KODI; +using namespace GAME; + +CControllerNode::CControllerNode() : m_hub(new CControllerHub) +{ +} + +CControllerNode::~CControllerNode() = default; + +CControllerNode& CControllerNode::operator=(const CControllerNode& rhs) +{ + if (this != &rhs) + { + m_controller = rhs.m_controller; + m_portAddress = rhs.m_portAddress; + m_controllerAddress = rhs.m_controllerAddress; + m_hub.reset(new CControllerHub(*rhs.m_hub)); + } + + return *this; +} + +CControllerNode& CControllerNode::operator=(CControllerNode&& rhs) noexcept +{ + if (this != &rhs) + { + m_controller = std::move(rhs.m_controller); + m_portAddress = std::move(rhs.m_portAddress); + m_controllerAddress = std::move(rhs.m_controllerAddress); + m_hub = std::move(rhs.m_hub); + } + + return *this; +} + +void CControllerNode::Clear() +{ + m_controller.reset(); + m_portAddress.clear(); + m_controllerAddress.clear(); + m_hub.reset(new CControllerHub); +} + +void CControllerNode::SetController(ControllerPtr controller) +{ + m_controller = std::move(controller); +} + +void CControllerNode::GetControllers(ControllerVector& controllers) const +{ + if (m_controller) + { + const ControllerPtr& myController = m_controller; + + auto it = std::find_if(controllers.begin(), controllers.end(), + [&myController](const ControllerPtr& controller) { + return myController->ID() == controller->ID(); + }); + + if (it == controllers.end()) + controllers.emplace_back(m_controller); + } + + m_hub->GetControllers(controllers); +} + +void CControllerNode::SetPortAddress(std::string portAddress) +{ + m_portAddress = std::move(portAddress); +} + +void CControllerNode::SetControllerAddress(std::string controllerAddress) +{ + m_controllerAddress = std::move(controllerAddress); +} + +void CControllerNode::SetHub(CControllerHub hub) +{ + m_hub.reset(new CControllerHub(std::move(hub))); +} + +bool CControllerNode::IsControllerAccepted(const std::string& controllerId) const +{ + bool bAccepted = false; + + for (const auto& port : m_hub->GetPorts()) + { + if (port.IsControllerAccepted(controllerId)) + { + bAccepted = true; + break; + } + } + + return bAccepted; +} + +bool CControllerNode::IsControllerAccepted(const std::string& portAddress, + const std::string& controllerId) const +{ + bool bAccepted = false; + + for (const auto& port : m_hub->GetPorts()) + { + if (port.IsControllerAccepted(portAddress, controllerId)) + { + bAccepted = true; + break; + } + } + + return bAccepted; +} + +bool CControllerNode::ProvidesInput() const +{ + return m_controller && m_controller->Topology().ProvidesInput(); +} diff --git a/xbmc/games/controllers/types/ControllerNode.h b/xbmc/games/controllers/types/ControllerNode.h new file mode 100644 index 0000000..d4d86e5 --- /dev/null +++ b/xbmc/games/controllers/types/ControllerNode.h @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2017-2021 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 "games/controllers/ControllerTypes.h" + +#include <memory> +#include <string> +#include <vector> + +namespace KODI +{ +namespace GAME +{ +class CControllerHub; + +/*! + * \brief Node in the controller tree + * + * The node identifies the controller profile, and optionally the available + * controller ports. + */ +class CControllerNode +{ +public: + CControllerNode(); + CControllerNode(const CControllerNode& other) { *this = other; } + CControllerNode(CControllerNode&& other) = default; + CControllerNode& operator=(const CControllerNode& rhs); + CControllerNode& operator=(CControllerNode&& rhs) noexcept; + ~CControllerNode(); + + void Clear(); + + /*! + * \brief Controller profile of this code + * + * \return Controller profile, or empty if this node is invalid + * + * \sa IsValid() + */ + const ControllerPtr& GetController() const { return m_controller; } + void SetController(ControllerPtr controller); + + void GetControllers(ControllerVector& controllers) const; + + /*! + * \brief Address given to the controller's port by the implementation + */ + const std::string& GetPortAddress() const { return m_portAddress; } + void SetPortAddress(std::string portAddress); + + /*! + * \brief Address given to the controller node by the implementation + */ + const std::string& GetControllerAddress() const { return m_controllerAddress; } + void SetControllerAddress(std::string controllerAddress); + + /*! + * \brief Collection of ports on this controller + * + * \return A hub with controller ports, or an empty hub if this controller + * has no available ports + */ + const CControllerHub& GetHub() const { return *m_hub; } + CControllerHub& GetHub() { return *m_hub; } + void SetHub(CControllerHub hub); + + /*! + * \brief Check if this node has a valid controller profile + */ + bool IsValid() const { return static_cast<bool>(m_controller); } + + /*! + * \brief Check to see if a controller is compatible with a controller port + * + * \param controllerId The ID of the controller + * + * \return True if the controller is compatible with a port, false otherwise + */ + bool IsControllerAccepted(const std::string& controllerId) const; + + /*! + * \brief Check to see if a controller is compatible with a controller port + * + * \param portAddress The port address + * \param controllerId The ID of the controller + * + * \return True if the controller is compatible with a port, false otherwise + */ + bool IsControllerAccepted(const std::string& portAddress, const std::string& controllerId) const; + + /*! + * \brief Check if this node provides input + */ + bool ProvidesInput() const; + +private: + ControllerPtr m_controller; + + // Address of the port this controller is connected to + std::string m_portAddress; + + // Address of this controller: m_portAddress + "/" + m_controller->ID() + std::string m_controllerAddress; + + std::unique_ptr<CControllerHub> m_hub; +}; + +using ControllerNodeVec = std::vector<CControllerNode>; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/types/ControllerTree.h b/xbmc/games/controllers/types/ControllerTree.h new file mode 100644 index 0000000..47e42e9 --- /dev/null +++ b/xbmc/games/controllers/types/ControllerTree.h @@ -0,0 +1,30 @@ +/* + * 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 + +// +// Note: Hierarchy of headers is: +// +// - ControllerTree.h (this file) +// - ControllerHub.h +// - PortNode.h +// - ControllerNode.h +// +#include "ControllerHub.h" + +namespace KODI +{ +namespace GAME +{ +/*! + * \brief Collection of ports on a console + */ +using CControllerTree = CControllerHub; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/windows/CMakeLists.txt b/xbmc/games/controllers/windows/CMakeLists.txt new file mode 100644 index 0000000..72c8154 --- /dev/null +++ b/xbmc/games/controllers/windows/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES GUIConfigurationWizard.cpp + GUIControllerList.cpp + GUIControllerWindow.cpp + GUIFeatureList.cpp +) + +set(HEADERS GUIConfigurationWizard.h + GUIControllerDefines.h + GUIControllerList.h + GUIControllerWindow.h + GUIFeatureList.h + IConfigurationWindow.h +) + +core_add_library(games_controller_windows) diff --git a/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp b/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp new file mode 100644 index 0000000..c2ac2f5 --- /dev/null +++ b/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp @@ -0,0 +1,494 @@ +/* + * 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 "GUIConfigurationWizard.h" + +#include "ServiceBroker.h" +#include "games/controllers/Controller.h" +#include "games/controllers/dialogs/GUIDialogAxisDetection.h" +#include "games/controllers/guicontrols/GUIFeatureButton.h" +#include "games/controllers/input/PhysicalFeature.h" +#include "input/IKeymap.h" +#include "input/InputManager.h" +#include "input/joysticks/JoystickUtils.h" +#include "input/joysticks/interfaces/IButtonMap.h" +#include "input/joysticks/interfaces/IButtonMapCallback.h" +#include "input/keyboard/KeymapActionMap.h" +#include "peripherals/Peripherals.h" +#include "threads/SingleLock.h" +#include "utils/log.h" + +#include <mutex> + +using namespace KODI; +using namespace GAME; +using namespace std::chrono_literals; + +namespace +{ + +#define ESC_KEY_CODE 27 +#define SKIPPING_DETECTION_MS 200 + +// Duration to wait for axes to neutralize after mapping is finished +constexpr auto POST_MAPPING_WAIT_TIME_MS = 5000ms; + +} // namespace + +CGUIConfigurationWizard::CGUIConfigurationWizard() + : CThread("GUIConfigurationWizard"), m_actionMap(new KEYBOARD::CKeymapActionMap) +{ + InitializeState(); +} + +CGUIConfigurationWizard::~CGUIConfigurationWizard(void) = default; + +void CGUIConfigurationWizard::InitializeState(void) +{ + m_currentButton = nullptr; + m_cardinalDirection = INPUT::CARDINAL_DIRECTION::NONE; + m_wheelDirection = JOYSTICK::WHEEL_DIRECTION::NONE; + m_throttleDirection = JOYSTICK::THROTTLE_DIRECTION::NONE; + m_history.clear(); + m_lateAxisDetected = false; + m_location.clear(); +} + +void CGUIConfigurationWizard::Run(const std::string& strControllerId, + const std::vector<IFeatureButton*>& buttons) +{ + Abort(); + + { + std::unique_lock<CCriticalSection> lock(m_stateMutex); + + // Set Run() parameters + m_strControllerId = strControllerId; + m_buttons = buttons; + + // Reset synchronization variables + m_inputEvent.Reset(); + m_motionlessEvent.Reset(); + m_bInMotion.clear(); + + // Initialize state variables + InitializeState(); + } + + Create(); +} + +void CGUIConfigurationWizard::OnUnfocus(IFeatureButton* button) +{ + std::unique_lock<CCriticalSection> lock(m_stateMutex); + + if (button == m_currentButton) + Abort(false); +} + +bool CGUIConfigurationWizard::Abort(bool bWait /* = true */) +{ + bool bWasRunning = !m_bStop; + + StopThread(false); + + m_inputEvent.Set(); + m_motionlessEvent.Set(); + + if (bWait) + StopThread(true); + + return bWasRunning; +} + +void CGUIConfigurationWizard::RegisterKey(const CPhysicalFeature& key) +{ + if (key.Keycode() != XBMCK_UNKNOWN) + m_keyMap[key.Keycode()] = key; +} + +void CGUIConfigurationWizard::UnregisterKeys() +{ + m_keyMap.clear(); +} + +void CGUIConfigurationWizard::Process(void) +{ + CLog::Log(LOGDEBUG, "Starting configuration wizard"); + + InstallHooks(); + + bool bLateAxisDetected = false; + + { + std::unique_lock<CCriticalSection> lock(m_stateMutex); + for (IFeatureButton* button : m_buttons) + { + // Allow other threads to access the button we're using + m_currentButton = button; + + while (!button->IsFinished()) + { + // Allow other threads to access which direction the prompt is on + m_cardinalDirection = button->GetCardinalDirection(); + m_wheelDirection = button->GetWheelDirection(); + m_throttleDirection = button->GetThrottleDirection(); + + // Wait for input + { + using namespace JOYSTICK; + + CSingleExit exit(m_stateMutex); + + if (button->Feature().Type() == FEATURE_TYPE::UNKNOWN) + CLog::Log(LOGDEBUG, "{}: Waiting for input", m_strControllerId); + else + CLog::Log(LOGDEBUG, "{}: Waiting for input for feature \"{}\"", m_strControllerId, + button->Feature().Name()); + + if (!button->PromptForInput(m_inputEvent)) + Abort(false); + } + + if (m_bStop) + break; + } + + button->Reset(); + + if (m_bStop) + break; + } + + bLateAxisDetected = m_lateAxisDetected; + + // Finished mapping + InitializeState(); + } + + for (auto callback : ButtonMapCallbacks()) + callback.second->SaveButtonMap(); + + if (bLateAxisDetected) + { + CGUIDialogAxisDetection dialog; + dialog.Show(); + } + else + { + // Wait for motion to stop to avoid sending analog actions for the button + // that is pressed immediately after button mapping finishes. + bool bInMotion; + + { + std::unique_lock<CCriticalSection> lock(m_motionMutex); + bInMotion = !m_bInMotion.empty(); + } + + if (bInMotion) + { + CLog::Log(LOGDEBUG, "Configuration wizard: waiting {}ms for axes to neutralize", + POST_MAPPING_WAIT_TIME_MS.count()); + m_motionlessEvent.Wait(POST_MAPPING_WAIT_TIME_MS); + } + } + + RemoveHooks(); + + CLog::Log(LOGDEBUG, "Configuration wizard ended"); +} + +bool CGUIConfigurationWizard::MapPrimitive(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) +{ + using namespace INPUT; + using namespace JOYSTICK; + + bool bHandled = false; + + // Abort if another controller cancels the prompt + if (IsMapping() && !IsMapping(buttonMap->Location())) + { + //! @todo This only succeeds for game.controller.default; no actions are + // currently defined for other controllers + if (keymap) + { + std::string feature; + if (buttonMap->GetFeature(primitive, feature)) + { + const auto& actions = keymap->GetActions(CJoystickUtils::MakeKeyName(feature)).actions; + if (!actions.empty()) + { + //! @todo Handle multiple actions mapped to the same key + OnAction(actions.begin()->actionId); + } + } + } + + // Discard input + bHandled = true; + } + else if (m_history.find(primitive) != m_history.end()) + { + // Primitive has already been mapped this round, ignore it + bHandled = true; + } + else if (buttonMap->IsIgnored(primitive)) + { + bHandled = true; + } + else + { + // Get the current state of the thread + IFeatureButton* currentButton; + CARDINAL_DIRECTION cardinalDirection; + WHEEL_DIRECTION wheelDirection; + THROTTLE_DIRECTION throttleDirection; + { + std::unique_lock<CCriticalSection> lock(m_stateMutex); + currentButton = m_currentButton; + cardinalDirection = m_cardinalDirection; + wheelDirection = m_wheelDirection; + throttleDirection = m_throttleDirection; + } + + if (currentButton) + { + // Check if we were expecting a keyboard key + if (currentButton->NeedsKey()) + { + if (primitive.Type() == PRIMITIVE_TYPE::KEY) + { + auto it = m_keyMap.find(primitive.Keycode()); + if (it != m_keyMap.end()) + { + const CPhysicalFeature& key = it->second; + currentButton->SetKey(key); + m_inputEvent.Set(); + } + } + else + { + //! @todo Check if primitive is a cancel or motion action + } + bHandled = true; + } + else + { + const CPhysicalFeature& feature = currentButton->Feature(); + + if (primitive.Type() == PRIMITIVE_TYPE::RELATIVE_POINTER && + feature.Type() != FEATURE_TYPE::RELPOINTER) + { + // Don't allow relative pointers to map to other features + } + else + { + CLog::Log(LOGDEBUG, "{}: mapping feature \"{}\" for device {} to \"{}\"", + m_strControllerId, feature.Name(), buttonMap->Location(), primitive.ToString()); + + switch (feature.Type()) + { + case FEATURE_TYPE::SCALAR: + { + buttonMap->AddScalar(feature.Name(), primitive); + bHandled = true; + break; + } + case FEATURE_TYPE::ANALOG_STICK: + { + buttonMap->AddAnalogStick(feature.Name(), cardinalDirection, primitive); + bHandled = true; + break; + } + case FEATURE_TYPE::RELPOINTER: + { + buttonMap->AddRelativePointer(feature.Name(), cardinalDirection, primitive); + bHandled = true; + break; + } + case FEATURE_TYPE::WHEEL: + { + buttonMap->AddWheel(feature.Name(), wheelDirection, primitive); + bHandled = true; + break; + } + case FEATURE_TYPE::THROTTLE: + { + buttonMap->AddThrottle(feature.Name(), throttleDirection, primitive); + bHandled = true; + break; + } + case FEATURE_TYPE::KEY: + { + buttonMap->AddKey(feature.Name(), primitive); + bHandled = true; + break; + } + default: + break; + } + } + + if (bHandled) + { + m_history.insert(primitive); + + // Don't record motion for relative pointers + if (primitive.Type() != PRIMITIVE_TYPE::RELATIVE_POINTER) + OnMotion(buttonMap); + + m_inputEvent.Set(); + + if (m_location.empty()) + { + m_location = buttonMap->Location(); + m_bIsKeyboard = (primitive.Type() == PRIMITIVE_TYPE::KEY); + } + } + } + } + } + + return bHandled; +} + +void CGUIConfigurationWizard::OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion) +{ + std::unique_lock<CCriticalSection> lock(m_motionMutex); + + if (m_bInMotion.find(buttonMap) != m_bInMotion.end() && !bMotion) + OnMotionless(buttonMap); +} + +void CGUIConfigurationWizard::OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, + unsigned int axisIndex) +{ + std::unique_lock<CCriticalSection> lock(m_stateMutex); + + m_lateAxisDetected = true; + Abort(false); +} + +void CGUIConfigurationWizard::OnMotion(const JOYSTICK::IButtonMap* buttonMap) +{ + std::unique_lock<CCriticalSection> lock(m_motionMutex); + + m_motionlessEvent.Reset(); + m_bInMotion.insert(buttonMap); +} + +void CGUIConfigurationWizard::OnMotionless(const JOYSTICK::IButtonMap* buttonMap) +{ + m_bInMotion.erase(buttonMap); + if (m_bInMotion.empty()) + m_motionlessEvent.Set(); +} + +bool CGUIConfigurationWizard::OnKeyPress(const CKey& key) +{ + bool bHandled = false; + + if (!m_bStop) + { + // Only allow key to abort the prompt if we know for sure that we're mapping + // a controller + const bool bIsMappingController = (IsMapping() && !m_bIsKeyboard); + + if (bIsMappingController) + { + bHandled = OnAction(m_actionMap->GetActionID(key)); + } + else + { + // Allow key press to fall through to the button mapper + } + } + + return bHandled; +} + +bool CGUIConfigurationWizard::OnAction(unsigned int actionId) +{ + bool bHandled = false; + + switch (actionId) + { + case ACTION_MOVE_LEFT: + case ACTION_MOVE_RIGHT: + case ACTION_MOVE_UP: + case ACTION_MOVE_DOWN: + case ACTION_PAGE_UP: + case ACTION_PAGE_DOWN: + // Abort and allow motion + Abort(false); + bHandled = false; + break; + + case ACTION_PARENT_DIR: + case ACTION_PREVIOUS_MENU: + case ACTION_STOP: + case ACTION_NAV_BACK: + // Abort and prevent action + Abort(false); + bHandled = true; + break; + + default: + // Absorb keypress + bHandled = true; + break; + } + + return bHandled; +} + +bool CGUIConfigurationWizard::IsMapping() const +{ + return !m_location.empty(); +} + +bool CGUIConfigurationWizard::IsMapping(const std::string& location) const +{ + return m_location == location; +} + +void CGUIConfigurationWizard::InstallHooks(void) +{ + // Install button mapper with lowest priority + CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this); + + // Install hook to reattach button mapper when peripherals change + CServiceBroker::GetPeripherals().RegisterObserver(this); + + // Install hook to cancel the button mapper + CServiceBroker::GetInputManager().RegisterKeyboardDriverHandler(this); +} + +void CGUIConfigurationWizard::RemoveHooks(void) +{ + CServiceBroker::GetInputManager().UnregisterKeyboardDriverHandler(this); + CServiceBroker::GetPeripherals().UnregisterObserver(this); + CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this); +} + +void CGUIConfigurationWizard::Notify(const Observable& obs, const ObservableMessage msg) +{ + switch (msg) + { + case ObservableMessagePeripheralsChanged: + { + CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this); + CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this); + break; + } + default: + break; + } +} diff --git a/xbmc/games/controllers/windows/GUIConfigurationWizard.h b/xbmc/games/controllers/windows/GUIConfigurationWizard.h new file mode 100644 index 0000000..b69badf --- /dev/null +++ b/xbmc/games/controllers/windows/GUIConfigurationWizard.h @@ -0,0 +1,117 @@ +/* + * 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 "IConfigurationWindow.h" +#include "games/controllers/input/PhysicalFeature.h" +#include "input/XBMC_keysym.h" +#include "input/joysticks/DriverPrimitive.h" +#include "input/joysticks/interfaces/IButtonMapper.h" +#include "input/keyboard/interfaces/IKeyboardDriverHandler.h" +#include "threads/CriticalSection.h" +#include "threads/Event.h" +#include "threads/Thread.h" +#include "utils/Observer.h" + +#include <map> +#include <memory> +#include <set> +#include <string> +#include <vector> + +namespace KODI +{ +namespace KEYBOARD +{ +class IActionMap; +} + +namespace GAME +{ +class CGUIConfigurationWizard : public IConfigurationWizard, + public JOYSTICK::IButtonMapper, + public KEYBOARD::IKeyboardDriverHandler, + public Observer, + protected CThread +{ +public: + CGUIConfigurationWizard(); + + ~CGUIConfigurationWizard() override; + + // implementation of IConfigurationWizard + void Run(const std::string& strControllerId, + const std::vector<IFeatureButton*>& buttons) override; + void OnUnfocus(IFeatureButton* button) override; + bool Abort(bool bWait = true) override; + void RegisterKey(const CPhysicalFeature& key) override; + void UnregisterKeys() override; + + // implementation of IButtonMapper + std::string ControllerID() const override { return m_strControllerId; } + bool NeedsCooldown() const override { return true; } + bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override { return true; } + bool MapPrimitive(JOYSTICK::IButtonMap* buttonMap, + IKeymap* keymap, + const JOYSTICK::CDriverPrimitive& primitive) override; + void OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion) override; + void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override; + + // implementation of IKeyboardDriverHandler + bool OnKeyPress(const CKey& key) override; + void OnKeyRelease(const CKey& key) override {} + + // implementation of Observer + void Notify(const Observable& obs, const ObservableMessage msg) override; + +protected: + // implementation of CThread + void Process() override; + +private: + void InitializeState(void); + + bool IsMapping() const; + bool IsMapping(const std::string& location) const; + + void InstallHooks(void); + void RemoveHooks(void); + + void OnMotion(const JOYSTICK::IButtonMap* buttonMap); + void OnMotionless(const JOYSTICK::IButtonMap* buttonMap); + + bool OnAction(unsigned int actionId); + + // Run() parameters + std::string m_strControllerId; + std::vector<IFeatureButton*> m_buttons; + + // State variables and mutex + IFeatureButton* m_currentButton; + INPUT::CARDINAL_DIRECTION m_cardinalDirection; + JOYSTICK::WHEEL_DIRECTION m_wheelDirection; + JOYSTICK::THROTTLE_DIRECTION m_throttleDirection; + std::set<JOYSTICK::CDriverPrimitive> m_history; // History to avoid repeated features + bool m_lateAxisDetected; // Set to true if an axis is detected during button mapping + std::string m_location; // Peripheral location of device that we're mapping + bool m_bIsKeyboard = false; // True if we're mapping keyboard keys + CCriticalSection m_stateMutex; + + // Synchronization events + CEvent m_inputEvent; + CEvent m_motionlessEvent; + CCriticalSection m_motionMutex; + std::set<const JOYSTICK::IButtonMap*> m_bInMotion; + + // Keyboard handling + std::unique_ptr<KEYBOARD::IActionMap> m_actionMap; + std::map<XBMCKey, CPhysicalFeature> m_keyMap; // Keycode -> feature +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/windows/GUIControllerDefines.h b/xbmc/games/controllers/windows/GUIControllerDefines.h new file mode 100644 index 0000000..64d2142 --- /dev/null +++ b/xbmc/games/controllers/windows/GUIControllerDefines.h @@ -0,0 +1,45 @@ +/* + * 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 + +// Duration to wait for input from the user +#define COUNTDOWN_DURATION_SEC 6 + +// Warn the user that time is running out after this duration +#define WAIT_TO_WARN_SEC 2 + +// GUI Control IDs +#define CONTROL_CONTROLLER_LIST 3 +#define CONTROL_FEATURE_LIST 5 +#define CONTROL_FEATURE_BUTTON_TEMPLATE 7 +#define CONTROL_FEATURE_GROUP_TITLE 8 +#define CONTROL_FEATURE_SEPARATOR 9 +#define CONTROL_CONTROLLER_BUTTON_TEMPLATE 10 +#define CONTROL_GAME_CONTROLLER 31 +#define CONTROL_CONTROLLER_DESCRIPTION 32 + +// GUI button IDs +#define CONTROL_HELP_BUTTON 17 +#define CONTROL_CLOSE_BUTTON 18 +#define CONTROL_RESET_BUTTON 19 +#define CONTROL_GET_MORE 20 +#define CONTROL_FIX_SKIPPING 21 +#define CONTROL_GET_ALL 22 + +#define MAX_CONTROLLER_COUNT 100 // large enough +#define MAX_FEATURE_COUNT 200 // large enough + +#define CONTROL_CONTROLLER_BUTTONS_START 100 +#define CONTROL_CONTROLLER_BUTTONS_END (CONTROL_CONTROLLER_BUTTONS_START + MAX_CONTROLLER_COUNT) +#define CONTROL_FEATURE_BUTTONS_START CONTROL_CONTROLLER_BUTTONS_END +#define CONTROL_FEATURE_BUTTONS_END (CONTROL_FEATURE_BUTTONS_START + MAX_FEATURE_COUNT) +#define CONTROL_FEATURE_GROUPS_START CONTROL_FEATURE_BUTTONS_END +#define CONTROL_FEATURE_GROUPS_END (CONTROL_FEATURE_GROUPS_START + MAX_FEATURE_COUNT) +#define CONTROL_FEATURE_SEPARATORS_START CONTROL_FEATURE_GROUPS_END +#define CONTROL_FEATURE_SEPARATORS_END (CONTROL_FEATURE_SEPARATORS_START + MAX_FEATURE_COUNT) diff --git a/xbmc/games/controllers/windows/GUIControllerList.cpp b/xbmc/games/controllers/windows/GUIControllerList.cpp new file mode 100644 index 0000000..02f8d37 --- /dev/null +++ b/xbmc/games/controllers/windows/GUIControllerList.cpp @@ -0,0 +1,239 @@ +/* + * 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 "GUIControllerList.h" + +#include "GUIControllerDefines.h" +#include "GUIControllerWindow.h" +#include "GUIFeatureList.h" +#include "ServiceBroker.h" +#include "addons/AddonManager.h" +#include "dialogs/GUIDialogYesNo.h" +#include "games/GameServices.h" +#include "games/addons/GameClient.h" +#include "games/addons/input/GameClientInput.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerIDs.h" +#include "games/controllers/ControllerLayout.h" +#include "games/controllers/guicontrols/GUIControllerButton.h" +#include "games/controllers/guicontrols/GUIGameController.h" +#include "games/controllers/types/ControllerTree.h" +#include "guilib/GUIButtonControl.h" +#include "guilib/GUIControlGroupList.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIWindow.h" +#include "messaging/ApplicationMessenger.h" +#include "peripherals/Peripherals.h" +#include "utils/StringUtils.h" + +#include <algorithm> +#include <assert.h> +#include <iterator> + +using namespace KODI; +using namespace ADDON; +using namespace GAME; + +CGUIControllerList::CGUIControllerList(CGUIWindow* window, + IFeatureList* featureList, + GameClientPtr gameClient) + : m_guiWindow(window), + m_featureList(featureList), + m_controllerList(nullptr), + m_controllerButton(nullptr), + m_focusedController(-1), // Initially unfocused + m_gameClient(std::move(gameClient)) +{ + assert(m_featureList != nullptr); +} + +bool CGUIControllerList::Initialize(void) +{ + m_controllerList = + dynamic_cast<CGUIControlGroupList*>(m_guiWindow->GetControl(CONTROL_CONTROLLER_LIST)); + m_controllerButton = + dynamic_cast<CGUIButtonControl*>(m_guiWindow->GetControl(CONTROL_CONTROLLER_BUTTON_TEMPLATE)); + + if (m_controllerButton) + m_controllerButton->SetVisible(false); + + CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIControllerList::OnEvent); + Refresh(""); + + return m_controllerList != nullptr && m_controllerButton != nullptr; +} + +void CGUIControllerList::Deinitialize(void) +{ + CServiceBroker::GetAddonMgr().Events().Unsubscribe(this); + + CleanupButtons(); + + m_controllerList = nullptr; + m_controllerButton = nullptr; +} + +bool CGUIControllerList::Refresh(const std::string& controllerId) +{ + // Focus specified controller after refresh + std::string focusController = controllerId; + + if (focusController.empty() && m_focusedController >= 0) + { + // If controller ID wasn't provided, focus current controller + focusController = m_controllers[m_focusedController]->ID(); + } + + if (!RefreshControllers()) + return false; + + CleanupButtons(); + + if (m_controllerList) + { + unsigned int buttonId = 0; + for (const auto& controller : m_controllers) + { + CGUIButtonControl* pButton = + new CGUIControllerButton(*m_controllerButton, controller->Layout().Label(), buttonId++); + m_controllerList->AddControl(pButton); + + if (!focusController.empty() && controller->ID() == focusController) + { + CGUIMessage msg(GUI_MSG_SETFOCUS, m_guiWindow->GetID(), pButton->GetID()); + m_guiWindow->OnMessage(msg); + } + + // Just in case + if (buttonId >= MAX_CONTROLLER_COUNT) + break; + } + } + + return true; +} + +void CGUIControllerList::OnFocus(unsigned int controllerIndex) +{ + if (controllerIndex < m_controllers.size()) + { + m_focusedController = controllerIndex; + + const ControllerPtr& controller = m_controllers[controllerIndex]; + m_featureList->Load(controller); + + //! @todo Activate controller for all game controller controls + CGUIGameController* pController = + dynamic_cast<CGUIGameController*>(m_guiWindow->GetControl(CONTROL_GAME_CONTROLLER)); + if (pController) + pController->ActivateController(controller); + + // Update controller description + CGUIMessage msg(GUI_MSG_LABEL_SET, m_guiWindow->GetID(), CONTROL_CONTROLLER_DESCRIPTION); + msg.SetLabel(controller->Description()); + m_guiWindow->OnMessage(msg); + } +} + +void CGUIControllerList::OnSelect(unsigned int controllerIndex) +{ + m_featureList->OnSelect(0); +} + +void CGUIControllerList::ResetController(void) +{ + if (0 <= m_focusedController && m_focusedController < (int)m_controllers.size()) + { + const std::string strControllerId = m_controllers[m_focusedController]->ID(); + + //! @todo Choose peripheral + // For now, ask the user if they would like to reset all peripherals + // "Reset controller profile" + // "Would you like to reset this controller profile for all devices?" + if (!CGUIDialogYesNo::ShowAndGetInput(35060, 35061)) + return; + + CServiceBroker::GetPeripherals().ResetButtonMaps(strControllerId); + } +} + +void CGUIControllerList::OnEvent(const ADDON::AddonEvent& event) +{ + if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) || // also called on install, + typeid(event) == typeid(ADDON::AddonEvents::Disabled) || // not called on uninstall + typeid(event) == typeid(ADDON::AddonEvents::ReInstalled) || + typeid(event) == typeid(ADDON::AddonEvents::UnInstalled)) + { + CGUIMessage msg(GUI_MSG_REFRESH_LIST, m_guiWindow->GetID(), CONTROL_CONTROLLER_LIST); + + // Focus installed add-on + if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) || + typeid(event) == typeid(ADDON::AddonEvents::ReInstalled)) + msg.SetStringParam(event.addonId); + + CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow->GetID()); + } +} + +bool CGUIControllerList::RefreshControllers(void) +{ + // Get current controllers + CGameServices& gameServices = CServiceBroker::GetGameServices(); + ControllerVector newControllers = gameServices.GetControllers(); + + // Filter by current game add-on + if (m_gameClient) + { + const CControllerTree& controllers = m_gameClient->Input().GetDefaultControllerTree(); + + auto ControllerNotAccepted = [&controllers](const ControllerPtr& controller) { + return !controllers.IsControllerAccepted(controller->ID()); + }; + + if (!std::all_of(newControllers.begin(), newControllers.end(), ControllerNotAccepted)) + newControllers.erase( + std::remove_if(newControllers.begin(), newControllers.end(), ControllerNotAccepted), + newControllers.end()); + } + + // Check for changes + std::set<std::string> oldControllerIds; + std::set<std::string> newControllerIds; + + auto GetControllerID = [](const ControllerPtr& controller) { return controller->ID(); }; + + std::transform(m_controllers.begin(), m_controllers.end(), + std::inserter(oldControllerIds, oldControllerIds.begin()), GetControllerID); + std::transform(newControllers.begin(), newControllers.end(), + std::inserter(newControllerIds, newControllerIds.begin()), GetControllerID); + + const bool bChanged = (oldControllerIds != newControllerIds); + if (bChanged) + { + m_controllers = std::move(newControllers); + + // Sort add-ons, with default controller first + std::sort(m_controllers.begin(), m_controllers.end(), + [](const ControllerPtr& i, const ControllerPtr& j) { + if (i->ID() == DEFAULT_CONTROLLER_ID && j->ID() != DEFAULT_CONTROLLER_ID) + return true; + if (i->ID() != DEFAULT_CONTROLLER_ID && j->ID() == DEFAULT_CONTROLLER_ID) + return false; + + return StringUtils::CompareNoCase(i->Layout().Label(), j->Layout().Label()) < 0; + }); + } + + return bChanged; +} + +void CGUIControllerList::CleanupButtons(void) +{ + if (m_controllerList) + m_controllerList->ClearAll(); +} diff --git a/xbmc/games/controllers/windows/GUIControllerList.h b/xbmc/games/controllers/windows/GUIControllerList.h new file mode 100644 index 0000000..8292889 --- /dev/null +++ b/xbmc/games/controllers/windows/GUIControllerList.h @@ -0,0 +1,62 @@ +/* + * 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 "IConfigurationWindow.h" +#include "addons/AddonEvents.h" +#include "games/GameTypes.h" +#include "games/controllers/ControllerTypes.h" + +#include <set> +#include <string> + +class CGUIButtonControl; +class CGUIControlGroupList; +class CGUIWindow; + +namespace KODI +{ +namespace GAME +{ +class CGUIControllerWindow; + +class CGUIControllerList : public IControllerList +{ +public: + CGUIControllerList(CGUIWindow* window, IFeatureList* featureList, GameClientPtr gameClient); + ~CGUIControllerList() override { Deinitialize(); } + + // implementation of IControllerList + bool Initialize() override; + void Deinitialize() override; + bool Refresh(const std::string& controllerId) override; + void OnFocus(unsigned int controllerIndex) override; + void OnSelect(unsigned int controllerIndex) override; + int GetFocusedController() const override { return m_focusedController; } + void ResetController() override; + +private: + bool RefreshControllers(void); + + void CleanupButtons(void); + void OnEvent(const ADDON::AddonEvent& event); + + // GUI stuff + CGUIWindow* const m_guiWindow; + IFeatureList* const m_featureList; + CGUIControlGroupList* m_controllerList; + CGUIButtonControl* m_controllerButton; + + // Game stuff + ControllerVector m_controllers; + int m_focusedController; + GameClientPtr m_gameClient; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/windows/GUIControllerWindow.cpp b/xbmc/games/controllers/windows/GUIControllerWindow.cpp new file mode 100644 index 0000000..6b2d2ac --- /dev/null +++ b/xbmc/games/controllers/windows/GUIControllerWindow.cpp @@ -0,0 +1,376 @@ +/* + * 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 "GUIControllerWindow.h" + +#include "GUIControllerDefines.h" +#include "GUIControllerList.h" +#include "GUIFeatureList.h" +#include "ServiceBroker.h" +#include "addons/AddonManager.h" +#include "addons/IAddon.h" +#include "addons/addoninfo/AddonType.h" +#include "addons/gui/GUIWindowAddonBrowser.h" +#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h" +#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h" +#include "games/addons/GameClient.h" +#include "games/controllers/dialogs/ControllerInstaller.h" +#include "games/controllers/dialogs/GUIDialogIgnoreInput.h" +#include "guilib/GUIButtonControl.h" +#include "guilib/GUIControl.h" +#include "guilib/GUIMessage.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogOKHelper.h" + +// To enable button mapping support +#include "peripherals/Peripherals.h" + +using namespace KODI; +using namespace GAME; +using namespace KODI::MESSAGING; + +CGUIControllerWindow::CGUIControllerWindow(void) + : CGUIDialog(WINDOW_DIALOG_GAME_CONTROLLERS, "DialogGameControllers.xml"), + m_installer(new CControllerInstaller) +{ + // initialize CGUIWindow + m_loadType = KEEP_IN_MEMORY; +} + +CGUIControllerWindow::~CGUIControllerWindow(void) +{ + delete m_controllerList; + delete m_featureList; +} + +void CGUIControllerWindow::DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + /* + * Apply the faded focus texture to the current controller when unfocused + */ + + CGUIControl* control = nullptr; // The controller button + bool bAlphaFaded = false; // True if the controller button has been focused and faded this frame + + if (m_controllerList && m_controllerList->GetFocusedController() >= 0) + { + control = GetFirstFocusableControl(CONTROL_CONTROLLER_BUTTONS_START + + m_controllerList->GetFocusedController()); + if (control && !control->HasFocus()) + { + if (control->GetControlType() == CGUIControl::GUICONTROL_BUTTON) + { + control->SetFocus(true); + static_cast<CGUIButtonControl*>(control)->SetAlpha(0x80); + bAlphaFaded = true; + } + } + } + + CGUIDialog::DoProcess(currentTime, dirtyregions); + + if (control && bAlphaFaded) + { + control->SetFocus(false); + if (control->GetControlType() == CGUIControl::GUICONTROL_BUTTON) + static_cast<CGUIButtonControl*>(control)->SetAlpha(0xFF); + } +} + +bool CGUIControllerWindow::OnMessage(CGUIMessage& message) +{ + // Set to true to block the call to the super class + bool bHandled = false; + + switch (message.GetMessage()) + { + case GUI_MSG_CLICKED: + { + int controlId = message.GetSenderId(); + + if (controlId == CONTROL_CLOSE_BUTTON) + { + Close(); + bHandled = true; + } + else if (controlId == CONTROL_GET_MORE) + { + GetMoreControllers(); + bHandled = true; + } + else if (controlId == CONTROL_GET_ALL) + { + GetAllControllers(); + bHandled = true; + } + else if (controlId == CONTROL_RESET_BUTTON) + { + ResetController(); + bHandled = true; + } + else if (controlId == CONTROL_HELP_BUTTON) + { + ShowHelp(); + bHandled = true; + } + else if (controlId == CONTROL_FIX_SKIPPING) + { + ShowButtonCaptureDialog(); + } + else if (CONTROL_CONTROLLER_BUTTONS_START <= controlId && + controlId < CONTROL_CONTROLLER_BUTTONS_END) + { + OnControllerSelected(controlId - CONTROL_CONTROLLER_BUTTONS_START); + bHandled = true; + } + else if (CONTROL_FEATURE_BUTTONS_START <= controlId && + controlId < CONTROL_FEATURE_BUTTONS_END) + { + OnFeatureSelected(controlId - CONTROL_FEATURE_BUTTONS_START); + bHandled = true; + } + break; + } + case GUI_MSG_FOCUSED: + { + int controlId = message.GetControlId(); + + if (CONTROL_CONTROLLER_BUTTONS_START <= controlId && + controlId < CONTROL_CONTROLLER_BUTTONS_END) + { + OnControllerFocused(controlId - CONTROL_CONTROLLER_BUTTONS_START); + } + else if (CONTROL_FEATURE_BUTTONS_START <= controlId && + controlId < CONTROL_FEATURE_BUTTONS_END) + { + OnFeatureFocused(controlId - CONTROL_FEATURE_BUTTONS_START); + } + break; + } + case GUI_MSG_SETFOCUS: + { + int controlId = message.GetControlId(); + + if (CONTROL_CONTROLLER_BUTTONS_START <= controlId && + controlId < CONTROL_CONTROLLER_BUTTONS_END) + { + OnControllerFocused(controlId - CONTROL_CONTROLLER_BUTTONS_START); + } + else if (CONTROL_FEATURE_BUTTONS_START <= controlId && + controlId < CONTROL_FEATURE_BUTTONS_END) + { + OnFeatureFocused(controlId - CONTROL_FEATURE_BUTTONS_START); + } + break; + } + case GUI_MSG_REFRESH_LIST: + { + int controlId = message.GetControlId(); + + if (controlId == CONTROL_CONTROLLER_LIST) + { + const std::string controllerId = message.GetStringParam(); + if (m_controllerList && m_controllerList->Refresh(controllerId)) + { + CGUIDialog::OnMessage(message); + bHandled = true; + } + } + break; + } + default: + break; + } + + if (!bHandled) + bHandled = CGUIDialog::OnMessage(message); + + return bHandled; +} + +void CGUIControllerWindow::OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event) +{ + UpdateButtons(); +} + +void CGUIControllerWindow::OnEvent(const ADDON::AddonEvent& event) +{ + using namespace ADDON; + + if (typeid(event) == typeid(AddonEvents::Enabled) || // also called on install, + typeid(event) == typeid(AddonEvents::Disabled) || // not called on uninstall + typeid(event) == typeid(AddonEvents::UnInstalled) || + typeid(event) == typeid(AddonEvents::ReInstalled)) + { + if (CServiceBroker::GetAddonMgr().HasType(event.addonId, AddonType::GAME_CONTROLLER)) + { + UpdateButtons(); + } + } +} + +void CGUIControllerWindow::OnInitWindow(void) +{ + // Get active game add-on + GameClientPtr gameClient; + { + auto gameSettingsHandle = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog(); + if (gameSettingsHandle) + { + ADDON::AddonPtr addon; + if (CServiceBroker::GetAddonMgr().GetAddon(gameSettingsHandle->GameClientID(), addon, + ADDON::AddonType::GAMEDLL, + ADDON::OnlyEnabled::CHOICE_YES)) + gameClient = std::static_pointer_cast<CGameClient>(addon); + } + } + m_gameClient = std::move(gameClient); + + CGUIDialog::OnInitWindow(); + + if (!m_featureList) + { + m_featureList = new CGUIFeatureList(this, m_gameClient); + if (!m_featureList->Initialize()) + { + delete m_featureList; + m_featureList = nullptr; + } + } + + if (!m_controllerList && m_featureList) + { + m_controllerList = new CGUIControllerList(this, m_featureList, m_gameClient); + if (!m_controllerList->Initialize()) + { + delete m_controllerList; + m_controllerList = nullptr; + } + } + + // Focus the first controller so that the feature list is loaded properly + CGUIMessage msgFocus(GUI_MSG_SETFOCUS, GetID(), CONTROL_CONTROLLER_BUTTONS_START); + OnMessage(msgFocus); + + // Enable button mapping support + CServiceBroker::GetPeripherals().EnableButtonMapping(); + + UpdateButtons(); + + // subscribe to events + CServiceBroker::GetRepositoryUpdater().Events().Subscribe(this, &CGUIControllerWindow::OnEvent); + CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIControllerWindow::OnEvent); +} + +void CGUIControllerWindow::OnDeinitWindow(int nextWindowID) +{ + CServiceBroker::GetRepositoryUpdater().Events().Unsubscribe(this); + CServiceBroker::GetAddonMgr().Events().Unsubscribe(this); + + if (m_controllerList) + { + m_controllerList->Deinitialize(); + delete m_controllerList; + m_controllerList = nullptr; + } + + if (m_featureList) + { + m_featureList->Deinitialize(); + delete m_featureList; + m_featureList = nullptr; + } + + CGUIDialog::OnDeinitWindow(nextWindowID); + + m_gameClient.reset(); +} + +void CGUIControllerWindow::OnControllerFocused(unsigned int controllerIndex) +{ + if (m_controllerList) + m_controllerList->OnFocus(controllerIndex); +} + +void CGUIControllerWindow::OnControllerSelected(unsigned int controllerIndex) +{ + if (m_controllerList) + m_controllerList->OnSelect(controllerIndex); +} + +void CGUIControllerWindow::OnFeatureFocused(unsigned int buttonIndex) +{ + if (m_featureList) + m_featureList->OnFocus(buttonIndex); +} + +void CGUIControllerWindow::OnFeatureSelected(unsigned int buttonIndex) +{ + if (m_featureList) + m_featureList->OnSelect(buttonIndex); +} + +void CGUIControllerWindow::UpdateButtons(void) +{ + using namespace ADDON; + + VECADDONS addons; + if (m_gameClient) + { + SET_CONTROL_HIDDEN(CONTROL_GET_MORE); + SET_CONTROL_HIDDEN(CONTROL_GET_ALL); + } + else + { + const bool bEnable = CServiceBroker::GetAddonMgr().GetInstallableAddons( + addons, ADDON::AddonType::GAME_CONTROLLER) && + !addons.empty(); + CONTROL_ENABLE_ON_CONDITION(CONTROL_GET_MORE, bEnable); + CONTROL_ENABLE_ON_CONDITION(CONTROL_GET_ALL, bEnable); + } +} + +void CGUIControllerWindow::GetMoreControllers(void) +{ + std::string strAddonId; + if (CGUIWindowAddonBrowser::SelectAddonID(ADDON::AddonType::GAME_CONTROLLER, strAddonId, false, + true, false, true, false) < 0) + { + // "Controller profiles" + // "All available controller profiles are installed." + HELPERS::ShowOKDialogText(CVariant{35050}, CVariant{35062}); + return; + } +} + +void CGUIControllerWindow::GetAllControllers() +{ + if (m_installer->IsRunning()) + return; + + m_installer->Create(false); +} + +void CGUIControllerWindow::ResetController(void) +{ + if (m_controllerList) + m_controllerList->ResetController(); +} + +void CGUIControllerWindow::ShowHelp(void) +{ + // "Help" + // <help text> + HELPERS::ShowOKDialogText(CVariant{10043}, CVariant{35055}); +} + +void CGUIControllerWindow::ShowButtonCaptureDialog(void) +{ + CGUIDialogIgnoreInput dialog; + dialog.Show(); +} diff --git a/xbmc/games/controllers/windows/GUIControllerWindow.h b/xbmc/games/controllers/windows/GUIControllerWindow.h new file mode 100644 index 0000000..137fd56 --- /dev/null +++ b/xbmc/games/controllers/windows/GUIControllerWindow.h @@ -0,0 +1,68 @@ +/* + * 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 "addons/RepositoryUpdater.h" +#include "games/GameTypes.h" +#include "guilib/GUIDialog.h" + +#include <memory> + +namespace KODI +{ +namespace GAME +{ +class CControllerInstaller; +class IControllerList; +class IFeatureList; + +class CGUIControllerWindow : public CGUIDialog +{ +public: + CGUIControllerWindow(void); + ~CGUIControllerWindow() override; + + // implementation of CGUIControl via CGUIDialog + void DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions) override; + bool OnMessage(CGUIMessage& message) override; + +protected: + // implementation of CGUIWindow via CGUIDialog + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + +private: + void OnControllerFocused(unsigned int controllerIndex); + void OnControllerSelected(unsigned int controllerIndex); + void OnFeatureFocused(unsigned int featureIndex); + void OnFeatureSelected(unsigned int featureIndex); + void UpdateButtons(void); + + // Callbacks for events + void OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event); + void OnEvent(const ADDON::AddonEvent& event); + + // Action for the available button + void GetMoreControllers(void); + void GetAllControllers(); + void ResetController(void); + void ShowHelp(void); + void ShowButtonCaptureDialog(void); + + IControllerList* m_controllerList = nullptr; + IFeatureList* m_featureList = nullptr; + + // Game parameters + GameClientPtr m_gameClient; + + // Controller parameters + std::unique_ptr<CControllerInstaller> m_installer; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/windows/GUIFeatureList.cpp b/xbmc/games/controllers/windows/GUIFeatureList.cpp new file mode 100644 index 0000000..d43c16d --- /dev/null +++ b/xbmc/games/controllers/windows/GUIFeatureList.cpp @@ -0,0 +1,297 @@ +/* + * 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 "GUIFeatureList.h" + +#include "GUIConfigurationWizard.h" +#include "GUIControllerDefines.h" +#include "games/addons/GameClient.h" +#include "games/addons/input/GameClientInput.h" +#include "games/controllers/Controller.h" +#include "games/controllers/guicontrols/GUIFeatureButton.h" +#include "games/controllers/guicontrols/GUIFeatureControls.h" +#include "games/controllers/guicontrols/GUIFeatureFactory.h" +#include "games/controllers/guicontrols/GUIFeatureTranslator.h" +#include "games/controllers/input/PhysicalFeature.h" +#include "guilib/GUIButtonControl.h" +#include "guilib/GUIControlGroupList.h" +#include "guilib/GUIImage.h" +#include "guilib/GUILabelControl.h" +#include "guilib/GUIWindow.h" +#include "guilib/LocalizeStrings.h" + +using namespace KODI; +using namespace GAME; + +CGUIFeatureList::CGUIFeatureList(CGUIWindow* window, GameClientPtr gameClient) + : m_window(window), + m_guiList(nullptr), + m_guiButtonTemplate(nullptr), + m_guiGroupTitle(nullptr), + m_guiFeatureSeparator(nullptr), + m_gameClient(std::move(gameClient)), + m_wizard(new CGUIConfigurationWizard) +{ +} + +CGUIFeatureList::~CGUIFeatureList(void) +{ + Deinitialize(); + delete m_wizard; +} + +bool CGUIFeatureList::Initialize(void) +{ + m_guiList = dynamic_cast<CGUIControlGroupList*>(m_window->GetControl(CONTROL_FEATURE_LIST)); + m_guiButtonTemplate = + dynamic_cast<CGUIButtonControl*>(m_window->GetControl(CONTROL_FEATURE_BUTTON_TEMPLATE)); + m_guiGroupTitle = + dynamic_cast<CGUILabelControl*>(m_window->GetControl(CONTROL_FEATURE_GROUP_TITLE)); + m_guiFeatureSeparator = dynamic_cast<CGUIImage*>(m_window->GetControl(CONTROL_FEATURE_SEPARATOR)); + + if (m_guiButtonTemplate) + m_guiButtonTemplate->SetVisible(false); + + if (m_guiGroupTitle) + m_guiGroupTitle->SetVisible(false); + + if (m_guiFeatureSeparator) + m_guiFeatureSeparator->SetVisible(false); + + return m_guiList != nullptr && m_guiButtonTemplate != nullptr; +} + +void CGUIFeatureList::Deinitialize(void) +{ + CleanupButtons(); + + m_guiList = nullptr; + m_guiButtonTemplate = nullptr; + m_guiGroupTitle = nullptr; + m_guiFeatureSeparator = nullptr; +} + +void CGUIFeatureList::Load(const ControllerPtr& controller) +{ + if (m_controller && m_controller->ID() == controller->ID()) + return; // Already loaded + + CleanupButtons(); + + // Set new controller + m_controller = controller; + + // Get features + const std::vector<CPhysicalFeature>& features = controller->Features(); + + // Split into groups + auto featureGroups = GetFeatureGroups(features); + + // Create controls + m_buttonCount = 0; + for (auto itGroup = featureGroups.begin(); itGroup != featureGroups.end(); ++itGroup) + { + const std::string& groupName = itGroup->groupName; + const bool bIsVirtualKey = itGroup->bIsVirtualKey; + + std::vector<CGUIButtonControl*> buttons; + + // Create buttons + if (bIsVirtualKey) + { + CGUIButtonControl* button = GetSelectKeyButton(itGroup->features, m_buttonCount); + if (button != nullptr) + buttons.push_back(button); + } + else + { + buttons = GetButtons(itGroup->features, m_buttonCount); + } + + // Just in case + if (m_buttonCount + buttons.size() >= MAX_FEATURE_COUNT) + break; + + // Add a separator if the group list isn't empty + if (m_guiFeatureSeparator && m_guiList->GetTotalSize() > 0) + { + CGUIFeatureSeparator* pSeparator = + new CGUIFeatureSeparator(*m_guiFeatureSeparator, m_buttonCount); + m_guiList->AddControl(pSeparator); + } + + // Add the group title + if (m_guiGroupTitle && !groupName.empty()) + { + CGUIFeatureGroupTitle* pGroupTitle = + new CGUIFeatureGroupTitle(*m_guiGroupTitle, groupName, m_buttonCount); + m_guiList->AddControl(pGroupTitle); + } + + // Add the buttons + for (CGUIButtonControl* pButton : buttons) + m_guiList->AddControl(pButton); + + m_buttonCount += static_cast<unsigned int>(buttons.size()); + } +} + +void CGUIFeatureList::OnSelect(unsigned int buttonIndex) +{ + // Generate list of buttons for the wizard + std::vector<IFeatureButton*> buttons; + for (; buttonIndex < m_buttonCount; buttonIndex++) + { + IFeatureButton* control = GetButtonControl(buttonIndex); + if (control == nullptr) + continue; + + if (control->AllowWizard()) + buttons.push_back(control); + else + { + // Only map this button if it's the only one + if (buttons.empty()) + buttons.push_back(control); + break; + } + } + + m_wizard->Run(m_controller->ID(), buttons); +} + +IFeatureButton* CGUIFeatureList::GetButtonControl(unsigned int buttonIndex) +{ + CGUIControl* control = m_guiList->GetControl(CONTROL_FEATURE_BUTTONS_START + buttonIndex); + + return static_cast<IFeatureButton*>(dynamic_cast<CGUIFeatureButton*>(control)); +} + +void CGUIFeatureList::CleanupButtons(void) +{ + m_buttonCount = 0; + + m_wizard->Abort(true); + m_wizard->UnregisterKeys(); + + if (m_guiList) + m_guiList->ClearAll(); +} + +std::vector<CGUIFeatureList::FeatureGroup> CGUIFeatureList::GetFeatureGroups( + const std::vector<CPhysicalFeature>& features) const +{ + std::vector<FeatureGroup> groups; + + // Get group names + std::vector<std::string> groupNames; + for (const CPhysicalFeature& feature : features) + { + // Skip features not supported by the game client + if (m_gameClient) + { + if (!m_gameClient->Input().HasFeature(m_controller->ID(), feature.Name())) + continue; + } + + bool bAdded = false; + + if (!groups.empty()) + { + FeatureGroup& previousGroup = *groups.rbegin(); + if (feature.CategoryLabel() == previousGroup.groupName) + { + // Add feature to previous group + previousGroup.features.emplace_back(feature); + bAdded = true; + + // If feature is a key, add it to the preceding virtual group as well + if (feature.Category() == JOYSTICK::FEATURE_CATEGORY::KEY && groups.size() >= 2) + { + FeatureGroup& virtualGroup = *(groups.rbegin() + 1); + if (virtualGroup.bIsVirtualKey) + virtualGroup.features.emplace_back(feature); + } + } + } + + if (!bAdded) + { + // If feature is a key, create a virtual group that allows the user to + // select which key to map + if (feature.Category() == JOYSTICK::FEATURE_CATEGORY::KEY) + { + FeatureGroup virtualGroup; + virtualGroup.groupName = g_localizeStrings.Get(35166); // "All keys" + virtualGroup.bIsVirtualKey = true; + virtualGroup.features.emplace_back(feature); + groups.emplace_back(std::move(virtualGroup)); + } + + // Create new group and add feature + FeatureGroup group; + group.groupName = feature.CategoryLabel(); + group.features.emplace_back(feature); + groups.emplace_back(std::move(group)); + } + } + + // If there are no features, add an empty group + if (groups.empty()) + { + FeatureGroup group; + group.groupName = g_localizeStrings.Get(35022); // "Nothing to map" + groups.emplace_back(std::move(group)); + } + + return groups; +} + +bool CGUIFeatureList::HasButton(JOYSTICK::FEATURE_TYPE type) const +{ + return CGUIFeatureTranslator::GetButtonType(type) != BUTTON_TYPE::UNKNOWN; +} + +std::vector<CGUIButtonControl*> CGUIFeatureList::GetButtons( + const std::vector<CPhysicalFeature>& features, unsigned int startIndex) +{ + std::vector<CGUIButtonControl*> buttons; + + // Create buttons + unsigned int buttonIndex = startIndex; + for (const CPhysicalFeature& feature : features) + { + BUTTON_TYPE buttonType = CGUIFeatureTranslator::GetButtonType(feature.Type()); + + CGUIButtonControl* pButton = CGUIFeatureFactory::CreateButton(buttonType, *m_guiButtonTemplate, + m_wizard, feature, buttonIndex); + + // If successful, add button to result + if (pButton != nullptr) + { + buttons.push_back(pButton); + buttonIndex++; + } + } + + return buttons; +} + +CGUIButtonControl* CGUIFeatureList::GetSelectKeyButton( + const std::vector<CPhysicalFeature>& features, unsigned int buttonIndex) +{ + // Expose keycodes to the wizard + for (const CPhysicalFeature& feature : features) + { + if (feature.Type() == JOYSTICK::FEATURE_TYPE::KEY) + m_wizard->RegisterKey(feature); + } + + return CGUIFeatureFactory::CreateButton(BUTTON_TYPE::SELECT_KEY, *m_guiButtonTemplate, m_wizard, + CPhysicalFeature(), buttonIndex); +} diff --git a/xbmc/games/controllers/windows/GUIFeatureList.h b/xbmc/games/controllers/windows/GUIFeatureList.h new file mode 100644 index 0000000..c7df54f --- /dev/null +++ b/xbmc/games/controllers/windows/GUIFeatureList.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 "IConfigurationWindow.h" +#include "games/GameTypes.h" +#include "games/controllers/ControllerTypes.h" +#include "games/controllers/input/PhysicalFeature.h" +#include "input/joysticks/JoystickTypes.h" + +class CGUIButtonControl; +class CGUIControlGroupList; +class CGUIImage; +class CGUILabelControl; +class CGUIWindow; + +namespace KODI +{ +namespace GAME +{ +class CGUIFeatureList : public IFeatureList +{ +public: + CGUIFeatureList(CGUIWindow* window, GameClientPtr gameClient); + ~CGUIFeatureList() override; + + // implementation of IFeatureList + bool Initialize() override; + void Deinitialize() override; + bool HasButton(JOYSTICK::FEATURE_TYPE type) const override; + void Load(const ControllerPtr& controller) override; + void OnFocus(unsigned int buttonIndex) override {} + void OnSelect(unsigned int buttonIndex) override; + +private: + IFeatureButton* GetButtonControl(unsigned int buttonIndex); + + void CleanupButtons(void); + + // Helper functions + struct FeatureGroup + { + std::string groupName; + std::vector<CPhysicalFeature> features; + /*! + * True if this group is a button that allows the user to map a key of + * their choosing. + */ + bool bIsVirtualKey = false; + }; + std::vector<FeatureGroup> GetFeatureGroups(const std::vector<CPhysicalFeature>& features) const; + std::vector<CGUIButtonControl*> GetButtons(const std::vector<CPhysicalFeature>& features, + unsigned int startIndex); + CGUIButtonControl* GetSelectKeyButton(const std::vector<CPhysicalFeature>& features, + unsigned int buttonIndex); + + // GUI stuff + CGUIWindow* const m_window; + unsigned int m_buttonCount = 0; + CGUIControlGroupList* m_guiList; + CGUIButtonControl* m_guiButtonTemplate; + CGUILabelControl* m_guiGroupTitle; + CGUIImage* m_guiFeatureSeparator; + + // Game window stuff + GameClientPtr m_gameClient; + ControllerPtr m_controller; + IConfigurationWizard* m_wizard; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/controllers/windows/IConfigurationWindow.h b/xbmc/games/controllers/windows/IConfigurationWindow.h new file mode 100644 index 0000000..017adf0 --- /dev/null +++ b/xbmc/games/controllers/windows/IConfigurationWindow.h @@ -0,0 +1,262 @@ +/* + * 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 "games/controllers/ControllerTypes.h" +#include "input/InputTypes.h" +#include "input/joysticks/JoystickTypes.h" + +#include <string> +#include <vector> + +class CEvent; + +/*! + * \brief Controller configuration window + * + * The configuration window presents a list of controllers. Also on the screen + * is a list of features belonging to that controller. + * + * The configuration utility reacts to several events: + * + * 1) When a controller is focused, the feature list is populated with the + * controller's features. + * + * 2) When a feature is selected, the user is prompted for controller input. + * This initiates a "wizard" that walks the user through the subsequent + * features. + * + * 3) When the wizard's active feature loses focus, the wizard is cancelled + * and the prompt for input ends. + */ + +namespace KODI +{ +namespace GAME +{ +class CPhysicalFeature; + +/*! + * \brief A list populated by installed controllers + */ +class IControllerList +{ +public: + virtual ~IControllerList() = default; + + /*! + * \brief Initialize the resource + * \return true if the resource is initialized and can be used + * false if the resource failed to initialize and must not be used + */ + virtual bool Initialize(void) = 0; + + /*! + * \brief Deinitialize the resource + */ + virtual void Deinitialize(void) = 0; + + /*! + * \brief Refresh the contents of the list + * \param controllerId The controller to focus, or empty to leave focus unchanged + * \return True if the list was changed + */ + virtual bool Refresh(const std::string& controllerId) = 0; + + /* + * \brief The specified controller has been focused + * \param controllerIndex The index of the controller being focused + */ + virtual void OnFocus(unsigned int controllerIndex) = 0; + + /*! + * \brief The specified controller has been selected + * \param controllerIndex The index of the controller being selected + */ + virtual void OnSelect(unsigned int controllerIndex) = 0; + + /*! + * \brief Get the index of the focused controller + * \return The index of the focused controller, or -1 if no controller has been focused yet + */ + virtual int GetFocusedController() const = 0; + + /*! + * \brief Reset the focused controller + */ + virtual void ResetController(void) = 0; +}; + +/*! + * \brief A list populated by the controller's features + */ +class IFeatureList +{ +public: + virtual ~IFeatureList() = default; + + /*! + * \brief Initialize the resource + * \return true if the resource is initialized and can be used + * false if the resource failed to initialize and must not be used + */ + virtual bool Initialize(void) = 0; + + /*! + * \brief Deinitialize the resource + * \remark This must be called if Initialize() returned true + */ + virtual void Deinitialize(void) = 0; + + /*! + * \brief Check if the feature type has any buttons in the GUI + * \param The type of the feature being added to the GUI + * \return True if the type is support, false otherwise + */ + virtual bool HasButton(JOYSTICK::FEATURE_TYPE type) const = 0; + + /*! + * \brief Load the features for the specified controller + * \param controller The controller to load + */ + virtual void Load(const ControllerPtr& controller) = 0; + + /*! + * \brief Focus has been set to the specified GUI button + * \param buttonIndex The index of the button being focused + */ + virtual void OnFocus(unsigned int buttonIndex) = 0; + + /*! + * \brief The specified GUI button has been selected + * \param buttonIndex The index of the button being selected + */ + virtual void OnSelect(unsigned int buttonIndex) = 0; +}; + +/*! + * \brief A GUI button in a feature list + */ +class IFeatureButton +{ +public: + virtual ~IFeatureButton() = default; + + /*! + * \brief Get the feature represented by this button + */ + virtual const CPhysicalFeature& Feature(void) const = 0; + + /*! + * \brief Allow the wizard to include this feature in a list of buttons + * to map + */ + virtual bool AllowWizard() const { return true; } + + /*! + * \brief Prompt the user for a single input element + * \param waitEvent The event to block on while prompting for input + * \return true if input was received (event fired), false if the prompt timed out + * + * After the button has finished prompting the user for all the input + * elements it requires, this will return false until Reset() is called. + */ + virtual bool PromptForInput(CEvent& waitEvent) = 0; + + /*! + * \brief Check if the button supports further calls to PromptForInput() + * \return true if the button requires no more input elements from the user + */ + virtual bool IsFinished(void) const = 0; + + /*! + * \brief Get the direction of the next analog stick or relative pointer + * prompt + * \return The next direction to be prompted, or UNKNOWN if this isn't a + * cardinal feature or the prompt is finished + */ + virtual INPUT::CARDINAL_DIRECTION GetCardinalDirection(void) const = 0; + + /*! + * \brief Get the direction of the next wheel prompt + * \return The next direction to be prompted, or UNKNOWN if this isn't a + * wheel or the prompt is finished + */ + virtual JOYSTICK::WHEEL_DIRECTION GetWheelDirection(void) const = 0; + + /*! + * \brief Get the direction of the next throttle prompt + * \return The next direction to be prompted, or UNKNOWN if this isn't a + * throttle or the prompt is finished + */ + virtual JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection(void) const = 0; + + /*! + * \brief True if the button is waiting for a key press + */ + virtual bool NeedsKey() const { return false; } + + /*! + * \brief Set the pressed key that the user will be prompted to map + * + * \param key The key that was pressed + */ + virtual void SetKey(const CPhysicalFeature& key) {} + + /*! + * \brief Reset button after prompting for input has finished + */ + virtual void Reset(void) = 0; +}; + +/*! + * \brief A wizard to direct user input + */ +class IConfigurationWizard +{ +public: + virtual ~IConfigurationWizard() = default; + + /*! + * \brief Start the wizard for the specified buttons + * \param controllerId The controller ID being mapped + * \param buttons The buttons to map + */ + virtual void Run(const std::string& strControllerId, + const std::vector<IFeatureButton*>& buttons) = 0; + + /*! + * \brief Callback for feature losing focus + * \param button The feature button losing focus + */ + virtual void OnUnfocus(IFeatureButton* button) = 0; + + /*! + * \brief Abort a running wizard + * \param bWait True if the call should block until the wizard is fully aborted + * \return true if aborted, or false if the wizard wasn't running + */ + virtual bool Abort(bool bWait = true) = 0; + + /*! + * \brief Register a key by its keycode + * \param key A key with a valid keycode + * + * This should be called before Run(). It allows the user to choose a key + * to map instead of scrolling through a long list. + */ + virtual void RegisterKey(const CPhysicalFeature& key) = 0; + + /*! + * \brief Unregister all registered keys + */ + virtual void UnregisterKeys() = 0; +}; +} // namespace GAME +} // namespace KODI |