mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-28 22:53:09 +00:00
Slide-out panel with brightness, volume, Wi-Fi, Bluetooth, and airplane mode. Fully gamepad-navigable; D-pad and stick input routed to the panel while open. System status bar (clock, battery, connectivity) added to game center header. Lutris games discovered from ~/.local/share/lutris/pga.db, Heroic from ~/.config/heroic/store_cache/ JSON files. Deduplication extended to cover all launcher sources. Desktop entries launched via KIO::ApplicationLauncherJob to expand Exec field codes. Generic commands parsed with KShell::splitArgs. SQLite uses RAII cleanup guard. Clamped SDL axis, rumble, and LED values. Renamed rumble params from frequency to intensity. GamepadManager uses singleton factory. setPlayerIndex checks SDL return. FavouritesBar popup uses screen virtualX/Y for multi-monitor.
273 lines
6.1 KiB
C++
273 lines
6.1 KiB
C++
// 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 <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);
|
|
}
|
|
|
|
// --- Lifecycle ---
|
|
|
|
void GamepadManager::start()
|
|
{
|
|
if (m_sdlInitialized) {
|
|
return;
|
|
}
|
|
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
|
|
qWarning() << "GamepadManager: SDL_Init failed:" << SDL_GetError();
|
|
return;
|
|
}
|
|
m_sdlInitialized = 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;
|
|
}
|