summaryrefslogtreecommitdiffstats
path: root/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/games/controllers/windows/GUIConfigurationWizard.cpp')
-rw-r--r--xbmc/games/controllers/windows/GUIConfigurationWizard.cpp494
1 files changed, 494 insertions, 0 deletions
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;
+ }
+}