diff --git a/include/pugl/pugl.h b/include/pugl/pugl.h index 5ba5a46e..665a5a97 100644 --- a/include/pugl/pugl.h +++ b/include/pugl/pugl.h @@ -1494,6 +1494,34 @@ puglGrabFocus(PuglView* view); PUGL_API bool puglHasFocus(const PuglView* view); +/** + Return true if the event is wanted. Will either be a PUGL_TEXT or PUGL_KEY_*. +*/ +typedef bool (*PuglKeyboardEventFilter)(PuglView* view, const PuglEvent* event); + +/** + Request all keyboard input events. + + This tries to ensure that the view receives keyboard events even when they + might normally be intercepted or consumed by the system or other handlers. + This is particularly useful for audio plugins when the host blocks key + events from reaching the plugin. + + If you use this, you do not need to use puglGrabFocus. + + Use the filter function to allow the exact events that you directly need, + allowing other handlers on the system to consume them if not. + + Note that this will fail if the view is not mapped and so should not, for + example, be called immediately after puglShow(). + + @return #PUGL_SUCCESS if the focus was successfully grabbed, or an error. +*/ +PUGL_API PuglStatus +puglSetWantsAllKeyboardEvents(PuglView* view, + bool wantsEvents, + PuglKeyboardEventFilter filterFunction); + /** Request data from the general copy/paste clipboard. diff --git a/src/mac.h b/src/mac.h index 305cf30b..bafe6b12 100644 --- a/src/mac.h +++ b/src/mac.h @@ -30,13 +30,15 @@ struct PuglWorldInternalsImpl { }; struct PuglInternalsImpl { - NSApplication* app; - PuglWrapperView* wrapperView; - NSView* drawView; - NSCursor* cursor; - PuglWindow* window; - uint32_t mods; - bool mouseTracked; + NSApplication* app; + PuglWrapperView* wrapperView; + NSView* drawView; + NSCursor* cursor; + PuglWindow* window; + uint32_t mods; + bool mouseTracked; + id keyEventMonitor; + PuglKeyboardEventFilter keyboardEventFilter; }; #endif // PUGL_SRC_MAC_H diff --git a/src/mac.m b/src/mac.m index 41d358a2..58e889e9 100644 --- a/src/mac.m +++ b/src/mac.m @@ -666,6 +666,15 @@ - (void)scrollWheel:(NSEvent*)event - (void)keyDown:(NSEvent*)event { + // When the local key-event monitor is active it is the sole path for events + // the plugin wants (it consumes them before they reach here). Anything that + // reaches the responder chain is something the plugin did not want, so pass + // it to the host instead of dispatching/swallowing it here. + if (puglview->impl->keyboardEventFilter) { + [super keyDown:event]; + return; + } + if (puglview->hints[PUGL_IGNORE_KEY_REPEAT] && [event isARepeat]) { return; } @@ -701,6 +710,12 @@ - (void)keyDown:(NSEvent*)event - (void)keyUp:(NSEvent*)event { + // Forward to the host while the monitor is active, as in -keyDown:. + if (puglview->impl->keyboardEventFilter) { + [super keyUp:event]; + return; + } + const NSPoint wloc = [self eventLocation:event]; const NSPoint rloc = [NSEvent mouseLocation]; const PuglKey spec = keySymToSpecial(event); @@ -1410,6 +1425,12 @@ - (void)windowDidExitFullScreen:(NSNotification*)notification } if (view->impl) { + if (view->impl->keyEventMonitor) { + [NSEvent removeMonitor:view->impl->keyEventMonitor]; + [view->impl->keyEventMonitor release]; + view->impl->keyEventMonitor = nil; + } + if (view->impl->wrapperView) { [view->impl->wrapperView removeFromSuperview]; view->impl->wrapperView->puglview = NULL; @@ -1451,6 +1472,186 @@ - (void)windowDidExitFullScreen:(NSNotification*)notification [[impl->wrapperView window] firstResponder] == impl->wrapperView); } +// Synthesise PUGL_TEXT events directly from an NSEvent's characters. AppKit's +// normal text-input path is -interpretKeyEvents: (called from -keyDown:), which +// routes through NSTextInputContext and replies via -insertText:. But we handle +// wanted key events inside the local event monitor and consume them, so they +// never reach -keyDown:; we therefore produce the text ourselves. This bypasses +// the input context, so IME composition and dead keys are unsupported. +static void +dispatchMonitoredText(PuglWrapperView* const wrapperView, NSEvent* const event) +{ + // Command-modified keys are shortcuts, not text input. + if (getModifiers(event) & PUGL_MOD_SUPER) { + return; + } + + NSString* const characters = [event characters]; + if ([characters length] == 0) { + return; + } + + // Skip AppKit's private-use range for function/arrow keys and the DEL control + // character; these are not text to insert. + if ([characters length] == 1) { + const unichar c = [characters characterAtIndex:0]; + if ((c >= NSUpArrowFunctionKey && c <= 0xF8FF) || c == 0x7F) { + return; + } + } + + const NSPoint wloc = [wrapperView eventLocation:event]; + const NSPoint rloc = [NSEvent mouseLocation]; + for (NSUInteger i = 0; i < [characters length]; ++i) { + const uint32_t code = [characters characterAtIndex:i]; + char utf8[8] = {0}; + NSUInteger len = 0; + + [characters getBytes:utf8 + maxLength:sizeof(utf8) + usedLength:&len + encoding:NSUTF8StringEncoding + options:0 + range:NSMakeRange(i, i + 1) + remainingRange:nil]; + + PuglTextEvent ev = { + PUGL_TEXT, + 0U, + [event timestamp], + wloc.x, + wloc.y, + rloc.x, + [[NSScreen mainScreen] frame].size.height - rloc.y, + getModifiers(event), + [event keyCode], + code, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + }; + memcpy(ev.string, utf8, len); + + PuglEvent textEvent; + textEvent.text = ev; + puglDispatchEvent(wrapperView->puglview, &textEvent); + } +} + +static NSEvent* +handleMonitoredKeyEvent(PuglView* const view, NSEvent* const event) +{ + PuglWrapperView* const wrapperView = view->impl->wrapperView; + PuglKeyboardEventFilter const filter = view->impl->keyboardEventFilter; + if (!wrapperView || !filter) { + return event; + } + + const bool isDown = ([event type] == NSEventTypeKeyDown); + const PuglKey spec = keySymToSpecial(event); + const NSString* chars = [event charactersIgnoringModifiers]; + const char* str = [[chars lowercaseString] UTF8String]; + const uint32_t code = (spec ? spec : puglDecodeUTF8((const uint8_t*)str)); + + const NSPoint wloc = [wrapperView eventLocation:event]; + const NSPoint rloc = [NSEvent mouseLocation]; + + const PuglKeyEvent ev = { + isDown ? PUGL_KEY_PRESS : PUGL_KEY_RELEASE, + 0U, + [event timestamp], + wloc.x, + wloc.y, + rloc.x, + [[NSScreen mainScreen] frame].size.height - rloc.y, + puglFilterMods(getModifiers(event), spec), + [event keyCode], + (code != 0xFFFD) ? code : 0, + }; + + // Ask the plugin whether it wants this event. If not, return it so it flows + // on through the responder chain to the host (transport shortcuts etc.). + PuglEvent keyEvent; + keyEvent.key = ev; + if (!filter(view, &keyEvent)) { + return event; + } + + if (view->hints[PUGL_IGNORE_KEY_REPEAT] && isDown && [event isARepeat]) { + return nil; + } + + puglDispatchEvent(view, &keyEvent); + + // Only generate text while our window is key, i.e. the user is typing into us + // rather than into another window in the host process. + if (isDown && !spec) { + NSWindow* const window = [wrapperView window]; + if (window && [window isKeyWindow]) { + dispatchMonitoredText(wrapperView, event); + } + } + + return nil; // Consume: the host must not also see this event. +} + +PuglStatus +puglSetWantsAllKeyboardEvents(PuglView* view, + bool wantsEvents, + PuglKeyboardEventFilter filterFunction) +{ + // Capture keyboard input for a plugin window without stealing it from the + // host. Two mechanisms combine (technique adapted from CPLUG, link below): + // + // 1. Make our view the first responder. This is what causes the host to route + // key events to our window at all; without it, in hosts like Logic Pro the + // local monitor below never fires because the events never reach us. We do + // NOT call makeKeyWindow - that is what steals the host's transport + // shortcuts (spacebar play/stop etc.). + // + // 2. Install a process-wide local NSEvent monitor. For each key event we + // synthesise a PuglEvent and ask the filter whether the plugin wants it. + // If so we dispatch it to the plugin and consume it (return nil); if not + // we return it so it flows on through the responder chain. + // + // Because the monitor consumes every event the plugin wants, the only key + // events reaching -keyDown:/-keyUp: are ones the plugin did not want; those + // forward to the host (via super) so its shortcuts keep working. + // + // https://github.com/Tremus/CPLUG/blob/master/src/cplug_extensions/window_osx.m + PuglInternals* const impl = view->impl; + + if (impl->keyEventMonitor) { + [NSEvent removeMonitor:impl->keyEventMonitor]; + [impl->keyEventMonitor release]; + impl->keyEventMonitor = nil; + } + + NSWindow* const window = [impl->wrapperView window]; + + if (!wantsEvents || !filterFunction) { + impl->keyboardEventFilter = NULL; + if (window && [window firstResponder] == impl->wrapperView) { + [window makeFirstResponder:nil]; + } + return PUGL_SUCCESS; + } + + impl->keyboardEventFilter = filterFunction; + + if (window && [window firstResponder] != impl->wrapperView) { + [window makeFirstResponder:impl->wrapperView]; + } + + const NSEventMask mask = NSEventMaskKeyDown | NSEventMaskKeyUp; + id monitor = [NSEvent + addLocalMonitorForEventsMatchingMask:mask + handler:^NSEvent*(NSEvent* e) { + return handleMonitoredKeyEvent(view, e); + }]; + + impl->keyEventMonitor = [monitor retain]; + return PUGL_SUCCESS; +} + static bool styleIsMaximized(const PuglViewStyleFlags flags) { diff --git a/src/win.c b/src/win.c index a8cd841e..dd922908 100644 --- a/src/win.c +++ b/src/win.c @@ -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)) { + // 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 || + 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; + } +} diff --git a/src/win.h b/src/win.h index b37db3fb..dbef02ec 100644 --- a/src/win.h +++ b/src/win.h @@ -19,22 +19,26 @@ struct PuglWorldInternalsImpl { }; struct PuglInternalsImpl { - PuglWinPFD pfd; - int pfId; - HWND hwnd; - HCURSOR cursor; - HDC hdc; - WINDOWPLACEMENT oldPlacement; - PAINTSTRUCT paint; - PuglBlob clipboard; - PuglSurface* surface; - double scaleFactor; - bool mapped; - bool flashing; - bool mouseTracked; - bool minimized; - bool maximized; - bool fullscreen; + PuglWinPFD pfd; + int pfId; + HWND hwnd; + HCURSOR cursor; + HDC hdc; + WINDOWPLACEMENT oldPlacement; + PAINTSTRUCT paint; + PuglBlob clipboard; + PuglSurface* surface; + double scaleFactor; + bool mapped; + bool flashing; + bool mouseTracked; + bool minimized; + bool maximized; + bool fullscreen; + HWND lastFocus; + HHOOK keyboardHook; + bool wantsAllKeyboardEvents; + PuglKeyboardEventFilter keyboardEventFilter; }; PUGL_API PuglWinPFD diff --git a/src/x11.c b/src/x11.c index 6f05c590..b4a1864f 100644 --- a/src/x11.c +++ b/src/x11.c @@ -1357,6 +1357,22 @@ puglHasFocus(const PuglView* const view) return focusedWindow == view->impl->win; } +PuglStatus +puglSetWantsAllKeyboardEvents(PuglView* view, + bool wantsEvents, + PuglKeyboardEventFilter filterFunction) +{ + (void)filterFunction; + + if (wantsEvents) { + if (!puglHasFocus(view)) { + puglGrabFocus(view); + } + } + + return PUGL_SUCCESS; +} + PuglStatus puglStartTimer(PuglView* const view, const uintptr_t id, const double timeout) {