#include #include #include "streaming/session.h" #include "settings/mappingmanager.h" #include "path.h" #include "streamutils.h" #include #include #include #include #define VK_0 0x30 #define VK_A 0x41 // These are real Windows VK_* codes #ifndef VK_F1 #define VK_F1 0x70 #define VK_F13 0x7C #define VK_NUMPAD0 0x60 #endif #define MOUSE_POLLING_INTERVAL 5 // How long the fingers must be stationary to start a right click #define LONG_PRESS_ACTIVATION_DELAY 650 // How far the finger can move before it cancels a right click #define LONG_PRESS_ACTIVATION_DELTA 0.01f // How long the double tap deadzone stays in effect between touch up and touch down #define DOUBLE_TAP_DEAD_ZONE_DELAY 250 // How far the finger can move before it can override the double tap deadzone #define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f // How long the Start button must be pressed to toggle mouse emulation #define MOUSE_EMULATION_LONG_PRESS_TIME 750 // How long between polling the gamepad to send virtual mouse input #define MOUSE_EMULATION_POLLING_INTERVAL 50 // Determines how fast the mouse will move each interval #define MOUSE_EMULATION_MOTION_MULTIPLIER 4 // Determines the maximum motion amount before allowing movement #define MOUSE_EMULATION_DEADZONE 2 // Haptic capabilities (in addition to those from SDL_HapticQuery()) #define ML_HAPTIC_GC_RUMBLE (1U << 16) #define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17) const int SdlInputHandler::k_ButtonMap[] = { A_FLAG, B_FLAG, X_FLAG, Y_FLAG, BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG, LS_CLK_FLAG, RS_CLK_FLAG, LB_FLAG, RB_FLAG, UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG }; 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) { // 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); #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_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); #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::handleKeyEvent(SDL_KeyboardEvent* event) { short keyCode; char modifiers; // Check for our special key combos if ((event->state == SDL_PRESSED) && (event->keysym.mod & KMOD_CTRL) && (event->keysym.mod & KMOD_ALT) && (event->keysym.mod & KMOD_SHIFT)) { // First we test the SDLK combos for matches, // that way we ensure that latin keyboard users // can match to the key they see on their keyboards. // If nothing matches that, we'll then go on to // checking scancodes so non-latin keyboard users // can have working hotkeys (though possibly in // odd positions). We must do all SDLK tests before // any scancode tests to avoid issues in cases // where the SDLK for one shortcut collides with // the scancode of another. // Check for quit combo (Ctrl+Alt+Shift+Q) if (event->keysym.sym == SDLK_q) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected quit key combo (SDLK)"); // Push a quit event to the main loop SDL_Event event; event.type = SDL_QUIT; event.quit.timestamp = SDL_GetTicks(); SDL_PushEvent(&event); return; } // Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected mouse capture toggle combo (SDLK)"); // Stop handling future input setCaptureActive(!isCaptureActive()); // Force raise all keys to ensure they aren't stuck, // since we won't get their key up events. raiseAllKeys(); return; } // Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected mouse mode toggle combo (SDLK)"); // Uncapture input setCaptureActive(false); // Toggle mouse mode m_AbsoluteMouseMode = !m_AbsoluteMouseMode; // Recapture input setCaptureActive(true); return; } else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected full-screen toggle combo (SDLK)"); Session::s_ActiveSession->toggleFullscreen(); // Force raise all keys just be safe across this full-screen/windowed // transition just in case key events get lost. raiseAllKeys(); return; } // Check for overlay combo (Ctrl+Alt+Shift+S) else if (event->keysym.sym == SDLK_s) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected stats toggle combo (SDLK)"); // Toggle the stats overlay Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug, !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)); // Force raise all keys just be safe across this full-screen/windowed // transition just in case key events get lost. raiseAllKeys(); return; } // Check for quit combo (Ctrl+Alt+Shift+Q) else if (event->keysym.scancode == SDL_SCANCODE_Q) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected quit key combo (scancode)"); // Push a quit event to the main loop SDL_Event event; event.type = SDL_QUIT; event.quit.timestamp = SDL_GetTicks(); SDL_PushEvent(&event); return; } // Check for the unbind combo (Ctrl+Alt+Shift+Z) else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected mouse capture toggle combo (scancode)"); // Stop handling future input setCaptureActive(!isCaptureActive()); // Force raise all keys to ensure they aren't stuck, // since we won't get their key up events. raiseAllKeys(); return; } // Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected full-screen toggle combo (scancode)"); Session::s_ActiveSession->toggleFullscreen(); // Force raise all keys just be safe across this full-screen/windowed // transition just in case key events get lost. raiseAllKeys(); return; } // Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected mouse mode toggle combo (scancode)"); // Uncapture input setCaptureActive(false); // Toggle mouse mode m_AbsoluteMouseMode = !m_AbsoluteMouseMode; // Recapture input setCaptureActive(true); return; } else if (event->keysym.scancode == SDL_SCANCODE_S) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected stats toggle combo (scancode)"); // Toggle the stats overlay Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug, !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)); // Force raise all keys just be safe across this full-screen/windowed // transition just in case key events get lost. raiseAllKeys(); return; } } if (event->repeat) { // Ignore repeat key down events SDL_assert(event->state == SDL_PRESSED); return; } // Set modifier flags modifiers = 0; if (event->keysym.mod & KMOD_CTRL) { modifiers |= MODIFIER_CTRL; } if (event->keysym.mod & KMOD_ALT) { modifiers |= MODIFIER_ALT; } if (event->keysym.mod & KMOD_SHIFT) { modifiers |= MODIFIER_SHIFT; } if (event->keysym.mod & KMOD_GUI) { modifiers |= MODIFIER_META; } // Set keycode. We explicitly use scancode here because GFE will try to correct // for AZERTY layouts on the host but it depends on receiving VK_ values matching // a QWERTY layout to work. if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) { // SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1; } else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) { keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A; } else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) { keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1; } else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) { keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13; } else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) { // SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1; } else { switch (event->keysym.scancode) { case SDL_SCANCODE_BACKSPACE: keyCode = 0x08; break; case SDL_SCANCODE_TAB: keyCode = 0x09; break; case SDL_SCANCODE_CLEAR: keyCode = 0x0C; break; case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct? case SDL_SCANCODE_RETURN: keyCode = 0x0D; break; case SDL_SCANCODE_PAUSE: keyCode = 0x13; break; case SDL_SCANCODE_CAPSLOCK: keyCode = 0x14; break; case SDL_SCANCODE_ESCAPE: keyCode = 0x1B; break; case SDL_SCANCODE_SPACE: keyCode = 0x20; break; case SDL_SCANCODE_PAGEUP: keyCode = 0x21; break; case SDL_SCANCODE_PAGEDOWN: keyCode = 0x22; break; case SDL_SCANCODE_END: keyCode = 0x23; break; case SDL_SCANCODE_HOME: keyCode = 0x24; break; case SDL_SCANCODE_LEFT: keyCode = 0x25; break; case SDL_SCANCODE_UP: keyCode = 0x26; break; case SDL_SCANCODE_RIGHT: keyCode = 0x27; break; case SDL_SCANCODE_DOWN: keyCode = 0x28; break; case SDL_SCANCODE_SELECT: keyCode = 0x29; break; case SDL_SCANCODE_EXECUTE: keyCode = 0x2B; break; case SDL_SCANCODE_PRINTSCREEN: keyCode = 0x2C; break; case SDL_SCANCODE_INSERT: keyCode = 0x2D; break; case SDL_SCANCODE_DELETE: keyCode = 0x2E; break; case SDL_SCANCODE_HELP: keyCode = 0x2F; break; case SDL_SCANCODE_KP_0: // See comment above about why we only handle SDL_SCANCODE_KP_0 here keyCode = VK_NUMPAD0; break; case SDL_SCANCODE_0: // See comment above about why we only handle SDL_SCANCODE_0 here keyCode = VK_0; break; case SDL_SCANCODE_KP_MULTIPLY: keyCode = 0x6A; break; case SDL_SCANCODE_KP_PLUS: keyCode = 0x6B; break; case SDL_SCANCODE_KP_COMMA: keyCode = 0x6C; break; case SDL_SCANCODE_KP_MINUS: keyCode = 0x6D; break; case SDL_SCANCODE_KP_PERIOD: keyCode = 0x6E; break; case SDL_SCANCODE_KP_DIVIDE: keyCode = 0x6F; break; case SDL_SCANCODE_NUMLOCKCLEAR: keyCode = 0x90; break; case SDL_SCANCODE_SCROLLLOCK: keyCode = 0x91; break; case SDL_SCANCODE_LSHIFT: keyCode = 0xA0; break; case SDL_SCANCODE_RSHIFT: keyCode = 0xA1; break; case SDL_SCANCODE_LCTRL: keyCode = 0xA2; break; case SDL_SCANCODE_RCTRL: keyCode = 0xA3; break; case SDL_SCANCODE_LALT: keyCode = 0xA4; break; case SDL_SCANCODE_RALT: keyCode = 0xA5; break; // Until we can fully capture these on all platforms (without conflicting with // OS-provided shortcuts), we should avoid passing them through to the host. #if 0 case SDL_SCANCODE_LGUI: keyCode = 0x5B; break; case SDL_SCANCODE_RGUI: keyCode = 0x5C; break; #endif case SDL_SCANCODE_AC_BACK: keyCode = 0xA6; break; case SDL_SCANCODE_AC_FORWARD: keyCode = 0xA7; break; case SDL_SCANCODE_AC_REFRESH: keyCode = 0xA8; break; case SDL_SCANCODE_AC_STOP: keyCode = 0xA9; break; case SDL_SCANCODE_AC_SEARCH: keyCode = 0xAA; break; case SDL_SCANCODE_AC_BOOKMARKS: keyCode = 0xAB; break; case SDL_SCANCODE_AC_HOME: keyCode = 0xAC; break; case SDL_SCANCODE_SEMICOLON: keyCode = 0xBA; break; case SDL_SCANCODE_EQUALS: keyCode = 0xBB; break; case SDL_SCANCODE_COMMA: keyCode = 0xBC; break; case SDL_SCANCODE_MINUS: keyCode = 0xBD; break; case SDL_SCANCODE_PERIOD: keyCode = 0xBE; break; case SDL_SCANCODE_SLASH: keyCode = 0xBF; break; case SDL_SCANCODE_GRAVE: keyCode = 0xC0; break; case SDL_SCANCODE_LEFTBRACKET: keyCode = 0xDB; break; case SDL_SCANCODE_BACKSLASH: keyCode = 0xDC; break; case SDL_SCANCODE_RIGHTBRACKET: keyCode = 0xDD; break; case SDL_SCANCODE_APOSTROPHE: keyCode = 0xDE; break; case SDL_SCANCODE_NONUSBACKSLASH: keyCode = 0xE2; break; default: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Unhandled button event: %d", event->keysym.scancode); return; } } // Track the key state so we always know which keys are down if (event->state == SDL_PRESSED) { m_KeysDown.insert(keyCode); } else { m_KeysDown.remove(keyCode); } LiSendKeyboardEvent(keyCode, event->state == SDL_PRESSED ? KEY_ACTION_DOWN : KEY_ACTION_UP, modifiers); } void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event) { int button; if (event->which == SDL_TOUCH_MOUSEID) { // Ignore synthetic mouse events return; } else if (!isCaptureActive()) { if (event->button == SDL_BUTTON_LEFT && event->state == SDL_RELEASED) { // Capture the mouse again if clicked when unbound. // We start capture on left button released instead of // pressed to avoid sending an errant mouse button released // event to the host when clicking into our window (since // the pressed event was consumed by this code). setCaptureActive(true); } // Not capturing return; } switch (event->button) { case SDL_BUTTON_LEFT: button = BUTTON_LEFT; break; case SDL_BUTTON_MIDDLE: button = BUTTON_MIDDLE; break; case SDL_BUTTON_RIGHT: button = BUTTON_RIGHT; break; case SDL_BUTTON_X1: button = BUTTON_X1; break; case SDL_BUTTON_X2: button = BUTTON_X2; break; default: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Unhandled button event: %d", event->button); return; } LiSendMouseButtonEvent(event->state == SDL_PRESSED ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, button); } void SdlInputHandler::handleMouseMotionEvent(SDL_Window* window, SDL_MouseMotionEvent* event) { if (!isCaptureActive()) { // Not capturing return; } else if (event->which == SDL_TOUCH_MOUSEID) { // Ignore synthetic mouse events return; } if (m_AbsoluteMouseMode) { SDL_Rect src, dst; src.x = src.y = 0; src.w = m_StreamWidth; src.h = m_StreamHeight; dst.x = dst.y = 0; SDL_GetWindowSize(window, &dst.w, &dst.h); // Use the stream and window sizes to determine the video region StreamUtils::scaleSourceToDestinationSurface(&src, &dst); // Clamp motion to the video region short x = qMin(qMax(event->x - dst.x, 0), dst.w); short y = qMin(qMax(event->y - dst.y, 0), dst.h); // Send the mouse position update LiSendMousePositionEvent(x, y, dst.w, dst.h); } else { // Batch until the next mouse polling window or we'll get awful // input lag everything except GFE 3.14 and 3.15. SDL_AtomicAdd(&m_MouseDeltaX, event->xrel); SDL_AtomicAdd(&m_MouseDeltaY, event->yrel); } } void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) { if (!isCaptureActive()) { // Not capturing return; } else if (event->which == SDL_TOUCH_MOUSEID) { // Ignore synthetic mouse events return; } if (event->y != 0) { LiSendScrollEvent((signed char)event->y); } } GamepadState* SdlInputHandler::findStateForGamepad(SDL_JoystickID id) { int i; for (i = 0; i < MAX_GAMEPADS; i++) { if (m_GamepadState[i].jsId == id) { SDL_assert(!m_MultiController || m_GamepadState[i].index == i); return &m_GamepadState[i]; } } // This should only happen with > 4 gamepads SDL_assert(SDL_NumJoysticks() > 4); return nullptr; } void SdlInputHandler::sendGamepadState(GamepadState* state) { SDL_assert(m_GamepadMask == 0x1 || m_MultiController); LiSendMultiControllerEvent(state->index, m_GamepadMask, state->buttons, state->lt, state->rt, state->lsX, state->lsY, state->rsX, state->rsY); } Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*) { // Raise the left click and start a right click LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); return 0; } Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param) { auto me = reinterpret_cast(param); short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0); short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0); if (deltaX != 0 || deltaY != 0) { LiSendMouseMoveEvent(deltaX, deltaY); } return interval; } Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param) { auto gamepad = reinterpret_cast(param); short rawX; short rawY; // Determine which analog stick is currently receiving the strongest input if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) { rawX = gamepad->lsX; rawY = -gamepad->lsY; } else { rawX = gamepad->rsX; rawY = -gamepad->rsY; } float deltaX; float deltaY; // Produce a base vector for mouse movement with increased speed as we deviate further from center deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3); deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3); // Enforce deadzones deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0; deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0; if (deltaX != 0 || deltaY != 0) { LiSendMouseMoveEvent((short)deltaX, (short)deltaY); } return interval; } void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) { SDL_JoystickID gameControllerId = event->which; GamepadState* state = findStateForGamepad(gameControllerId); if (state == NULL) { return; } // Batch all pending axis motion events for this gamepad to save CPU time SDL_Event nextEvent; for (;;) { switch (event->axis) { case SDL_CONTROLLER_AXIS_LEFTX: state->lsX = event->value; break; case SDL_CONTROLLER_AXIS_LEFTY: // Signed values have one more negative value than // positive value, so inverting the sign on -32768 // could actually cause the value to overflow and // wrap around to be negative again. Avoid that by // capping the value at 32767. state->lsY = -qMax(event->value, (short)-32767); break; case SDL_CONTROLLER_AXIS_RIGHTX: state->rsX = event->value; break; case SDL_CONTROLLER_AXIS_RIGHTY: state->rsY = -qMax(event->value, (short)-32767); break; case SDL_CONTROLLER_AXIS_TRIGGERLEFT: state->lt = (unsigned char)(event->value * 255UL / 32767); break; case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: state->rt = (unsigned char)(event->value * 255UL / 32767); break; default: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Unhandled controller axis: %d", event->axis); return; } // Check for another event to batch with if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) { break; } event = &nextEvent.caxis; if (event->which != gameControllerId) { // Stop batching if a different gamepad interrupts us break; } // Remove the next event to batch SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION); } // Only send the gamepad state to the host if it's not in mouse emulation mode if (state->mouseEmulationTimer == 0) { sendGamepadState(state); } } void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event) { GamepadState* state = findStateForGamepad(event->which); if (state == NULL) { return; } if (event->state == SDL_PRESSED) { state->buttons |= k_ButtonMap[event->button]; if (event->button == SDL_CONTROLLER_BUTTON_START) { state->lastStartDownTime = SDL_GetTicks(); } else if (state->mouseEmulationTimer != 0) { if (event->button == SDL_CONTROLLER_BUTTON_A) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); } else if (event->button == SDL_CONTROLLER_BUTTON_B) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); } else if (event->button == SDL_CONTROLLER_BUTTON_X) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); } else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); } else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) { LiSendScrollEvent(1); } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) { LiSendScrollEvent(-1); } } } else { state->buttons &= ~k_ButtonMap[event->button]; if (event->button == SDL_CONTROLLER_BUTTON_START) { if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) { if (state->mouseEmulationTimer != 0) { SDL_RemoveTimer(state->mouseEmulationTimer); state->mouseEmulationTimer = 0; SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Mouse emulation deactivated"); Session::get()->notifyMouseEmulationMode(false); } else if (m_GamepadMouse) { // Send the start button up event to the host, since we won't do it below sendGamepadState(state); state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Mouse emulation active"); Session::get()->notifyMouseEmulationMode(true); } } } else if (state->mouseEmulationTimer != 0) { if (event->button == SDL_CONTROLLER_BUTTON_A) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); } else if (event->button == SDL_CONTROLLER_BUTTON_B) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); } else if (event->button == SDL_CONTROLLER_BUTTON_X) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); } else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); } else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); } } } // Handle Start+Select+L1+R1 as a gamepad quit combo if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected quit gamepad button combo"); // Push a quit event to the main loop SDL_Event event; event.type = SDL_QUIT; event.quit.timestamp = SDL_GetTicks(); SDL_PushEvent(&event); // Clear buttons down on this gameapd LiSendMultiControllerEvent(state->index, m_GamepadMask, 0, 0, 0, 0, 0, 0, 0); return; } // Only send the gamepad state to the host if it's not in mouse emulation mode if (state->mouseEmulationTimer == 0) { sendGamepadState(state); } } void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) { GamepadState* state; if (event->type == SDL_CONTROLLERDEVICEADDED) { int i; const char* name; SDL_GameController* controller; const char* mapping; char guidStr[33]; uint32_t hapticCaps; controller = SDL_GameControllerOpen(event->which); if (controller == NULL) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to open gamepad: %s", SDL_GetError()); return; } // We used to use SDL_GameControllerGetPlayerIndex() here but that // can lead to strange issues due to bugs in Windows where an Xbox // controller will join as player 2, even though no player 1 controller // is connected at all. This pretty much screws any attempt to use // the gamepad in single player games, so just assign them in order from 0. i = 0; for (; i < MAX_GAMEPADS; i++) { SDL_assert(m_GamepadState[i].controller != controller); if (m_GamepadState[i].controller == NULL) { // Found an empty slot break; } } if (i == MAX_GAMEPADS) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "No open gamepad slots found!"); SDL_GameControllerClose(controller); return; } state = &m_GamepadState[i]; if (m_MultiController) { state->index = i; #if SDL_VERSION_ATLEAST(2, 0, 12) // This will change indicators on the controller to show the assigned // player index. For Xbox 360 controllers, that means updating the LED // ring to light up the corresponding quadrant for this player. SDL_GameControllerSetPlayerIndex(controller, state->index); #endif } else { // Always player 1 in single controller mode state->index = 0; } state->controller = controller; state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller)); #if SDL_VERSION_ATLEAST(2, 0, 9) // Perform a tiny rumble to see if haptics are supported. // NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver // and we'll get a (potentially false) success value returned. hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ? ML_HAPTIC_GC_RUMBLE : 0; #else state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller)); state->hapticEffectId = -1; state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE; if (state->haptic != nullptr) { // Query for supported haptic effects hapticCaps = SDL_HapticQuery(state->haptic); hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ? ML_HAPTIC_SIMPLE_RUMBLE : 0; if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) { if (SDL_HapticRumbleSupported(state->haptic)) { if (SDL_HapticRumbleInit(state->haptic) == 0) { state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE; } } if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) { SDL_HapticClose(state->haptic); state->haptic = nullptr; } } else { state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT; } } else { hapticCaps = 0; } #endif SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)), guidStr, sizeof(guidStr)); mapping = SDL_GameControllerMapping(state->controller); name = SDL_GameControllerName(state->controller); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)", i, state->index, name != nullptr ? name : "", hapticCaps, guidStr, mapping != nullptr ? mapping : ""); if (mapping != nullptr) { SDL_free((void*)mapping); } // Add this gamepad to the gamepad mask if (m_MultiController) { // NB: Don't assert that it's unset here because we will already // have the mask set for initially attached gamepads to avoid confusing // apps running on the host. m_GamepadMask |= (1 << state->index); } else { SDL_assert(m_GamepadMask == 0x1); } // Send an empty event to tell the PC we've arrived sendGamepadState(state); } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { state = findStateForGamepad(event->which); if (state != NULL) { if (state->mouseEmulationTimer != 0) { Session::get()->notifyMouseEmulationMode(false); SDL_RemoveTimer(state->mouseEmulationTimer); } SDL_GameControllerClose(state->controller); #if !SDL_VERSION_ATLEAST(2, 0, 9) if (state->haptic != nullptr) { SDL_HapticClose(state->haptic); } #endif // Remove this from the gamepad mask in MC-mode if (m_MultiController) { SDL_assert(m_GamepadMask & (1 << state->index)); m_GamepadMask &= ~(1 << state->index); } else { SDL_assert(m_GamepadMask == 0x1); } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Gamepad %d is gone", state->index); // Send a final event to let the PC know this gamepad is gone LiSendMultiControllerEvent(state->index, m_GamepadMask, 0, 0, 0, 0, 0, 0, 0); // Clear all remaining state from this slot SDL_memset(state, 0, sizeof(*state)); } } } void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) { SDL_assert(event->type == SDL_JOYDEVICEADDED); if (!SDL_IsGameController(event->which)) { char guidStr[33]; SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which), guidStr, sizeof(guidStr)); const char* name = SDL_JoystickNameForIndex(event->which); SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Joystick discovered with no mapping: %s %s", name ? name : "", guidStr); SDL_Joystick* joy = SDL_JoystickOpen(event->which); if (joy != nullptr) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Number of axes: %d | Number of buttons: %d | Number of hats: %d", SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy), SDL_JoystickNumHats(joy)); SDL_JoystickClose(joy); } else { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to open joystick for query: %s", SDL_GetError()); } } } void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { // Make sure the controller number is within our supported count if (controllerNumber >= MAX_GAMEPADS) { return; } #if SDL_VERSION_ATLEAST(2, 0, 9) if (m_GamepadState[controllerNumber].controller != nullptr) { SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000); } #else // Check if the controller supports haptics (and if the controller exists at all) SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic; if (haptic == nullptr) { return; } // Stop the last effect we played if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) { if (m_GamepadState[controllerNumber].hapticEffectId >= 0) { SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId); } } else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) { SDL_HapticRumbleStop(haptic); } // If this callback is telling us to stop both motors, don't bother queuing a new effect if (lowFreqMotor == 0 && highFreqMotor == 0) { return; } if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) { SDL_HapticEffect effect; SDL_memset(&effect, 0, sizeof(effect)); effect.type = SDL_HAPTIC_LEFTRIGHT; // The effect should last until we are instructed to stop or change it effect.leftright.length = SDL_HAPTIC_INFINITY; // SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling effect.leftright.large_magnitude = lowFreqMotor / 2; effect.leftright.small_magnitude = highFreqMotor / 2; // Play the new effect m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect); if (m_GamepadState[controllerNumber].hapticEffectId >= 0) { SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1); } } else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) { SDL_HapticRumblePlay(haptic, std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor + GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0), SDL_HAPTIC_INFINITY); } #endif } void SdlInputHandler::handleTouchFingerEvent(SDL_Window* window, 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 // Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area. // Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher // or lower than those values as touch events continue for touches started within the client area that // leave the client area during a drag motion. // dx and dy are deltas from the last touch event, not the first touch down. // Ignore touch down events with more than one finger if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) { return; } // Ignore touch move and touch up events from the non-primary finger if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) { return; } SDL_Rect src, dst; int windowWidth, windowHeight; SDL_GetWindowSize(window, &windowWidth, &windowHeight); src.x = src.y = 0; src.w = m_StreamWidth; src.h = m_StreamHeight; dst.x = dst.y = 0; dst.w = windowWidth; dst.h = windowHeight; // Use the stream and window sizes to determine the video region StreamUtils::scaleSourceToDestinationSurface(&src, &dst); if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) { // Moved too far since touch down. Cancel the long press timer. SDL_RemoveTimer(m_LongPressTimer); m_LongPressTimer = 0; } // Don't reposition for finger down events within the deadzone. This makes double-clicking easier. if (event->type != SDL_FINGERDOWN || event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY || qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) { // Scale window-relative events to be video-relative and clamp to video region short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w); short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h); // Update the cursor position relative to the video region LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h); } if (event->type == SDL_FINGERDOWN) { m_LastTouchDownEvent = *event; // Start/restart the long press timer SDL_RemoveTimer(m_LongPressTimer); m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY, longPressTimerCallback, nullptr); // Left button down on finger down LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); } else if (event->type == SDL_FINGERUP) { m_LastTouchUpEvent = *event; // Cancel the long press timer SDL_RemoveTimer(m_LongPressTimer); m_LongPressTimer = 0; // Left button up on finger up LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); // Raise right button too in case we triggered a long press gesture LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); } } int SdlInputHandler::getAttachedGamepadMask() { int count; int mask; if (!m_MultiController) { // Player 1 is always present in non-MC mode return 0x1; } count = mask = 0; for (int i = 0; i < SDL_NumJoysticks(); i++) { if (SDL_IsGameController(i)) { mask |= (1 << count++); } } return mask; } 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(); } 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 relative mode, try to activate SDL's relative mouse mode if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) { // Relative mouse mode didn't work, so we'll use fake capture SDL_ShowCursor(SDL_DISABLE); m_FakeCaptureActive = true; } } else if (m_FakeCaptureActive) { SDL_ShowCursor(SDL_ENABLE); m_FakeCaptureActive = false; } else { SDL_SetRelativeMouseMode(SDL_FALSE); } } QString SdlInputHandler::getUnmappedGamepads() { QString ret; if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", SDL_GetError()); } MappingManager mappingManager; mappingManager.applyMappings(); for (int i = 0; i < SDL_NumJoysticks(); i++) { if (!SDL_IsGameController(i)) { char guidStr[33]; SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i), guidStr, sizeof(guidStr)); const char* name = SDL_JoystickNameForIndex(i); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Unmapped joystick: %s %s", name ? name : "", guidStr); SDL_Joystick* joy = SDL_JoystickOpen(i); if (joy != nullptr) { int numButtons = SDL_JoystickNumButtons(joy); int numHats = SDL_JoystickNumHats(joy); int numAxes = SDL_JoystickNumAxes(joy); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Number of axes: %d | Number of buttons: %d | Number of hats: %d", numAxes, numButtons, numHats); if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Joystick likely to be an unmapped game controller"); if (!ret.isEmpty()) { ret += ", "; } ret += name; } SDL_JoystickClose(joy); } else { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to open joystick for query: %s", SDL_GetError()); } } } SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); return ret; }