// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2024 A-La-Karte Contributors #include "gamepadmanager.h" #include #include #include #define SDL_MAIN_HANDLED 1 #include #include namespace { constexpr int PollIntervalMs = 16; constexpr int RepeatIntervalMs = 150; constexpr qint16 AxisThreshold = 16000; QString buttonLabelToString(SDL_GamepadButtonLabel label) { switch (label) { case SDL_GAMEPAD_BUTTON_LABEL_A: return QStringLiteral("A"); case SDL_GAMEPAD_BUTTON_LABEL_B: return QStringLiteral("B"); case SDL_GAMEPAD_BUTTON_LABEL_X: return QStringLiteral("X"); case SDL_GAMEPAD_BUTTON_LABEL_Y: return QStringLiteral("Y"); case SDL_GAMEPAD_BUTTON_LABEL_CROSS: return QStringLiteral("Cross"); case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: return QStringLiteral("Circle"); case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: return QStringLiteral("Square"); case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: return QStringLiteral("Triangle"); default: return QString(); } } GamepadManager::ControllerStyle controllerStyleFromTypeVendor(SDL_GamepadType type, Uint16 vendor) { switch (type) { case SDL_GAMEPAD_TYPE_XBOX360: case SDL_GAMEPAD_TYPE_XBOXONE: return GamepadManager::XboxController; case SDL_GAMEPAD_TYPE_PS3: case SDL_GAMEPAD_TYPE_PS4: case SDL_GAMEPAD_TYPE_PS5: return GamepadManager::PlayStationController; case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO: case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT: case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT: case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR: return GamepadManager::NintendoController; default: break; } if (vendor == 0x045E) { return GamepadManager::XboxController; } if (vendor == 0x054C) { return GamepadManager::PlayStationController; } if (vendor == 0x057E) { return GamepadManager::NintendoController; } if (type == SDL_GAMEPAD_TYPE_STANDARD) { return GamepadManager::GenericController; } return GamepadManager::UnknownController; } } GamepadManager *GamepadManager::s_instance = nullptr; GamepadManager::GamepadManager(QObject *parent) : QObject(parent) { SDL_SetMainReady(); m_sdlInitialized = SDL_Init(SDL_INIT_GAMEPAD); connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { m_pollTimer.stop(); m_repeatTimer.stop(); closeGamepad(); if (m_sdlInitialized) { SDL_Quit(); m_sdlInitialized = false; } }); m_pollTimer.setInterval(PollIntervalMs); connect(&m_pollTimer, &QTimer::timeout, this, &GamepadManager::pollEvents); m_repeatTimer.setInterval(RepeatIntervalMs); connect(&m_repeatTimer, &QTimer::timeout, this, &GamepadManager::processInput); if (m_sdlInitialized) { SDL_SetGamepadEventsEnabled(true); openFirstAvailableGamepad(); m_pollTimer.start(); } } GamepadManager *GamepadManager::instance() { if (!s_instance) { s_instance = new GamepadManager(); } return s_instance; } GamepadManager *GamepadManager::create(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) return instance(); } bool GamepadManager::connected() const { return m_connected; } bool GamepadManager::active() const { return m_active; } GamepadManager::ControllerStyle GamepadManager::controllerStyle() const { return m_controllerStyle; } QString GamepadManager::confirmButtonLabel() const { return m_confirmButtonLabel; } QString GamepadManager::backButtonLabel() const { return m_backButtonLabel; } QString GamepadManager::detailsButtonLabel() const { return m_detailsButtonLabel; } QString GamepadManager::searchButtonLabel() const { return m_searchButtonLabel; } void GamepadManager::setActive(bool active) { if (m_active != active) { m_active = active; Q_EMIT activeChanged(); } } void GamepadManager::openFirstAvailableGamepad() { if (!m_sdlInitialized || m_gamepad) { return; } int count = 0; std::unique_ptr ids(SDL_GetGamepads(&count), SDL_free); if (!ids || count <= 0) { return; } const SDL_JoystickID instanceId = ids[0]; if (SDL_IsGamepad(instanceId)) { openGamepad(static_cast(instanceId)); } } void GamepadManager::openGamepad(int deviceId) { if (!m_sdlInitialized || m_gamepad) { return; } SDL_Gamepad *pad = SDL_OpenGamepad(static_cast(deviceId)); if (!pad) { return; } m_gamepad = pad; m_gamepadId = deviceId; m_connected = true; Q_EMIT connectedChanged(); updateControllerInfo(); } void GamepadManager::closeGamepad() { if (!m_gamepad) { return; } SDL_CloseGamepad(m_gamepad); m_gamepad = nullptr; m_gamepadId = 0; m_connected = false; setActive(false); m_leftX = 0; m_leftY = 0; m_leftDirX = 0; m_leftDirY = 0; m_repeatTimer.stop(); Q_EMIT connectedChanged(); updateControllerInfo(); } void GamepadManager::updateControllerInfo() { ControllerStyle style = UnknownController; QString confirmLabel; QString backLabel; QString detailsLabel; QString searchLabel; if (m_gamepad) { const SDL_GamepadType type = SDL_GetGamepadType(m_gamepad); const Uint16 vendor = SDL_GetGamepadVendor(m_gamepad); style = controllerStyleFromTypeVendor(type, vendor); #if SDL_VERSION_ATLEAST(3, 2, 0) confirmLabel = buttonLabelToString(SDL_GetGamepadButtonLabel(m_gamepad, SDL_GAMEPAD_BUTTON_SOUTH)); backLabel = buttonLabelToString(SDL_GetGamepadButtonLabel(m_gamepad, SDL_GAMEPAD_BUTTON_EAST)); detailsLabel = buttonLabelToString(SDL_GetGamepadButtonLabel(m_gamepad, SDL_GAMEPAD_BUTTON_WEST)); searchLabel = buttonLabelToString(SDL_GetGamepadButtonLabel(m_gamepad, SDL_GAMEPAD_BUTTON_NORTH)); #endif if (confirmLabel.isEmpty() || backLabel.isEmpty() || detailsLabel.isEmpty() || searchLabel.isEmpty()) { if (style == PlayStationController) { if (confirmLabel.isEmpty()) { confirmLabel = QStringLiteral("Cross"); } if (backLabel.isEmpty()) { backLabel = QStringLiteral("Circle"); } if (detailsLabel.isEmpty()) { detailsLabel = QStringLiteral("Square"); } if (searchLabel.isEmpty()) { searchLabel = QStringLiteral("Triangle"); } } else if (style == NintendoController) { if (confirmLabel.isEmpty()) { confirmLabel = QStringLiteral("B"); } if (backLabel.isEmpty()) { backLabel = QStringLiteral("A"); } if (detailsLabel.isEmpty()) { detailsLabel = QStringLiteral("Y"); } if (searchLabel.isEmpty()) { searchLabel = QStringLiteral("X"); } } else { if (confirmLabel.isEmpty()) { confirmLabel = QStringLiteral("A"); } if (backLabel.isEmpty()) { backLabel = QStringLiteral("B"); } if (detailsLabel.isEmpty()) { detailsLabel = QStringLiteral("X"); } if (searchLabel.isEmpty()) { searchLabel = QStringLiteral("Y"); } } } } const bool changed = (style != m_controllerStyle) || (confirmLabel != m_confirmButtonLabel) || (backLabel != m_backButtonLabel) || (detailsLabel != m_detailsButtonLabel) || (searchLabel != m_searchButtonLabel); m_controllerStyle = style; m_confirmButtonLabel = confirmLabel; m_backButtonLabel = backLabel; m_detailsButtonLabel = detailsLabel; m_searchButtonLabel = searchLabel; if (changed) { Q_EMIT controllerInfoChanged(); } } void GamepadManager::pollEvents() { if (!m_sdlInitialized) { return; } SDL_Event e; while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_EVENT_GAMEPAD_ADDED: { if (!m_gamepad) { openGamepad(static_cast(e.gdevice.which)); } break; } case SDL_EVENT_GAMEPAD_REMOVED: { if (m_gamepad && static_cast(e.gdevice.which) == m_gamepadId) { closeGamepad(); openFirstAvailableGamepad(); } break; } case SDL_EVENT_GAMEPAD_AXIS_MOTION: { if (!m_gamepad || static_cast(e.gaxis.which) != m_gamepadId) { break; } if (e.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTX) { m_leftX = e.gaxis.value; } else if (e.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTY) { m_leftY = e.gaxis.value; } else { break; } const qint8 dirX = (m_leftX < -AxisThreshold) ? -1 : (m_leftX > AxisThreshold ? 1 : 0); const qint8 dirY = (m_leftY < -AxisThreshold) ? -1 : (m_leftY > AxisThreshold ? 1 : 0); if (dirX != 0 || dirY != 0) { setActive(true); } if (dirY != m_leftDirY) { m_leftDirY = dirY; if (dirY < 0) { Q_EMIT navigateUp(); } else if (dirY > 0) { Q_EMIT navigateDown(); } } if (dirX != m_leftDirX) { m_leftDirX = dirX; if (dirX < 0) { Q_EMIT navigateLeft(); } else if (dirX > 0) { Q_EMIT navigateRight(); } } if (m_leftDirX != 0 || m_leftDirY != 0) { if (!m_repeatTimer.isActive()) { m_repeatTimer.start(); } } else { m_repeatTimer.stop(); } break; } case SDL_EVENT_GAMEPAD_BUTTON_DOWN: { if (!m_gamepad || static_cast(e.gbutton.which) != m_gamepadId) { break; } setActive(true); switch (static_cast(e.gbutton.button)) { case SDL_GAMEPAD_BUTTON_SOUTH: Q_EMIT selectPressed(); break; case SDL_GAMEPAD_BUTTON_EAST: case SDL_GAMEPAD_BUTTON_BACK: Q_EMIT backPressed(); break; case SDL_GAMEPAD_BUTTON_WEST: Q_EMIT detailsPressed(); break; case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: Q_EMIT leftBumperPressed(); break; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: Q_EMIT rightBumperPressed(); break; case SDL_GAMEPAD_BUTTON_START: Q_EMIT menuPressed(); break; case SDL_GAMEPAD_BUTTON_NORTH: Q_EMIT searchPressed(); break; case SDL_GAMEPAD_BUTTON_DPAD_UP: Q_EMIT navigateUp(); break; case SDL_GAMEPAD_BUTTON_DPAD_DOWN: Q_EMIT navigateDown(); break; case SDL_GAMEPAD_BUTTON_DPAD_LEFT: Q_EMIT navigateLeft(); break; case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: Q_EMIT navigateRight(); break; default: break; } break; } default: break; } } } void GamepadManager::processInput() { if (!m_gamepad) { return; } if (m_leftY < -AxisThreshold) { Q_EMIT navigateUp(); } else if (m_leftY > AxisThreshold) { Q_EMIT navigateDown(); } if (m_leftX < -AxisThreshold) { Q_EMIT navigateLeft(); } else if (m_leftX > AxisThreshold) { Q_EMIT navigateRight(); } }