a-la-karte/src/gamepadmanager.cpp
Marco Allegretti 24c0137fff Input: Use SDL3 for gamepad navigation
Implement SDL3-based gamepad handling and expose controller-specific button labels to QML.
2026-01-24 13:22:57 +01:00

447 lines
12 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 A-La-Karte Contributors
#include "gamepadmanager.h"
#include <QCoreApplication>
#include <QString>
#include <memory>
#define SDL_MAIN_HANDLED 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
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<SDL_JoystickID[], decltype(&SDL_free)> ids(SDL_GetGamepads(&count), SDL_free);
if (!ids || count <= 0) {
return;
}
const SDL_JoystickID instanceId = ids[0];
if (SDL_IsGamepad(instanceId)) {
openGamepad(static_cast<int>(instanceId));
}
}
void GamepadManager::openGamepad(int deviceId)
{
if (!m_sdlInitialized || m_gamepad) {
return;
}
SDL_Gamepad *pad = SDL_OpenGamepad(static_cast<SDL_JoystickID>(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<int>(e.gdevice.which));
}
break;
}
case SDL_EVENT_GAMEPAD_REMOVED: {
if (m_gamepad && static_cast<int>(e.gdevice.which) == m_gamepadId) {
closeGamepad();
openFirstAvailableGamepad();
}
break;
}
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
if (!m_gamepad || static_cast<int>(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<int>(e.gbutton.which) != m_gamepadId) {
break;
}
setActive(true);
switch (static_cast<SDL_GamepadButton>(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();
}
}