Add SDL3-based controller input service
Integrate SDL3 controller support and wire it into the InputRouter.
- Add ControllerInputService (src/ControllerInputService.{h,cpp}) to discover, poll and translate SDL3 gamepad events into InputRouter actions, with axis repeat handling and debouncing.
- Update CMakeLists to find or fetch SDL3, add the new source files to the target, link the SDL3 target, and copy runtime DLLs on Windows.
- Add triggerAction(Action) to InputRouter and use it from existing keyboard handling to centralize action dispatch.
- Instantiate ControllerInputService in main so controllers feed the InputRouter.
- Update QML views (ShellWindow, HomeView, LibraryView, SettingsView) to use numeric action IDs and add small UI/status text and navigation tweaks for controller-driven flows.
These changes enable gamepad/controller input for Bigscreen via SDL3 and adapt UI code to handle the mapped actions.
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
#include "ControllerInputService.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
constexpr int PollIntervalMs = 8;
|
||||
constexpr int AxisDeadzone = 4000;
|
||||
constexpr int AxisReleaseDeadzone = 2000;
|
||||
constexpr qint64 InitialRepeatDelayMs = 260;
|
||||
constexpr qint64 RepeatIntervalMs = 95;
|
||||
constexpr qint64 DiscoveryIntervalMs = 1000;
|
||||
constexpr int AxisControlOffset = 100;
|
||||
}
|
||||
|
||||
ControllerInputService::ControllerInputService(InputRouter *router, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_router(router)
|
||||
{
|
||||
if (!m_router) {
|
||||
qWarning() << "ControllerInputService disabled: missing InputRouter";
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
qWarning() << "ControllerInputService disabled:" << SDL_GetError();
|
||||
return;
|
||||
}
|
||||
|
||||
m_sdlReady = true;
|
||||
m_clock.start();
|
||||
m_nextDiscoveryMs = DiscoveryIntervalMs;
|
||||
|
||||
discoverControllers();
|
||||
|
||||
connect(&m_pollTimer, &QTimer::timeout, this, &ControllerInputService::pollEvents);
|
||||
m_pollTimer.start(PollIntervalMs);
|
||||
}
|
||||
|
||||
ControllerInputService::~ControllerInputService()
|
||||
{
|
||||
m_pollTimer.stop();
|
||||
|
||||
for (SDL_Gamepad *gamepad : std::as_const(m_gamepads)) {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
m_gamepads.clear();
|
||||
|
||||
if (m_sdlReady) {
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerInputService::pollEvents()
|
||||
{
|
||||
if (!m_sdlReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_GAMEPAD_ADDED:
|
||||
openController(event.gdevice.which);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||
closeController(event.gdevice.which);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
handleButtonDown(event.gbutton);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
||||
handleAxisMotion(event.gaxis);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const qint64 now = m_clock.elapsed();
|
||||
if (now >= m_nextDiscoveryMs) {
|
||||
discoverControllers();
|
||||
m_nextDiscoveryMs = now + DiscoveryIntervalMs;
|
||||
}
|
||||
|
||||
pollControllerState();
|
||||
updateAxisRepeats();
|
||||
}
|
||||
|
||||
void ControllerInputService::discoverControllers()
|
||||
{
|
||||
int gamepadCount = 0;
|
||||
SDL_JoystickID *gamepads = SDL_GetGamepads(&gamepadCount);
|
||||
if (!gamepads) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < gamepadCount; ++i) {
|
||||
openController(gamepads[i]);
|
||||
}
|
||||
|
||||
SDL_free(gamepads);
|
||||
}
|
||||
|
||||
void ControllerInputService::pollControllerState()
|
||||
{
|
||||
QHash<SDL_JoystickID, SDL_Gamepad *>::iterator it = m_gamepads.begin();
|
||||
while (it != m_gamepads.end()) {
|
||||
const SDL_JoystickID instanceId = it.key();
|
||||
SDL_Gamepad *gamepad = it.value();
|
||||
|
||||
if (!gamepad) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int button = 0; button < SDL_GAMEPAD_BUTTON_COUNT; ++button) {
|
||||
const qint64 key = stateKey(instanceId, button);
|
||||
const bool pressed = SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(button));
|
||||
const bool wasPressed = m_pressedButtons.contains(key);
|
||||
|
||||
if (pressed && !wasPressed) {
|
||||
m_pressedButtons.insert(key);
|
||||
dispatchAction(actionForButton(static_cast<Uint8>(button)));
|
||||
} else if (!pressed && wasPressed) {
|
||||
m_pressedButtons.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (int axis = 0; axis < SDL_GAMEPAD_AXIS_COUNT; ++axis) {
|
||||
const Sint16 value = SDL_GetGamepadAxis(gamepad, static_cast<SDL_GamepadAxis>(axis));
|
||||
updateAxisHold(stateKey(instanceId, AxisControlOffset + axis),
|
||||
actionForAxis(static_cast<Uint8>(axis), value));
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerInputService::updateAxisRepeats()
|
||||
{
|
||||
const qint64 now = m_clock.elapsed();
|
||||
for (auto it = m_axisHolds.begin(); it != m_axisHolds.end(); ++it) {
|
||||
AxisHold &hold = it.value();
|
||||
if (!hold.active || hold.action == InputRouter::Action::None || now < hold.nextRepeatMs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dispatchAction(hold.action);
|
||||
hold.nextRepeatMs = now + RepeatIntervalMs;
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerInputService::openController(SDL_JoystickID instanceId)
|
||||
{
|
||||
if (m_gamepads.contains(instanceId) || !SDL_IsGamepad(instanceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Gamepad *gamepad = SDL_OpenGamepad(instanceId);
|
||||
if (!gamepad) {
|
||||
qWarning() << "Failed to open gamepad" << instanceId << SDL_GetError();
|
||||
return;
|
||||
}
|
||||
|
||||
m_gamepads.insert(instanceId, gamepad);
|
||||
qInfo() << "Opened gamepad" << instanceId << SDL_GetGamepadName(gamepad);
|
||||
}
|
||||
|
||||
void ControllerInputService::closeController(SDL_JoystickID instanceId)
|
||||
{
|
||||
SDL_Gamepad *gamepad = m_gamepads.take(instanceId);
|
||||
if (!gamepad) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_CloseGamepad(gamepad);
|
||||
m_axisHolds.clear();
|
||||
m_pressedButtons.clear();
|
||||
qInfo() << "Closed gamepad" << instanceId;
|
||||
}
|
||||
|
||||
void ControllerInputService::handleButtonDown(const SDL_GamepadButtonEvent &event)
|
||||
{
|
||||
const qint64 key = stateKey(event.which, event.button);
|
||||
if (m_pressedButtons.contains(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_pressedButtons.insert(key);
|
||||
dispatchAction(actionForButton(event.button));
|
||||
}
|
||||
|
||||
void ControllerInputService::handleAxisMotion(const SDL_GamepadAxisEvent &event)
|
||||
{
|
||||
updateAxisHold(stateKey(event.which, AxisControlOffset + event.axis),
|
||||
actionForAxis(event.axis, event.value));
|
||||
}
|
||||
|
||||
void ControllerInputService::updateAxisHold(qint64 axisKey, InputRouter::Action action)
|
||||
{
|
||||
if (action == InputRouter::Action::None) {
|
||||
releaseAxisHold(axisKey);
|
||||
return;
|
||||
}
|
||||
|
||||
AxisHold &hold = m_axisHolds[axisKey];
|
||||
if (hold.active && hold.action == action) {
|
||||
return;
|
||||
}
|
||||
|
||||
hold.active = true;
|
||||
hold.action = action;
|
||||
hold.nextRepeatMs = m_clock.elapsed() + InitialRepeatDelayMs;
|
||||
dispatchAction(action);
|
||||
}
|
||||
|
||||
void ControllerInputService::releaseAxisHold(qint64 axisKey)
|
||||
{
|
||||
AxisHold &hold = m_axisHolds[axisKey];
|
||||
hold.active = false;
|
||||
hold.action = InputRouter::Action::None;
|
||||
hold.nextRepeatMs = 0;
|
||||
}
|
||||
|
||||
void ControllerInputService::dispatchAction(InputRouter::Action action)
|
||||
{
|
||||
if (m_router && action != InputRouter::Action::None) {
|
||||
m_router->triggerAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
InputRouter::Action ControllerInputService::actionForButton(Uint8 button)
|
||||
{
|
||||
const SDL_GamepadButton buttonEnum = static_cast<SDL_GamepadButton>(button);
|
||||
|
||||
switch (buttonEnum) {
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||
return InputRouter::Action::Back;
|
||||
case SDL_GAMEPAD_BUTTON_EAST:
|
||||
return InputRouter::Action::Accept;
|
||||
case SDL_GAMEPAD_BUTTON_WEST:
|
||||
return InputRouter::Action::None;
|
||||
case SDL_GAMEPAD_BUTTON_NORTH:
|
||||
return InputRouter::Action::None;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||
return InputRouter::Action::Up;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||
return InputRouter::Action::Down;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||
return InputRouter::Action::Left;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||
return InputRouter::Action::Right;
|
||||
case SDL_GAMEPAD_BUTTON_START:
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
case SDL_GAMEPAD_BUTTON_GUIDE:
|
||||
return InputRouter::Action::Menu;
|
||||
default:
|
||||
return InputRouter::Action::None;
|
||||
}
|
||||
}
|
||||
|
||||
InputRouter::Action ControllerInputService::actionForAxis(Uint8 axis, Sint16 value)
|
||||
{
|
||||
if (value > -AxisReleaseDeadzone && value < AxisReleaseDeadzone) {
|
||||
return InputRouter::Action::None;
|
||||
}
|
||||
|
||||
const SDL_GamepadAxis axisEnum = static_cast<SDL_GamepadAxis>(axis);
|
||||
|
||||
switch (axisEnum) {
|
||||
case SDL_GAMEPAD_AXIS_LEFTX:
|
||||
if (value <= -AxisDeadzone) {
|
||||
return InputRouter::Action::Left;
|
||||
}
|
||||
if (value >= AxisDeadzone) {
|
||||
return InputRouter::Action::Right;
|
||||
}
|
||||
break;
|
||||
case SDL_GAMEPAD_AXIS_LEFTY:
|
||||
if (value <= -AxisDeadzone) {
|
||||
return InputRouter::Action::Up;
|
||||
}
|
||||
if (value >= AxisDeadzone) {
|
||||
return InputRouter::Action::Down;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return InputRouter::Action::None;
|
||||
}
|
||||
|
||||
qint64 ControllerInputService::stateKey(SDL_JoystickID instanceId, int control)
|
||||
{
|
||||
return (static_cast<qint64>(instanceId) << 16) | control;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "InputRouter.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
class ControllerInputService : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ControllerInputService(InputRouter *router, QObject *parent = nullptr);
|
||||
~ControllerInputService() override;
|
||||
|
||||
private:
|
||||
struct AxisHold {
|
||||
InputRouter::Action action = InputRouter::Action::None;
|
||||
qint64 nextRepeatMs = 0;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
void pollEvents();
|
||||
void discoverControllers();
|
||||
void pollControllerState();
|
||||
void updateAxisRepeats();
|
||||
void openController(SDL_JoystickID instanceId);
|
||||
void closeController(SDL_JoystickID instanceId);
|
||||
void handleButtonDown(const SDL_GamepadButtonEvent &event);
|
||||
void handleAxisMotion(const SDL_GamepadAxisEvent &event);
|
||||
void updateAxisHold(qint64 axisKey, InputRouter::Action action);
|
||||
void releaseAxisHold(qint64 axisKey);
|
||||
void dispatchAction(InputRouter::Action action);
|
||||
|
||||
static InputRouter::Action actionForButton(Uint8 button);
|
||||
static InputRouter::Action actionForAxis(Uint8 axis, Sint16 value);
|
||||
static qint64 stateKey(SDL_JoystickID instanceId, int control);
|
||||
|
||||
InputRouter *m_router = nullptr;
|
||||
QTimer m_pollTimer;
|
||||
QElapsedTimer m_clock;
|
||||
QHash<SDL_JoystickID, SDL_Gamepad *> m_gamepads;
|
||||
QSet<qint64> m_pressedButtons;
|
||||
QHash<qint64, AxisHold> m_axisHolds;
|
||||
qint64 m_nextDiscoveryMs = 0;
|
||||
bool m_sdlReady = false;
|
||||
};
|
||||
@@ -18,11 +18,20 @@ bool InputRouter::handleKeyPress(QKeyEvent *event)
|
||||
return false;
|
||||
}
|
||||
|
||||
emit actionTriggered(action);
|
||||
triggerAction(action);
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
|
||||
void InputRouter::triggerAction(Action action)
|
||||
{
|
||||
if (action == Action::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit actionTriggered(action);
|
||||
}
|
||||
|
||||
InputRouter::Action InputRouter::mapKey(int key)
|
||||
{
|
||||
switch (key) {
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
explicit InputRouter(QObject *parent = nullptr);
|
||||
|
||||
bool handleKeyPress(QKeyEvent *event);
|
||||
void triggerAction(Action action);
|
||||
|
||||
signals:
|
||||
void actionTriggered(InputRouter::Action action);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "ControllerInputService.h"
|
||||
#include "InputRouter.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
@@ -81,6 +82,8 @@ int main(int argc, char *argv[])
|
||||
|
||||
InputRouter inputRouter;
|
||||
InputFilter inputFilter(&inputRouter);
|
||||
ControllerInputService controllerInputService(&inputRouter);
|
||||
Q_UNUSED(controllerInputService);
|
||||
app.installEventFilter(&inputFilter);
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
Reference in New Issue
Block a user