-
Notifications
You must be signed in to change notification settings - Fork 40
WIP Add function for comprehensive capturing of all keyboard events #135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,10 @@ | |
| #define PUGL_LOCAL_CLIENT_MSG (WM_USER + 52) | ||
| #define PUGL_USER_TIMER_MIN 9470 | ||
|
|
||
| #define PUGL_WINDOW_MAGIC 0x5055474C // "PUGL" in ASCII | ||
| #define PUGL_WINDOW_MAGIC_OFFSET 0 | ||
| #define PUGL_WINDOW_EXTRA_SIZE sizeof(LONG_PTR) | ||
|
|
||
| #ifdef __cplusplus | ||
| # define PUGL_INIT_STRUCT \ | ||
| { \ | ||
|
|
@@ -57,6 +61,9 @@ typedef HRESULT(WINAPI* PFN_GetScaleFactorForMonitor)(HMONITOR, DWORD*); | |
| LRESULT CALLBACK | ||
| wndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); | ||
|
|
||
| static void | ||
| removeKeyboardHook(PuglView* view); | ||
|
|
||
| #ifdef UNICODE | ||
|
|
||
| typedef wchar_t ArgStringChar; | ||
|
|
@@ -139,6 +146,7 @@ puglRegisterWindowClass(const char* name) | |
| wc.cbSize = sizeof(wc); | ||
| wc.style = CS_OWNDC; | ||
| wc.lpfnWndProc = wndProc; | ||
| wc.cbWndExtra = PUGL_WINDOW_EXTRA_SIZE; | ||
| wc.hInstance = module; | ||
| wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); | ||
| wc.hCursor = LoadCursor(NULL, IDC_ARROW); | ||
|
|
@@ -332,6 +340,7 @@ puglRealize(PuglView* view) | |
| } | ||
|
|
||
| SetWindowLongPtr(impl->hwnd, GWLP_USERDATA, (LONG_PTR)view); | ||
| SetWindowLongPtr(impl->hwnd, PUGL_WINDOW_MAGIC_OFFSET, PUGL_WINDOW_MAGIC); | ||
|
|
||
| return puglDispatchSimpleEvent(view, PUGL_REALIZE); | ||
| } | ||
|
|
@@ -350,6 +359,11 @@ puglUnrealize(PuglView* const view) | |
| view->backend->destroy(view); | ||
| } | ||
|
|
||
| removeKeyboardHook(view); | ||
| if (view->impl->lastFocus) { | ||
| SetFocus(view->impl->lastFocus); | ||
| } | ||
|
|
||
| memset(&view->lastConfigure, 0, sizeof(PuglConfigureEvent)); | ||
| ReleaseDC(impl->hwnd, impl->hdc); | ||
| impl->hdc = NULL; | ||
|
|
@@ -921,6 +935,9 @@ handleMessage(PuglView* view, UINT message, WPARAM wParam, LPARAM lParam) | |
| break; | ||
| case WM_KILLFOCUS: | ||
| event.type = PUGL_FOCUS_OUT; | ||
| if (view->impl->keyboardHook) { | ||
| removeKeyboardHook(view); | ||
| } | ||
| break; | ||
| case WM_SYSKEYDOWN: | ||
| initKeyEvent(&event.key, view, true, wParam, lParam); | ||
|
|
@@ -1647,3 +1664,154 @@ puglWinLeave(PuglView* view, const PuglExposeEvent* expose) | |
|
|
||
| return PUGL_SUCCESS; | ||
| } | ||
|
|
||
| // Returns true if the message was consumed | ||
| static bool | ||
| handleHookMessage(PuglView* view, const MSG* msg, int code, WPARAM wParam) | ||
| { | ||
| if (code != HC_ACTION || wParam != PM_REMOVE || !msg->hwnd) { | ||
| return false; | ||
| } | ||
|
|
||
| PuglEvent event = PUGL_INIT_STRUCT; | ||
|
|
||
| switch (msg->message) { | ||
| case WM_CHAR: | ||
| case WM_SYSCHAR: | ||
| initCharEvent(&event, view, msg->wParam, msg->lParam); | ||
| if (!view->impl->keyboardEventFilter || | ||
| view->impl->keyboardEventFilter(view, &event)) { | ||
| puglDispatchEvent(view, &event); | ||
| return true; | ||
| } | ||
| return false; | ||
|
|
||
| case WM_KEYDOWN: | ||
| case WM_KEYUP: | ||
| case WM_SYSKEYDOWN: | ||
| case WM_SYSKEYUP: { | ||
| bool used = false; | ||
|
|
||
| // TODO: can we get the real CHAR event here rather than a made-up example | ||
| event.type = PUGL_TEXT; | ||
| event.text.character = 'a'; | ||
| event.text.string[0] = 'a'; | ||
| if (!view->impl->keyboardEventFilter || | ||
| view->impl->keyboardEventFilter(view, &event)) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see how this works / does anything useful if it's calling the filter function on a made-up event with no relation to actual input? This applies to everything in this block since it's conditional on what the filter function returns.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The important differentiation here is character events vs key down/up events. If the plugin GUI has a text input boxed focused then we need to steal all character events from the host. Alternatively, the plugin GUI might have no need for text input but instead wants to detect key press CTRL + F, for example. This is made messy because with Windows you first need to translate key events into character events using TranslateMessage. We should only call TranslateMessage when the plugin actually wants char events, otherwise it's not giving the host the opportunity to do this and things could behave strangely for them. That's as I understand it at least. Here's what Floe does with it's keyboard event filter: static void RequestAllKeyboardEvents(AppWindow& window, bool wants_focus) {
puglSetWantsAllKeyboardEvents(
window.view,
wants_focus,
[](PuglView* view, PuglEvent const* event) -> bool {
auto& window = *(AppWindow*)puglGetHandle(view);
switch (event->type) {
case PUGL_TEXT: return window.last_result.wants.text_input;
case PUGL_KEY_PRESS:
case PUGL_KEY_RELEASE:
if (window.last_result.wants.text_input) return true;
if (auto const key_code = RemapKeyCode(event->key.key);
key_code && window.last_result.wants.keyboard_keys.Get(ToInt(*key_code)))
return true;
return false;
default:
}
return false;
});
} |
||
| // Adds a WM_CHAR message to the queue if the key produces text | ||
| TranslateMessage(msg); | ||
|
|
||
| // We check if text was produced (removing the events as we do the check) | ||
| MSG peeked = PUGL_INIT_STRUCT; | ||
| if (PeekMessage(&peeked, msg->hwnd, WM_CHAR, WM_DEADCHAR, PM_REMOVE) || | ||
| PeekMessage( | ||
| &peeked, msg->hwnd, WM_SYSCHAR, WM_SYSDEADCHAR, PM_REMOVE)) { | ||
| used = true; | ||
| } | ||
| } | ||
|
|
||
| initKeyEvent(&event.key, | ||
| view, | ||
| msg->message == WM_KEYDOWN || msg->message == WM_SYSKEYDOWN, | ||
| msg->wParam, | ||
| msg->lParam); | ||
| if (!view->impl->keyboardEventFilter || | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignoring the above for a moment, this pattern here is what I was initially thinking about with my initial knee-jerk dislike of adding a new filter function. The current scheme is:
I think it would be better to instead do:
This would require a change in the rules for the event dispatch function so that e.g. I don't fully understand whether this is possible, though, because of the above funny business. What's the filter function you're using doing?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes that would work, but as I understand it, on Windows, we shouldn't be calling TranslateMessage unless we know we want the event.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The callback aspect could certainly still be avoided though. So long as the plugin has some way of communicating any mix of these requests:
|
||
| view->impl->keyboardEventFilter(view, &event)) { | ||
| puglDispatchEvent(view, &event); | ||
| return true; | ||
| } | ||
|
|
||
| return used; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| static LRESULT CALLBACK | ||
| keyboardHookProc(int code, WPARAM wParam, LPARAM lParam) | ||
| { | ||
| MSG* msg = (MSG*)lParam; | ||
|
|
||
| // Check if this window is actually a pugl window | ||
| if (GetClassLongPtr(msg->hwnd, GCL_CBWNDEXTRA) == | ||
| (LONG_PTR)PUGL_WINDOW_EXTRA_SIZE && | ||
| GetWindowLongPtr(msg->hwnd, PUGL_WINDOW_MAGIC_OFFSET) == | ||
| PUGL_WINDOW_MAGIC) { | ||
| PuglView* view = (PuglView*)GetWindowLongPtr(msg->hwnd, GWLP_USERDATA); | ||
| if (view && view->impl && view->impl->wantsAllKeyboardEvents) { | ||
| if (handleHookMessage(view, msg, code, wParam)) { | ||
| // Scrub the message so no one else gets it | ||
| memset(msg, 0, sizeof(MSG)); | ||
| msg->message = WM_USER; | ||
| return 0; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return CallNextHookEx(NULL, code, wParam, lParam); | ||
| } | ||
|
|
||
| static void | ||
| removeKeyboardHook(PuglView* view) | ||
| { | ||
| if (view->impl->keyboardHook) { | ||
| UnhookWindowsHookEx(view->impl->keyboardHook); | ||
| view->impl->keyboardHook = NULL; | ||
| } | ||
| } | ||
|
|
||
| static PuglStatus | ||
| installKeyboardHook(PuglView* view) | ||
| { | ||
| if (view->impl->keyboardHook) { | ||
| return PUGL_SUCCESS; | ||
| } | ||
|
|
||
| HMODULE module = NULL; | ||
| if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | | ||
| GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, | ||
| (LPCTSTR)keyboardHookProc, | ||
| &module)) { | ||
| module = GetModuleHandle(NULL); | ||
| } | ||
|
|
||
| if (!module) { | ||
| return PUGL_FAILURE; | ||
| } | ||
|
|
||
| view->impl->keyboardHook = SetWindowsHookEx( | ||
| WH_GETMESSAGE, keyboardHookProc, module, GetCurrentThreadId()); | ||
|
|
||
| return view->impl->keyboardHook ? PUGL_SUCCESS : PUGL_FAILURE; | ||
| } | ||
|
|
||
| PuglStatus | ||
| puglSetWantsAllKeyboardEvents(PuglView* view, | ||
| bool wantsEvents, | ||
| PuglKeyboardEventFilter filterFunction) | ||
| { | ||
| if (!view || !view->impl) { | ||
| return PUGL_BAD_PARAMETER; | ||
| } | ||
|
|
||
| view->impl->wantsAllKeyboardEvents = wantsEvents; | ||
| view->impl->keyboardEventFilter = filterFunction; | ||
|
|
||
| if (wantsEvents) { | ||
| // Set focus and install hook | ||
| HWND hwnd = view->impl->hwnd; | ||
| if (GetFocus() != hwnd) { | ||
| view->impl->lastFocus = SetFocus(hwnd); | ||
| } | ||
| return installKeyboardHook(view); | ||
| } else { | ||
| // Restore previous focus and remove hook | ||
| if (view->impl->lastFocus) { | ||
| SetFocus(view->impl->lastFocus); | ||
| view->impl->lastFocus = NULL; | ||
| } | ||
| removeKeyboardHook(view); | ||
| return PUGL_SUCCESS; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like trouble (across all implementations). I feel like it would be less sketchy if setting the flag was separate from grabbing focus, then the required setup done internally whenever focus is granted and released for whatever reason. This would work particularly nicely if PUGL_GREEDY_KEYS or some such was just a hint, and the filter function replaced with different semantics for the usual event handler (see other comments).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that could work and would be nicer.