Implement keyboard grab support for Windows

This is implemented via a low-level keyboard hook. Unfortunately, this is
rather invasive, but it's how Microsoft recommends that it be done [0].
We want to do as little as possible in the hook, so we only intercept a few
crucial modifier keys there, while leaving other keys to the normal event
processing flow.

We will only install this hook if SDL_HINT_GRAB_KEYBOARD=1, which is not
the default. This will reduce any compatibility concerns to just the SDL
applications that explicitly ask for this behavior.

We also remove the hook when the grab is terminated to ensure that we're
not unnecessarily staying involved in key event processing when it's not
required anymore.

[0]: https://docs.microsoft.com/en-us/windows/win32/dxtecharts/disabling-shortcut-keys-in-games
This commit is contained in:
Cameron Gutman 2021-01-22 19:40:26 -06:00
parent fd65aaa9a8
commit 8c921d8201
4 changed files with 93 additions and 0 deletions

View File

@ -428,6 +428,47 @@ static SDL_MOUSE_EVENT_SOURCE GetMouseMessageSource()
return SDL_MOUSE_EVENT_SOURCE_MOUSE; return SDL_MOUSE_EVENT_SOURCE_MOUSE;
} }
LRESULT CALLBACK
WIN_KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode < 0 || nCode != HC_ACTION) {
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
KBDLLHOOKSTRUCT* hookData = (KBDLLHOOKSTRUCT*)lParam;
SDL_Scancode scanCode;
switch (hookData->vkCode) {
case VK_LWIN:
scanCode = SDL_SCANCODE_LGUI;
break;
case VK_RWIN:
scanCode = SDL_SCANCODE_RGUI;
break;
case VK_LMENU:
scanCode = SDL_SCANCODE_LALT;
break;
case VK_RMENU:
scanCode = SDL_SCANCODE_RALT;
break;
case VK_LCONTROL:
scanCode = SDL_SCANCODE_LCTRL;
break;
case VK_RCONTROL:
scanCode = SDL_SCANCODE_RCTRL;
break;
default:
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
SDL_SendKeyboardKey(SDL_PRESSED, scanCode);
} else {
SDL_SendKeyboardKey(SDL_RELEASED, scanCode);
}
return 1;
}
LRESULT CALLBACK LRESULT CALLBACK
WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{ {

View File

@ -27,6 +27,7 @@ extern LPTSTR SDL_Appname;
extern Uint32 SDL_Appstyle; extern Uint32 SDL_Appstyle;
extern HINSTANCE SDL_Instance; extern HINSTANCE SDL_Instance;
extern LRESULT CALLBACK WIN_KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam);
extern LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, extern LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam,
LPARAM lParam); LPARAM lParam);
extern void WIN_PumpEvents(_THIS); extern void WIN_PumpEvents(_THIS);

View File

@ -738,11 +738,58 @@ WIN_GetWindowGammaRamp(_THIS, SDL_Window * window, Uint16 * ramp)
return succeeded ? 0 : -1; return succeeded ? 0 : -1;
} }
static void WIN_GrabKeyboard(SDL_Window *window)
{
SDL_WindowData *data = (SDL_WindowData *)window->driverdata;
HMODULE module;
if (data->keyboard_hook) {
return;
}
/* SetWindowsHookEx() needs to know which module contains the hook we
want to install. This is complicated by the fact that SDL can be
linked statically or dynamically. Fortunately XP and later provide
this nice API that will go through the loaded modules and find the
one containing our code.
*/
if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(LPTSTR)WIN_KeyboardHookProc,
&module)) {
return;
}
/* To grab the keyboard, we have to install a low-level keyboard hook to
intercept keys that would normally be captured by the OS. Intercepting
all key events on the system is rather invasive, but it's what Microsoft
actually documents that you do to capture these.
*/
data->keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, WIN_KeyboardHookProc, module, 0);
}
void WIN_UngrabKeyboard(SDL_Window *window)
{
SDL_WindowData *data = (SDL_WindowData *)window->driverdata;
if (data->keyboard_hook) {
UnhookWindowsHookEx(data->keyboard_hook);
data->keyboard_hook = NULL;
}
}
void void
WIN_SetWindowGrab(_THIS, SDL_Window * window, SDL_bool grabbed) WIN_SetWindowGrab(_THIS, SDL_Window * window, SDL_bool grabbed)
{ {
WIN_UpdateClipCursor(window); WIN_UpdateClipCursor(window);
if (grabbed) {
if (SDL_GetHintBoolean(SDL_HINT_GRAB_KEYBOARD, SDL_FALSE)) {
WIN_GrabKeyboard(window);
}
} else {
WIN_UngrabKeyboard(window);
}
if (window->flags & SDL_WINDOW_FULLSCREEN) { if (window->flags & SDL_WINDOW_FULLSCREEN) {
UINT flags = SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOSIZE; UINT flags = SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOSIZE;
@ -759,6 +806,9 @@ WIN_DestroyWindow(_THIS, SDL_Window * window)
SDL_WindowData *data = (SDL_WindowData *) window->driverdata; SDL_WindowData *data = (SDL_WindowData *) window->driverdata;
if (data) { if (data) {
if (data->keyboard_hook) {
UnhookWindowsHookEx(data->keyboard_hook);
}
ReleaseDC(data->hwnd, data->hdc); ReleaseDC(data->hwnd, data->hdc);
RemoveProp(data->hwnd, TEXT("SDL_WindowData")); RemoveProp(data->hwnd, TEXT("SDL_WindowData"));
if (data->created) { if (data->created) {

View File

@ -37,6 +37,7 @@ typedef struct
HINSTANCE hinstance; HINSTANCE hinstance;
HBITMAP hbm; HBITMAP hbm;
WNDPROC wndproc; WNDPROC wndproc;
HHOOK keyboard_hook;
SDL_bool created; SDL_bool created;
WPARAM mouse_button_flags; WPARAM mouse_button_flags;
LPARAM last_pointer_update; LPARAM last_pointer_update;