diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
commit | c04dcc2e7d834218ef2d4194331e383402495ae1 (patch) | |
tree | 7333e38d10d75386e60f336b80c2443c1166031d /xbmc/games/controllers/windows | |
parent | Initial commit. (diff) | |
download | kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip |
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/games/controllers/windows')
-rw-r--r-- | xbmc/games/controllers/windows/CMakeLists.txt | 15 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIConfigurationWizard.cpp | 494 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIConfigurationWizard.h | 117 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIControllerDefines.h | 45 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIControllerList.cpp | 239 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIControllerList.h | 62 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIControllerWindow.cpp | 376 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIControllerWindow.h | 68 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIFeatureList.cpp | 297 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/GUIFeatureList.h | 77 | ||||
-rw-r--r-- | xbmc/games/controllers/windows/IConfigurationWindow.h | 262 |
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 |