From 4e04e3ff423473af1087364ba8e31e110a978dc5 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Thu, 20 Jan 2022 15:55:18 -0800 Subject: [PATCH] [Keyboard] Dispatch solitary synthesized `KeyEvent`s (#96874) --- .../lib/src/services/hardware_keyboard.dart | 90 ++++++++++++------- .../lib/src/services/raw_keyboard.dart | 6 +- .../lib/src/widgets/focus_manager.dart | 4 +- .../test/services/hardware_keyboard_test.dart | 83 +++++++++++++++++ 4 files changed, 148 insertions(+), 35 deletions(-) diff --git a/packages/flutter/lib/src/services/hardware_keyboard.dart b/packages/flutter/lib/src/services/hardware_keyboard.dart index 82efc5a24e7..b8bbb88f7af 100644 --- a/packages/flutter/lib/src/services/hardware_keyboard.dart +++ b/packages/flutter/lib/src/services/hardware_keyboard.dart @@ -633,13 +633,15 @@ enum KeyDataTransitMode { /// platform, every native message might result in multiple [KeyEvent]s. For /// example, this might happen in order to synthesize missed modifier key /// presses or releases. +/// /// A [KeyMessage] bundles all information related to a native key message /// together for the convenience of propagation on the [FocusNode] tree. /// /// When dispatched to handlers or listeners, or propagated through the /// [FocusNode] tree, all handlers or listeners belonging to a node are /// executed regardless of their [KeyEventResult], and all results are combined -/// into the result of the node using [combineKeyEventResults]. +/// into the result of the node using [combineKeyEventResults]. Empty [events] +/// or [rawEvent] should be considered as a result of [KeyEventResult.ignored]. /// /// In very rare cases, a native key message might not result in a [KeyMessage]. /// For example, key messages for Fn key are ignored on macOS for the @@ -671,13 +673,16 @@ class KeyMessage { /// form as [RawKeyEvent]. Their stream is not as regular as [KeyEvent]'s, /// but keeps as much native information and structure as possible. /// - /// The [rawEvent] will be deprecated in the future. + /// The [rawEvent] field might be empty, for example, when the event + /// converting system dispatches solitary synthesized events. + /// + /// The [rawEvent] field will be deprecated in the future. /// /// See also: /// /// * [RawKeyboard.addListener], [RawKeyboardListener], [Focus.onKey], /// where [RawKeyEvent]s are commonly used. - final RawKeyEvent rawEvent; + final RawKeyEvent? rawEvent; @override String toString() { @@ -787,19 +792,60 @@ class KeyEventManager { assert(false, 'Should never encounter KeyData when transitMode is rawKeyData.'); return false; case KeyDataTransitMode.keyDataThenRawKeyData: - assert((data.physical == 0 && data.logical == 0) || - (data.physical != 0 && data.logical != 0)); - // Postpone key event dispatching until the handleRawKeyMessage. - // - // Having 0 as the physical or logical ID indicates an empty key data, - // transmitted to ensure that the transit mode is correctly inferred. - if (data.physical != 0 && data.logical != 0) { - _keyEventsSinceLastMessage.add(_eventFromData(data)); + // Having 0 as the physical and logical ID indicates an empty key data + // (the only occassion either field can be 0,) transmitted to ensure + // that the transit mode is correctly inferred. These events should be + // ignored. + if (data.physical == 0 && data.logical == 0) { + return false; + } + assert(data.physical != 0 && data.logical != 0); + final KeyEvent event = _eventFromData(data); + if (data.synthesized && _keyEventsSinceLastMessage.isEmpty) { + // Dispatch the event instantly if both conditions are met: + // + // - The event is synthesized, therefore the result does not matter. + // - The current queue is empty, therefore the order does not matter. + // + // This allows solitary synthesized `KeyEvent`s to be dispatched, + // since they won't be followed by `RawKeyEvent`s. + _hardwareKeyboard.handleKeyEvent(event); + _dispatchKeyMessage([event], null); + } else { + // Otherwise, postpone key event dispatching until the next raw + // event. Normal key presses always send 0 or more `KeyEvent`s first, + // then 1 `RawKeyEvent`. + _keyEventsSinceLastMessage.add(event); } return false; } } + bool _dispatchKeyMessage(List keyEvents, RawKeyEvent? rawEvent) { + if (keyMessageHandler != null) { + final KeyMessage message = KeyMessage(keyEvents, rawEvent); + try { + return keyMessageHandler!(message); + } catch (exception, stack) { + InformationCollector? collector; + assert(() { + collector = () => [ + DiagnosticsProperty('KeyMessage', message), + ]; + return true; + }()); + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'services library', + context: ErrorDescription('while processing the key message handler'), + informationCollector: collector, + )); + } + } + return false; + } + /// Handles a raw key message. /// /// This method is the handler to [SystemChannels.keyEvent], processing @@ -826,27 +872,7 @@ class KeyEventManager { 'while HardwareKeyboard reported ${_hardwareKeyboard.physicalKeysPressed}'); } - if (keyMessageHandler != null) { - final KeyMessage message = KeyMessage(_keyEventsSinceLastMessage, rawEvent); - try { - handled = keyMessageHandler!(message) || handled; - } catch (exception, stack) { - InformationCollector? collector; - assert(() { - collector = () => [ - DiagnosticsProperty('KeyMessage', message), - ]; - return true; - }()); - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'services library', - context: ErrorDescription('while processing the key message handler'), - informationCollector: collector, - )); - } - } + handled = _dispatchKeyMessage(_keyEventsSinceLastMessage, rawEvent) || handled; _keyEventsSinceLastMessage.clear(); return { 'handled': handled }; diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart index 490d970eeb7..c82973a7dc1 100644 --- a/packages/flutter/lib/src/services/raw_keyboard.dart +++ b/packages/flutter/lib/src/services/raw_keyboard.dart @@ -650,7 +650,11 @@ class RawKeyboard { _cachedKeyEventHandler = handler; _cachedKeyMessageHandler = handler == null ? null : - (KeyMessage message) => handler(message.rawEvent); + (KeyMessage message) { + if (message.rawEvent != null) + return handler(message.rawEvent!); + return false; + }; ServicesBinding.instance!.keyEventManager.keyMessageHandler = _cachedKeyMessageHandler; } diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index cd1c2e318b8..11111df78a3 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -1681,8 +1681,8 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { results.add(node.onKeyEvent!(node, event)); } } - if (node.onKey != null) { - results.add(node.onKey!(node, message.rawEvent)); + if (node.onKey != null && message.rawEvent != null) { + results.add(node.onKey!(node, message.rawEvent!)); } final KeyEventResult result = combineKeyEventResults(results); switch (result) { diff --git a/packages/flutter/test/services/hardware_keyboard_test.dart b/packages/flutter/test/services/hardware_keyboard_test.dart index 806ff17da63..e1c809b1a56 100644 --- a/packages/flutter/test/services/hardware_keyboard_test.dart +++ b/packages/flutter/test/services/hardware_keyboard_test.dart @@ -195,6 +195,89 @@ void main() { logs.clear(); }, variant: KeySimulatorTransitModeVariant.all()); + testWidgets('Instantly dispatch synthesized key events when the queue is empty', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + final List logs = []; + + await tester.pumpWidget( + KeyboardListener( + autofocus: true, + focusNode: focusNode, + child: Container(), + onKeyEvent: (KeyEvent event) { + logs.add(1); + }, + ), + ); + ServicesBinding.instance!.keyboard.addHandler((KeyEvent event) { + logs.add(2); + return false; + }); + + // Dispatch a solitary synthesized event. + expect(ServicesBinding.instance!.keyEventManager.handleKeyData(ui.KeyData( + timeStamp: Duration.zero, + type: ui.KeyEventType.down, + logical: LogicalKeyboardKey.keyA.keyId, + physical: PhysicalKeyboardKey.keyA.usbHidUsage, + character: null, + synthesized: true, + )), false); + expect(logs, [2, 1]); + logs.clear(); + }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData()); + + testWidgets('Postpone synthesized key events when the queue is not empty', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + final List logs = []; + + await tester.pumpWidget( + RawKeyboardListener( + focusNode: FocusNode(), + onKey: (RawKeyEvent event) { + logs.add('${event.runtimeType}'); + }, + child: KeyboardListener( + autofocus: true, + focusNode: focusNode, + child: Container(), + onKeyEvent: (KeyEvent event) { + logs.add('${event.runtimeType}'); + }, + ), + ), + ); + + // On macOS, a CapsLock tap yields a down event and a synthesized up event. + expect(ServicesBinding.instance!.keyEventManager.handleKeyData(ui.KeyData( + timeStamp: Duration.zero, + type: ui.KeyEventType.down, + logical: LogicalKeyboardKey.capsLock.keyId, + physical: PhysicalKeyboardKey.capsLock.usbHidUsage, + character: null, + synthesized: false, + )), false); + expect(ServicesBinding.instance!.keyEventManager.handleKeyData(ui.KeyData( + timeStamp: Duration.zero, + type: ui.KeyEventType.up, + logical: LogicalKeyboardKey.capsLock.keyId, + physical: PhysicalKeyboardKey.capsLock.usbHidUsage, + character: null, + synthesized: true, + )), false); + expect(await ServicesBinding.instance!.keyEventManager.handleRawKeyMessage({ + 'type': 'keydown', + 'keymap': 'macos', + 'keyCode': 0x00000039, + 'characters': '', + 'charactersIgnoringModifiers': '', + 'modifiers': 0x10000, + }), equals({'handled': false})); + + expect(logs, ['RawKeyDownEvent', 'KeyDownEvent', 'KeyUpEvent']); + logs.clear(); + }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData()); + // The first key data received from the engine might be an empty key data. // In that case, the key data should not be converted to any [KeyEvent]s, // but is only used so that *a* key data comes before the raw key message