-
Notifications
You must be signed in to change notification settings - Fork 5.9k
/
keyboard_binding.dart
700 lines (627 loc) · 25.1 KB
/
keyboard_binding.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
// 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 'package:web_locale_keymap/web_locale_keymap.dart' as locale_keymap;
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,
};
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);
}
// Returns a function that caches the result of `body`, ensuring that `body` is
// only run once.
ValueGetter<T> _cached<T>(ValueGetter<T> body) {
T? cache;
return () {
return cache ??= body();
};
}
class KeyboardBinding {
KeyboardBinding._() {
_addEventListener('keydown', allowInterop((DomEvent domEvent) {
final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent);
return _converter.handleEvent(event);
}));
_addEventListener('keyup', allowInterop((DomEvent event) {
return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent));
}));
}
/// 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;
}());
}
}
/// The platform as used in the initialization.
///
/// By default it is derived from [operatingSystem].
@protected
OperatingSystem get localPlatform {
return operatingSystem;
}
KeyboardConverter get converter => _converter;
late final KeyboardConverter _converter = KeyboardConverter(
_onKeyData,
localPlatform,
);
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 _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, OperatingSystem platform)
: onDarwin = platform == OperatingSystem.macOs || platform == OperatingSystem.iOs,
_mapping = _mappingFromPlatform(platform);
final DispatchKeyData performDispatchKeyData;
/// Whether the current platform is macOS or iOS, which affects how certain key
/// events are comprehended, including CapsLock and key guarding.
final bool onDarwin;
/// Maps logical keys from key event properties.
final locale_keymap.LocaleKeymap _mapping;
static locale_keymap.LocaleKeymap _mappingFromPlatform(OperatingSystem platform) {
switch (platform) {
case OperatingSystem.iOs:
case OperatingSystem.macOs:
return locale_keymap.LocaleKeymap.darwin();
case OperatingSystem.windows:
return locale_keymap.LocaleKeymap.win();
case OperatingSystem.android:
case OperatingSystem.linux:
case OperatingSystem.unknown:
return locale_keymap.LocaleKeymap.linux();
}
}
// 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 onDarwin;
}
// ## 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 (_kKeydownCancelDurationMac) we assume
// the user has released the key and we synthesize a keyup event.
bool _shouldDoKeyGuard() {
return onDarwin;
}
/// 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) {
if (code.isEmpty) {
return _kWebKeyIdPlane;
}
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` is a key name, such as "Shift", or otherwise a
// character, such as "S" or "ж".
//
// A key name always has more than 1 code unit, and they are all alnums.
// Character keys, however, can also have more than 1 code unit: en-in
// maps KeyL to L̥/l̥. To resolve this, we check the second code unit.
static bool _eventKeyIsKeyName(String key) {
return key.length > 1 && key.codeUnitAt(0) < 0x7F && key.codeUnitAt(1) < 0x7F;
}
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;
}
// 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 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);
// The function body might or might not be evaluated. If the event is a key
// up event, the resulting event will simply use the currently pressed
// logical key.
final ValueGetter<int> logicalKey = _cached<int>(() {
// Mapped logical keys, such as ArrowLeft, Escape, AudioVolumeDown.
final int? mappedLogicalKey = kWebToLogicalKey[eventKey];
if (mappedLogicalKey != null) {
return mappedLogicalKey;
}
// Keys with locations, such as modifier keys (Shift) or numpad keys.
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!;
}
// Locale-sensitive keys: letters, digits, and certain symbols.
if (logicalKeyIsCharacter) {
final int? localeLogicalKeys = _mapping.getLogicalKey(event.code, event.key, event.keyCode);
if (localeLogicalKeys != null) {
return localeLogicalKeys;
}
}
// Dead keys that are not handled by the locale mapping.
if (eventKey == _kLogicalDead) {
return _deadKeyToLogicalKey(physicalKey, event);
}
// Minted logical keys.
return eventKey.hashCode + _kWebKeyIdPlane;
});
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 String? character = logicalKeyIsCharacter ? eventKey : null;
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);
}
bool keyIsPressed(int physical) {
return _pressingRecords.containsKey(physical);
}
}