diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index cf22658a..8a2b22f3 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -9,6 +9,10 @@ #define MOUSE_POLLING_INTERVAL 5 +#ifdef Q_OS_WIN32 +HHOOK g_KeyboardHook; +#endif + SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight) : m_MultiController(prefs.multiController), m_GamepadMouse(prefs.gamepadMouse), @@ -20,6 +24,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s m_MouseWasInVideoRegion(false), m_PendingMouseButtonsAllUpOnVideoRegionLeave(false), m_FakeCaptureActive(false), + m_CaptureSystemKeysEnabled(false), m_LongPressTimer(0), m_StreamWidth(streamWidth), m_StreamHeight(streamHeight), @@ -43,6 +48,11 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s prefs.absoluteMouseMode ? "1" : "0", SDL_HINT_OVERRIDE); + // If we're grabbing system keys, enable the X11 keyboard grab in SDL + SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD, + m_CaptureSystemKeysEnabled ? "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 @@ -138,10 +148,25 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s } m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this); + +#ifdef Q_OS_WIN32 + if (m_CaptureSystemKeysEnabled) { + // If system key capture is enabled, install the window hook required to intercept + // these key presses and block them from the OS itself. + g_KeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, SdlInputHandler::keyboardHookProc, GetModuleHandle(NULL), 0); + } +#endif } SdlInputHandler::~SdlInputHandler() { +#ifdef Q_OS_WIN32 + if (g_KeyboardHook != nullptr) { + UnhookWindowsHookEx(g_KeyboardHook); + g_KeyboardHook = nullptr; + } +#endif + for (int i = 0; i < MAX_GAMEPADS; i++) { if (m_GamepadState[i].mouseEmulationTimer != 0) { Session::get()->notifyMouseEmulationMode(false); @@ -195,6 +220,132 @@ void SdlInputHandler::setWindow(SDL_Window *window) m_Window = window; } +#ifdef Q_OS_WIN32 +LRESULT CALLBACK SdlInputHandler::keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam) +{ + if (nCode < 0 || nCode != HC_ACTION) { + return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam); + } + + SDL_Event event = {}; + KBDLLHOOKSTRUCT* hookData = (KBDLLHOOKSTRUCT*)lParam; + switch (hookData->vkCode) { + case VK_LWIN: + event.key.keysym.sym = SDLK_LGUI; + event.key.keysym.scancode = SDL_SCANCODE_LGUI; + break; + case VK_RWIN: + event.key.keysym.sym = SDLK_RGUI; + event.key.keysym.scancode = SDL_SCANCODE_RGUI; + break; + case VK_LMENU: + event.key.keysym.sym = SDLK_LALT; + event.key.keysym.scancode = SDL_SCANCODE_LALT; + break; + case VK_RMENU: + event.key.keysym.sym = SDLK_RALT; + event.key.keysym.scancode = SDL_SCANCODE_RALT; + break; + case VK_LCONTROL: + event.key.keysym.sym = SDLK_LCTRL; + event.key.keysym.scancode = SDL_SCANCODE_LCTRL; + break; + case VK_RCONTROL: + event.key.keysym.sym = SDLK_RCTRL; + event.key.keysym.scancode = SDL_SCANCODE_RCTRL; + break; + default: + // Bail quickly if it's not a key we care about + goto NextHook; + } + + // Make sure we're in a state where we actually want to steal this event (and it is safe to do so) + Session* session = Session::get(); + if (session == nullptr || session->m_InputHandler == nullptr || !session->m_InputHandler->isSystemKeyCaptureActive()) { + goto NextHook; + } + + // If this is a key we're going to intercept, create the synthetic SDL event. + // This is necessary because we are also hiding this key event from ourselves too. + if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) { + event.type = SDL_KEYDOWN; + event.key.state = SDL_PRESSED; + } + else { + event.type = SDL_KEYUP; + event.key.state = SDL_RELEASED; + } + + event.key.timestamp = SDL_GetTicks(); + event.key.windowID = SDL_GetWindowID(session->m_Window); + + event.key.keysym.mod = SDL_GetModState(); + switch (hookData->vkCode) { + case VK_LWIN: + if (event.key.state == SDL_PRESSED) { + event.key.keysym.mod |= KMOD_LGUI; + } + else { + event.key.keysym.mod &= ~KMOD_LGUI; + } + break; + case VK_RWIN: + if (event.key.state == SDL_PRESSED) { + event.key.keysym.mod |= KMOD_RGUI; + } + else { + event.key.keysym.mod &= ~KMOD_RGUI; + } + break; + case VK_LMENU: + if (event.key.state == SDL_PRESSED) { + event.key.keysym.mod |= KMOD_LALT; + } + else { + event.key.keysym.mod &= ~KMOD_LALT; + } + break; + case VK_RMENU: + if (event.key.state == SDL_PRESSED) { + event.key.keysym.mod |= KMOD_RALT; + } + else { + event.key.keysym.mod &= ~KMOD_RALT; + } + break; + case VK_LCONTROL: + if (event.key.state == SDL_PRESSED) { + event.key.keysym.mod |= KMOD_LCTRL; + } + else { + event.key.keysym.mod &= ~KMOD_LCTRL; + } + break; + case VK_RCONTROL: + if (event.key.state == SDL_PRESSED) { + event.key.keysym.mod |= KMOD_RCTRL; + } + else { + event.key.keysym.mod &= ~KMOD_RCTRL; + } + break; + } + + // NOTE: event.key.repeat is not populated in this path! + SDL_PushEvent(&event); + + // Synchronize SDL's modifier state with the current state. + // SDL won't do this on its own because it will never see the + // WM_KEYUP/WM_KEYDOWN events. + SDL_SetModState((SDL_Keymod)event.key.keysym.mod); + + return 1; + +NextHook: + return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam); +} +#endif + void SdlInputHandler::raiseAllKeys() { if (m_KeysDown.isEmpty()) { @@ -255,11 +406,29 @@ bool SdlInputHandler::isCaptureActive() return m_FakeCaptureActive; } +bool SdlInputHandler::isSystemKeyCaptureActive() +{ + if (!m_CaptureSystemKeysEnabled) { + return false; + } + + if (m_Window == nullptr) { + return false; + } + + Uint32 windowFlags = SDL_GetWindowFlags(m_Window); + return (windowFlags & SDL_WINDOW_INPUT_FOCUS) && + (windowFlags & SDL_WINDOW_INPUT_GRABBED) && + (windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP); +} + 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 we're in full-screen desktop mode but system key capture is enabled, also grab the cursor (will grab the keyboard too on X11). + if (((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0 && m_CaptureSystemKeysEnabled) || + (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) { SDL_SetWindowGrab(m_Window, SDL_TRUE); } diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 6b183f18..d2919dce 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -5,6 +5,11 @@ #include +#ifdef Q_OS_WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + #define SDL_CODE_HIDE_CURSOR 1 #define SDL_CODE_SHOW_CURSOR 2 @@ -78,6 +83,8 @@ public: bool isCaptureActive(); + bool isSystemKeyCaptureActive(); + void setCaptureActive(bool active); bool isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth = -1, int windowHeight = -1); @@ -117,6 +124,11 @@ private: static Uint32 dragTimerCallback(Uint32 interval, void* param); +#ifdef Q_OS_WIN32 + static + LRESULT CALLBACK keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam); +#endif + SDL_Window* m_Window; bool m_MultiController; bool m_GamepadMouse; @@ -142,6 +154,7 @@ private: bool m_FakeCaptureActive; QString m_OldIgnoreDevices; QString m_OldIgnoreDevicesExcept; + bool m_CaptureSystemKeysEnabled; SDL_TouchFingerEvent m_LastTouchDownEvent; SDL_TouchFingerEvent m_LastTouchUpEvent; diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index e2e1b7ab..1502ceb0 100644 --- a/app/streaming/input/keyboard.cpp +++ b/app/streaming/input/keyboard.cpp @@ -5,10 +5,6 @@ #include -// Until we can fully capture these on all platforms (without conflicting with -// OS-provided shortcuts), we should avoid passing them through to the host. -//#define ENABLE_META - #define VK_0 0x30 #define VK_A 0x41 @@ -215,11 +211,11 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) if (event->keysym.mod & KMOD_SHIFT) { modifiers |= MODIFIER_SHIFT; } -#ifdef ENABLE_META if (event->keysym.mod & KMOD_GUI) { - modifiers |= MODIFIER_META; + if (isSystemKeyCaptureActive()) { + modifiers |= MODIFIER_META; + } } -#endif // 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 @@ -360,14 +356,18 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) case SDL_SCANCODE_RALT: keyCode = 0xA5; break; - #ifdef ENABLE_META case SDL_SCANCODE_LGUI: + if (!isSystemKeyCaptureActive()) { + return; + } keyCode = 0x5B; break; case SDL_SCANCODE_RGUI: + if (!isSystemKeyCaptureActive()) { + return; + } keyCode = 0x5C; break; - #endif case SDL_SCANCODE_AC_BACK: keyCode = 0xA6; break; @@ -441,7 +441,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) m_KeysDown.remove(keyCode); } - LiSendKeyboardEvent(keyCode, + LiSendKeyboardEvent(0x8000 | keyCode, event->state == SDL_PRESSED ? KEY_ACTION_DOWN : KEY_ACTION_UP, modifiers);