blob: e7f67bcb49ce25b6c4c6188182db96665c199080 [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 'package:meta/meta.dart';
import 'package:ui/ui.dart' as ui;
import '../engine.dart' show registerHotRestartListener;
import 'browser_detection.dart';
import 'dom.dart';
import 'key_map.g.dart';
import 'platform_dispatcher.dart';
import 'safe_browser_api.dart';
import 'semantics.dart';
typedef _VoidCallback = void Function();
typedef ValueGetter<T> = T Function();
typedef _ModifierGetter = bool Function(FlutterHtmlKeyboardEvent event);
// Set this flag to true to see all the fired events in the console.
const bool _debugLogKeyEvents = false;
const int _kLocationLeft = 1;
const int _kLocationRight = 2;
final int _kLogicalAltLeft = kWebLogicalLocationMap['Alt']![_kLocationLeft]!;
final int _kLogicalAltRight = kWebLogicalLocationMap['Alt']![_kLocationRight]!;
final int _kLogicalControlLeft = kWebLogicalLocationMap['Control']![_kLocationLeft]!;
final int _kLogicalControlRight = kWebLogicalLocationMap['Control']![_kLocationRight]!;
final int _kLogicalShiftLeft = kWebLogicalLocationMap['Shift']![_kLocationLeft]!;
final int _kLogicalShiftRight = kWebLogicalLocationMap['Shift']![_kLocationRight]!;
final int _kLogicalMetaLeft = kWebLogicalLocationMap['Meta']![_kLocationLeft]!;
final int _kLogicalMetaRight = kWebLogicalLocationMap['Meta']![_kLocationRight]!;
final int _kPhysicalAltLeft = kWebToPhysicalKey['AltLeft']!;
final int _kPhysicalAltRight = kWebToPhysicalKey['AltRight']!;
final int _kPhysicalControlLeft = kWebToPhysicalKey['ControlLeft']!;
final int _kPhysicalControlRight = kWebToPhysicalKey['ControlRight']!;
final int _kPhysicalShiftLeft = kWebToPhysicalKey['ShiftLeft']!;
final int _kPhysicalShiftRight = kWebToPhysicalKey['ShiftRight']!;
final int _kPhysicalMetaLeft = kWebToPhysicalKey['MetaLeft']!;
final int _kPhysicalMetaRight = kWebToPhysicalKey['MetaRight']!;
// Map logical keys for modifier keys to the functions that can get their
// modifier flag out of an event.
final Map<int, _ModifierGetter> _kLogicalKeyToModifierGetter = <int, _ModifierGetter>{
_kLogicalAltLeft: (FlutterHtmlKeyboardEvent event) => event.altKey,
_kLogicalAltRight: (FlutterHtmlKeyboardEvent event) => event.altKey,
_kLogicalControlLeft: (FlutterHtmlKeyboardEvent event) => event.ctrlKey,
_kLogicalControlRight: (FlutterHtmlKeyboardEvent event) => event.ctrlKey,
_kLogicalShiftLeft: (FlutterHtmlKeyboardEvent event) => event.shiftKey,
_kLogicalShiftRight: (FlutterHtmlKeyboardEvent event) => event.shiftKey,
_kLogicalMetaLeft: (FlutterHtmlKeyboardEvent event) => event.metaKey,
_kLogicalMetaRight: (FlutterHtmlKeyboardEvent event) => event.metaKey,
};
// ASCII for a, z, A, and Z
const int _kCharLowerA = 0x61;
const int _kCharLowerZ = 0x7a;
const int _kCharUpperA = 0x41;
const int _kCharUpperZ = 0x5a;
bool isAlphabet(int charCode) {
return (charCode >= _kCharLowerA && charCode <= _kCharLowerZ)
|| (charCode >= _kCharUpperA && charCode <= _kCharUpperZ);
}
const String _kPhysicalCapsLock = 'CapsLock';
const String _kLogicalDead = 'Dead';
const int _kWebKeyIdPlane = 0x1700000000;
// Bits in a Flutter logical event to generate the logical key for dead keys.
//
// Logical keys for dead keys are generated by annotating physical keys with
// modifiers (see `_getLogicalCode`).
const int _kDeadKeyCtrl = 0x10000000;
const int _kDeadKeyShift = 0x20000000;
const int _kDeadKeyAlt = 0x40000000;
const int _kDeadKeyMeta = 0x80000000;
const ui.KeyData _emptyKeyData = ui.KeyData(
type: ui.KeyEventType.down,
timeStamp: Duration.zero,
logical: 0,
physical: 0,
character: null,
synthesized: false,
);
typedef DispatchKeyData = bool Function(ui.KeyData data);
/// Converts a floating number timestamp (in milliseconds) to a [Duration] by
/// splitting it into two integer components: milliseconds + microseconds.
Duration _eventTimeStampToDuration(num milliseconds) {
final int ms = milliseconds.toInt();
final int micro = ((milliseconds - ms) * Duration.microsecondsPerMillisecond).toInt();
return Duration(milliseconds: ms, microseconds: micro);
}
class KeyboardBinding {
KeyboardBinding._() {
_setup();
}
/// The singleton instance of this object.
static KeyboardBinding? get instance => _instance;
static KeyboardBinding? _instance;
static void initInstance() {
if (_instance == null) {
_instance = KeyboardBinding._();
assert(() {
registerHotRestartListener(_instance!._reset);
return true;
}());
}
}
KeyboardConverter get converter => _converter;
late final KeyboardConverter _converter;
final Map<String, DomEventListener> _listeners = <String, DomEventListener>{};
void _addEventListener(String eventName, DomEventListener handler) {
dynamic loggedHandler(DomEvent event) {
if (_debugLogKeyEvents) {
print(event.type);
}
if (EngineSemanticsOwner.instance.receiveGlobalEvent(event)) {
return handler(event);
}
return null;
}
final DomEventListener wrappedHandler = allowInterop(loggedHandler);
assert(!_listeners.containsKey(eventName));
_listeners[eventName] = wrappedHandler;
domWindow.addEventListener(eventName, wrappedHandler, true);
}
/// Remove all active event listeners.
void _clearListeners() {
_listeners.forEach((String eventName, DomEventListener listener) {
domWindow.removeEventListener(eventName, listener, true);
});
_listeners.clear();
}
bool _onKeyData(ui.KeyData data) {
bool? result;
// This callback is designed to be invoked synchronously. This is enforced
// by `result`, which starts null and is asserted non-null when returned.
EnginePlatformDispatcher.instance.invokeOnKeyData(data,
(bool handled) { result = handled; });
return result!;
}
void _setup() {
_addEventListener('keydown', allowInterop((DomEvent event) {
return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent));
}));
_addEventListener('keyup', allowInterop((DomEvent event) {
return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent));
}));
_converter = KeyboardConverter(_onKeyData, onMacOs: operatingSystem == OperatingSystem.macOs);
}
void _reset() {
_clearListeners();
_converter.dispose();
}
}
class AsyncKeyboardDispatching {
AsyncKeyboardDispatching({
required this.keyData,
this.callback,
});
final ui.KeyData keyData;
final _VoidCallback? callback;
}
// A wrapper of [DomKeyboardEvent] with reduced methods delegated to the event
// for the convenience of testing.
class FlutterHtmlKeyboardEvent {
FlutterHtmlKeyboardEvent(this._event);
final DomKeyboardEvent _event;
String get type => _event.type;
String? get code => _event.code;
String? get key => _event.key;
int get keyCode => _event.keyCode.toInt();
bool? get repeat => _event.repeat;
int? get location => _event.location.toInt();
num? get timeStamp => _event.timeStamp;
bool get altKey => _event.altKey;
bool get ctrlKey => _event.ctrlKey;
bool get shiftKey => _event.shiftKey;
bool get metaKey => _event.metaKey;
bool get isComposing => _event.isComposing;
bool getModifierState(String key) => _event.getModifierState(key);
void preventDefault() => _event.preventDefault();
bool get defaultPrevented => _event.defaultPrevented;
}
// Reads [DomKeyboardEvent], then [dispatches ui.KeyData] accordingly.
//
// The events are read through [handleEvent], and dispatched through the
// [dispatchKeyData] as given in the constructor. Some key data might be
// dispatched asynchronously.
class KeyboardConverter {
KeyboardConverter(this.performDispatchKeyData, {this.onMacOs = false});
final DispatchKeyData performDispatchKeyData;
final bool onMacOs;
// The `performDispatchKeyData` wrapped with tracking logic.
//
// It is non-null only during `handleEvent`. All events during `handleEvent`
// should be dispatched with `_dispatchKeyData`, others with
// `performDispatchKeyData`.
DispatchKeyData? _dispatchKeyData;
bool _disposed = false;
void dispose() {
_disposed = true;
}
// On macOS, CapsLock behaves differently in that, a keydown event occurs when
// the key is pressed and the light turns on, while a keyup event occurs when the
// key is pressed and the light turns off. Flutter considers both events as
// key down, and synthesizes immediate cancel events following them. The state
// of "whether CapsLock is on" should be accessed by "activeLocks".
bool _shouldSynthesizeCapsLockUp() {
return onMacOs;
}
// ## About Key guards
//
// 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 ([_keydownCancelDurationMac]) we assume
// the user has released the key and we synthesize a keyup event.
bool _shouldDoKeyGuard() {
return onMacOs;
}
/// 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);
static int _getPhysicalCode(String code) {
return kWebToPhysicalKey[code] ?? (code.hashCode + _kWebKeyIdPlane);
}
static int _getModifierMask(FlutterHtmlKeyboardEvent event) {
final bool altDown = event.altKey;
final bool ctrlDown = event.ctrlKey;
final bool shiftDown = event.shiftKey;
final bool metaDown = event.metaKey;
return (altDown ? _kDeadKeyAlt : 0) +
(ctrlDown ? _kDeadKeyCtrl : 0) +
(shiftDown ? _kDeadKeyShift : 0) +
(metaDown ? _kDeadKeyMeta : 0);
}
// Whether `event.key` should be considered a key name.
//
// The `event.key` can either be a key name or the printable character. If the
// first character is an alphabet, it must be either 'A' to 'Z' ( and return
// true), or be a key name (and return false). Otherwise, return true.
static bool _eventKeyIsKeyname(String key) {
assert(key.isNotEmpty);
return isAlphabet(key.codeUnitAt(0)) && key.length > 1;
}
static int _characterToLogicalKey(String key) {
// Assume the length being <= 2 to be sufficient in all cases. If not,
// extend the algorithm.
assert(key.length <= 2);
int result = key.codeUnitAt(0) & 0xffff;
if (key.length == 2) {
result += key.codeUnitAt(1) << 16;
}
// Convert upper letters to lower letters
if (result >= _kCharUpperA && result <= _kCharUpperZ) {
result = result + _kCharLowerA - _kCharUpperA;
}
return result;
}
static int _deadKeyToLogicalKey(int physicalKey, FlutterHtmlKeyboardEvent event) {
// 'Dead' is used to represent dead keys, such as a diacritic to the
// following base letter (such as Option-e results in ´).
//
// Assume they can be told apart with the physical key and the modifiers
// pressed.
return physicalKey + _getModifierMask(event) + _kWebKeyIdPlane;
}
static int _otherLogicalKey(String key) {
return kWebToLogicalKey[key] ?? (key.hashCode + _kWebKeyIdPlane);
}
// Map from pressed physical key to corresponding pressed logical key.
//
// Multiple physical keys can be mapped to the same logical key, usually due
// to positioned keys (left/right/numpad) or multiple keyboards.
final Map<int, int> _pressingRecords = <int, int>{};
// Schedule the dispatching of an event in the future. The `callback` will
// invoked before that.
//
// Returns a callback that cancels the schedule. Disposal of
// `KeyBoardConverter` also cancels the shedule automatically.
_VoidCallback _scheduleAsyncEvent(Duration duration, ValueGetter<ui.KeyData> getData, _VoidCallback callback) {
bool canceled = false;
Future<void>.delayed(duration).then<void>((_) {
if (!canceled && !_disposed) {
callback();
// This dispatch is performed asynchronously, therefore should not use
// `_dispatchKeyData`.
performDispatchKeyData(getData());
}
});
return () { canceled = true; };
}
final Map<int, _VoidCallback> _keyGuards = <int, _VoidCallback>{};
// Call this method on the down or repeated event of a non-modifier key.
void _startGuardingKey(int physicalKey, int logicalKey, Duration currentTimeStamp) {
if (!_shouldDoKeyGuard()) {
return;
}
final _VoidCallback cancelingCallback = _scheduleAsyncEvent(
_kKeydownCancelDurationMac,
() => ui.KeyData(
timeStamp: currentTimeStamp + _kKeydownCancelDurationMac,
type: ui.KeyEventType.up,
physical: physicalKey,
logical: logicalKey,
character: null,
synthesized: true,
),
() {
_pressingRecords.remove(physicalKey);
}
);
_keyGuards.remove(physicalKey)?.call();
_keyGuards[physicalKey] = cancelingCallback;
}
// Call this method on an up event event of a non-modifier key.
void _stopGuardingKey(int physicalKey) {
_keyGuards.remove(physicalKey)?.call();
}
void _handleEvent(FlutterHtmlKeyboardEvent event) {
final Duration timeStamp = _eventTimeStampToDuration(event.timeStamp!);
final String eventKey = event.key!;
final int physicalKey = _getPhysicalCode(event.code!);
final bool logicalKeyIsCharacter = !_eventKeyIsKeyname(eventKey);
final String? character = logicalKeyIsCharacter ? eventKey : null;
final int logicalKey = () {
if (kWebLogicalLocationMap.containsKey(event.key)) {
final int? result = kWebLogicalLocationMap[event.key!]?[event.location!];
assert(result != null, 'Invalid modifier location: ${event.key}, ${event.location}');
return result!;
}
if (character != null) {
return _characterToLogicalKey(character);
}
if (eventKey == _kLogicalDead) {
return _deadKeyToLogicalKey(physicalKey, event);
}
return _otherLogicalKey(eventKey);
}();
assert(event.type == 'keydown' || event.type == 'keyup');
final bool isPhysicalDown = event.type == 'keydown' ||
// On macOS, both keydown and keyup events of CapsLock should be considered keydown,
// followed by an immediate cancel event.
(_shouldSynthesizeCapsLockUp() && event.code! == _kPhysicalCapsLock);
final ui.KeyEventType type;
if (_shouldSynthesizeCapsLockUp() && event.code! == _kPhysicalCapsLock) {
// Case 1: Handle CapsLock on macOS
//
// On macOS, both keydown and keyup events of CapsLock are considered
// keydown, followed by an immediate synchronized up event.
_scheduleAsyncEvent(
Duration.zero,
() => ui.KeyData(
timeStamp: timeStamp,
type: ui.KeyEventType.up,
physical: physicalKey,
logical: logicalKey,
character: null,
synthesized: true,
),
() {
_pressingRecords.remove(physicalKey);
}
);
type = ui.KeyEventType.down;
} else if (isPhysicalDown) {
// Case 2: Handle key down of normal keys
if (_pressingRecords[physicalKey] != null) {
// This physical key is being pressed according to the record.
if (event.repeat ?? false) {
// A normal repeated key.
type = ui.KeyEventType.repeat;
} else {
// A non-repeated key has been pressed that has the exact physical key as
// a currently pressed one. This can mean one of the following cases:
//
// * Multiple keyboards are pressing keys with the same physical key.
// * The up event was lost during a loss of focus.
// * The previous down event was a system shortcut and its release
// was skipped (see `_startGuardingKey`,) such as holding Ctrl and
// pressing V then V, within the "guard window".
//
// The three cases can't be distinguished, and in the 3rd case, the
// latter event must be dispatched as down events for the framework to
// correctly recognize and choose to not to handle. Therefore, an up
// event is synthesized before it.
_dispatchKeyData!(ui.KeyData(
timeStamp: timeStamp,
type: ui.KeyEventType.up,
physical: physicalKey,
logical: logicalKey,
character: null,
synthesized: true,
));
_pressingRecords.remove(physicalKey);
type = ui.KeyEventType.down;
}
} else {
// This physical key is not being pressed according to the record. It's a
// normal down event, whether the system event is a repeat or not.
type = ui.KeyEventType.down;
}
} else { // isPhysicalDown is false and not CapsLock
// Case 2: Handle key up of normal keys
if (_pressingRecords[physicalKey] == null) {
// The physical key has been released before. It indicates multiple
// keyboards pressed keys with the same physical key. Ignore the up event.
event.preventDefault();
return;
}
type = ui.KeyEventType.up;
}
// The _pressingRecords[physicalKey] might have been changed during the last
// `if` clause.
final int? lastLogicalRecord = _pressingRecords[physicalKey];
final int? nextLogicalRecord;
switch (type) {
case ui.KeyEventType.down:
assert(lastLogicalRecord == null);
nextLogicalRecord = logicalKey;
break;
case ui.KeyEventType.up:
assert(lastLogicalRecord != null);
nextLogicalRecord = null;
break;
case ui.KeyEventType.repeat:
assert(lastLogicalRecord != null);
nextLogicalRecord = lastLogicalRecord;
break;
}
if (nextLogicalRecord == null) {
_pressingRecords.remove(physicalKey);
} else {
_pressingRecords[physicalKey] = nextLogicalRecord;
}
// After updating _pressingRecords, synchronize modifier states. The
// `event.***Key` fields can be used to reduce some omitted modifier key
// events. We can synthesize key up events if they are false. Key down
// events can not be synthesized since we don't know which physical key they
// represent.
_kLogicalKeyToModifierGetter.forEach((int testeeLogicalKey, _ModifierGetter getModifier) {
// Do not synthesize for the key of the current event. The event is the
// ground truth.
if (logicalKey == testeeLogicalKey) {
return;
}
if (_pressingRecords.containsValue(testeeLogicalKey) && !getModifier(event)) {
_pressingRecords.removeWhere((int physicalKey, int logicalRecord) {
if (logicalRecord != testeeLogicalKey) {
return false;
}
_dispatchKeyData!(ui.KeyData(
timeStamp: timeStamp,
type: ui.KeyEventType.up,
physical: physicalKey,
logical: testeeLogicalKey,
character: null,
synthesized: true,
));
return true;
});
}
});
// Update key guards
if (logicalKeyIsCharacter) {
if (nextLogicalRecord != null) {
_startGuardingKey(physicalKey, logicalKey, timeStamp);
} else {
_stopGuardingKey(physicalKey);
}
}
final ui.KeyData keyData = ui.KeyData(
timeStamp: timeStamp,
type: type,
physical: physicalKey,
logical: lastLogicalRecord ?? logicalKey,
character: type == ui.KeyEventType.up ? null : character,
synthesized: false,
);
final bool primaryHandled = _dispatchKeyData!(keyData);
if (primaryHandled) {
event.preventDefault();
}
}
// Parse the HTML event, update states, and dispatch Flutter key data through
// [performDispatchKeyData].
//
// * The method might dispatch some synthesized key data first to update states,
// results discarded.
// * Then it dispatches exactly one non-synthesized key data that corresponds
// to the `event`, i.e. the primary key data. If this dispatching returns
// true, then this event will be invoked `preventDefault`.
// * Some key data might be synthesized to update states after the main key
// data. They are always scheduled asynchronously with results discarded.
void handleEvent(FlutterHtmlKeyboardEvent event) {
assert(_dispatchKeyData == null);
bool sentAnyEvents = false;
_dispatchKeyData = (ui.KeyData data) {
sentAnyEvents = true;
return performDispatchKeyData(data);
};
try {
_handleEvent(event);
} finally {
if (!sentAnyEvents) {
_dispatchKeyData!(_emptyKeyData);
}
_dispatchKeyData = null;
}
}
// Synthesize modifier keys up or down events only when the known pressing states are different.
void synthesizeModifiersIfNeeded(
bool altPressed,
bool controlPressed,
bool metaPressed,
bool shiftPressed,
num eventTimestamp,
) {
_synthesizeModifierIfNeeded(
_kPhysicalAltLeft,
_kPhysicalAltRight,
_kLogicalAltLeft,
altPressed ? ui.KeyEventType.down : ui.KeyEventType.up,
eventTimestamp,
);
_synthesizeModifierIfNeeded(
_kPhysicalControlLeft,
_kPhysicalControlRight,
_kLogicalControlLeft,
controlPressed ? ui.KeyEventType.down : ui.KeyEventType.up,
eventTimestamp,
);
_synthesizeModifierIfNeeded(
_kPhysicalMetaLeft,
_kPhysicalMetaRight,
_kLogicalMetaLeft,
metaPressed ? ui.KeyEventType.down : ui.KeyEventType.up,
eventTimestamp,
);
_synthesizeModifierIfNeeded(
_kPhysicalShiftLeft,
_kPhysicalShiftRight,
_kLogicalShiftLeft,
shiftPressed ? ui.KeyEventType.down : ui.KeyEventType.up,
eventTimestamp,
);
}
void _synthesizeModifierIfNeeded(
int physicalLeft,
int physicalRight,
int logicalLeft,
ui.KeyEventType type,
num domTimestamp,
) {
final bool leftPressed = _pressingRecords.containsKey(physicalLeft);
final bool rightPressed = _pressingRecords.containsKey(physicalRight);
final bool alreadyPressed = leftPressed || rightPressed;
final bool synthesizeDown = type == ui.KeyEventType.down && !alreadyPressed;
final bool synthesizeUp = type == ui.KeyEventType.up && alreadyPressed;
// Synthesize a down event only for the left key if right and left are not pressed
if (synthesizeDown) {
_synthesizeKeyDownEvent(domTimestamp, physicalLeft, logicalLeft);
}
// Synthesize an up event for left key if pressed
if (synthesizeUp && leftPressed) {
final int knownLogicalKey = _pressingRecords[physicalLeft]!;
_synthesizeKeyUpEvent(domTimestamp, physicalLeft, knownLogicalKey);
}
// Synthesize an up event for right key if pressed
if (synthesizeUp && rightPressed) {
final int knownLogicalKey = _pressingRecords[physicalRight]!;
_synthesizeKeyUpEvent(domTimestamp, physicalRight, knownLogicalKey);
}
}
void _synthesizeKeyDownEvent(num domTimestamp, int physical, int logical) {
performDispatchKeyData(ui.KeyData(
timeStamp: _eventTimeStampToDuration(domTimestamp),
type: ui.KeyEventType.down,
physical: physical,
logical: logical,
character: null,
synthesized: true,
));
// Update pressing state
_pressingRecords[physical] = logical;
}
void _synthesizeKeyUpEvent(num domTimestamp, int physical, int logical) {
performDispatchKeyData(ui.KeyData(
timeStamp: _eventTimeStampToDuration(domTimestamp),
type: ui.KeyEventType.up,
physical: physical,
logical: logical,
character: null,
synthesized: true,
));
// Update pressing states
_pressingRecords.remove(physical);
}
@visibleForTesting
bool debugKeyIsPressed(int physical) {
return _pressingRecords.containsKey(physical);
}
}