#include "sdlgamepadkeynavigation.h" #include #include #include #include "settings/streamingpreferences.h" #include "settings/mappingmanager.h" #define AXIS_NAVIGATION_REPEAT_DELAY 150 SdlGamepadKeyNavigation::SdlGamepadKeyNavigation() : m_Enabled(false), m_UiNavMode(false), m_LastAxisNavigationEventTime(0) { m_PollingTimer = new QTimer(this); connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired); } SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation() { disable(); } void SdlGamepadKeyNavigation::enable() { if (m_Enabled) { return; } // We have to initialize and uninitialize this in enable()/disable() // because we need to get out of the way of the Session class. If it // doesn't get to reinitialize the GC subsystem, it won't get initial // arrival events. Additionally, there's a race condition between // our QML objects being destroyed and SDL being deinitialized that // this solves too. if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", SDL_GetError()); return; } MappingManager mappingManager; mappingManager.applyMappings(); // Drop all pending gamepad add events. SDL will generate these for us // on first init of the GC subsystem. We can't depend on them due to // overlapping lifetimes of SdlGamepadKeyNavigation instances, so we // will attach ourselves. SDL_PumpEvents(); SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED); // Open all currently attached game controllers for (int i = 0; i < SDL_NumJoysticks(); i++) { if (SDL_IsGameController(i)) { SDL_GameController* gc = SDL_GameControllerOpen(i); if (gc != nullptr) { m_Gamepads.append(gc); } } } // Poll every 50 ms for a new joystick event m_PollingTimer->start(50); m_Enabled = true; } void SdlGamepadKeyNavigation::disable() { if (!m_Enabled) { return; } m_PollingTimer->stop(); while (!m_Gamepads.isEmpty()) { SDL_GameControllerClose(m_Gamepads[0]); m_Gamepads.removeAt(0); } SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); m_Enabled = false; } void SdlGamepadKeyNavigation::onPollingTimerFired() { SDL_Event event; StreamingPreferences prefs; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: // SDL may send us a quit event since we initialize // the video subsystem on startup. If we get one, // forward it on for Qt to take care of. QCoreApplication::instance()->quit(); break; case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: { QEvent::Type type = event.type == SDL_CONTROLLERBUTTONDOWN ? QEvent::Type::KeyPress : QEvent::Type::KeyRelease; // Swap face buttons if needed if (prefs.swapFaceButtons) { switch (event.cbutton.button) { case SDL_CONTROLLER_BUTTON_A: event.cbutton.button = SDL_CONTROLLER_BUTTON_B; break; case SDL_CONTROLLER_BUTTON_B: event.cbutton.button = SDL_CONTROLLER_BUTTON_A; break; case SDL_CONTROLLER_BUTTON_X: event.cbutton.button = SDL_CONTROLLER_BUTTON_Y; break; case SDL_CONTROLLER_BUTTON_Y: event.cbutton.button = SDL_CONTROLLER_BUTTON_X; break; } } switch (event.cbutton.button) { case SDL_CONTROLLER_BUTTON_DPAD_UP: if (m_UiNavMode) { // Back-tab sendKey(type, Qt::Key_Tab, Qt::ShiftModifier); } else { sendKey(type, Qt::Key_Up); } break; case SDL_CONTROLLER_BUTTON_DPAD_DOWN: if (m_UiNavMode) { sendKey(type, Qt::Key_Tab); } else { sendKey(type, Qt::Key_Down); } break; case SDL_CONTROLLER_BUTTON_DPAD_LEFT: sendKey(type, Qt::Key_Left); break; case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: sendKey(type, Qt::Key_Right); break; case SDL_CONTROLLER_BUTTON_A: if (m_UiNavMode) { sendKey(type, Qt::Key_Space); } else { sendKey(type, Qt::Key_Return); } break; case SDL_CONTROLLER_BUTTON_B: sendKey(type, Qt::Key_Escape); break; case SDL_CONTROLLER_BUTTON_X: sendKey(type, Qt::Key_Menu); break; case SDL_CONTROLLER_BUTTON_Y: case SDL_CONTROLLER_BUTTON_START: // HACK: We use this keycode to inform main.qml // to show the settings when Key_Menu is handled // by the control in focus. sendKey(type, Qt::Key_Hangup); break; default: break; } break; } case SDL_CONTROLLERDEVICEADDED: SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which); if (gc != nullptr) { m_Gamepads.append(gc); } break; } } // Handle analog sticks by polling for (auto gc : m_Gamepads) { short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX); short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY); if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) { // Do nothing } else if (leftY < -30000) { if (m_UiNavMode) { // Back-tab sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier); sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier); } else { sendKey(QEvent::Type::KeyPress, Qt::Key_Up); sendKey(QEvent::Type::KeyRelease, Qt::Key_Up); } m_LastAxisNavigationEventTime = SDL_GetTicks(); } else if (leftY > 30000) { if (m_UiNavMode) { sendKey(QEvent::Type::KeyPress, Qt::Key_Tab); sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab); } else { sendKey(QEvent::Type::KeyPress, Qt::Key_Down); sendKey(QEvent::Type::KeyRelease, Qt::Key_Down); } m_LastAxisNavigationEventTime = SDL_GetTicks(); } else if (leftX < -30000) { sendKey(QEvent::Type::KeyPress, Qt::Key_Left); sendKey(QEvent::Type::KeyRelease, Qt::Key_Left); m_LastAxisNavigationEventTime = SDL_GetTicks(); } else if (leftX > 30000) { sendKey(QEvent::Type::KeyPress, Qt::Key_Right); sendKey(QEvent::Type::KeyRelease, Qt::Key_Right); m_LastAxisNavigationEventTime = SDL_GetTicks(); } // In UI navigation mode (settings page), use the right stick to scroll if (m_UiNavMode) { short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX); short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY); QPoint wheelDelta; if (rightX > 30000) { wheelDelta.setX(30); } else if (rightX < -30000) { wheelDelta.setX(-30); } if (rightY > 30000) { wheelDelta.setY(-30); } else if (rightY < -30000) { wheelDelta.setY(30); } if (!wheelDelta.isNull()) { sendWheel(wheelDelta); } } } } void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers) { QGuiApplication* app = static_cast(QGuiApplication::instance()); QWindow* focusWindow = app->focusWindow(); if (focusWindow != nullptr) { QKeyEvent keyPressEvent(type, key, modifiers); app->sendEvent(focusWindow, &keyPressEvent); } } void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta) { QGuiApplication* app = static_cast(QGuiApplication::instance()); QWindow* focusWindow = app->focusWindow(); if (focusWindow != nullptr) { QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2); QPoint globalPos(focusWindow->mapToGlobal(mousePos)); #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication); #else QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false); #endif app->sendEvent(focusWindow, &wheelEvent); } } void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode) { m_UiNavMode = uiNavMode; } int SdlGamepadKeyNavigation::getConnectedGamepads() { Q_ASSERT(m_Enabled); int count = 0; for (int i = 0; i < SDL_NumJoysticks(); i++) { if (SDL_IsGameController(i)) { count++; } } return count; }