#include #include #include "streaming/session.h" #include "settings/mappingmanager.h" #include "path.h" #include #include #define MOUSE_POLLING_INTERVAL 5 SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight) : m_MultiController(prefs.multiController), m_GamepadMouse(prefs.gamepadMouse), m_MouseMoveTimer(0), m_FakeCaptureActive(false), m_LongPressTimer(0), m_StreamWidth(streamWidth), m_StreamHeight(streamHeight), m_AbsoluteMouseMode(prefs.absoluteMouseMode), m_AbsoluteTouchMode(prefs.absoluteTouchMode), m_LeftButtonReleaseTimer(0), m_RightButtonReleaseTimer(0), m_DragTimer(0), m_DragButton(0), m_NumFingersDown(0) { // Allow gamepad input when the app doesn't have focus SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); // If absolute mouse mode is enabled, use relative mode warp (which // is via normal motion events that are influenced by mouse acceleration). // Otherwise, we'll use raw input capture which is straight from the device // without modification by the OS. SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, prefs.absoluteMouseMode ? "1" : "0", SDL_HINT_OVERRIDE); // Allow clicks to pass through to us when focusing the window. If we're in // absolute mouse mode, this will avoid the user having to click twice to // trigger a click on the host if the Moonlight window is not focused. In // relative mode, the click event will trigger the mouse to be recaptured. SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); #if defined(Q_OS_DARWIN) && !SDL_VERSION_ATLEAST(2, 0, 10) // SDL 2.0.9 on macOS has a broken HIDAPI mapping for the older Xbox One S // firmware, so we have to disable HIDAPI for Xbox gamepads on macOS until // SDL 2.0.10 where the bug is fixed. // https://github.com/moonlight-stream/moonlight-qt/issues/133 // https://bugzilla.libsdl.org/show_bug.cgi?id=4395 SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0"); #endif #if SDL_VERSION_ATLEAST(2, 0, 9) // Enabling extended input reports allows rumble to function on Bluetooth PS4 // controllers, but breaks DirectInput applications. We will enable it because // it's likely that working rumble is what the user is expecting. If they don't // want this behavior, they can override it with the environment variable. SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); #endif // We must initialize joystick explicitly before gamecontroller in order // to ensure we receive gamecontroller attach events for gamepads where // SDL doesn't have a built-in mapping. By starting joystick first, we // can allow mapping manager to update the mappings before GC attach // events are generated. SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK)); if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s", SDL_GetError()); } MappingManager mappingManager; mappingManager.applyMappings(); // Flush gamepad arrival and departure events which may be queued before // starting the gamecontroller subsystem again. This prevents us from // receiving duplicate arrival and departure events for the same gamepad. SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED); SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED); // We need to reinit this each time, since you only get // an initial set of gamepad arrival events once per init. SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", SDL_GetError()); } #if !SDL_VERSION_ATLEAST(2, 0, 9) SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC)); if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s", SDL_GetError()); } #endif // Initialize the gamepad mask with currently attached gamepads to avoid // causing gamepads to unexpectedly disappear and reappear on the host // during stream startup as we detect currently attached gamepads one at a time. m_GamepadMask = getAttachedGamepadMask(); SDL_zero(m_GamepadState); SDL_zero(m_LastTouchDownEvent); SDL_zero(m_LastTouchUpEvent); SDL_zero(m_TouchDownEvent); SDL_AtomicSet(&m_MouseDeltaX, 0); SDL_AtomicSet(&m_MouseDeltaY, 0); Uint32 pollingInterval = QString(qgetenv("MOUSE_POLLING_INTERVAL")).toUInt(); if (pollingInterval == 0) { pollingInterval = MOUSE_POLLING_INTERVAL; } else { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Using custom mouse polling interval: %u ms", pollingInterval); } m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this); } SdlInputHandler::~SdlInputHandler() { for (int i = 0; i < MAX_GAMEPADS; i++) { if (m_GamepadState[i].mouseEmulationTimer != 0) { Session::get()->notifyMouseEmulationMode(false); SDL_RemoveTimer(m_GamepadState[i].mouseEmulationTimer); } #if !SDL_VERSION_ATLEAST(2, 0, 9) if (m_GamepadState[i].haptic != nullptr) { SDL_HapticClose(m_GamepadState[i].haptic); } #endif if (m_GamepadState[i].controller != nullptr) { SDL_GameControllerClose(m_GamepadState[i].controller); } } SDL_RemoveTimer(m_MouseMoveTimer); SDL_RemoveTimer(m_LongPressTimer); SDL_RemoveTimer(m_LeftButtonReleaseTimer); SDL_RemoveTimer(m_RightButtonReleaseTimer); SDL_RemoveTimer(m_DragTimer); #if !SDL_VERSION_ATLEAST(2, 0, 9) SDL_QuitSubSystem(SDL_INIT_HAPTIC); SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC)); #endif SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); SDL_QuitSubSystem(SDL_INIT_JOYSTICK); SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK)); // Return background event handling to off SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0"); #ifdef STEAM_LINK // Hide SDL's cursor on Steam Link after quitting the stream. // FIXME: We should also do this for other situations where SDL // and Qt will draw their own mouse cursors like KMSDRM or RPi // video backends. SDL_ShowCursor(SDL_DISABLE); #endif } void SdlInputHandler::setWindow(SDL_Window *window) { m_Window = window; } void SdlInputHandler::raiseAllKeys() { if (m_KeysDown.isEmpty()) { return; } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Raising %d keys", m_KeysDown.count()); for (auto keyDown : m_KeysDown) { LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0); } m_KeysDown.clear(); } void SdlInputHandler::notifyFocusLost() { // Release mouse cursor when another window is activated (e.g. by using ALT+TAB). // This lets user to interact with our window's title bar and with the buttons in it. // Doing this while the window is full-screen breaks the transition out of FS // (desktop and exclusive), so we must check for that before releasing mouse capture. if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) && !m_AbsoluteMouseMode) { setCaptureActive(false); } // Raise all keys that are currently pressed. If we don't do this, certain keys // used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down. raiseAllKeys(); } bool SdlInputHandler::isCaptureActive() { if (SDL_GetRelativeMouseMode()) { return true; } // Some platforms don't support SDL_SetRelativeMouseMode return m_FakeCaptureActive; } void SdlInputHandler::setCaptureActive(bool active) { if (active) { // If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window. if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) { SDL_SetWindowGrab(m_Window, SDL_TRUE); } if (!m_AbsoluteMouseMode) { // If our window is occluded when mouse is captured, the mouse may // get stuck on top of the occluding window and not be properly // captured. We can avoid this by raising our window before we // capture the mouse. SDL_RaiseWindow(m_Window); } // If we're in relative mode, try to activate SDL's relative mouse mode if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) { // Relative mouse mode didn't work or was disabled, so we'll just hide the cursor SDL_ShowCursor(SDL_DISABLE); m_FakeCaptureActive = true; } } else { if (m_FakeCaptureActive) { // Display the cursor again SDL_ShowCursor(SDL_ENABLE); m_FakeCaptureActive = false; } else { SDL_SetRelativeMouseMode(SDL_FALSE); } // Allow the cursor to leave the bounds of our window again. SDL_SetWindowGrab(m_Window, SDL_FALSE); } } void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event) { #if SDL_VERSION_ATLEAST(2, 0, 10) if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) { // Ignore anything that isn't a touchscreen. We may get callbacks // for trackpads, but we want to handle those in the mouse path. return; } #elif defined(Q_OS_DARWIN) // SDL2 sends touch events from trackpads by default on // macOS. This totally screws our actual mouse handling, // so we must explicitly ignore touch events on macOS // until SDL 2.0.10 where we have SDL_GetTouchDeviceType() // to tell them apart. return; #endif if (m_AbsoluteTouchMode) { handleAbsoluteFingerEvent(event); } else { handleRelativeFingerEvent(event); } }