summaryrefslogtreecommitdiffstats
path: root/xbmc/games/controllers/windows
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/games/controllers/windows')
-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
11 files changed, 2052 insertions, 0 deletions
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