Skip to content

Commit

Permalink
[Keyboard] Send empty key events when no key data should (#27774)
Browse files Browse the repository at this point in the history
The keyboard system on each platform now sends an empty key data instead of nothing if no key data should be sent.
  • Loading branch information
dkwingsmt authored Jul 30, 2021
1 parent 9e0f3ff commit 0112d4b
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 44 deletions.
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

0 comments on commit 0112d4b

Please sign in to comment.