WIP Add function for comprehensive capturing of all keyboard events#135
WIP Add function for comprehensive capturing of all keyboard events#135SamWindell wants to merge 2 commits into
Conversation
For audio plugins on Windows, hosts routinely block keyboard messages from reaching plugins. The workaround is for audio plugins is to install a keyboard hook and carefully consume key events that are needed, leaving the unused ones for the host, should they want them. Additionally, it's important for an audio plugin on Windows to return keyboard focus back to the previous window. For example after pressing enter on a text input field on the plugin, return focus back to the host so that keyboard shortcuts work again (spacebar for play, etc.). Neither of these features were possible with the existing puglGrabFocus API. This new API needs some feedback regard how best it should be implemented. On Linux and macOS, it just a simple stud. For macOS it may be necessary to perform a similar 'hooking' feature, but this time using `NSEvent addLocalMonitorForEventsMatchingMask`.
7117cb9 to
cf13e47
Compare
|
Thanks. I'm not familiar with this issue or if/why it's needed, but at first glance the amount of API burden and code weight seems vaguely reasonable anyway. Why is the filter thing needed? Since this seems to be controlling whether events are processed by other system functions, my knee-jerk is that this part feels "too entwined with the native details" and likely fragile |
|
Thanks for taking a look, I can provide a little more information: Pretty much every DAW on Windows blocks key events from reaching plugins (Reaper, Cubase, Ableton, etc.), and I believe that the only work around is for the plugin to install a keyboard hook. You would think that doing SetFocus() is enough, but the DAWs still steal some key events for their shortcuts such as spacebar to play transport, or R to enable repeat, etc. leaving it impossible to type anything meaning into the plugin. The event filtering is necessary because otherwise the keyboard hook would consume all events, leaving nothing for the DAW. That's fine if you have a text input selected, but if for example you have a custom preset browser UI open and have a CTRL+F shortcut to use the find function, we certainly need to consume the 'F' key from the DAW's interference, but we shouldn't consume all key events - the user expects that spacebar still trigger the DAW to play/pause. |
|
I see. Never-ending fun, this stuff. I'm this close to abandoning the entire idea of embeddable UIs at this point.
So this mechanism allows the plugin to "steal" key events from the DAW, i.e., only the plugin will get it (not both)? |
|
Oh yes it's all good fun!
Yes, my understanding is that the hook is called before the DAW gets the events. If the plugin wants the event exclusively, it scrubs it (set it to an empty WM_USER message). It's all rather nasty but I don't see any way around it! JUCE have been doing this for years: |
|
Ok. Well, if it needs doing, it needs doing. I might try to think of ways to avoid adding a new callback (generally ramming everything though a single even stream being the ideal of the API), but this sort of thing is best done after the implementation works everywhere. Do you have a good read on what the situation is on X11 and MacOS (and, eventually, Wayland)? Is a test or demo program possible? I don't currently have a workflow going that involves building whole plugins and hosting them in whatever proprietary DAW, so it's much easier to confidently land things when there's a simple program in the repo that uses it. I'm not sure if a simple pugl "host" would replicate whatever the DAWs are doing that steals events though |
|
I agree callbacks aren't usually a nice thing in an API. So I would be happy to see any other ideas for the design of that aspect. I've been using this patch for a couple of weeks in production and its seems to have resolved all my keyboard related issues for my users on Windows. Regarding the implementation of this feature on macOS, it's less of a ubiquitious problem. However, I have heard another dev talk about keys being stolen by the DAW (and therefore not reaching the plugin) when using the CMD on Logic Pro, Ableton and Reaper. And the workaround they use is this: https://github.com/Tremus/CPLUG/blob/b8b964b805fae6728203a05782d90bb4d39f6696/src/cplug_extensions/window_osx.m#L278-L291 On Linux, my impression is that nothing needs to be done, stealing doesn't occur. However I haven't done as much testing. Testing isn't super simple. I honestly don't know what the DAW is doing exactly to prevent events from reaching the plugin. Maybe some research could reveal some results though. Making a minimal test plugin to manually test in the DAWs would be possible, but not trival because we'd need it in at least both AU and VST3 formats on multiple OS. My experience is mostly with CLAP plugins. So off the top of my head it would be feasible to add a pugl GUI to this project and then wrap it to VST3 and AU using the clap-wrapper, although clap-wrapper is C++, not C. I will probably get round to making the macOS implementation myself. This pull request is related to Floe, which could serve as a plugin to test this in too. |
|
Alright, thanks for the explanation. It's not mandatory or anything, just way easier to work on things with a simple case (plus it avoids people from different projects all fighting over various incompatible "works for me" solutions that work for them and only them, a common nightmare with plugin stuff). I'll take a more serious look into how landable this is at some point hopefully soonish, but can't make any more specific timeline commitments at the moment. |
|
Thanks, I appreciate you taking a look at this and for making pugl. If you do make an alternative implementation of this feature, I can probably add it to Floe and release it to our beta testers to get some more widespread testing. |
|
|
||
| if (wantsEvents) { | ||
| if (!puglHasFocus(view)) { | ||
| puglGrabFocus(view); |
There was a problem hiding this comment.
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.
I think that could work and would be nicer.
| event.text.character = 'a'; | ||
| event.text.string[0] = 'a'; | ||
| if (!view->impl->keyboardEventFilter || | ||
| view->impl->keyboardEventFilter(view, &event)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;
});
}| msg->message == WM_KEYDOWN || msg->message == WM_SYSKEYDOWN, | ||
| msg->wParam, | ||
| msg->lParam); | ||
| if (!view->impl->keyboardEventFilter || |
There was a problem hiding this comment.
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:
- Call filter function to see if event is desired
- If so, dispatch
I think it would be better to instead do:
- Dispatch, and check the return value to see if the plugin consumed the event.
This would require a change in the rules for the event dispatch function so that e.g. PUGL_FAILURE is returned if the event isn't handled, but that's fine, because it would only affect code that opts in to "greedy" mode, and I think it's a good (and very common) principle for the event handler anyway.
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?
There was a problem hiding this comment.
Yes that would work, but as I understand it, on Windows, we shouldn't be calling TranslateMessage unless we know we want the event.
There was a problem hiding this comment.
The callback aspect could certainly still be avoided though. So long as the plugin has some way of communicating any mix of these requests:
- I want to greedily steal all CHAR events
- I want to greedily steal key events for X keys. (where X is one or more keys such as: F, for keyboard shortcut CTRL+F find, C, for CTRL+C copy, etc.)
puglSetWantsAllKeyboardEvents previously called puglGrabFocus, which makes the plugin's NSWindow the key window. In hosts like Logic Pro that left the host unable to receive transport shortcuts (spacebar play/stop, Cmd+K, etc.) for as long as the plugin window was open. It also ignored the filterFunction argument and had no way to release focus again. Reimplement it using the approach from CPLUG [1]: take first-responder status and install a local NSEvent monitor, but never make the host's window key. - Make the wrapper view the first responder (but not the key window). This is what makes the host route key events to our window in the first place; without it the local monitor never fires in hosts like Logic. Avoiding makeKeyWindow is what preserves the host's shortcuts. - The local NSEvent monitor synthesises a PuglEvent for each key down/up and asks filterFunction whether the plugin wants it. Wanted events are dispatched to the plugin and consumed (monitor returns nil); unwanted events are returned so they flow on to the host. As the monitor runs before sendEvent: dispatches the event, the plugin can also see keys the host would otherwise consume from its own responder chain. - -keyDown:/-keyUp: now forward to the host (via super) while the monitor is active, since the monitor already consumed everything wanted; what reaches the responder chain is unwanted and belongs to the host. - Text is synthesised directly from the NSEvent's characters, because consumed events never reach -keyDown: and so never go through -interpretKeyEvents:/NSTextInputContext. The trade-off is that IME composition and dead keys are unsupported on this path. The monitor and filter live on PuglInternals and are torn down both when the plugin stops wanting keyboard events and in puglFreeViewInternals. [1] https://github.com/Tremus/CPLUG/blob/master/src/cplug_extensions/window_osx.m
I was trying to get my plugin receiving all keyboard events on Windows and had to resort to the keyboard hook approach, as used by JUCE. It seems it's the only way to get reliable text input in Windows hosts like Ableton, Reaper, etc. It proved too difficult to do this outside of pugl, the functionality is too entwined with the native details, so I added the code in win.c.
I'm making this PR to begin a dialog about if this feature could be eventually added to pugl, and, if so, get direction on what you think the API should be.
On macOS we might need a similar backend eventually using NSEvent addLocalMonitorForEventsMatchingMask.