Skip to content

Commit

Permalink
[web] Synthesize keyup event when the browser doesn't trigger a keyup (
Browse files Browse the repository at this point in the history
  • Loading branch information
mdebbar authored Apr 20, 2020
1 parent b6bb7e7 commit b0c4be9
Show file tree
Hide file tree
Showing 2 changed files with 332 additions and 1 deletion.
72 changes: 71 additions & 1 deletion lib/web_ui/lib/src/engine/keyboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
// @dart = 2.6
part of engine;

/// After a keydown is received, this is the duration we wait for a repeat event
/// before we decide to synthesize a keyup event.
///
/// On Linux and Windows, the typical ranges for keyboard repeat delay go up to
/// 1000ms. On Mac, the range goes up to 2000ms.
const Duration _keydownCancelDuration = Duration(milliseconds: 1000);

/// Provides keyboard bindings, such as the `flutter/keyevent` channel.
class Keyboard {
Expand All @@ -19,6 +25,12 @@ class Keyboard {
static Keyboard get instance => _instance;
static Keyboard _instance;

/// A mapping of [KeyboardEvent.code] to [Timer].
///
/// The timer is for when to synthesize a keyup for the [KeyboardEvent.code]
/// if no repeat events were received.
final Map<String, Timer> _keydownTimers = <String, Timer>{};

html.EventListener _keydownListener;
html.EventListener _keyupListener;

Expand All @@ -44,13 +56,24 @@ class Keyboard {
void dispose() {
html.window.removeEventListener('keydown', _keydownListener);
html.window.removeEventListener('keyup', _keyupListener);

for (final String key in _keydownTimers.keys) {
_keydownTimers[key].cancel();
}
_keydownTimers.clear();

_keydownListener = null;
_keyupListener = null;
_instance = null;
}

static const JSONMessageCodec _messageCodec = JSONMessageCodec();

/// Contains meta state from the latest event.
///
/// Initializing with `0x0` which means no meta keys are pressed.
int _lastMetaState = 0x0;

void _handleHtmlEvent(html.KeyboardEvent event) {
if (window._onPlatformMessage == null) {
return;
Expand All @@ -60,12 +83,37 @@ class Keyboard {
event.preventDefault();
}

final String timerKey = event.code;

// Don't synthesize a keyup event for modifier keys because the browser always
// sends a keyup event for those.
if (!_isModifierKey(event)) {
// When the user enters a browser/system shortcut (e.g. `cmd+alt+i`) the
// browser doesn't send a keyup for it. This puts the framework in a
// corrupt state because it thinks the key was never released.
//
// To avoid this, we rely on the fact that browsers send repeat events
// while the key is held down by the user. If we don't receive a repeat
// event within a specific duration ([_keydownCancelDuration]) we assume
// the user has released the key and we synthesize a keyup event.
_keydownTimers[timerKey]?.cancel();
if (event.type == 'keydown') {
_keydownTimers[timerKey] = Timer(_keydownCancelDuration, () {
_keydownTimers.remove(timerKey);
_synthesizeKeyup(event);
});
} else {
_keydownTimers.remove(timerKey);
}
}

_lastMetaState = _getMetaState(event);
final Map<String, dynamic> eventData = <String, dynamic>{
'type': event.type,
'keymap': 'web',
'code': event.code,
'key': event.key,
'metaState': _getMetaState(event),
'metaState': _lastMetaState,
};

window.invokeOnPlatformMessage('flutter/keyevent',
Expand All @@ -81,6 +129,19 @@ class Keyboard {
return false;
}
}

void _synthesizeKeyup(html.KeyboardEvent event) {
final Map<String, dynamic> eventData = <String, dynamic>{
'type': 'keyup',
'keymap': 'web',
'code': event.code,
'key': event.key,
'metaState': _lastMetaState,
};

window.invokeOnPlatformMessage('flutter/keyevent',
_messageCodec.encodeMessage(eventData), _noopCallback);
}
}

const int _modifierNone = 0x00;
Expand Down Expand Up @@ -109,4 +170,13 @@ int _getMetaState(html.KeyboardEvent event) {
return metaState;
}

/// Returns true if the [event] was caused by a modifier key.
///
/// Modifier keys are shift, alt, ctrl and meta/cmd/win. These are the keys used
/// to perform keyboard shortcuts (e.g. `cmd+c`, `cmd+l`).
bool _isModifierKey(html.KeyboardEvent event) {
final String key = event.key;
return key == 'Meta' || key == 'Shift' || key == 'Alt' || key == 'Control';
}

void _noopCallback(ByteData data) {}
Loading

0 comments on commit b0c4be9

Please sign in to comment.