blob: 6157bd7477c857844498d2707bf6c06651ae4b0c [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:typed_data';
import '../engine.dart' show registerHotRestartListener;
import 'dom.dart';
import 'keyboard_binding.dart';
import 'platform_dispatcher.dart';
import 'safe_browser_api.dart';
import 'services.dart';
/// Provides keyboard bindings, such as the `flutter/keyevent` channel.
class RawKeyboard {
RawKeyboard._(this._onMacOs) {
_keydownListener = allowInterop((DomEvent event) {
_handleHtmlEvent(event);
});
domWindow.addEventListener('keydown', _keydownListener);
_keyupListener = allowInterop((DomEvent event) {
_handleHtmlEvent(event);
});
domWindow.addEventListener('keyup', _keyupListener);
registerHotRestartListener(() {
dispose();
});
}
/// Initializes the [RawKeyboard] singleton.
///
/// Use the [instance] getter to get the singleton after calling this method.
static void initialize({bool onMacOs = false}) {
_instance ??= RawKeyboard._(onMacOs);
}
/// The [RawKeyboard] singleton.
static RawKeyboard? get instance => _instance;
static RawKeyboard? _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>{};
DomEventListener? _keydownListener;
DomEventListener? _keyupListener;
/// Uninitializes the [RawKeyboard] singleton.
///
/// After calling this method this object becomes unusable and [instance]
/// becomes `null`. Call [initialize] again to initialize a new singleton.
void dispose() {
domWindow.removeEventListener('keydown', _keydownListener);
domWindow.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;
final bool _onMacOs;
// When the user enters a browser/system shortcut (e.g. `cmd+alt+i`) on macOS,
// 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 ([_kKeydownCancelDurationMac]) we assume
// the user has released the key and we synthesize a keyup event.
bool _shouldDoKeyGuard() {
return _onMacOs;
}
bool _shouldIgnore(FlutterHtmlKeyboardEvent event) {
// During IME composition, Tab fires twice (once for composition and once
// for regular tabbing behavior), which causes issues. Intercepting the
// tab keydown event during composition prevents these issues from occurring.
// https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event#ignoring_keydown_during_ime_composition
return event.type == 'keydown' && event.key == 'Tab' && event.isComposing;
}
void _handleHtmlEvent(DomEvent domEvent) {
if (!domInstanceOfString(domEvent, 'KeyboardEvent')) {
return;
}
final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent);
final String timerKey = event.code!;
if (_shouldIgnore(event)) {
return;
}
// Don't handle synthesizing a keyup event for modifier keys
if (!_isModifierKey(event) && _shouldDoKeyGuard()) {
_keydownTimers[timerKey]?.cancel();
// Only keys affected by modifiers, require synthesizing
// because the browser always sends a keyup event otherwise
if (event.type == 'keydown' && _isAffectedByModifiers(event)) {
_keydownTimers[timerKey] = Timer(_kKeydownCancelDurationMac, () {
_keydownTimers.remove(timerKey);
_synthesizeKeyup(event);
});
} else {
_keydownTimers.remove(timerKey);
}
}
_lastMetaState = _getMetaState(event);
if (event.type == 'keydown') {
// For lock modifiers _getMetaState won't report a metaState at keydown.
// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState.
if (event.key == 'CapsLock') {
_lastMetaState |= modifierCapsLock;
} else if (event.code == 'NumLock') {
_lastMetaState |= modifierNumLock;
} else if (event.key == 'ScrollLock') {
_lastMetaState |= modifierScrollLock;
}
}
final Map<String, dynamic> eventData = <String, dynamic>{
'type': event.type,
'keymap': 'web',
'code': event.code,
'key': event.key,
'location': event.location,
'metaState': _lastMetaState,
'keyCode': event.keyCode,
};
EnginePlatformDispatcher.instance.invokeOnPlatformMessage('flutter/keyevent',
_messageCodec.encodeMessage(eventData), (ByteData? data) {
if (data == null) {
return;
}
final Map<String, dynamic> jsonResponse = _messageCodec.decodeMessage(data) as Map<String, dynamic>;
if (jsonResponse['handled'] as bool) {
// If the framework handled it, then don't propagate it any further.
event.preventDefault();
}
},
);
}
void _synthesizeKeyup(FlutterHtmlKeyboardEvent event) {
final Map<String, dynamic> eventData = <String, dynamic>{
'type': 'keyup',
'keymap': 'web',
'code': event.code,
'key': event.key,
'location': event.location,
'metaState': _lastMetaState,
'keyCode': event.keyCode,
};
EnginePlatformDispatcher.instance.invokeOnPlatformMessage('flutter/keyevent',
_messageCodec.encodeMessage(eventData), _noopCallback);
}
/// After a keydown is received, this is the duration we wait for a repeat event
/// before we decide to synthesize a keyup event.
///
/// This value is only for macOS, where the keyboard repeat delay goes up to
/// 2000ms.
static const Duration _kKeydownCancelDurationMac = Duration(milliseconds: 2000);
}
const int _modifierNone = 0x00;
const int _modifierShift = 0x01;
const int _modifierAlt = 0x02;
const int _modifierControl = 0x04;
const int _modifierMeta = 0x08;
const int modifierNumLock = 0x10;
const int modifierCapsLock = 0x20;
const int modifierScrollLock = 0x40;
/// Creates a bitmask representing the meta state of the [event].
int _getMetaState(FlutterHtmlKeyboardEvent event) {
int metaState = _modifierNone;
if (event.getModifierState('Shift')) {
metaState |= _modifierShift;
}
if (event.getModifierState('Alt') || event.getModifierState('AltGraph')) {
metaState |= _modifierAlt;
}
if (event.getModifierState('Control')) {
metaState |= _modifierControl;
}
if (event.getModifierState('Meta')) {
metaState |= _modifierMeta;
}
// See https://github.com/flutter/flutter/issues/66601 for why we don't
// set the ones below based on persistent state.
// if (event.getModifierState("CapsLock")) {
// metaState |= modifierCapsLock;
// }
// if (event.getModifierState("NumLock")) {
// metaState |= modifierNumLock;
// }
// if (event.getModifierState("ScrollLock")) {
// metaState |= modifierScrollLock;
// }
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(FlutterHtmlKeyboardEvent event) {
final String key = event.key!;
return key == 'Meta' || key == 'Shift' || key == 'Alt' || key == 'Control';
}
/// Returns true if the [event] is been affects by any of the modifiers key
///
/// This is a strong indication that this key is been used for a shortcut
bool _isAffectedByModifiers(FlutterHtmlKeyboardEvent event) {
return event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
}
void _noopCallback(ByteData? data) {}