Skip to content

Commit 0112d4b

Browse files
authored
[Keyboard] Send empty key events when no key data should (flutter#27774)
The keyboard system on each platform now sends an empty key data instead of nothing if no key data should be sent.
1 parent 9e0f3ff commit 0112d4b

13 files changed

+342
-44
lines changed

lib/web_ui/lib/src/engine/keyboard_binding.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ const int _kDeadKeyShift = 0x20000000;
7676
const int _kDeadKeyAlt = 0x40000000;
7777
const int _kDeadKeyMeta = 0x80000000;
7878

79+
const ui.KeyData _emptyKeyData = ui.KeyData(
80+
type: ui.KeyEventType.down,
81+
timeStamp: Duration.zero,
82+
logical: 0,
83+
physical: 0,
84+
character: null,
85+
synthesized: false,
86+
);
87+
7988
typedef DispatchKeyData = bool Function(ui.KeyData data);
8089

8190
/// Converts a floating number timestamp (in milliseconds) to a [Duration] by
@@ -401,6 +410,8 @@ class KeyboardConverter {
401410
// a currently pressed one, usually indicating multiple keyboards are
402411
// pressing keys with the same physical key, or the up event was lost
403412
// during a loss of focus. The down event is ignored.
413+
dispatchKeyData(_emptyKeyData);
414+
event.preventDefault();
404415
return;
405416
}
406417
} else {
@@ -413,6 +424,8 @@ class KeyboardConverter {
413424
if (lastLogicalRecord == null) {
414425
// The physical key has been released before. It indicates multiple
415426
// keyboards pressed keys with the same physical key. Ignore the up event.
427+
dispatchKeyData(_emptyKeyData);
428+
event.preventDefault();
416429
return;
417430
}
418431

lib/web_ui/test/keyboard_converter_test.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,9 +369,12 @@ void testMain() {
369369
converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)
370370
..onPreventDefault = onPreventDefault
371371
);
372-
expect(keyDataList, isEmpty);
373-
expect(preventedDefault, isFalse);
372+
expect(keyDataList, hasLength(1));
373+
expect(keyDataList[0].physical, 0);
374+
expect(keyDataList[0].logical, 0);
375+
expect(preventedDefault, isTrue);
374376

377+
keyDataList.clear();
375378
converter.handleEvent(keyUpEvent('ShiftLeft', 'Shift', 0, kLocationLeft)
376379
..onPreventDefault = onPreventDefault
377380
);
@@ -398,8 +401,10 @@ void testMain() {
398401
converter.handleEvent(keyUpEvent('ShiftRight', 'Shift', 0, kLocationRight)
399402
..onPreventDefault = onPreventDefault
400403
);
401-
expect(keyDataList, isEmpty);
402-
expect(preventedDefault, isFalse);
404+
expect(keyDataList, hasLength(1));
405+
expect(keyDataList[0].physical, 0);
406+
expect(keyDataList[0].logical, 0);
407+
expect(preventedDefault, isTrue);
403408
});
404409

