Unverified Commit 4e04e3ff authored by Tong Mu's avatar Tong Mu Committed by GitHub

[Keyboard] Dispatch solitary synthesized `KeyEvent`s (#96874)

parent 2cdef81e
...@@ -633,13 +633,15 @@ enum KeyDataTransitMode { ...@@ -633,13 +633,15 @@ enum KeyDataTransitMode {
/// platform, every native message might result in multiple [KeyEvent]s. For /// platform, every native message might result in multiple [KeyEvent]s. For
/// example, this might happen in order to synthesize missed modifier key /// example, this might happen in order to synthesize missed modifier key
/// presses or releases. /// presses or releases.
///
/// A [KeyMessage] bundles all information related to a native key message /// A [KeyMessage] bundles all information related to a native key message
/// together for the convenience of propagation on the [FocusNode] tree. /// together for the convenience of propagation on the [FocusNode] tree.
/// ///
/// When dispatched to handlers or listeners, or propagated through the /// When dispatched to handlers or listeners, or propagated through the
/// [FocusNode] tree, all handlers or listeners belonging to a node are /// [FocusNode] tree, all handlers or listeners belonging to a node are
/// executed regardless of their [KeyEventResult], and all results are combined /// 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]. /// 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 /// For example, key messages for Fn key are ignored on macOS for the
...@@ -671,13 +673,16 @@ class KeyMessage { ...@@ -671,13 +673,16 @@ class KeyMessage {
/// form as [RawKeyEvent]. Their stream is not as regular as [KeyEvent]'s, /// form as [RawKeyEvent]. Their stream is not as regular as [KeyEvent]'s,
/// but keeps as much native information and structure as possible. /// 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: /// See also:
/// ///
/// * [RawKeyboard.addListener], [RawKeyboardListener], [Focus.onKey], /// * [RawKeyboard.addListener], [RawKeyboardListener], [Focus.onKey],
/// where [RawKeyEvent]s are commonly used. /// where [RawKeyEvent]s are commonly used.
final RawKeyEvent rawEvent; final RawKeyEvent? rawEvent;
@override @override
String toString() { String toString() {
...@@ -787,19 +792,60 @@ class KeyEventManager { ...@@ -787,19 +792,60 @@ class KeyEventManager {
assert(false, 'Should never encounter KeyData when transitMode is rawKeyData.'); assert(false, 'Should never encounter KeyData when transitMode is rawKeyData.');
return false; return false;
case KeyDataTransitMode.keyDataThenRawKeyData: case KeyDataTransitMode.keyDataThenRawKeyData:
assert((data.physical == 0 && data.logical == 0) || // Having 0 as the physical and logical ID indicates an empty key data
(data.physical != 0 && data.logical != 0)); // (the only occassion either field can be 0,) transmitted to ensure
// Postpone key event dispatching until the handleRawKeyMessage. // 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.
// //
// Having 0 as the physical or logical ID indicates an empty key data, // This allows solitary synthesized `KeyEvent`s to be dispatched,
// transmitted to ensure that the transit mode is correctly inferred. // since they won't be followed by `RawKeyEvent`s.
if (data.physical != 0 && data.logical != 0) { _hardwareKeyboard.handleKeyEvent(event);
_keyEventsSinceLastMessage.add(_eventFromData(data)); _dispatchKeyMessage(<KeyEvent>[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; return false;
} }
} }
bool _dispatchKeyMessage(List<KeyEvent> keyEvents, RawKeyEvent? rawEvent) {
if (keyMessageHandler != null) {
final KeyMessage message = KeyMessage(keyEvents, rawEvent);
try {
return keyMessageHandler!(message);
} catch (exception, stack) {
InformationCollector? collector;
assert(() {
collector = () => <DiagnosticsNode>[
DiagnosticsProperty<KeyMessage>('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. /// Handles a raw key message.
/// ///
/// This method is the handler to [SystemChannels.keyEvent], processing /// This method is the handler to [SystemChannels.keyEvent], processing
...@@ -826,27 +872,7 @@ class KeyEventManager { ...@@ -826,27 +872,7 @@ class KeyEventManager {
'while HardwareKeyboard reported ${_hardwareKeyboard.physicalKeysPressed}'); 'while HardwareKeyboard reported ${_hardwareKeyboard.physicalKeysPressed}');
} }
if (keyMessageHandler != null) { handled = _dispatchKeyMessage(_keyEventsSinceLastMessage, rawEvent) || handled;
final KeyMessage message = KeyMessage(_keyEventsSinceLastMessage, rawEvent);
try {
handled = keyMessageHandler!(message) || handled;
} catch (exception, stack) {
InformationCollector? collector;
assert(() {
collector = () => <DiagnosticsNode>[
DiagnosticsProperty<KeyMessage>('KeyMessage', message),
];
return true;
}());
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('while processing the key message handler'),
informationCollector: collector,
));
}
}
_keyEventsSinceLastMessage.clear(); _keyEventsSinceLastMessage.clear();
return <String, dynamic>{ 'handled': handled }; return <String, dynamic>{ 'handled': handled };
......
...@@ -650,7 +650,11 @@ class RawKeyboard { ...@@ -650,7 +650,11 @@ class RawKeyboard {
_cachedKeyEventHandler = handler; _cachedKeyEventHandler = handler;
_cachedKeyMessageHandler = handler == null ? _cachedKeyMessageHandler = handler == null ?
null : null :
(KeyMessage message) => handler(message.rawEvent); (KeyMessage message) {
if (message.rawEvent != null)
return handler(message.rawEvent!);
return false;
};
ServicesBinding.instance!.keyEventManager.keyMessageHandler = _cachedKeyMessageHandler; ServicesBinding.instance!.keyEventManager.keyMessageHandler = _cachedKeyMessageHandler;
} }
......
...@@ -1681,8 +1681,8 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -1681,8 +1681,8 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
results.add(node.onKeyEvent!(node, event)); results.add(node.onKeyEvent!(node, event));
} }
} }
if (node.onKey != null) { if (node.onKey != null && message.rawEvent != null) {
results.add(node.onKey!(node, message.rawEvent)); results.add(node.onKey!(node, message.rawEvent!));
} }
final KeyEventResult result = combineKeyEventResults(results); final KeyEventResult result = combineKeyEventResults(results);
switch (result) { switch (result) {
......
...@@ -195,6 +195,89 @@ void main() { ...@@ -195,6 +195,89 @@ void main() {
logs.clear(); logs.clear();
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Instantly dispatch synthesized key events when the queue is empty', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final List<int> logs = <int>[];
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, <int>[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<String> logs = <String>[];
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(<String, dynamic>{
'type': 'keydown',
'keymap': 'macos',
'keyCode': 0x00000039,
'characters': '',
'charactersIgnoringModifiers': '',
'modifiers': 0x10000,
}), equals(<String, dynamic>{'handled': false}));
expect(logs, <String>['RawKeyDownEvent', 'KeyDownEvent', 'KeyUpEvent']);
logs.clear();
}, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());
// The first key data received from the engine might be an empty key data. // 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, // 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 // but is only used so that *a* key data comes before the raw key message
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment