shift-shell/components/gamingshellplugin/gamepadmanager.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

315 lines
7.4 KiB
C++
Raw Normal View History

// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#include "gamepadmanager.h"
#include "gamepaddevice.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_hints.h>
#include <QDebug>
#include <QQmlEngine>
#include <algorithm>
GamepadManager::GamepadManager(QObject *parent)
: QAbstractListModel(parent)
{
m_pollTimer.setInterval(16); // ~60 Hz
connect(&m_pollTimer, &QTimer::timeout, this, &GamepadManager::poll);
}
GamepadManager::~GamepadManager()
{
stop();
}
GamepadManager *GamepadManager::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)
{
Q_UNUSED(jsEngine)
static GamepadManager *s_instance = nullptr;
if (!s_instance) {
s_instance = new GamepadManager(qmlEngine);
}
return s_instance;
}
// --- QAbstractListModel ---
int GamepadManager::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_gamepads.size();
}
QVariant GamepadManager::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_gamepads.size()) {
return {};
}
auto *dev = m_gamepads.at(index.row());
switch (role) {
case DeviceRole:
return QVariant::fromValue(dev);
case NameRole:
return dev->name();
case TypeRole:
return dev->type();
case BatteryRole:
return dev->batteryPercent();
}
return {};
}
QHash<int, QByteArray> GamepadManager::roleNames() const
{
return {
{DeviceRole, "device"},
{NameRole, "name"},
{TypeRole, "type"},
{BatteryRole, "battery"},
};
}
// --- Properties ---
bool GamepadManager::active() const
{
return m_active;
}
void GamepadManager::setActive(bool active)
{
if (m_active == active) {
return;
}
m_active = active;
if (active) {
start();
} else {
stop();
}
Q_EMIT activeChanged();
}
int GamepadManager::count() const
{
return m_gamepads.size();
}
bool GamepadManager::hasGamepad() const
{
return !m_gamepads.isEmpty();
}
GamepadDevice *GamepadManager::primaryGamepad() const
{
return m_gamepads.isEmpty() ? nullptr : m_gamepads.first();
}
GamepadDevice *GamepadManager::gamepadAt(int index) const
{
if (index < 0 || index >= m_gamepads.size()) {
return nullptr;
}
return m_gamepads.at(index);
}
QString GamepadManager::buttonLabel(int button, int gamepadIndex) const
{
GamepadDevice *device = nullptr;
if (gamepadIndex >= 0 && gamepadIndex < m_gamepads.size()) {
device = m_gamepads.at(gamepadIndex);
} else {
device = primaryGamepad();
}
if (!device) {
switch (button) {
case SDL_GAMEPAD_BUTTON_SOUTH:
return QStringLiteral("A");
case SDL_GAMEPAD_BUTTON_EAST:
return QStringLiteral("B");
case SDL_GAMEPAD_BUTTON_WEST:
return QStringLiteral("X");
case SDL_GAMEPAD_BUTTON_NORTH:
return QStringLiteral("Y");
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
return QStringLiteral("LB");
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
return QStringLiteral("RB");
case SDL_GAMEPAD_BUTTON_BACK:
return QStringLiteral("Back");
case SDL_GAMEPAD_BUTTON_START:
return QStringLiteral("Start");
case SDL_GAMEPAD_BUTTON_GUIDE:
return QStringLiteral("Guide");
default:
return QStringLiteral("?");
}
}
return device->buttonLabel(button);
}
// --- Lifecycle ---
void GamepadManager::start()
{
if (m_sdlInitialized) {
return;
}
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
qWarning() << "GamepadManager: SDL_Init failed:" << SDL_GetError();
return;
}
m_sdlInitialized = true;
SDL_SetGamepadEventsEnabled(true);
// Enumerate already-connected gamepads
int count = 0;
SDL_JoystickID *ids = SDL_GetGamepads(&count);
if (ids) {
for (int i = 0; i < count; ++i) {
addGamepad(ids[i]);
}
SDL_free(ids);
}
m_pollTimer.start();
}
void GamepadManager::stop()
{
m_pollTimer.stop();
if (!m_gamepads.isEmpty()) {
beginResetModel();
qDeleteAll(m_gamepads);
m_gamepads.clear();
endResetModel();
Q_EMIT countChanged();
Q_EMIT primaryGamepadChanged();
}
if (m_sdlInitialized) {
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
m_sdlInitialized = false;
}
}
// --- Event polling ---
void GamepadManager::poll()
{
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_GAMEPAD_ADDED:
addGamepad(event.gdevice.which);
break;
case SDL_EVENT_GAMEPAD_REMOVED:
removeGamepad(event.gdevice.which);
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN: {
int idx = indexForInstanceId(event.gbutton.which);
if (idx >= 0) {
Q_EMIT buttonPressed(event.gbutton.button, idx);
}
break;
}
case SDL_EVENT_GAMEPAD_BUTTON_UP: {
int idx = indexForInstanceId(event.gbutton.which);
if (idx >= 0) {
Q_EMIT buttonReleased(event.gbutton.button, idx);
}
break;
}
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
int idx = indexForInstanceId(event.gaxis.which);
if (idx >= 0) {
float normalized = std::clamp(static_cast<float>(event.gaxis.value) / 32767.0f, -1.0f, 1.0f);
Q_EMIT axisChanged(event.gaxis.axis, normalized, idx);
}
break;
}
default:
break;
}
}
// Refresh battery state periodically (every ~5 seconds = 300 frames)
if (++m_batteryCounter >= 300) {
m_batteryCounter = 0;
for (auto *dev : std::as_const(m_gamepads)) {
dev->refreshBattery();
}
}
}
// --- Hotplug ---
void GamepadManager::addGamepad(int instanceId)
{
// Already tracked?
if (indexForInstanceId(instanceId) >= 0) {
return;
}
SDL_Gamepad *pad = SDL_OpenGamepad(instanceId);
if (!pad) {
qWarning() << "GamepadManager: failed to open gamepad" << instanceId << SDL_GetError();
return;
}
auto *device = new GamepadDevice(pad, instanceId, this);
int row = m_gamepads.size();
beginInsertRows(QModelIndex(), row, row);
m_gamepads.append(device);
endInsertRows();
Q_EMIT countChanged();
if (m_gamepads.size() == 1) {
Q_EMIT primaryGamepadChanged();
}
qDebug() << "GamepadManager: connected" << device->name() << "(" << device->type() << ")";
}
void GamepadManager::removeGamepad(int instanceId)
{
int idx = indexForInstanceId(instanceId);
if (idx < 0) {
return;
}
beginRemoveRows(QModelIndex(), idx, idx);
auto *dev = m_gamepads.takeAt(idx);
endRemoveRows();
qDebug() << "GamepadManager: disconnected" << dev->name();
dev->deleteLater();
Q_EMIT countChanged();
if (idx == 0) {
Q_EMIT primaryGamepadChanged();
}
}
int GamepadManager::indexForInstanceId(int instanceId) const
{
for (int i = 0; i < m_gamepads.size(); ++i) {
if (m_gamepads.at(i)->deviceId() == instanceId) {
return i;
}
}
return -1;
}