// SPDX-FileCopyrightText: 2026 Marco Allegretti // SPDX-License-Identifier: EUPL-1.2 #include "gamepadmanager.h" #include "gamepaddevice.h" #include #include #include #include #include #include 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 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(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; }