diff --git a/src/gamepadmanager.cpp b/src/gamepadmanager.cpp index 1d4d914..6a871b8 100644 --- a/src/gamepadmanager.cpp +++ b/src/gamepadmanager.cpp @@ -3,27 +3,111 @@ #include "gamepadmanager.h" -#if defined(ALAKARTE_HAVE_QT_GAMEPAD) +#include +#include -#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) { - auto *manager = QGamepadManager::instance(); + SDL_SetMainReady(); - connect(manager, &QGamepadManager::gamepadConnected, this, &GamepadManager::onGamepadConnected); - connect(manager, &QGamepadManager::gamepadDisconnected, this, &GamepadManager::onGamepadDisconnected); + m_sdlInitialized = SDL_Init(SDL_INIT_GAMEPAD); - m_repeatTimer.setInterval(150); + 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); - const auto gamepads = manager->connectedGamepads(); - if (!gamepads.isEmpty()) { - setupGamepad(gamepads.first()); + if (m_sdlInitialized) { + SDL_SetGamepadEventsEnabled(true); + openFirstAvailableGamepad(); + m_pollTimer.start(); } } @@ -52,6 +136,31 @@ 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) { @@ -60,173 +169,279 @@ void GamepadManager::setActive(bool active) } } -void GamepadManager::onGamepadConnected(int deviceId) +void GamepadManager::openFirstAvailableGamepad() { - if (!m_gamepad) { - setupGamepad(deviceId); + 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::onGamepadDisconnected(int deviceId) +void GamepadManager::openGamepad(int deviceId) { - if (m_gamepad && m_gamepad->deviceId() == deviceId) { - m_gamepad->deleteLater(); - m_gamepad = nullptr; - m_connected = false; - m_repeatTimer.stop(); - Q_EMIT connectedChanged(); - - const auto gamepads = QGamepadManager::instance()->connectedGamepads(); - if (!gamepads.isEmpty()) { - setupGamepad(gamepads.first()); - } + if (!m_sdlInitialized || m_gamepad) { + return; } -} -void GamepadManager::setupGamepad(int deviceId) -{ - m_gamepad = new QGamepad(deviceId, this); + 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; + } - connect(m_gamepad, &QGamepad::axisLeftYChanged, this, [this](double value) { - if (qAbs(value) > 0.5) { setActive(true); - if (value < -0.5) { + + 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(); - } else { + break; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: Q_EMIT navigateDown(); - } - m_repeatTimer.start(); - } else { - m_repeatTimer.stop(); - } - }); - - connect(m_gamepad, &QGamepad::axisLeftXChanged, this, [this](double value) { - if (qAbs(value) > 0.5) { - setActive(true); - if (value < -0.5) { + break; + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: Q_EMIT navigateLeft(); - } else { + break; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: Q_EMIT navigateRight(); + break; + default: + break; } - m_repeatTimer.start(); - } else { - m_repeatTimer.stop(); + break; } - }); - - connect(m_gamepad, &QGamepad::buttonAChanged, this, [this](bool pressed) { - if (pressed) { - setActive(true); - Q_EMIT selectPressed(); + default: + break; } - }); - - connect(m_gamepad, &QGamepad::buttonBChanged, this, [this](bool pressed) { - if (pressed) { - setActive(true); - Q_EMIT backPressed(); - } - }); - - connect(m_gamepad, &QGamepad::buttonStartChanged, this, [this](bool pressed) { - if (pressed) { - setActive(true); - Q_EMIT menuPressed(); - } - }); - - connect(m_gamepad, &QGamepad::buttonYChanged, this, [this](bool pressed) { - if (pressed) { - setActive(true); - Q_EMIT searchPressed(); - } - }); - - connect(m_gamepad, &QGamepad::buttonUpChanged, this, [this](bool pressed) { - if (pressed) { - setActive(true); - Q_EMIT navigateUp(); - } - }); - - connect(m_gamepad, &QGamepad::buttonDownChanged, this, [this](bool pressed) { - if (pressed) { - setActive(true); - Q_EMIT navigateDown(); - } - }); - - connect(m_gamepad, &QGamepad::buttonLeftChanged, this, [this](bool pressed) { - if (pressed) { - setActive(true); - Q_EMIT navigateLeft(); - } - }); - - connect(m_gamepad, &QGamepad::buttonRightChanged, this, [this](bool pressed) { - if (pressed) { - setActive(true); - Q_EMIT navigateRight(); - } - }); + } } void GamepadManager::processInput() { - if (!m_gamepad) + if (!m_gamepad) { return; + } - double y = m_gamepad->axisLeftY(); - double x = m_gamepad->axisLeftX(); - - if (y < -0.5) { + if (m_leftY < -AxisThreshold) { Q_EMIT navigateUp(); - } else if (y > 0.5) { + } else if (m_leftY > AxisThreshold) { Q_EMIT navigateDown(); } - if (x < -0.5) { + if (m_leftX < -AxisThreshold) { Q_EMIT navigateLeft(); - } else if (x > 0.5) { + } else if (m_leftX > AxisThreshold) { Q_EMIT navigateRight(); } } - -#else - -GamepadManager *GamepadManager::s_instance = nullptr; - -GamepadManager::GamepadManager(QObject *parent) - : QObject(parent) -{ -} - -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 false; -} - -bool GamepadManager::active() const -{ - return false; -} - -#endif diff --git a/src/gamepadmanager.h b/src/gamepadmanager.h index d0c1085..0741392 100644 --- a/src/gamepadmanager.h +++ b/src/gamepadmanager.h @@ -5,10 +5,10 @@ #include #include +#include #include -class QGamepad; -class QGamepadManager; +struct SDL_Gamepad; class GamepadManager : public QObject { @@ -18,17 +18,38 @@ class GamepadManager : public QObject Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY(bool active READ active NOTIFY activeChanged) + Q_PROPERTY(ControllerStyle controllerStyle READ controllerStyle NOTIFY controllerInfoChanged) + Q_PROPERTY(QString confirmButtonLabel READ confirmButtonLabel NOTIFY controllerInfoChanged) + Q_PROPERTY(QString backButtonLabel READ backButtonLabel NOTIFY controllerInfoChanged) + Q_PROPERTY(QString detailsButtonLabel READ detailsButtonLabel NOTIFY controllerInfoChanged) + Q_PROPERTY(QString searchButtonLabel READ searchButtonLabel NOTIFY controllerInfoChanged) public: + enum ControllerStyle { + UnknownController = 0, + XboxController, + PlayStationController, + NintendoController, + GenericController, + }; + Q_ENUM(ControllerStyle) + static GamepadManager *instance(); static GamepadManager *create(QQmlEngine *engine, QJSEngine *scriptEngine); bool connected() const; bool active() const; + ControllerStyle controllerStyle() const; + QString confirmButtonLabel() const; + QString backButtonLabel() const; + QString detailsButtonLabel() const; + QString searchButtonLabel() const; + Q_SIGNALS: void connectedChanged(); void activeChanged(); + void controllerInfoChanged(); void navigateUp(); void navigateDown(); @@ -36,22 +57,40 @@ Q_SIGNALS: void navigateRight(); void selectPressed(); void backPressed(); + void detailsPressed(); void menuPressed(); void searchPressed(); + void leftBumperPressed(); + void rightBumperPressed(); + private: explicit GamepadManager(QObject *parent = nullptr); static GamepadManager *s_instance; - QGamepad *m_gamepad = nullptr; + SDL_Gamepad *m_gamepad = nullptr; + int m_gamepadId = 0; bool m_connected = false; bool m_active = false; + bool m_sdlInitialized = false; + ControllerStyle m_controllerStyle = UnknownController; + QString m_confirmButtonLabel; + QString m_backButtonLabel; + QString m_detailsButtonLabel; + QString m_searchButtonLabel; + QTimer m_pollTimer; QTimer m_repeatTimer; + qint16 m_leftX = 0; + qint16 m_leftY = 0; + qint8 m_leftDirX = 0; + qint8 m_leftDirY = 0; - void onGamepadConnected(int deviceId); - void onGamepadDisconnected(int deviceId); - void setupGamepad(int deviceId); + void openFirstAvailableGamepad(); + void openGamepad(int deviceId); + void closeGamepad(); + void updateControllerInfo(); + void pollEvents(); void processInput(); void setActive(bool active); };