405410
test('Conflict from multiple keyboards do not crash', () {

shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,15 @@ - (void)synthesizeCapsLockTapWithTimestamp:(NSTimeInterval)timestamp;
426426
- (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event
427427
callback:(nonnull FlutterKeyCallbackGuard*)callback;
428428

429+
/**
430+
* Send an empty key event.
431+
*
432+
* The event is never synthesized, and never expects an event result. An empty
433+
* event is sent when no other events should be sent, such as upon back-to-back
434+
* keydown events of the same key.
435+
*/
436+
- (void)sendEmptyEvent;
437+
429438
/**
430439
* Send a key event for a modifier key.
431440
*/
@@ -619,6 +628,19 @@ - (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event
619628
_sendEvent(event, HandleResponse, pending);
620629
}
621630

631+
- (void)sendEmptyEvent {
632+
FlutterKeyEvent event = {
633+
.struct_size = sizeof(FlutterKeyEvent),
634+
.timestamp = 0,
635+
.type = kFlutterKeyEventTypeDown,
636+
.physical = 0,
637+
.logical = 0,
638+
.character = nil,
639+
.synthesized = false,
640+
};
641+
_sendEvent(event, nil, nil);
642+
}
643+
622644
- (void)synthesizeModifierEventOfType:(BOOL)isDownEvent
623645
timestamp:(NSTimeInterval)timestamp
624646
keyCode:(UInt32)keyCode {
@@ -659,6 +681,7 @@ - (void)handlePressBegin:(nonnull FlutterUIPressProxy*)press
659681
// However this might happen in add-to-app scenarios if the focus is changed
660682
// from the native view to the Flutter view amid the key tap.
661683
[callback resolveTo:TRUE];
684+
[self sendEmptyEvent];
662685
return;
663686
}
664687
}
@@ -695,6 +718,7 @@ - (void)handlePressEnd:(nonnull FlutterUIPressProxy*)press
695718
// However this might happen in add-to-app scenarios if the focus is changed
696719
// from the native view to the Flutter view amid the key tap.
697720
[callback resolveTo:TRUE];
721+
[self sendEmptyEvent];
698722
return;
699723
}
700724
[self updateKey:physicalKey asPressed:0];

shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,16 @@ - (void)testIgnoreDuplicateDownEvent API_AVAILABLE(ios(13.4)) {
301301
last_handled = handled;
302302
}];
303303

304-
XCTAssertEqual([events count], 0u);
304+
XCTAssertEqual([events count], 1u);
305+
event = [events lastObject].data;
306+
XCTAssertEqual(event->physical, 0ull);
307+
XCTAssertEqual(event->logical, 0ull);
308+
XCTAssertEqual(event->synthesized, false);
309+
XCTAssertFalse([[events lastObject] hasCallback]);
305310
XCTAssertEqual(last_handled, TRUE);
306311

312+
[events removeAllObjects];
313+
307314
last_handled = FALSE;
308315
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
309316
callback:^(BOOL handled) {
@@ -327,6 +334,7 @@ - (void)testIgnoreDuplicateDownEvent API_AVAILABLE(ios(13.4)) {
327334
- (void)testIgnoreAbruptUpEvent API_AVAILABLE(ios(13.4)) {
328335
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
329336
__block BOOL last_handled = TRUE;
337+
FlutterKeyEvent* event;
330338

331339
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
332340
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
@@ -342,8 +350,15 @@ - (void)testIgnoreAbruptUpEvent API_AVAILABLE(ios(13.4)) {
342350
last_handled = handled;
343351
}];
344352

345-
XCTAssertEqual([events count], 0u);
353+
XCTAssertEqual([events count], 1u);
354+
event = [events lastObject].data;
355+
XCTAssertEqual(event->physical, 0ull);
356+
XCTAssertEqual(event->logical, 0ull);
357+
XCTAssertEqual(event->synthesized, false);
358+
XCTAssertFalse([[events lastObject] hasCallback]);
346359
XCTAssertEqual(last_handled, TRUE);
360+
361+
[events removeAllObjects];
347362
}
348363

349364
// Press R-Shift, A, then release R-Shift then A, on a US keyboard.
@@ -433,6 +448,10 @@ - (void)testToggleModifiersDuringKeyTap API_AVAILABLE(ios(13.4)) {
433448
- (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
434449
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
435450
FlutterKeyEvent* event;
451+
__block BOOL last_handled = TRUE;
452+
id keyEventCallback = ^(BOOL handled) {
453+
last_handled = handled;
454+
};
436455

437456
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
438457
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
@@ -448,8 +467,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
448467
// Numpad 1
449468
// OS provides: char: "1", code: 0x59, modifiers: 0x200000
450469
[responder handlePress:keyDownEvent(kKeyCodeNumpad1, kModifierFlagNumPadKey, 123.0, "1", "1")
451-
callback:^(BOOL handled){
452-
}];
470+
callback:keyEventCallback];
453471

454472
XCTAssertEqual([events count], 1u);
455473
event = [events lastObject].data;
@@ -465,8 +483,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
465483
// Fn Key (sends HID undefined)
466484
// OS provides: char: nil, keycode: 0x3, modifiers: 0x0
467485
[responder handlePress:keyDownEvent(kKeyCodeUndefined, kModifierFlagNone, 123.0)
468-
callback:^(BOOL handled){
469-
}];
486+
callback:keyEventCallback];
470487

471488
XCTAssertEqual([events count], 1u);
472489
event = [events lastObject].data;
@@ -482,8 +499,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
482499
// F1 Down
483500
// OS provides: char: UIKeyInputF1, code: 0x3a, modifiers: 0x0
484501
[responder handlePress:keyDownEvent(kKeyCodeF1, kModifierFlagNone, 123.0f, "\\^P", "\\^P")
485-
callback:^(BOOL handled){
486-
}];
502+
callback:keyEventCallback];
487503

488504
XCTAssertEqual([events count], 1u);
489505
event = [events lastObject].data;
@@ -499,8 +515,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
499515
// KeyA Down
500516
// OS provides: char: "q", code: 0x4, modifiers: 0x0
501517
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
502-
callback:^(BOOL handled){
503-
}];
518+
callback:keyEventCallback];
504519

505520
XCTAssertEqual([events count], 1u);
506521
event = [events lastObject].data;
@@ -516,8 +531,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
516531
// ShiftLeft Down
517532
// OS Provides: char: nil, code: 0xe1, modifiers: 0x20000
518533
[responder handlePress:keyDownEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
519-
callback:^(BOOL handled){
520-
}];
534+
callback:keyEventCallback];
521535

522536
XCTAssertEqual([events count], 1u);
523537
event = [events lastObject].data;
@@ -532,8 +546,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
532546
// Numpad 1 Up
533547
// OS provides: char: "1", code: 0x59, modifiers: 0x200000
534548
[responder handlePress:keyUpEvent(kKeyCodeNumpad1, kModifierFlagNumPadKey, 123.0f)
535-
callback:^(BOOL handled){
536-
}];
549+
callback:keyEventCallback];
537550

538551
XCTAssertEqual([events count], 2u);
539552

@@ -559,8 +572,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
559572
// F1 Up
560573
// OS provides: char: UIKeyInputF1, code: 0x3a, modifiers: 0x0
561574
[responder handlePress:keyUpEvent(kKeyCodeF1, kModifierFlagNone, 123.0f)
562-
callback:^(BOOL handled){
563-
}];
575+
callback:keyEventCallback];
564576

565577
XCTAssertEqual([events count], 1u);
566578
event = [events lastObject].data;
@@ -576,8 +588,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
576588
// Fn Key (sends HID undefined)
577589
// OS provides: char: nil, code: 0x3, modifiers: 0x0
578590
[responder handlePress:keyUpEvent(kKeyCodeUndefined, kModifierFlagNone, 123.0)
579-
callback:^(BOOL handled){
580-
}];
591+
callback:keyEventCallback];
581592

582593
XCTAssertEqual([events count], 1u);
583594
event = [events lastObject].data;
@@ -592,8 +603,7 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
592603
// KeyA Up
593604
// OS provides: char: "a", code: 0x4, modifiers: 0x0
594605
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
595-
callback:^(BOOL handled){
596-
}];
606+
callback:keyEventCallback];
597607

598608
XCTAssertEqual([events count], 1u);
599609
event = [events lastObject].data;
@@ -609,10 +619,17 @@ - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
609619
// ShiftLeft Up
610620
// OS provides: char: nil, code: 0xe1, modifiers: 0x20000
611621
[responder handlePress:keyUpEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
612-
callback:^(BOOL handled){
613-
}];
622+
callback:keyEventCallback];
614623

615-
XCTAssertEqual([events count], 0u);
624+
XCTAssertEqual([events count], 1u);
625+
event = [events lastObject].data;
626+
XCTAssertEqual(event->physical, 0ull);
627+
XCTAssertEqual(event->logical, 0ull);
628+
XCTAssertEqual(event->synthesized, false);
629+
XCTAssertFalse([[events lastObject] hasCallback]);
630+
XCTAssertEqual(last_handled, TRUE);
631+
632+
[events removeAllObjects];
616633
}
617634

618635
- (void)testIdentifyLeftAndRightModifiers API_AVAILABLE(ios(13.4)) {

shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ - (void)notifyLowMemory {
5454
- (void)sendKeyEvent:(const FlutterKeyEvent&)event
5555
callback:(FlutterKeyEventCallback)callback
5656
userData:(void*)userData API_AVAILABLE(ios(9.0)) {
57-
NSAssert(callback != nullptr, @"Invalid callback");
57+
if (callback == nil)
58+
return;
59+
// NSAssert(callback != nullptr, @"Invalid callback");
5860
// Response is async, so we have to post it to the run loop instead of calling
5961
// it directly.
6062
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() {

shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,15 @@ - (void)sendModifierEventOfType:(BOOL)isDownEvent
423423
keyCode:(unsigned short)keyCode
424424
callback:(nullable FlutterKeyCallbackGuard*)callback;
425425

426+
/**
427+
* Send an empty key event.
428+
*
429+
* The event is never synthesized, and never expects an event result. An empty
430+
* event is sent when no other events should be sent, such as upon back-to-back
431+
* keydown events of the same key.
432+
*/
433+
- (void)sendEmptyEvent;
434+
426435
/**
427436
* Processes a down event from the system.
428437
*/
@@ -579,6 +588,7 @@ - (void)sendModifierEventOfType:(BOOL)isDownEvent
579588
uint64_t logicalKey = GetLogicalKeyForModifier(keyCode, physicalKey);
580589
if (physicalKey == 0 || logicalKey == 0) {
581590
NSLog(@"Unrecognized modifier key: keyCode 0x%hx, physical key 0x%llx", keyCode, physicalKey);
591+
[self sendEmptyEvent];
582592
[callback resolveTo:TRUE];
583593
return;
584594
}
@@ -599,6 +609,19 @@ - (void)sendModifierEventOfType:(BOOL)isDownEvent
599609
}
600610
}
601611

612+
- (void)sendEmptyEvent {
613+
FlutterKeyEvent flutterEvent = {
614+
.struct_size = sizeof(FlutterKeyEvent),
615+
.timestamp = 0,
616+
.type = kFlutterKeyEventTypeDown,
617+
.physical = 0,
618+
.logical = 0,
619+
.character = nil,
620+
.synthesized = false,
621+
};
622+
_sendEvent(flutterEvent, nullptr, nullptr);
623+
}
624+
602625
- (void)handleDownEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callback {
603626
uint64_t physicalKey = GetPhysicalKeyForKeyCode(event.keyCode);
604627
uint64_t logicalKey = GetLogicalKeyForEvent(event, physicalKey);
@@ -611,6 +634,7 @@ - (void)handleDownEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callb
611634
// key up event to the window where the corresponding key down occurred.
612635
// However this might happen in add-to-app scenarios if the focus is changed
613636
// from the native view to the Flutter view amid the key tap.
637+
[self sendEmptyEvent];
614638
[callback resolveTo:TRUE];
615639
return;
616640
}
@@ -643,6 +667,7 @@ - (void)handleUpEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callbac
643667
// key up event to the window where the corresponding key down occurred.
644668
// However this might happen in add-to-app scenarios if the focus is changed
645669
// from the native view to the Flutter view amid the key tap.
670+
[self sendEmptyEvent];
646671
[callback resolveTo:TRUE];
647672
return;
648673
}
@@ -669,6 +694,7 @@ - (void)handleCapsLockEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)c
669694
[self sendCapsLockTapWithTimestamp:event.timestamp callback:callback];
670695
_lastModifierFlagsOfInterest = _lastModifierFlagsOfInterest ^ NSEventModifierFlagCapsLock;
671696
} else {
697+
[self sendEmptyEvent];
672698
[callback resolveTo:TRUE];
673699
}
674700
}

0 commit comments

Comments
 (0)