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:
@@ -9,6 +9,30 @@ set(CMAKE_AUTOMOC ON)
|
|||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
|
|
||||||
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Gui)
|
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Gui)
|
||||||
|
find_package(SDL3 CONFIG QUIET)
|
||||||
|
|
||||||
|
if(NOT SDL3_FOUND)
|
||||||
|
include(FetchContent)
|
||||||
|
set(SDL_SHARED ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_STATIC OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
|
|
||||||
|
FetchContent_Declare(SDL3
|
||||||
|
URL https://github.com/libsdl-org/SDL/releases/download/release-3.2.30/SDL3-3.2.30.tar.gz
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(SDL3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET SDL3::SDL3)
|
||||||
|
set(BIGSCREEN_SDL_TARGET SDL3::SDL3)
|
||||||
|
elseif(TARGET SDL3::SDL3-shared)
|
||||||
|
set(BIGSCREEN_SDL_TARGET SDL3::SDL3-shared)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "SDL3 target was not found after package lookup/fetch")
|
||||||
|
endif()
|
||||||
|
|
||||||
qt_standard_project_setup(REQUIRES 6.5)
|
qt_standard_project_setup(REQUIRES 6.5)
|
||||||
qt_policy(SET QTP0004 NEW)
|
qt_policy(SET QTP0004 NEW)
|
||||||
@@ -31,6 +55,8 @@ set_source_files_properties(qml/Theme.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
|||||||
|
|
||||||
qt_add_executable(nebula-bigscreen
|
qt_add_executable(nebula-bigscreen
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
src/ControllerInputService.cpp
|
||||||
|
src/ControllerInputService.h
|
||||||
src/InputRouter.cpp
|
src/InputRouter.cpp
|
||||||
src/InputRouter.h
|
src/InputRouter.h
|
||||||
)
|
)
|
||||||
@@ -45,8 +71,21 @@ target_link_libraries(nebula-bigscreen
|
|||||||
PRIVATE
|
PRIVATE
|
||||||
Qt6::Quick
|
Qt6::Quick
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
|
${BIGSCREEN_SDL_TARGET}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
add_custom_command(
|
||||||
|
TARGET nebula-bigscreen
|
||||||
|
POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
$<TARGET_RUNTIME_DLLS:nebula-bigscreen>
|
||||||
|
$<TARGET_FILE_DIR:nebula-bigscreen>
|
||||||
|
COMMAND_EXPAND_LISTS
|
||||||
|
COMMENT "Copying runtime DLLs next to nebula-bigscreen"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set_target_properties(nebula-bigscreen PROPERTIES WIN32_EXECUTABLE TRUE)
|
set_target_properties(nebula-bigscreen PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ ApplicationWindow {
|
|||||||
window.handlePowerInput(action)
|
window.handlePowerInput(action)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (action === InputRouter.Menu) {
|
if (action === 7) {
|
||||||
window.powerOverlayVisible = true
|
window.powerOverlayVisible = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -120,16 +120,16 @@ ApplicationWindow {
|
|||||||
|
|
||||||
function handlePowerInput(action) {
|
function handlePowerInput(action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case InputRouter.Up:
|
case 1:
|
||||||
powerOverlay.moveFocus(-1)
|
powerOverlay.moveFocus(-1)
|
||||||
break
|
break
|
||||||
case InputRouter.Down:
|
case 2:
|
||||||
powerOverlay.moveFocus(1)
|
powerOverlay.moveFocus(1)
|
||||||
break
|
break
|
||||||
case InputRouter.Accept:
|
case 5:
|
||||||
powerOverlay.activateFocused()
|
powerOverlay.activateFocused()
|
||||||
break
|
break
|
||||||
case InputRouter.Back:
|
case 6:
|
||||||
window.powerOverlayVisible = false
|
window.powerOverlayVisible = false
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -39,15 +39,18 @@ ColumnLayout {
|
|||||||
|
|
||||||
function handleInput(action) {
|
function handleInput(action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case InputRouter.Left:
|
case 3:
|
||||||
rail.moveFocus(-1)
|
rail.moveFocus(-1)
|
||||||
break
|
break
|
||||||
case InputRouter.Right:
|
case 4:
|
||||||
rail.moveFocus(1)
|
rail.moveFocus(1)
|
||||||
break
|
break
|
||||||
case InputRouter.Accept:
|
case 5:
|
||||||
rail.activateFocused()
|
rail.activateFocused()
|
||||||
break
|
break
|
||||||
|
case 6:
|
||||||
|
root.navigate("power")
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ ColumnLayout {
|
|||||||
]
|
]
|
||||||
|
|
||||||
property int focusIndex: 0
|
property int focusIndex: 0
|
||||||
|
property string statusText: "Mock entries for v0 — scanners and launchers come later."
|
||||||
|
|
||||||
spacing: 20
|
spacing: 20
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Mock entries for v0 — scanners and launchers come later."
|
text: root.statusText
|
||||||
font: Theme.metaFont
|
font: Theme.metaFont
|
||||||
color: Theme.textMuted
|
color: Theme.textMuted
|
||||||
Layout.leftMargin: 40
|
Layout.leftMargin: 40
|
||||||
@@ -79,18 +80,19 @@ ColumnLayout {
|
|||||||
|
|
||||||
function handleInput(action) {
|
function handleInput(action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case InputRouter.Up:
|
case 1:
|
||||||
focusIndex = Math.max(0, focusIndex - 1)
|
focusIndex = Math.max(0, focusIndex - 1)
|
||||||
list.currentIndex = focusIndex
|
list.currentIndex = focusIndex
|
||||||
break
|
break
|
||||||
case InputRouter.Down:
|
case 2:
|
||||||
focusIndex = Math.min(entries.length - 1, focusIndex + 1)
|
focusIndex = Math.min(entries.length - 1, focusIndex + 1)
|
||||||
list.currentIndex = focusIndex
|
list.currentIndex = focusIndex
|
||||||
break
|
break
|
||||||
case InputRouter.Back:
|
case 6:
|
||||||
root.goBack()
|
root.goBack()
|
||||||
break
|
break
|
||||||
case InputRouter.Accept:
|
case 5:
|
||||||
|
statusText = entries[focusIndex].title + " selected — launcher integration comes later."
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ ColumnLayout {
|
|||||||
|
|
||||||
property int categoryIndex: 0
|
property int categoryIndex: 0
|
||||||
property int itemIndex: 0
|
property int itemIndex: 0
|
||||||
|
readonly property int settingCount: 3
|
||||||
|
property string statusText: "Choose a setting with Accept. Detailed controls come later."
|
||||||
|
|
||||||
spacing: 20
|
spacing: 20
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: 3
|
model: root.settingCount
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -93,26 +95,37 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.statusText
|
||||||
|
font: Theme.metaFont
|
||||||
|
color: Theme.textMuted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInput(action) {
|
function handleInput(action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case InputRouter.Left:
|
case 3:
|
||||||
categoryIndex = Math.max(0, categoryIndex - 1)
|
categoryIndex = Math.max(0, categoryIndex - 1)
|
||||||
|
itemIndex = 0
|
||||||
break
|
break
|
||||||
case InputRouter.Right:
|
case 4:
|
||||||
categoryIndex = Math.min(categories.length - 1, categoryIndex + 1)
|
categoryIndex = Math.min(categories.length - 1, categoryIndex + 1)
|
||||||
|
itemIndex = 0
|
||||||
break
|
break
|
||||||
case InputRouter.Up:
|
case 1:
|
||||||
itemIndex = Math.max(0, itemIndex - 1)
|
itemIndex = Math.max(0, itemIndex - 1)
|
||||||
break
|
break
|
||||||
case InputRouter.Down:
|
case 2:
|
||||||
itemIndex = Math.min(2, itemIndex + 1)
|
itemIndex = Math.min(settingCount - 1, itemIndex + 1)
|
||||||
break
|
break
|
||||||
case InputRouter.Back:
|
case 6:
|
||||||
root.goBack()
|
root.goBack()
|
||||||
break
|
break
|
||||||
|
case 5:
|
||||||
|
statusText = categories[categoryIndex] + " setting " + (itemIndex + 1) + " selected."
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit actionTriggered(action);
|
triggerAction(action);
|
||||||
event->accept();
|
event->accept();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InputRouter::triggerAction(Action action)
|
||||||
|
{
|
||||||
|
if (action == Action::None) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit actionTriggered(action);
|
||||||
|
}
|
||||||
|
|
||||||
InputRouter::Action InputRouter::mapKey(int key)
|
InputRouter::Action InputRouter::mapKey(int key)
|
||||||
{
|
{
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
explicit InputRouter(QObject *parent = nullptr);
|
explicit InputRouter(QObject *parent = nullptr);
|
||||||
|
|
||||||
bool handleKeyPress(QKeyEvent *event);
|
bool handleKeyPress(QKeyEvent *event);
|
||||||
|
void triggerAction(Action action);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void actionTriggered(InputRouter::Action action);
|
void actionTriggered(InputRouter::Action action);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include "ControllerInputService.h"
|
||||||
#include "InputRouter.h"
|
#include "InputRouter.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
@@ -81,6 +82,8 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
InputRouter inputRouter;
|
InputRouter inputRouter;
|
||||||
InputFilter inputFilter(&inputRouter);
|
InputFilter inputFilter(&inputRouter);
|
||||||
|
ControllerInputService controllerInputService(&inputRouter);
|
||||||
|
Q_UNUSED(controllerInputService);
|
||||||
app.installEventFilter(&inputFilter);
|
app.installEventFilter(&inputFilter);
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
|
|||||||
Reference in New Issue
Block a user