summaryrefslogtreecommitdiffstats
path: root/xbmc/games/controllers
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--xbmc/games/controllers/CMakeLists.txt18
-rw-r--r--xbmc/games/controllers/Controller.cpp157
-rw-r--r--xbmc/games/controllers/Controller.h120
-rw-r--r--xbmc/games/controllers/ControllerDefinitions.h55
-rw-r--r--xbmc/games/controllers/ControllerIDs.h15
-rw-r--r--xbmc/games/controllers/ControllerLayout.cpp159
-rw-r--r--xbmc/games/controllers/ControllerLayout.h92
-rw-r--r--xbmc/games/controllers/ControllerManager.cpp122
-rw-r--r--xbmc/games/controllers/ControllerManager.h93
-rw-r--r--xbmc/games/controllers/ControllerTranslator.cpp744
-rw-r--r--xbmc/games/controllers/ControllerTranslator.h53
-rw-r--r--xbmc/games/controllers/ControllerTypes.h33
-rw-r--r--xbmc/games/controllers/DefaultController.cpp34
-rw-r--r--xbmc/games/controllers/DefaultController.h50
-rw-r--r--xbmc/games/controllers/dialogs/CMakeLists.txt15
-rw-r--r--xbmc/games/controllers/dialogs/ControllerInstaller.cpp138
-rw-r--r--xbmc/games/controllers/dialogs/ControllerInstaller.h28
-rw-r--r--xbmc/games/controllers/dialogs/ControllerSelect.cpp133
-rw-r--r--xbmc/games/controllers/dialogs/ControllerSelect.h44
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp84
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h54
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp130
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h65
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp132
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h47
-rw-r--r--xbmc/games/controllers/guicontrols/CMakeLists.txt28
-rw-r--r--xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp100
-rw-r--r--xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h49
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControlTypes.h29
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControllerButton.cpp26
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControllerButton.h29
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp86
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureButton.h68
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp36
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureControls.h38
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp51
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureFactory.h37
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp39
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h27
-rw-r--r--xbmc/games/controllers/guicontrols/GUIGameController.cpp64
-rw-r--r--xbmc/games/controllers/guicontrols/GUIGameController.h39
-rw-r--r--xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp59
-rw-r--r--xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h42
-rw-r--r--xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp87
-rw-r--r--xbmc/games/controllers/guicontrols/GUISelectKeyButton.h51
-rw-r--r--xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp88
-rw-r--r--xbmc/games/controllers/guicontrols/GUIThrottleButton.h44
-rw-r--r--xbmc/games/controllers/guicontrols/GUIWheelButton.cpp88
-rw-r--r--xbmc/games/controllers/guicontrols/GUIWheelButton.h44
-rw-r--r--xbmc/games/controllers/input/CMakeLists.txt11
-rw-r--r--xbmc/games/controllers/input/InputSink.cpp67
-rw-r--r--xbmc/games/controllers/input/InputSink.h53
-rw-r--r--xbmc/games/controllers/input/PhysicalFeature.cpp162
-rw-r--r--xbmc/games/controllers/input/PhysicalFeature.h65
-rw-r--r--xbmc/games/controllers/input/PhysicalTopology.cpp56
-rw-r--r--xbmc/games/controllers/input/PhysicalTopology.h61
-rw-r--r--xbmc/games/controllers/types/CMakeLists.txt12
-rw-r--r--xbmc/games/controllers/types/ControllerGrid.cpp242
-rw-r--r--xbmc/games/controllers/types/ControllerGrid.h167
-rw-r--r--xbmc/games/controllers/types/ControllerHub.cpp109
-rw-r--r--xbmc/games/controllers/types/ControllerHub.h53
-rw-r--r--xbmc/games/controllers/types/ControllerNode.cpp136
-rw-r--r--xbmc/games/controllers/types/ControllerNode.h118
-rw-r--r--xbmc/games/controllers/types/ControllerTree.h30
-rw-r--r--xbmc/games/controllers/windows/CMakeLists.txt15
-rw-r--r--xbmc/games/controllers/windows/GUIConfigurationWizard.cpp494
-rw-r--r--xbmc/games/controllers/windows/GUIConfigurationWizard.h117
-rw-r--r--xbmc/games/controllers/windows/GUIControllerDefines.h45
-rw-r--r--xbmc/games/controllers/windows/GUIControllerList.cpp239
-rw-r--r--xbmc/games/controllers/windows/GUIControllerList.h62
-rw-r--r--xbmc/games/controllers/windows/GUIControllerWindow.cpp376
-rw-r--r--xbmc/games/controllers/windows/GUIControllerWindow.h68
-rw-r--r--xbmc/games/controllers/windows/GUIFeatureList.cpp297
-rw-r--r--xbmc/games/controllers/windows/GUIFeatureList.h77
-rw-r--r--xbmc/games/controllers/windows/IConfigurationWindow.h262
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