[Keyboard] Dispatch solitary synthesized KeyEvents (#96874)

This commit is contained in:
Tong Mu 2022-01-20 15:55:18 -08:00 committed by GitHub
parent 2cdef81ecf
commit 4e04e3ff42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 148 additions and 35 deletions

View file

@ -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(<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;
}
}
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.
///
/// 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 = () => <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,
));
}
}
handled = _dispatchKeyMessage(_keyEventsSinceLastMessage, rawEvent) || handled;
_keyEventsSinceLastMessage.clear();
return <String, dynamic>{ 'handled': handled };

View file

@ -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;
}

View file

@ -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) {

View file

@ -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<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.
// 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