scope: expose `keyup` event to registered handler for better handling of repeated keypress
Use case
Currently in scope.register(mod, key, handler), only keydown event is exposed to handler. if plugin want to handle key repeats (long press on the key), they can check repeat prop in provided KeyboardEvent to see if the key is being held. But without keyup event, plugin can't tell if key is released and long press has ended.
One workaround can be a debounce function that is dispatched when no keyboard event with repeat prop is triggered within given amount of time like the following:
const registerRepeatHandler = (scope) => {
let repeating = false;
const onRepeatEnd = debounce(
() => {
// do somthing when repeat ends
repeating = false;
},
200,
true,
);
scope.register([], "ArrowRight", (evt) => {
evt.preventDefault();
if (!evt.repeat) {
// do something if key not repeated
} else {
if (!repeating) {
repeating = true;
// do somthing when key is repeated
}
onRepeatEnd();
}
});
};
but as repeat rate (how frequent will repeated key event being trigger) is configurable in OS preference (For example in windows), this workaround may fail to work if repeat rate is too slow.
possible implementation
adding an additional arg to pass keyup event handler like scope.register(mod, key, downHandler, upHandler) should not cause existing down handler being called twice.
I think because the keymap and scoping mechanism is overwhelmingly used for keydown events, it doesn't make sense for us to implement a full keyup mechanism for event distribution (also performance wise).
However, what you could do is to hook a global keyup event the first time you encounter a repeat keydown event, and unhook it the moment it fires. We use this trick fairly often with our drag-and-drop handlers: only hook mousedown, and when the mousedown fires, we will hook a global mousemove and a mouseup event, and in the mouseup we will unhook both events.
Thanks, the reply is so fast! one small question though, is it possible to check if key is repeated in the similar fashion in global command callback triggered by hotkey? of course, register to global scope is an option, but it will lost all the benefits of hotkey management and conflict resolution.
of course, if the event is not available somewhere, temporary global keyup handler can also be a workaround for this use case.
Hmm I didn't consider that a possibility but I will look into it - Most likely I will just disregard repeat triggers though.
I've tested the command callbacks and it will be triggered repetitively when hotkey is being held, similar to scope handlers. Hopefully when disregarding repeat triggers has landed, an additional hotkeyCallback can be included as an option to handle such use case.
Glad to see v0.14.11 came out! repeat triggers are disregarded now, I'm wondering if is it possible to just pass a boolean to let callback to know if it's triggered by hotkeys so that it can decide whether to hook global keydown/keyup event or not?
Yes, I am working on that and it will be part of the next release.
yeah, in v0.14.13 I found the new repeatable prop in addCommand argument 🎉 , but I'm wondering if there is any particular reason why in app.hotkeyManager.onTrigger, the keyboard event is not passed to app.commands.executeCommand, which will save the second argument in app.lastEvent if present? maybe app.lastEvent can be used to check if the command is triggered by hotkey?
Yup. Originally, lastEvent was designed such that commands can check whether the Ctrl/Cmd key was held or if the event was triggered by middle clicking, to open things in new panes. I realized that because hotkeys could have those keys involved, it would be weird to have commands behave differently depending on what hotkey you set it to.
I'm still working out the details about that part of the code, and unfortunately I don't have any great ideas for the moment.