#include #include #include "streaming/session.h" #include "settings/mappingmanager.h" #include "path.h" #include "utils.h" #include #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_SwapMouseButtons(prefs.swapMouseButtons), m_ReverseScrollDirection(prefs.reverseScrollDirection), m_SwapFaceButtons(prefs.swapFaceButtons), m_MouseMoveTimer(0), m_MousePositionLock(0), m_MouseWasInVideoRegion(false), m_PendingMouseButtonsAllUpOnVideoRegionLeave(false), m_FakeCaptureActive(false), m_CaptureSystemKeysMode(prefs.captureSysKeysMode), m_MouseCursorCapturedVisibilityState(SDL_DISABLE), 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), m_ClipboardData() { // System keys are always captured when running without a DE if (!WMUtils::isRunningDesktopEnvironment()) { m_CaptureSystemKeysMode = StreamingPreferences::CSK_ALWAYS; } // Allow gamepad input when the app doesn't have focus if requested SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0"); // 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); #if !SDL_VERSION_ATLEAST(2, 0, 15) // For older versions of SDL (2.0.14 and earlier), use SDL_HINT_GRAB_KEYBOARD SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD, m_CaptureSystemKeysMode != StreamingPreferences::CSK_OFF ? "1" : "0", SDL_HINT_OVERRIDE); #endif // Opt-out of SDL's built-in Alt+Tab handling while keyboard grab is enabled SDL_SetHint("SDL_ALLOW_ALT_TAB_WHILE_GRABBED", "0"); // 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"); // Enabling extended input reports allows rumble to function on Bluetooth PS4/PS5 // 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_JOYSTICK_HIDAPI_PS4_RUMBLE", "1"); SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1"); // Populate special key combo configuration m_SpecialKeyCombos[KeyComboQuit].keyCombo = KeyComboQuit; m_SpecialKeyCombos[KeyComboQuit].keyCode = SDLK_q; m_SpecialKeyCombos[KeyComboQuit].scanCode = SDL_SCANCODE_Q; m_SpecialKeyCombos[KeyComboQuit].enabled = true; m_SpecialKeyCombos[KeyComboUngrabInput].keyCombo = KeyComboUngrabInput; m_SpecialKeyCombos[KeyComboUngrabInput].keyCode = SDLK_z; m_SpecialKeyCombos[KeyComboUngrabInput].scanCode = SDL_SCANCODE_Z; m_SpecialKeyCombos[KeyComboUngrabInput].enabled = QGuiApplication::platformName() != "eglfs"; m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCombo = KeyComboToggleFullScreen; m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCode = SDLK_x; m_SpecialKeyCombos[KeyComboToggleFullScreen].scanCode = SDL_SCANCODE_X; m_SpecialKeyCombos[KeyComboToggleFullScreen].enabled = QGuiApplication::platformName() != "eglfs"; m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCombo = KeyComboToggleStatsOverlay; m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCode = SDLK_s; m_SpecialKeyCombos[KeyComboToggleStatsOverlay].scanCode = SDL_SCANCODE_S; m_SpecialKeyCombos[KeyComboToggleStatsOverlay].enabled = true; m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCombo = KeyComboToggleMouseMode; m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCode = SDLK_m; m_SpecialKeyCombos[KeyComboToggleMouseMode].scanCode = SDL_SCANCODE_M; m_SpecialKeyCombos[KeyComboToggleMouseMode].enabled = true; m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCombo = KeyComboToggleCursorHide; m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCode = SDLK_c; m_SpecialKeyCombos[KeyComboToggleCursorHide].scanCode = SDL_SCANCODE_C; m_SpecialKeyCombos[KeyComboToggleCursorHide].enabled = true; m_SpecialKeyCombos[KeyComboToggleMinimize].keyCombo = KeyComboToggleMinimize; m_SpecialKeyCombos[KeyComboToggleMinimize].keyCode = SDLK_d; m_SpecialKeyCombos[KeyComboToggleMinimize].scanCode = SDL_SCANCODE_D; m_SpecialKeyCombos[KeyComboToggleMinimize].enabled = QGuiApplication::platformName() != "eglfs"; m_SpecialKeyCombos[KeyComboPasteText].keyCombo = KeyComboPasteText; m_SpecialKeyCombos[KeyComboPasteText].keyCode = SDLK_v; m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V; m_SpecialKeyCombos[KeyComboPasteText].enabled = true; m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES); m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT); QString streamIgnoreDevices = qgetenv("STREAM_GAMECONTROLLER_IGNORE_DEVICES"); QString streamIgnoreDevicesExcept = qgetenv("STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT"); if (!streamIgnoreDevices.isEmpty() && !streamIgnoreDevices.endsWith(',')) { streamIgnoreDevices += ','; } streamIgnoreDevices += m_OldIgnoreDevices; // For SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, we use the union of SDL_GAMECONTROLLER_IGNORE_DEVICES // and STREAM_GAMECONTROLLER_IGNORE_DEVICES while streaming. STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT // overrides SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT while streaming. SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, streamIgnoreDevices.toUtf8()); SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, streamIgnoreDevicesExcept.toUtf8()); // 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 #ifdef Q_OS_DARWIN CGSGetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), &m_OldHotKeyMode); #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_zero(m_MousePositionReport); SDL_AtomicSet(&m_MouseDeltaX, 0); SDL_AtomicSet(&m_MouseDeltaY, 0); SDL_AtomicSet(&m_MousePositionUpdated, 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); // Initialize state used by the clipboard thread before we start it SDL_AtomicSet(&m_ShutdownClipboardThread, 0); m_ClipboardHasData = SDL_CreateCond(); m_ClipboardLock = SDL_CreateMutex(); m_ClipboardThread = SDL_CreateThread(SdlInputHandler::clipboardThreadProc, "Clipboard Sender", 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); // Wake up the clipboard thread to terminate it SDL_AtomicSet(&m_ShutdownClipboardThread, 1); SDL_CondBroadcast(m_ClipboardHasData); // Wait for it to terminate SDL_WaitThread(m_ClipboardThread, nullptr); // Now we can safely clean up its resources SDL_DestroyCond(m_ClipboardHasData); SDL_DestroyMutex(m_ClipboardLock); #ifdef Q_OS_DARWIN CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode); #endif #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"); // Restore the ignored devices SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, m_OldIgnoreDevices.toUtf8()); SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, m_OldIgnoreDevicesExcept.toUtf8()); #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", (int)m_KeysDown.count()); for (auto keyDown : m_KeysDown) { LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0); } m_KeysDown.clear(); } void SdlInputHandler::notifyMouseLeave() { // SDL on Windows doesn't send the mouse button up until the mouse re-enters the window // after leaving it. This breaks some of the Aero snap gestures, so we'll capture it to // allow us to receive the mouse button up events later. // // On macOS and X11, capturing the mouse allows us to receive mouse motion outside the // window (button up already worked without capture). if (m_AbsoluteMouseMode && isCaptureActive()) { // NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr); for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) { if (mouseState & SDL_BUTTON(button)) { SDL_CaptureMouse(SDL_TRUE); break; } } } } 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); } #ifdef Q_OS_DARWIN // Ungrab the keyboard updateKeyboardGrabState(); #endif // 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(); } void SdlInputHandler::notifyFocusGained() { #ifdef Q_OS_DARWIN // Re-grab the keyboard if it was grabbed before focus loss // FIXME: We only do this on macOS because we get a spurious // focus gain when in SDL_WINDOW_FULLSCREEN_DESKTOP on Windows // immediately after losing focus by clicking on another window. updateKeyboardGrabState(); #endif } bool SdlInputHandler::isCaptureActive() { if (SDL_GetRelativeMouseMode()) { return true; } // Some platforms don't support SDL_SetRelativeMouseMode return m_FakeCaptureActive; } void SdlInputHandler::updateKeyboardGrabState() { if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) { return; } bool shouldGrab = isCaptureActive(); Uint32 windowFlags = SDL_GetWindowFlags(m_Window); if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN && !(windowFlags & SDL_WINDOW_FULLSCREEN)) { // Ungrab if it's fullscreen only and we left fullscreen shouldGrab = false; } else if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)) { // Ungrab if we lose input focus (SDL will do this internally, but // not for macOS where SDL is not handling the grab logic). shouldGrab = false; } // Don't close the window on Alt+F4 when keyboard grab is enabled SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, shouldGrab ? "1" : "0"); if (shouldGrab) { #if SDL_VERSION_ATLEAST(2, 0, 15) // On SDL 2.0.15, we can get keyboard-only grab on Win32, X11, and Wayland. // This does nothing on macOS but it sets the SDL_WINDOW_KEYBOARD_GRABBED flag // that we look for to see if keyboard capture is enabled. We'll handle macOS // ourselves below using the private CGSSetGlobalHotKeyOperatingMode() API. SDL_SetWindowKeyboardGrab(m_Window, SDL_TRUE); #else // If we're in full-screen desktop mode and SDL doesn't have keyboard grab yet, // grab the cursor (will grab the keyboard too on X11). if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) { SDL_SetWindowGrab(m_Window, SDL_TRUE); } #endif #ifdef Q_OS_DARWIN // SDL doesn't support this private macOS API CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), CGSGlobalHotKeyDisable); #endif } else { #if SDL_VERSION_ATLEAST(2, 0, 15) // Allow the keyboard to leave the window SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE); #endif #ifdef Q_OS_DARWIN // SDL doesn't support this private macOS API CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode); #endif } } bool SdlInputHandler::isSystemKeyCaptureActive() { if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) { return false; } if (m_Window == nullptr) { return false; } Uint32 windowFlags = SDL_GetWindowFlags(m_Window); if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS) #if SDL_VERSION_ATLEAST(2, 0, 15) || !(windowFlags & SDL_WINDOW_KEYBOARD_GRABBED) #else || !(windowFlags & SDL_WINDOW_INPUT_GRABBED) #endif ) { return false; } if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN && !(windowFlags & SDL_WINDOW_FULLSCREEN)) { return false; } return true; } 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) { #if SDL_VERSION_ATLEAST(2, 0, 15) SDL_SetWindowMouseGrab(m_Window, SDL_TRUE); #else SDL_SetWindowGrab(m_Window, SDL_TRUE); #endif } 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(m_MouseCursorCapturedVisibilityState); m_FakeCaptureActive = true; } // Synchronize the client and host cursor when activating absolute capture if (m_AbsoluteMouseMode) { int mouseX, mouseY; int windowX, windowY; // We have to use SDL_GetGlobalMouseState() because macOS may not reflect // the new position of the mouse when outside the window. SDL_GetGlobalMouseState(&mouseX, &mouseY); // Convert global mouse state to window-relative SDL_GetWindowPosition(m_Window, &windowX, &windowY); mouseX -= windowX; mouseY -= windowY; if (isMouseInVideoRegion(mouseX, mouseY)) { updateMousePositionReport(mouseX, mouseY); } } } else { if (m_FakeCaptureActive) { // Display the cursor again SDL_ShowCursor(SDL_ENABLE); m_FakeCaptureActive = false; } else { SDL_SetRelativeMouseMode(SDL_FALSE); } #if SDL_VERSION_ATLEAST(2, 0, 15) // Allow the cursor to leave the bounds of our window again. SDL_SetWindowMouseGrab(m_Window, SDL_FALSE); #else // Allow the cursor to leave the bounds of our window again. SDL_SetWindowGrab(m_Window, SDL_FALSE); #endif } // Now update the keyboard grab updateKeyboardGrabState(); } 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); } }