From efd9a7b2e7549eca43bc3b5249999d3e1acb3579 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 4 Oct 2021 14:01:51 -0700 Subject: [PATCH] Keyboard guarantee non empty events (#28648) * Web * macos * Linux * Easier web impl * doc and format * Better linux impl * Format * Better impl mac * Format * Windows * Format * Apply suggestions from code review Co-authored-by: Greg Spencer Co-authored-by: Greg Spencer --- .../lib/src/engine/keyboard_binding.dart | 60 ++++++--- .../Source/FlutterEmbedderKeyResponder.mm | 123 ++++++++++-------- .../FlutterEmbedderKeyResponderUnittests.mm | 12 +- .../linux/fl_key_embedder_responder.cc | 43 ++++-- .../linux/fl_key_embedder_responder_test.cc | 18 +-- .../windows/keyboard_key_embedder_handler.cc | 83 +++++++----- .../windows/keyboard_key_embedder_handler.h | 40 ++++-- 7 files changed, 236 insertions(+), 143 deletions(-) diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 471752dc0c4d7..480f790d53c1c 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -204,11 +204,18 @@ class FlutterHtmlKeyboardEvent { // [dispatchKeyData] as given in the constructor. Some key data might be // dispatched asynchronously. class KeyboardConverter { - KeyboardConverter(this.dispatchKeyData, {this.onMacOs = false}); + KeyboardConverter(this.performDispatchKeyData, {this.onMacOs = false}); - final DispatchKeyData dispatchKeyData; + 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; @@ -294,7 +301,9 @@ class KeyboardConverter { Future.delayed(duration).then((_) { if (!canceled && !_disposed) { callback(); - dispatchKeyData(getData()); + // This dispatch is performed asynchronously, therefore should not use + // `_dispatchKeyData`. + performDispatchKeyData(getData()); } }); return () { canceled = true; }; @@ -335,17 +344,7 @@ class KeyboardConverter { _keyGuards.remove(physicalKey)?.call(); } - // Parse the HTML event, update states, and dispatch Flutter key data through - // [dispatchKeyData]. - // - // * 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) { + void _handleEvent(FlutterHtmlKeyboardEvent event) { final Duration timeStamp = _eventTimeStampToDuration(event.timeStamp!); final String eventKey = event.key!; @@ -411,7 +410,6 @@ 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; } @@ -425,7 +423,6 @@ 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; } @@ -465,7 +462,7 @@ class KeyboardConverter { if (logicalRecord != logicalKey) return false; - dispatchKeyData(ui.KeyData( + _dispatchKeyData!(ui.KeyData( timeStamp: timeStamp, type: ui.KeyEventType.up, physical: physicalKey, @@ -497,9 +494,36 @@ class KeyboardConverter { synthesized: false, ); - final bool primaryHandled = dispatchKeyData(keyData); + 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; + } + } } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm index 4e80d4228fe71..bcab9b0ecf1c7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm @@ -274,6 +274,7 @@ - (void)pendTo:(nonnull NSMutableDictionary* - (void)resolveTo:(BOOL)handled; @property(nonatomic) BOOL handled; +@property(nonatomic) BOOL sentAnyEvents; /** * A string indicating how the callback is handled. * @@ -293,6 +294,7 @@ - (nonnull instancetype)initWithCallback:(FlutterAsyncKeyCallback)callback { if (self != nil) { _callback = callback; _handled = FALSE; + _sentAnyEvents = FALSE; } return self; } @@ -384,10 +386,14 @@ @interface FlutterEmbedderKeyResponder () * * The flags compared are all flags after masking with * |modifierFlagOfInterestMask| and excluding |ignoringFlags|. + * + * The |guard| is basically a regular guarded callback, but instead of being + * called, it is only used to record whether an event is sent. */ - (void)synchronizeModifiers:(NSUInteger)currentFlags ignoringFlags:(NSUInteger)ignoringFlags - timestamp:(NSTimeInterval)timestamp; + timestamp:(NSTimeInterval)timestamp + guard:(nonnull FlutterKeyCallbackGuard*)guard; /** * Update the pressing state. @@ -403,34 +409,34 @@ - (void)updateKey:(uint64_t)physicalKey asPressed:(uint64_t)logicalKey; - (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event callback:(nonnull FlutterKeyCallbackGuard*)callback; +/** + * Send a synthesized key event, never expecting its event result. + * + * The |guard| is basically a regular guarded callback, but instead of being + * called, it is only used to record whether an event is sent. + */ +- (void)sendSynthesizedFlutterEvent:(const FlutterKeyEvent&)event + guard:(FlutterKeyCallbackGuard*)guard; + /** * Send a CapsLock down event, then a CapsLock up event. * - * If downCallback is nil, then both events will be synthesized. Otherwise, the - * downCallback will be used as the callback for the down event, which is not - * synthesized. + * If synthesizeDown is TRUE, then both events will be synthesized. Otherwise, + * the callback will be used as the callback for the down event, which is not + * synthesized, while the up event will always be synthesized. */ - (void)sendCapsLockTapWithTimestamp:(NSTimeInterval)timestamp - callback:(nullable FlutterKeyCallbackGuard*)downCallback; + synthesizeDown:(bool)synthesizeDown + callback:(nonnull FlutterKeyCallbackGuard*)callback; /** * Send a key event for a modifier key. - * - * If callback is nil, then the event is synthesized. */ - (void)sendModifierEventOfType:(BOOL)isDownEvent timestamp:(NSTimeInterval)timestamp 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; + synthesized:(bool)synthesized + callback:(nonnull FlutterKeyCallbackGuard*)callback; /** * Processes a down event from the system. @@ -495,6 +501,18 @@ - (void)handleEvent:(NSEvent*)event callback:(FlutterAsyncKeyCallback)callback { NSAssert(false, @"Unexpected key event type: |%@|.", @(event.type)); } NSAssert(guardedCallback.handled, @"The callback is returned without being handled."); + if (!guardedCallback.sentAnyEvents) { + FlutterKeyEvent flutterEvent = { + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = 0, + .type = kFlutterKeyEventTypeDown, + .physical = 0, + .logical = 0, + .character = nil, + .synthesized = false, + }; + _sendEvent(flutterEvent, nullptr, nullptr); + } NSAssert(_lastModifierFlagsOfInterest == (event.modifierFlags & _modifierFlagOfInterestMask), @"The modifier flags are not properly updated: recorded 0x%lx, event with mask 0x%lx", _lastModifierFlagsOfInterest, event.modifierFlags & _modifierFlagOfInterestMask); @@ -504,13 +522,14 @@ - (void)handleEvent:(NSEvent*)event callback:(FlutterAsyncKeyCallback)callback { - (void)synchronizeModifiers:(NSUInteger)currentFlags ignoringFlags:(NSUInteger)ignoringFlags - timestamp:(NSTimeInterval)timestamp { + timestamp:(NSTimeInterval)timestamp + guard:(FlutterKeyCallbackGuard*)guard { const NSUInteger updatingMask = _modifierFlagOfInterestMask & ~ignoringFlags; const NSUInteger currentFlagsOfInterest = currentFlags & updatingMask; const NSUInteger lastFlagsOfInterest = _lastModifierFlagsOfInterest & updatingMask; NSUInteger flagDifference = currentFlagsOfInterest ^ lastFlagsOfInterest; if (flagDifference & NSEventModifierFlagCapsLock) { - [self sendCapsLockTapWithTimestamp:timestamp callback:nil]; + [self sendCapsLockTapWithTimestamp:timestamp synthesizeDown:true callback:guard]; flagDifference = flagDifference & ~NSEventModifierFlagCapsLock; } while (true) { @@ -528,7 +547,8 @@ - (void)synchronizeModifiers:(NSUInteger)currentFlags [self sendModifierEventOfType:isDownEvent timestamp:timestamp keyCode:[keyCode unsignedShortValue] - callback:nil]; + synthesized:true + callback:guard]; } _lastModifierFlagsOfInterest = (_lastModifierFlagsOfInterest & ~updatingMask) | currentFlagsOfInterest; @@ -551,10 +571,18 @@ - (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event [callback pendTo:_pendingResponses withId:responseId]; // The `__bridge_retained` here is matched by `__bridge_transfer` in HandleResponse. _sendEvent(event, HandleResponse, (__bridge_retained void*)pending); + callback.sentAnyEvents = TRUE; +} + +- (void)sendSynthesizedFlutterEvent:(const FlutterKeyEvent&)event + guard:(FlutterKeyCallbackGuard*)guard { + _sendEvent(event, nullptr, nullptr); + guard.sentAnyEvents = TRUE; } - (void)sendCapsLockTapWithTimestamp:(NSTimeInterval)timestamp - callback:(FlutterKeyCallbackGuard*)downCallback { + synthesizeDown:(bool)synthesizeDown + callback:(FlutterKeyCallbackGuard*)callback { // MacOS sends a down *or* an up when CapsLock is tapped, alternatively on // even taps and odd taps. A CapsLock down or CapsLock up should always be // converted to a down *and* an up, and the up should always be a synthesized @@ -567,28 +595,28 @@ - (void)sendCapsLockTapWithTimestamp:(NSTimeInterval)timestamp .physical = kCapsLockPhysicalKey, .logical = kCapsLockLogicalKey, .character = nil, - .synthesized = downCallback == nil, + .synthesized = synthesizeDown, }; - if (downCallback != nil) { - [self sendPrimaryFlutterEvent:flutterEvent callback:downCallback]; + if (!synthesizeDown) { + [self sendPrimaryFlutterEvent:flutterEvent callback:callback]; } else { - _sendEvent(flutterEvent, nullptr, nullptr); + [self sendSynthesizedFlutterEvent:flutterEvent guard:callback]; } flutterEvent.type = kFlutterKeyEventTypeUp; flutterEvent.synthesized = true; - _sendEvent(flutterEvent, nullptr, nullptr); + [self sendSynthesizedFlutterEvent:flutterEvent guard:callback]; } - (void)sendModifierEventOfType:(BOOL)isDownEvent timestamp:(NSTimeInterval)timestamp keyCode:(unsigned short)keyCode + synthesized:(bool)synthesized callback:(FlutterKeyCallbackGuard*)callback { uint64_t physicalKey = GetPhysicalKeyForKeyCode(keyCode); 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; } @@ -599,33 +627,23 @@ - (void)sendModifierEventOfType:(BOOL)isDownEvent .physical = physicalKey, .logical = logicalKey, .character = nil, - .synthesized = callback == nil, + .synthesized = synthesized, }; [self updateKey:physicalKey asPressed:isDownEvent ? logicalKey : 0]; - if (callback != nil) { + if (!synthesized) { [self sendPrimaryFlutterEvent:flutterEvent callback:callback]; } else { - _sendEvent(flutterEvent, nullptr, nullptr); + [self sendSynthesizedFlutterEvent:flutterEvent guard:callback]; } } -- (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); - [self synchronizeModifiers:event.modifierFlags ignoringFlags:0 timestamp:event.timestamp]; + [self synchronizeModifiers:event.modifierFlags + ignoringFlags:0 + timestamp:event.timestamp + guard:callback]; bool isARepeat = event.isARepeat; NSNumber* pressedLogicalKey = _pressingRecords[@(physicalKey)]; @@ -634,7 +652,6 @@ - (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; } @@ -658,7 +675,10 @@ - (void)handleDownEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callb - (void)handleUpEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callback { NSAssert(!event.isARepeat, @"Unexpected repeated Up event: keyCode %d, char %@, charIM %@", event.keyCode, event.characters, event.charactersIgnoringModifiers); - [self synchronizeModifiers:event.modifierFlags ignoringFlags:0 timestamp:event.timestamp]; + [self synchronizeModifiers:event.modifierFlags + ignoringFlags:0 + timestamp:event.timestamp + guard:callback]; uint64_t physicalKey = GetPhysicalKeyForKeyCode(event.keyCode); NSNumber* pressedLogicalKey = _pressingRecords[@(physicalKey)]; @@ -667,7 +687,6 @@ - (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; } @@ -688,13 +707,13 @@ - (void)handleUpEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callbac - (void)handleCapsLockEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callback { [self synchronizeModifiers:event.modifierFlags ignoringFlags:NSEventModifierFlagCapsLock - timestamp:event.timestamp]; + timestamp:event.timestamp + guard:callback]; if ((_lastModifierFlagsOfInterest & NSEventModifierFlagCapsLock) != (event.modifierFlags & NSEventModifierFlagCapsLock)) { - [self sendCapsLockTapWithTimestamp:event.timestamp callback:callback]; + [self sendCapsLockTapWithTimestamp:event.timestamp synthesizeDown:false callback:callback]; _lastModifierFlagsOfInterest = _lastModifierFlagsOfInterest ^ NSEventModifierFlagCapsLock; } else { - [self sendEmptyEvent]; [callback resolveTo:TRUE]; } } @@ -710,7 +729,8 @@ - (void)handleFlagEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callb [self synchronizeModifiers:event.modifierFlags ignoringFlags:targetModifierFlag - timestamp:event.timestamp]; + timestamp:event.timestamp + guard:callback]; NSNumber* pressedLogicalKey = [_pressingRecords objectForKey:@(targetKey)]; BOOL lastTargetPressed = pressedLogicalKey != nil; @@ -732,6 +752,7 @@ - (void)handleFlagEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callb [self sendModifierEventOfType:shouldBePressed timestamp:event.timestamp keyCode:event.keyCode + synthesized:false callback:callback]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm index 38c7ccdd2a9f2..4a5cde65583b8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm @@ -750,8 +750,12 @@ - (void)dealloc { handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, kKeyCodeShiftLeft) callback:keyEventCallback]; - EXPECT_EQ([events count], 0u); + EXPECT_EQ([events count], 1u); + EXPECT_EQ([events lastObject].data->physical, 0u); + EXPECT_EQ([events lastObject].data->logical, 0u); + EXPECT_FALSE([[events lastObject] hasCallback]); EXPECT_EQ(last_handled, TRUE); + [events removeAllObjects]; last_handled = FALSE; [responder @@ -782,8 +786,12 @@ - (void)dealloc { handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeShiftLeft) callback:keyEventCallback]; - EXPECT_EQ([events count], 0u); + EXPECT_EQ([events count], 1u); + EXPECT_EQ([events lastObject].data->physical, 0u); + EXPECT_EQ([events lastObject].data->logical, 0u); + EXPECT_FALSE([[events lastObject] hasCallback]); EXPECT_EQ(last_handled, TRUE); + [events removeAllObjects]; // Case 3: // In: L down, (L up), (R down), R up diff --git a/shell/platform/linux/fl_key_embedder_responder.cc b/shell/platform/linux/fl_key_embedder_responder.cc index 56c27b6c72d7b..982273ffb746e 100644 --- a/shell/platform/linux/fl_key_embedder_responder.cc +++ b/shell/platform/linux/fl_key_embedder_responder.cc @@ -154,6 +154,10 @@ struct _FlKeyEmbedderResponder { // For more information, see #update_caps_lock_state_logic_inferrence. StateLogicInferrence caps_lock_state_logic_inferrence; + // Record if any events has been sent through the engine during a + // |fl_key_embedder_responder_handle_event| call. + bool sent_any_events; + // A static map from GTK modifier bits to #FlKeyEmbedderCheckedKey to // configure the modifier keys that needs to be tracked and kept synchronous // on. @@ -344,6 +348,7 @@ static void synthesize_simple_event(FlKeyEmbedderResponder* self, out_event.character = nullptr; out_event.synthesized = true; if (self->engine != nullptr) { + self->sent_any_events = true; fl_engine_send_key_event(self->engine, &out_event, nullptr, nullptr); } } @@ -670,8 +675,7 @@ static void synchronize_lock_states_loop_body(gpointer key, } } -// Sends a key event to the framework. -static void fl_key_embedder_responder_handle_event( +static void fl_key_embedder_responder_handle_event_impl( FlKeyResponder* responder, FlKeyEvent* event, FlKeyResponderAsyncCallback callback, @@ -686,22 +690,21 @@ static void fl_key_embedder_responder_handle_event( const double timestamp = event_to_timestamp(event); const bool is_down_event = event->is_press; - SyncStateLoopContext sync_pressed_state_context; - sync_pressed_state_context.self = self; - sync_pressed_state_context.state = event->state; - sync_pressed_state_context.timestamp = timestamp; - sync_pressed_state_context.is_down = is_down_event; - sync_pressed_state_context.event_logical_key = logical_key; + SyncStateLoopContext sync_state_context; + sync_state_context.self = self; + sync_state_context.state = event->state; + sync_state_context.timestamp = timestamp; + sync_state_context.is_down = is_down_event; + sync_state_context.event_logical_key = logical_key; // Update lock mode states g_hash_table_foreach(self->lock_bit_to_checked_keys, - synchronize_lock_states_loop_body, - &sync_pressed_state_context); + synchronize_lock_states_loop_body, &sync_state_context); // Update pressing states g_hash_table_foreach(self->modifier_bit_to_checked_keys, synchronize_pressed_states_loop_body, - &sync_pressed_state_context); + &sync_state_context); // Construct the real event const uint64_t last_logical_record = @@ -722,7 +725,6 @@ static void fl_key_embedder_responder_handle_event( // 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. - fl_engine_send_key_event(self->engine, &empty_event, nullptr, nullptr); callback(true, user_data); return; } else { @@ -735,7 +737,6 @@ static void fl_key_embedder_responder_handle_event( // The physical key has been released before. It might indicate a missed // event due to loss of focus, or multiple keyboards pressed keys with the // same physical key. Ignore the up event. - fl_engine_send_key_event(self->engine, &empty_event, nullptr, nullptr); callback(true, user_data); return; } else { @@ -751,9 +752,25 @@ static void fl_key_embedder_responder_handle_event( if (self->engine != nullptr) { FlKeyEmbedderUserData* response_data = fl_key_embedder_user_data_new(callback, user_data); + self->sent_any_events = true; fl_engine_send_key_event(self->engine, &out_event, handle_response, response_data); } else { callback(true, user_data); } } + +// Sends a key event to the framework. +static void fl_key_embedder_responder_handle_event( + FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data) { + FlKeyEmbedderResponder* self = FL_KEY_EMBEDDER_RESPONDER(responder); + self->sent_any_events = false; + fl_key_embedder_responder_handle_event_impl(responder, event, callback, + user_data); + if (!self->sent_any_events) { + fl_engine_send_key_event(self->engine, &empty_event, nullptr, nullptr); + } +} diff --git a/shell/platform/linux/fl_key_embedder_responder_test.cc b/shell/platform/linux/fl_key_embedder_responder_test.cc index 71743d529f55b..48d2681f0dac3 100644 --- a/shell/platform/linux/fl_key_embedder_responder_test.cc +++ b/shell/platform/linux/fl_key_embedder_responder_test.cc @@ -1061,7 +1061,7 @@ TEST(FlKeyEmbedderResponderTest, SynthesizeForDesyncPressingStateOnSelfEvents) { // Reason: The ControlLeft down is synthesized to synchronize the state // showing Control as pressed. The ControlRight event is ignored because // the event is considered a duplicate up event. - EXPECT_EQ(g_call_records->len, 2u); + EXPECT_EQ(g_call_records->len, 1u); record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); EXPECT_EQ(record->event->timestamp, 105000); EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); @@ -1070,13 +1070,6 @@ TEST(FlKeyEmbedderResponderTest, SynthesizeForDesyncPressingStateOnSelfEvents) { EXPECT_STREQ(record->event->character, nullptr); EXPECT_EQ(record->event->synthesized, true); - record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); - EXPECT_EQ(record->event->physical, 0ull); - EXPECT_EQ(record->event->logical, 0ull); - EXPECT_STREQ(record->event->character, nullptr); - EXPECT_EQ(record->event->synthesized, false); - EXPECT_EQ(record->callback, nullptr); - g_ptr_array_clear(g_call_records); clear_g_call_records(); @@ -1522,7 +1515,7 @@ TEST(FlKeyEmbedderResponderTest, SynthesizationOccursOnIgnoredEvents) { kIsNotModifier), verify_response_handled, &user_data); - EXPECT_EQ(g_call_records->len, 3u); + EXPECT_EQ(g_call_records->len, 2u); record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); EXPECT_EQ(record->event->timestamp, 101000); EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); @@ -1539,13 +1532,6 @@ TEST(FlKeyEmbedderResponderTest, SynthesizationOccursOnIgnoredEvents) { EXPECT_STREQ(record->event->character, nullptr); EXPECT_EQ(record->event->synthesized, true); - record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 2)); - EXPECT_EQ(record->event->physical, 0ull); - EXPECT_EQ(record->event->logical, 0ull); - EXPECT_STREQ(record->event->character, nullptr); - EXPECT_EQ(record->event->synthesized, false); - EXPECT_EQ(record->callback, nullptr); - g_ptr_array_clear(g_call_records); clear_g_call_records(); diff --git a/shell/platform/windows/keyboard_key_embedder_handler.cc b/shell/platform/windows/keyboard_key_embedder_handler.cc index 4ac09b0eac5cc..b30a17c279b19 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.cc +++ b/shell/platform/windows/keyboard_key_embedder_handler.cc @@ -70,9 +70,11 @@ std::string ConvertChar32ToUtf8(char32_t ch) { } KeyboardKeyEmbedderHandler::KeyboardKeyEmbedderHandler( - SendEvent send_event, + SendEventHandler send_event, GetKeyStateHandler get_key_state) - : sendEvent_(send_event), get_key_state_(get_key_state), response_id_(1) { + : perform_send_event_(send_event), + get_key_state_(get_key_state), + response_id_(1) { InitCriticalKeys(); } @@ -155,7 +157,7 @@ uint64_t KeyboardKeyEmbedderHandler::GetLogicalKey(int key, return ApplyPlaneToId(toLower(key), windowsPlane); } -void KeyboardKeyEmbedderHandler::KeyboardHook( +void KeyboardKeyEmbedderHandler::KeyboardHookImpl( int key, int scancode, int action, @@ -198,7 +200,6 @@ void KeyboardKeyEmbedderHandler::KeyboardHook( // as 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. - sendEvent_(CreateEmptyEvent(), nullptr, nullptr); callback(true); return; } @@ -215,7 +216,6 @@ void KeyboardKeyEmbedderHandler::KeyboardHook( // The physical key has been released before. It might indicate a missed // event due to loss of focus, or multiple keyboards pressed keys with the // same physical key. Ignore the up event. - sendEvent_(CreateEmptyEvent(), nullptr, nullptr); callback(true); return; } else { @@ -245,7 +245,6 @@ void KeyboardKeyEmbedderHandler::KeyboardHook( // presses are considered handled and not sent to Flutter. These events must // be filtered by result_logical_key because the key up event of such // presses uses the "original" logical key. - sendEvent_(CreateEmptyEvent(), nullptr, nullptr); callback(true); return; } @@ -279,8 +278,36 @@ void KeyboardKeyEmbedderHandler::KeyboardHook( }; auto pending_ptr = std::make_unique(std::move(pending)); pending_responses_[response_id] = std::move(pending_ptr); - sendEvent_(key_data, KeyboardKeyEmbedderHandler::HandleResponse, - reinterpret_cast(pending_responses_[response_id].get())); + SendEvent(key_data, KeyboardKeyEmbedderHandler::HandleResponse, + reinterpret_cast(pending_responses_[response_id].get())); +} + +void KeyboardKeyEmbedderHandler::KeyboardHook( + int key, + int scancode, + int action, + char32_t character, + bool extended, + bool was_down, + std::function callback) { + sent_any_events = false; + KeyboardHookImpl(key, scancode, action, character, extended, was_down, + std::move(callback)); + if (!sent_any_events) { + FlutterKeyEvent empty_event{ + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = static_cast( + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()) + .count()), + .type = kFlutterKeyEventTypeDown, + .physical = 0, + .logical = 0, + .character = empty_character, + .synthesized = false, + }; + SendEvent(empty_event, nullptr, nullptr); + } } void KeyboardKeyEmbedderHandler::UpdateLastSeenCritialKey( @@ -321,18 +348,18 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates( // If the key is pressed, release it first. if (pressingRecords_.find(key_info.physical_key) != pressingRecords_.end()) { - sendEvent_(SynthesizeSimpleEvent( - kFlutterKeyEventTypeUp, key_info.physical_key, - key_info.logical_key, empty_character), - nullptr, nullptr); + SendEvent(SynthesizeSimpleEvent( + kFlutterKeyEventTypeUp, key_info.physical_key, + key_info.logical_key, empty_character), + nullptr, nullptr); } else { // This key will always be pressed in the following synthesized event. pressingRecords_[key_info.physical_key] = key_info.logical_key; } - sendEvent_(SynthesizeSimpleEvent(kFlutterKeyEventTypeDown, - key_info.physical_key, - key_info.logical_key, empty_character), - nullptr, nullptr); + SendEvent(SynthesizeSimpleEvent(kFlutterKeyEventTypeDown, + key_info.physical_key, + key_info.logical_key, empty_character), + nullptr, nullptr); } key_info.toggled_on = should_toggled; } @@ -366,7 +393,7 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates() { pressingRecords_.erase(recorded_pressed_iter); } const char* empty_character = ""; - sendEvent_( + SendEvent( SynthesizeSimpleEvent(should_pressed ? kFlutterKeyEventTypeDown : kFlutterKeyEventTypeUp, key_info.physical_key, key_info.logical_key, @@ -434,21 +461,6 @@ void KeyboardKeyEmbedderHandler::ConvertUtf32ToUtf8_(char* out, char32_t ch) { strcpy_s(out, kCharacterCacheSize, result.c_str()); } -FlutterKeyEvent KeyboardKeyEmbedderHandler::CreateEmptyEvent() { - return FlutterKeyEvent{ - .struct_size = sizeof(FlutterKeyEvent), - .timestamp = static_cast( - std::chrono::duration_cast( - std::chrono::high_resolution_clock::now().time_since_epoch()) - .count()), - .type = kFlutterKeyEventTypeDown, - .physical = 0, - .logical = 0, - .character = empty_character, - .synthesized = false, - }; -} - FlutterKeyEvent KeyboardKeyEmbedderHandler::SynthesizeSimpleEvent( FlutterKeyEventType type, uint64_t physical, @@ -468,4 +480,11 @@ FlutterKeyEvent KeyboardKeyEmbedderHandler::SynthesizeSimpleEvent( }; } +void KeyboardKeyEmbedderHandler::SendEvent(const FlutterKeyEvent& event, + FlutterKeyEventCallback callback, + void* user_data) { + sent_any_events = true; + perform_send_event_(event, callback, user_data); +} + } // namespace flutter diff --git a/shell/platform/windows/keyboard_key_embedder_handler.h b/shell/platform/windows/keyboard_key_embedder_handler.h index 7ee88a33e626d..d7fc69ede952c 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.h +++ b/shell/platform/windows/keyboard_key_embedder_handler.h @@ -15,6 +15,9 @@ namespace flutter { +// Encode a 32-bit unicode code point into a UTF-8 byte array. +// +// See https://en.wikipedia.org/wiki/UTF-8#Encoding for the algorithm. std::string ConvertChar32ToUtf8(char32_t ch); // A delegate of |KeyboardKeyHandler| that handles events by sending @@ -30,9 +33,10 @@ std::string ConvertChar32ToUtf8(char32_t ch); class KeyboardKeyEmbedderHandler : public KeyboardKeyHandler::KeyboardKeyHandlerDelegate { public: - using SendEvent = std::function; + using SendEventHandler = + std::function; using GetKeyStateHandler = std::function; // Build a KeyboardKeyEmbedderHandler. @@ -40,12 +44,12 @@ class KeyboardKeyEmbedderHandler // Use `send_event` to define how the class should dispatch converted // flutter events, as well as how to receive the response, to the engine. It's // typically FlutterWindowsEngine::SendKeyEvent. The 2nd and 3rd parameter - // of the SendEvent call might be nullptr. + // of the SendEventHandler call might be nullptr. // // Use `get_key_state` to define how the class should get a reliable result of // the state for a virtual key. It's typically Win32's GetKeyState, but can // also be nullptr (for UWP). - explicit KeyboardKeyEmbedderHandler(SendEvent send_event, + explicit KeyboardKeyEmbedderHandler(SendEventHandler send_event, GetKeyStateHandler get_key_state); virtual ~KeyboardKeyEmbedderHandler(); @@ -85,6 +89,16 @@ class KeyboardKeyEmbedderHandler bool toggled_on; }; + // Implements the core logic of |KeyboardHook|, leaving out some state + // guards. + void KeyboardHookImpl(int key, + int scancode, + int action, + char32_t character, + bool extended, + bool was_down, + std::function callback); + // Assign |critical_keys_| with basic information. void InitCriticalKeys(); // Update |critical_keys_| with last seen logical and physical key. @@ -98,8 +112,14 @@ class KeyboardKeyEmbedderHandler // if their pressing states have been desynchronized. void SynchronizeCritialPressedStates(); + // Wraps perform_send_event_ with state tracking. Use this instead of + // |perform_send_event_| to send events to the framework. + void SendEvent(const FlutterKeyEvent& event, + FlutterKeyEventCallback callback, + void* user_data); + std::function - sendEvent_; + perform_send_event_; GetKeyStateHandler get_key_state_; // A map from physical keys to logical keys, each entry indicating a pressed @@ -111,6 +131,9 @@ class KeyboardKeyEmbedderHandler // A self-incrementing integer, used as the ID for the next entry for // |pending_responses_|. uint64_t response_id_; + // Whether any events has been sent with |PerformSendEvent| during a + // |KeyboardHook|. + bool sent_any_events; // Important keys whose states are checked and guaranteed synchronized // on every key event. @@ -123,11 +146,6 @@ class KeyboardKeyEmbedderHandler static uint64_t GetLogicalKey(int key, bool extended, int scancode); static void HandleResponse(bool handled, void* user_data); static void ConvertUtf32ToUtf8_(char* out, char32_t ch); - // Create an empty event. - // - // This is used when no key data needs to be sent. For the reason, see the - // |KeyboardKeyEmbedderHandler| class. - static FlutterKeyEvent CreateEmptyEvent(); static FlutterKeyEvent SynthesizeSimpleEvent(FlutterKeyEventType type, uint64_t physical, uint64_t logical,