mirror of
https://github.com/flutter/flutter
synced 2024-10-02 22:44:13 +00:00
[Keyboard] Dispatch solitary synthesized KeyEvent
s (#96874)
This commit is contained in:
parent
2cdef81ecf
commit
4e04e3ff42
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue