Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[Keyboard] Send empty key events when no key data should #27774

Merged
merged 12 commits into from
Jul 30, 2021
13 changes: 13 additions & 0 deletions lib/web_ui/lib/src/engine/keyboard_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ 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
Expand Down Expand Up @@ -401,6 +410,8 @@ class KeyboardConverter {
// a currently pressed one, usually indicating multiple keyboards are
// pressing keys with the same physical key, or the up event was lost
// during a loss of focus. The down event is ignored.
dispatchKeyData(_emptyKeyData);
event.preventDefault();
return;
}
} else {
Expand All @@ -413,6 +424,8 @@ class KeyboardConverter {
if (lastLogicalRecord == null) {
// The physical key has been released before. It indicates multiple
// keyboards pressed keys with the same physical key. Ignore the up event.
dispatchKeyData(_emptyKeyData);
event.preventDefault();
return;
}

Expand Down
13 changes: 9 additions & 4 deletions lib/web_ui/test/keyboard_converter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -369,9 +369,12 @@ void testMain() {
converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)
..onPreventDefault = onPreventDefault
);
expect(keyDataList, isEmpty);
expect(preventedDefault, isFalse);
expect(keyDataList, hasLength(1));
expect(keyDataList[0].physical, 0);
expect(keyDataList[0].logical, 0);
expect(preventedDefault, isTrue);

keyDataList.clear();
converter.handleEvent(keyUpEvent('ShiftLeft', 'Shift', 0, kLocationLeft)
..onPreventDefault = onPreventDefault
);
Expand All @@ -398,8 +401,10 @@ void testMain() {
converter.handleEvent(keyUpEvent('ShiftRight', 'Shift', 0, kLocationRight)
..onPreventDefault = onPreventDefault
);
expect(keyDataList, isEmpty);
expect(preventedDefault, isFalse);
expect(keyDataList, hasLength(1));
expect(keyDataList[0].physical, 0);
expect(keyDataList[0].logical, 0);
expect(preventedDefault, isTrue);
});

test('Conflict from multiple keyboards do not crash', () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,15 @@ - (void)synthesizeCapsLockTapWithTimestamp:(NSTimeInterval)timestamp;
- (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event
callback:(nonnull FlutterKeyCallbackGuard*)callback;

/**
* Send an empty key event.
*
* The event is never synthesized, and never expects an event result. An empty
* event is sent when no other events should be sent, such as upon back-to-back
* keydown events of the same key.
*/
- (void)sendEmptyEvent;

/**
* Send a key event for a modifier key.
*/
Expand Down Expand Up @@ -619,6 +628,19 @@ - (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event
_sendEvent(event, HandleResponse, pending);
}

- (void)sendEmptyEvent {
FlutterKeyEvent event = {
.struct_size = sizeof(FlutterKeyEvent),
.timestamp = 0,
.type = kFlutterKeyEventTypeDown,
.physical = 0,
.logical = 0,
.character = nil,
.synthesized = false,
};
_sendEvent(event, nil, nil);
}

- (void)synthesizeModifierEventOfType:(BOOL)isDownEvent
timestamp:(NSTimeInterval)timestamp
keyCode:(UInt32)keyCode {
Expand Down Expand Up @@ -659,6 +681,7 @@ - (void)handlePressBegin:(nonnull FlutterUIPressProxy*)press
// However this might happen in add-to-app scenarios if the focus is changed
// from the native view to the Flutter view amid the key tap.
[callback resolveTo:TRUE];
[self sendEmptyEvent];
return;
}
}
Expand Down Expand Up @@ -695,6 +718,7 @@ - (void)handlePressEnd:(nonnull FlutterUIPressProxy*)press
// However this might happen in add-to-app scenarios if the focus is changed
// from the native view to the Flutter view amid the key tap.
[callback resolveTo:TRUE];
[self sendEmptyEvent];
return;
}
[self updateKey:physicalKey asPressed:0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,16 @@ - (void)testIgnoreDuplicateDownEvent API_AVAILABLE(ios(13.4)) {
last_handled = handled;
}];

XCTAssertEqual([events count], 0u);
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->physical, 0ull);
XCTAssertEqual(event->logical, 0ull);
XCTAssertEqual(event->synthesized, false);
XCTAssertFalse([[events lastObject] hasCallback]);
XCTAssertEqual(last_handled, TRUE);

[events removeAllObjects];

last_handled = FALSE;
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
callback:^(BOOL handled) {
Expand All @@ -327,6 +334,7 @@ - (void)testIgnoreDuplicateDownEvent API_AVAILABLE(ios(13.4)) {
- (void)testIgnoreAbruptUpEvent API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
__block BOOL last_handled = TRUE;
FlutterKeyEvent* event;

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

XCTAssertEqual([events count], 0u);
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->physical, 0ull);
XCTAssertEqual(event->logical, 0ull);
XCTAssertEqual(event->synthesized, false);
XCTAssertFalse([[events lastObject] hasCallback]);
XCTAssertEqual(last_handled, TRUE);

[events removeAllObjects];
}

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

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

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

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

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

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

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

XCTAssertEqual([events count], 2u);

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

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

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

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

XCTAssertEqual([events count], 0u);
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->physical, 0ull);
XCTAssertEqual(event->logical, 0ull);
XCTAssertEqual(event->synthesized, false);
XCTAssertFalse([[events lastObject] hasCallback]);
XCTAssertEqual(last_handled, TRUE);

[events removeAllObjects];
}

- (void)testIdentifyLeftAndRightModifiers API_AVAILABLE(ios(13.4)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ - (void)notifyLowMemory {
- (void)sendKeyEvent:(const FlutterKeyEvent&)event
callback:(FlutterKeyEventCallback)callback
userData:(void*)userData API_AVAILABLE(ios(9.0)) {
NSAssert(callback != nullptr, @"Invalid callback");
if (callback == nil)
return;
// NSAssert(callback != nullptr, @"Invalid callback");
// Response is async, so we have to post it to the run loop instead of calling
// it directly.
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,15 @@ - (void)sendModifierEventOfType:(BOOL)isDownEvent
keyCode:(unsigned short)keyCode
callback:(nullable FlutterKeyCallbackGuard*)callback;

/**
* Send an empty key event.
*
* The event is never synthesized, and never expects an event result. An empty
* event is sent when no other events should be sent, such as upon back-to-back
* keydown events of the same key.
*/
- (void)sendEmptyEvent;

/**
* Processes a down event from the system.
*/
Expand Down Expand Up @@ -579,6 +588,7 @@ - (void)sendModifierEventOfType:(BOOL)isDownEvent
uint64_t logicalKey = GetLogicalKeyForModifier(keyCode, physicalKey);
if (physicalKey == 0 || logicalKey == 0) {
NSLog(@"Unrecognized modifier key: keyCode 0x%hx, physical key 0x%llx", keyCode, physicalKey);
[self sendEmptyEvent];
[callback resolveTo:TRUE];
return;
}
Expand All @@ -599,6 +609,19 @@ - (void)sendModifierEventOfType:(BOOL)isDownEvent
}
}

- (void)sendEmptyEvent {
FlutterKeyEvent flutterEvent = {
.struct_size = sizeof(FlutterKeyEvent),
.timestamp = 0,
.type = kFlutterKeyEventTypeDown,
.physical = 0,
.logical = 0,
.character = nil,
.synthesized = false,
};
_sendEvent(flutterEvent, nullptr, nullptr);
}

- (void)handleDownEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callback {
uint64_t physicalKey = GetPhysicalKeyForKeyCode(event.keyCode);
uint64_t logicalKey = GetLogicalKeyForEvent(event, physicalKey);
Expand All @@ -611,6 +634,7 @@ - (void)handleDownEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callb
// key up event to the window where the corresponding key down occurred.
// However this might happen in add-to-app scenarios if the focus is changed
// from the native view to the Flutter view amid the key tap.
[self sendEmptyEvent];
[callback resolveTo:TRUE];
return;
}
Expand Down Expand Up @@ -643,6 +667,7 @@ - (void)handleUpEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callbac
// key up event to the window where the corresponding key down occurred.
// However this might happen in add-to-app scenarios if the focus is changed
// from the native view to the Flutter view amid the key tap.
[self sendEmptyEvent];
[callback resolveTo:TRUE];
return;
}
Expand All @@ -669,6 +694,7 @@ - (void)handleCapsLockEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)c
[self sendCapsLockTapWithTimestamp:event.timestamp callback:callback];
_lastModifierFlagsOfInterest = _lastModifierFlagsOfInterest ^ NSEventModifierFlagCapsLock;
} else {
[self sendEmptyEvent];
[callback resolveTo:TRUE];
}
}
Expand Down
Loading