Input: Use SDL3 for gamepad navigation

Implement SDL3-based gamepad handling and expose controller-specific button labels to QML.
This commit is contained in:
Marco Allegretti 2026-01-24 13:22:57 +01:00
parent 40f1c5ff38
commit 24c0137fff
2 changed files with 408 additions and 154 deletions

View file

@ -3,27 +3,111 @@
#include "gamepadmanager.h" #include "gamepadmanager.h"
#if defined(ALAKARTE_HAVE_QT_GAMEPAD) #include <QCoreApplication>
#include <QString>
#include <QGamepad> #include <memory>
#include <QGamepadManager>
#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::s_instance = nullptr;
GamepadManager::GamepadManager(QObject *parent) GamepadManager::GamepadManager(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
auto *manager = QGamepadManager::instance(); SDL_SetMainReady();
connect(manager, &QGamepadManager::gamepadConnected, this, &GamepadManager::onGamepadConnected); m_sdlInitialized = SDL_Init(SDL_INIT_GAMEPAD);
connect(manager, &QGamepadManager::gamepadDisconnected, this, &GamepadManager::onGamepadDisconnected);
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); connect(&m_repeatTimer, &QTimer::timeout, this, &GamepadManager::processInput);
const auto gamepads = manager->connectedGamepads(); if (m_sdlInitialized) {
if (!gamepads.isEmpty()) { SDL_SetGamepadEventsEnabled(true);
setupGamepad(gamepads.first()); openFirstAvailableGamepad();
m_pollTimer.start();
} }
} }
@ -52,6 +136,31 @@ bool GamepadManager::active() const
return m_active; 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) void GamepadManager::setActive(bool active)
{ {
if (m_active != 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) { if (!m_sdlInitialized || m_gamepad) {
setupGamepad(deviceId); 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::onGamepadDisconnected(int deviceId) void GamepadManager::openGamepad(int deviceId)
{ {
if (m_gamepad && m_gamepad->deviceId() == deviceId) { if (!m_sdlInitialized || m_gamepad) {
m_gamepad->deleteLater(); return;
m_gamepad = nullptr;
m_connected = false;
m_repeatTimer.stop();
Q_EMIT connectedChanged();
const auto gamepads = QGamepadManager::instance()->connectedGamepads();
if (!gamepads.isEmpty()) {
setupGamepad(gamepads.first());
} }
}
}
void GamepadManager::setupGamepad(int deviceId) SDL_Gamepad *pad = SDL_OpenGamepad(static_cast<SDL_JoystickID>(deviceId));
{ if (!pad) {
m_gamepad = new QGamepad(deviceId, this); return;
}
m_gamepad = pad;
m_gamepadId = deviceId;
m_connected = true; m_connected = true;
Q_EMIT connectedChanged(); Q_EMIT connectedChanged();
updateControllerInfo();
}
connect(m_gamepad, &QGamepad::axisLeftYChanged, this, [this](double value) { void GamepadManager::closeGamepad()
if (qAbs(value) > 0.5) { {
setActive(true); if (!m_gamepad) {
if (value < -0.5) { return;
Q_EMIT navigateUp(); }
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 { } 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(); Q_EMIT navigateDown();
} }
m_repeatTimer.start();
} else {
m_repeatTimer.stop();
} }
});
connect(m_gamepad, &QGamepad::axisLeftXChanged, this, [this](double value) { if (dirX != m_leftDirX) {
if (qAbs(value) > 0.5) { m_leftDirX = dirX;
setActive(true); if (dirX < 0) {
if (value < -0.5) {
Q_EMIT navigateLeft(); Q_EMIT navigateLeft();
} else { } else if (dirX > 0) {
Q_EMIT navigateRight(); Q_EMIT navigateRight();
} }
}
if (m_leftDirX != 0 || m_leftDirY != 0) {
if (!m_repeatTimer.isActive()) {
m_repeatTimer.start(); m_repeatTimer.start();
}
} else { } else {
m_repeatTimer.stop(); m_repeatTimer.stop();
} }
});
connect(m_gamepad, &QGamepad::buttonAChanged, this, [this](bool pressed) { break;
if (pressed) { }
case SDL_EVENT_GAMEPAD_BUTTON_DOWN: {
if (!m_gamepad || static_cast<int>(e.gbutton.which) != m_gamepadId) {
break;
}
setActive(true); setActive(true);
switch (static_cast<SDL_GamepadButton>(e.gbutton.button)) {
case SDL_GAMEPAD_BUTTON_SOUTH:
Q_EMIT selectPressed(); Q_EMIT selectPressed();
} break;
}); case SDL_GAMEPAD_BUTTON_EAST:
case SDL_GAMEPAD_BUTTON_BACK:
connect(m_gamepad, &QGamepad::buttonBChanged, this, [this](bool pressed) {
if (pressed) {
setActive(true);
Q_EMIT backPressed(); Q_EMIT backPressed();
} break;
}); case SDL_GAMEPAD_BUTTON_WEST:
Q_EMIT detailsPressed();
connect(m_gamepad, &QGamepad::buttonStartChanged, this, [this](bool pressed) { break;
if (pressed) { case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
setActive(true); Q_EMIT leftBumperPressed();
break;
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
Q_EMIT rightBumperPressed();
break;
case SDL_GAMEPAD_BUTTON_START:
Q_EMIT menuPressed(); Q_EMIT menuPressed();
} break;
}); case SDL_GAMEPAD_BUTTON_NORTH:
connect(m_gamepad, &QGamepad::buttonYChanged, this, [this](bool pressed) {
if (pressed) {
setActive(true);
Q_EMIT searchPressed(); Q_EMIT searchPressed();
} break;
}); case SDL_GAMEPAD_BUTTON_DPAD_UP:
connect(m_gamepad, &QGamepad::buttonUpChanged, this, [this](bool pressed) {
if (pressed) {
setActive(true);
Q_EMIT navigateUp(); Q_EMIT navigateUp();
} break;
}); case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
connect(m_gamepad, &QGamepad::buttonDownChanged, this, [this](bool pressed) {
if (pressed) {
setActive(true);
Q_EMIT navigateDown(); Q_EMIT navigateDown();
} break;
}); case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
connect(m_gamepad, &QGamepad::buttonLeftChanged, this, [this](bool pressed) {
if (pressed) {
setActive(true);
Q_EMIT navigateLeft(); Q_EMIT navigateLeft();
} break;
}); case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
connect(m_gamepad, &QGamepad::buttonRightChanged, this, [this](bool pressed) {
if (pressed) {
setActive(true);
Q_EMIT navigateRight(); Q_EMIT navigateRight();
break;
default:
break;
}
break;
}
default:
break;
}
} }
});
} }
void GamepadManager::processInput() void GamepadManager::processInput()
{ {
if (!m_gamepad) if (!m_gamepad) {
return; return;
}
double y = m_gamepad->axisLeftY(); if (m_leftY < -AxisThreshold) {
double x = m_gamepad->axisLeftX();
if (y < -0.5) {
Q_EMIT navigateUp(); Q_EMIT navigateUp();
} else if (y > 0.5) { } else if (m_leftY > AxisThreshold) {
Q_EMIT navigateDown(); Q_EMIT navigateDown();
} }
if (x < -0.5) { if (m_leftX < -AxisThreshold) {
Q_EMIT navigateLeft(); Q_EMIT navigateLeft();
} else if (x > 0.5) { } else if (m_leftX > AxisThreshold) {
Q_EMIT navigateRight(); 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

View file

@ -5,10 +5,10 @@
#include <QObject> #include <QObject>
#include <QQmlEngine> #include <QQmlEngine>
#include <QString>
#include <QTimer> #include <QTimer>
class QGamepad; struct SDL_Gamepad;
class QGamepadManager;
class GamepadManager : public QObject class GamepadManager : public QObject
{ {
@ -18,17 +18,38 @@ class GamepadManager : public QObject
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
Q_PROPERTY(bool active READ active NOTIFY activeChanged) 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: public:
enum ControllerStyle {
UnknownController = 0,
XboxController,
PlayStationController,
NintendoController,
GenericController,
};
Q_ENUM(ControllerStyle)
static GamepadManager *instance(); static GamepadManager *instance();
static GamepadManager *create(QQmlEngine *engine, QJSEngine *scriptEngine); static GamepadManager *create(QQmlEngine *engine, QJSEngine *scriptEngine);
bool connected() const; bool connected() const;
bool active() const; bool active() const;
ControllerStyle controllerStyle() const;
QString confirmButtonLabel() const;
QString backButtonLabel() const;
QString detailsButtonLabel() const;
QString searchButtonLabel() const;
Q_SIGNALS: Q_SIGNALS:
void connectedChanged(); void connectedChanged();
void activeChanged(); void activeChanged();
void controllerInfoChanged();
void navigateUp(); void navigateUp();
void navigateDown(); void navigateDown();
@ -36,22 +57,40 @@ Q_SIGNALS:
void navigateRight(); void navigateRight();
void selectPressed(); void selectPressed();
void backPressed(); void backPressed();
void detailsPressed();
void menuPressed(); void menuPressed();
void searchPressed(); void searchPressed();
void leftBumperPressed();
void rightBumperPressed();
private: private:
explicit GamepadManager(QObject *parent = nullptr); explicit GamepadManager(QObject *parent = nullptr);
static GamepadManager *s_instance; static GamepadManager *s_instance;
QGamepad *m_gamepad = nullptr; SDL_Gamepad *m_gamepad = nullptr;
int m_gamepadId = 0;
bool m_connected = false; bool m_connected = false;
bool m_active = 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; QTimer m_repeatTimer;
qint16 m_leftX = 0;
qint16 m_leftY = 0;
qint8 m_leftDirX = 0;
qint8 m_leftDirY = 0;
void onGamepadConnected(int deviceId); void openFirstAvailableGamepad();
void onGamepadDisconnected(int deviceId); void openGamepad(int deviceId);
void setupGamepad(int deviceId); void closeGamepad();
void updateControllerInfo();
void pollEvents();
void processInput(); void processInput();
void setActive(bool active); void setActive(bool active);
}; };