mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 21:13:08 +00:00
Implement SDL3-based gamepad handling and expose controller-specific button labels to QML.
447 lines
12 KiB
C++
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();
|
|
}
|
|
}
|