| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'binding.dart'; |
| import 'debug.dart'; |
| import 'raw_keyboard.dart'; |
| import 'system_channels.dart'; |
| |
| export 'dart:ui' show KeyData; |
| |
| export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder; |
| |
| export 'keyboard_key.g.dart' show LogicalKeyboardKey, PhysicalKeyboardKey; |
| export 'raw_keyboard.dart' show RawKeyEvent, RawKeyboard; |
| |
| // When using _keyboardDebug, always call it like so: |
| // |
| // assert(_keyboardDebug(() => 'Blah $foo')); |
| // |
| // It needs to be inside the assert in order to be removed in release mode, and |
| // it needs to use a closure to generate the string in order to avoid string |
| // interpolation when debugPrintKeyboardEvents is false. |
| // |
| // It will throw a StateError if you try to call it when the app is in release |
| // mode. |
| bool _keyboardDebug( |
| String Function() messageFunc, [ |
| Iterable<Object> Function()? detailsFunc, |
| ]) { |
| if (kReleaseMode) { |
| throw StateError( |
| '_keyboardDebug was called in Release mode, which means they are called ' |
| 'without being wrapped in an assert. Always call _keyboardDebug like so:\n' |
| r" assert(_keyboardDebug(() => 'Blah $foo'));" |
| ); |
| } |
| if (!debugPrintKeyboardEvents) { |
| return true; |
| } |
| debugPrint('KEYBOARD: ${messageFunc()}'); |
| final Iterable<Object> details = detailsFunc?.call() ?? const <Object>[]; |
| if (details.isNotEmpty) { |
| for (final Object detail in details) { |
| debugPrint(' $detail'); |
| } |
| } |
| // Return true so that it can be used inside of an assert. |
| return true; |
| } |
| |
| /// Represents a lock mode of a keyboard, such as [KeyboardLockMode.capsLock]. |
| /// |
| /// A lock mode locks some of a keyboard's keys into a distinct mode of operation, |
| /// depending on the lock settings selected. The status of the mode is toggled |
| /// with each key down of its corresponding logical key. A [KeyboardLockMode] |
| /// object is used to query whether this mode is enabled on the keyboard. |
| /// |
| /// Only a limited number of modes are supported, which are enumerated as |
| /// static members of this class. Manual constructing of this class is |
| /// prohibited. |
| enum KeyboardLockMode { |
| /// Represents the number lock mode on the keyboard. |
| /// |
| /// On supporting systems, enabling number lock mode usually allows key |
| /// presses of the number pad to input numbers, instead of acting as up, down, |
| /// left, right, page up, end, etc. |
| numLock._(LogicalKeyboardKey.numLock), |
| |
| /// Represents the scrolling lock mode on the keyboard. |
| /// |
| /// On supporting systems and applications (such as a spreadsheet), enabling |
| /// scrolling lock mode usually allows key presses of the cursor keys to |
| /// scroll the document instead of the cursor. |
| scrollLock._(LogicalKeyboardKey.scrollLock), |
| |
| /// Represents the capital letters lock mode on the keyboard. |
| /// |
| /// On supporting systems, enabling capital lock mode allows key presses of |
| /// the letter keys to input uppercase letters instead of lowercase. |
| capsLock._(LogicalKeyboardKey.capsLock); |
| |
| // KeyboardLockMode has a fixed pool of supported keys, enumerated as static |
| // members of this class, therefore constructing is prohibited. |
| const KeyboardLockMode._(this.logicalKey); |
| |
| /// The logical key that triggers this lock mode. |
| final LogicalKeyboardKey logicalKey; |
| |
| static final Map<int, KeyboardLockMode> _knownLockModes = <int, KeyboardLockMode>{ |
| numLock.logicalKey.keyId: numLock, |
| scrollLock.logicalKey.keyId: scrollLock, |
| capsLock.logicalKey.keyId: capsLock, |
| }; |
| |
| /// Returns the [KeyboardLockMode] constant from the logical key, or |
| /// null, if not found. |
| static KeyboardLockMode? findLockByLogicalKey(LogicalKeyboardKey logicalKey) => _knownLockModes[logicalKey.keyId]; |
| } |
| |
| /// Defines the interface for keyboard key events. |
| /// |
| /// The [KeyEvent] provides a universal model for key event information from a |
| /// hardware keyboard across platforms. |
| /// |
| /// See also: |
| /// |
| /// * [HardwareKeyboard] for full introduction to key event model and handling. |
| /// * [KeyDownEvent], a subclass for events representing the user pressing a |
| /// key. |
| /// * [KeyRepeatEvent], a subclass for events representing the user holding a |
| /// key, causing repeated events. |
| /// * [KeyUpEvent], a subclass for events representing the user releasing a |
| /// key. |
| @immutable |
| abstract class KeyEvent with Diagnosticable { |
| /// Create a const KeyEvent by providing each field. |
| const KeyEvent({ |
| required this.physicalKey, |
| required this.logicalKey, |
| this.character, |
| required this.timeStamp, |
| this.synthesized = false, |
| }); |
| |
| /// Returns an object representing the physical location of this key. |
| /// |
| /// A [PhysicalKeyboardKey] represents a USB HID code sent from the keyboard, |
| /// ignoring the key map, modifier keys (like SHIFT), and the label on the key. |
| /// |
| /// [PhysicalKeyboardKey]s are used to describe and test for keys in a |
| /// particular location. A [PhysicalKeyboardKey] may have a name, but the name |
| /// is a mnemonic ("keyA" is easier to remember than 0x70004), derived from the |
| /// key's effect on a QWERTY keyboard. The name does not represent the key's |
| /// effect whatsoever (a physical "keyA" can be the Q key on an AZERTY |
| /// keyboard). |
| /// |
| /// For instance, if you wanted to make a game where the key to the right of |
| /// the CAPS LOCK key made the player move left, you would be comparing a |
| /// physical key with [PhysicalKeyboardKey.keyA], since that is the key next to |
| /// the CAPS LOCK key on a QWERTY keyboard. This would return the same thing |
| /// even on an AZERTY keyboard where the key next to the CAPS LOCK produces a |
| /// "Q" when pressed. |
| /// |
| /// If you want to make your app respond to a key with a particular character |
| /// on it regardless of location of the key, use [KeyEvent.logicalKey] instead. |
| /// |
| /// Also, even though physical keys are defined with USB HID codes, their |
| /// values are not necessarily the same HID codes produced by the hardware and |
| /// presented to the driver. On most platforms, Flutter has to map the |
| /// platform representation back to a HID code because the original HID |
| /// code is not provided. USB HID was chosen because it is a well-defined |
| /// standard for referring to keys such as those a Flutter app may encounter. |
| /// |
| /// See also: |
| /// |
| /// * [logicalKey] for the non-location specific key generated by this event. |
| /// * [character] for the character generated by this keypress (if any). |
| final PhysicalKeyboardKey physicalKey; |
| |
| /// Returns an object representing the logical key that was pressed. |
| /// |
| /// {@template flutter.services.KeyEvent.logicalKey} |
| /// This method takes into account the key map and modifier keys (like SHIFT) |
| /// to determine which logical key to return. |
| /// |
| /// If you are looking for the character produced by a key event, use |
| /// [KeyEvent.character] instead. |
| /// |
| /// If you are collecting text strings, use the [TextField] or |
| /// [CupertinoTextField] widgets, since those automatically handle many of the |
| /// complexities of managing keyboard input, like showing a soft keyboard or |
| /// interacting with an input method editor (IME). |
| /// {@endtemplate} |
| final LogicalKeyboardKey logicalKey; |
| |
| /// Returns the Unicode character (grapheme cluster) completed by this |
| /// keystroke, if any. |
| /// |
| /// This will only return a character if this keystroke, combined with any |
| /// preceding keystroke(s), generates a character, and only on a "key down" |
| /// event. It will return null if no character has been generated by the |
| /// keystroke (e.g. a "dead" or "combining" key), or if the corresponding key |
| /// is a key without a visual representation, such as a modifier key or a |
| /// control key. It will also return null if this is a "key up" event. |
| /// |
| /// This can return multiple Unicode code points, since some characters (more |
| /// accurately referred to as grapheme clusters) are made up of more than one |
| /// code point. |
| /// |
| /// The [character] doesn't take into account edits by an input method editor |
| /// (IME), or manage the visibility of the soft keyboard on touch devices. For |
| /// composing text, use the [TextField] or [CupertinoTextField] widgets, since |
| /// those automatically handle many of the complexities of managing keyboard |
| /// input. |
| /// |
| /// The [character] is not available on [KeyUpEvent]s. |
| final String? character; |
| |
| /// Time of event, relative to an arbitrary start point. |
| /// |
| /// All events share the same timeStamp origin. |
| final Duration timeStamp; |
| |
| /// Whether this event is synthesized by Flutter to synchronize key states. |
| /// |
| /// An non-[synthesized] event is converted from a native event, and a native |
| /// event can only be converted to one non-[synthesized] event. Some properties |
| /// might be changed during the conversion (for example, a native repeat event |
| /// might be converted to a Flutter down event when necessary.) |
| /// |
| /// A [synthesized] event is created without a source native event in order to |
| /// synchronize key states. For example, if the native platform shows that a |
| /// shift key that was previously held has been released somehow without the |
| /// key up event dispatched (probably due to loss of focus), a synthesized key |
| /// up event will be added to regularized the event stream. |
| /// |
| /// For detailed introduction to the regularized event model, see |
| /// [HardwareKeyboard]. |
| /// |
| /// Defaults to false. |
| final bool synthesized; |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<PhysicalKeyboardKey>('physicalKey', physicalKey)); |
| properties.add(DiagnosticsProperty<LogicalKeyboardKey>('logicalKey', logicalKey)); |
| properties.add(StringProperty('character', character)); |
| properties.add(DiagnosticsProperty<Duration>('timeStamp', timeStamp)); |
| properties.add(FlagProperty('synthesized', value: synthesized, ifTrue: 'synthesized')); |
| } |
| } |
| |
| /// An event indicating that the user has pressed a key down on the keyboard. |
| /// |
| /// See also: |
| /// |
| /// * [KeyRepeatEvent], a key event representing the user |
| /// holding a key, causing repeated events. |
| /// * [KeyUpEvent], a key event representing the user |
| /// releasing a key. |
| /// * [HardwareKeyboard], which produces this event. |
| class KeyDownEvent extends KeyEvent { |
| /// Creates a key event that represents the user pressing a key. |
| const KeyDownEvent({ |
| required super.physicalKey, |
| required super.logicalKey, |
| super.character, |
| required super.timeStamp, |
| super.synthesized, |
| }); |
| } |
| |
| /// An event indicating that the user has released a key on the keyboard. |
| /// |
| /// See also: |
| /// |
| /// * [KeyDownEvent], a key event representing the user |
| /// pressing a key. |
| /// * [KeyRepeatEvent], a key event representing the user |
| /// holding a key, causing repeated events. |
| /// * [HardwareKeyboard], which produces this event. |
| class KeyUpEvent extends KeyEvent { |
| /// Creates a key event that represents the user pressing a key. |
| const KeyUpEvent({ |
| required super.physicalKey, |
| required super.logicalKey, |
| required super.timeStamp, |
| super.synthesized, |
| }); |
| } |
| |
| /// An event indicating that the user has been holding a key on the keyboard |
| /// and causing repeated events. |
| /// |
| /// Repeat events are not guaranteed and are provided only if supported by the |
| /// underlying platform. |
| /// |
| /// See also: |
| /// |
| /// * [KeyDownEvent], a key event representing the user |
| /// pressing a key. |
| /// * [KeyUpEvent], a key event representing the user |
| /// releasing a key. |
| /// * [HardwareKeyboard], which produces this event. |
| class KeyRepeatEvent extends KeyEvent { |
| /// Creates a key event that represents the user pressing a key. |
| const KeyRepeatEvent({ |
| required super.physicalKey, |
| required super.logicalKey, |
| super.character, |
| required super.timeStamp, |
| }); |
| } |
| |
| /// The signature for [HardwareKeyboard.addHandler], a callback to decide whether |
| /// the entire framework handles a key event. |
| typedef KeyEventCallback = bool Function(KeyEvent event); |
| |
| /// Manages key events from hardware keyboards. |
| /// |
| /// [HardwareKeyboard] manages all key events of the Flutter application from |
| /// hardware keyboards (in contrast to on-screen keyboards). It receives key |
| /// data from the native platform, dispatches key events to registered |
| /// handlers, and records the keyboard state. |
| /// |
| /// To stay notified whenever keys are pressed, held, or released, add a |
| /// handler with [addHandler]. To only be notified when a specific part of the |
| /// app is focused, use a [Focus] widget's `onFocusChanged` attribute instead |
| /// of [addHandler]. Handlers should be removed with [removeHandler] when |
| /// notification is no longer necessary, or when the handler is being disposed. |
| /// |
| /// To query whether a key is being held, or a lock mode is enabled, use |
| /// [physicalKeysPressed], [logicalKeysPressed], or [lockModesEnabled]. |
| /// These states will have been updated with the event when used during a key |
| /// event handler. |
| /// |
| /// The singleton [HardwareKeyboard] instance is held by the [ServicesBinding] |
| /// as [ServicesBinding.keyboard], and can be conveniently accessed using the |
| /// [HardwareKeyboard.instance] static accessor. |
| /// |
| /// ## Event model |
| /// |
| /// Flutter uses a universal event model ([KeyEvent]) and key options |
| /// ([LogicalKeyboardKey] and [PhysicalKeyboardKey]) regardless of the native |
| /// platform, while preserving platform-specific features as much as |
| /// possible. |
| /// |
| /// [HardwareKeyboard] guarantees that the key model is "regularized": The key |
| /// event stream consists of "key tap sequences", where a key tap sequence is |
| /// defined as one [KeyDownEvent], zero or more [KeyRepeatEvent]s, and one |
| /// [KeyUpEvent] in order, all with the same physical key and logical key. |
| /// |
| /// Example: |
| /// |
| /// * Tap and hold key A, US layout: |
| /// * KeyDownEvent(physicalKey: keyA, logicalKey: keyA, character: "a") |
| /// * KeyRepeatEvent(physicalKey: keyA, logicalKey: keyA, character: "a") |
| /// * KeyUpEvent(physicalKey: keyA, logicalKey: keyA) |
| /// * Press ShiftLeft, tap key A, then release ShiftLeft, US layout: |
| /// * KeyDownEvent(physicalKey: shiftLeft, logicalKey: shiftLeft) |
| /// * KeyDownEvent(physicalKey: keyA, logicalKey: keyA, character: "A") |
| /// * KeyRepeatEvent(physicalKey: keyA, logicalKey: keyA, character: "A") |
| /// * KeyUpEvent(physicalKey: keyA, logicalKey: keyA) |
| /// * KeyUpEvent(physicalKey: shiftLeft, logicalKey: shiftLeft) |
| /// * Tap key Q, French layout: |
| /// * KeyDownEvent(physicalKey: keyA, logicalKey: keyQ, character: "q") |
| /// * KeyUpEvent(physicalKey: keyA, logicalKey: keyQ) |
| /// * Tap CapsLock: |
| /// * KeyDownEvent(physicalKey: capsLock, logicalKey: capsLock) |
| /// * KeyUpEvent(physicalKey: capsLock, logicalKey: capsLock) |
| /// |
| /// When the Flutter application starts, all keys are released, and all lock |
| /// modes are disabled. Upon key events, [HardwareKeyboard] will update its |
| /// states, then dispatch callbacks: [KeyDownEvent]s and [KeyUpEvent]s set |
| /// or reset the pressing state, while [KeyDownEvent]s also toggle lock modes. |
| /// |
| /// Flutter will try to synchronize with the ground truth of keyboard states |
| /// using synthesized events ([KeyEvent.synthesized]), subject to the |
| /// availability of the platform. The desynchronization can be caused by |
| /// non-empty initial state or a change in the focused window or application. |
| /// For example, if CapsLock is enabled when the application starts, then |
| /// immediately before the first key event, a synthesized [KeyDownEvent] and |
| /// [KeyUpEvent] of CapsLock will be dispatched. |
| /// |
| /// The resulting event stream does not map one-to-one to the native key event |
| /// stream. Some native events might be skipped, while some events might be |
| /// synthesized and do not correspond to native events. Synthesized events will |
| /// be indicated by [KeyEvent.synthesized]. |
| /// |
| /// Example: |
| /// |
| /// * Flutter starts with CapsLock on, the first press of keyA: |
| /// * KeyDownEvent(physicalKey: capsLock, logicalKey: capsLock, synthesized: true) |
| /// * KeyUpEvent(physicalKey: capsLock, logicalKey: capsLock, synthesized: true) |
| /// * KeyDownEvent(physicalKey: keyA, logicalKey: keyA, character: "a") |
| /// * While holding ShiftLeft, lose window focus, release shiftLeft, then focus |
| /// back and press keyA: |
| /// * KeyUpEvent(physicalKey: shiftLeft, logicalKey: shiftLeft, synthesized: true) |
| /// * KeyDownEvent(physicalKey: keyA, logicalKey: keyA, character: "a") |
| /// |
| /// Flutter does not distinguish between multiple keyboards. Flutter will |
| /// process all events as if they come from a single keyboard, and try to |
| /// resolve any conflicts and provide a regularized key event stream, which |
| /// can deviate from the ground truth. |
| /// |
| /// ## Compared to [RawKeyboard] |
| /// |
| /// [RawKeyboard] is the legacy API, and will be deprecated and removed in the |
| /// future. It is recommended to always use [HardwareKeyboard] and [KeyEvent] |
| /// APIs (such as [FocusNode.onKeyEvent]) to handle key events. |
| /// |
| /// Behavior-wise, [RawKeyboard] provides a less unified, less regular |
| /// event model than [HardwareKeyboard]. For example: |
| /// |
| /// * Down events might not be matched with an up event, and vice versa (the |
| /// set of pressed keys is silently updated). |
| /// * The logical key of the down event might not be the same as that of the up |
| /// event. |
| /// * Down events and repeat events are not easily distinguishable (must be |
| /// tracked manually). |
| /// * Lock modes (such as CapsLock) only have their "enabled" state recorded. |
| /// There's no way to acquire their pressing state. |
| /// |
| /// See also: |
| /// |
| /// * [KeyDownEvent], [KeyRepeatEvent], and [KeyUpEvent], the classes used to |
| /// describe specific key events. |
| /// * [instance], the singleton instance of this class. |
| /// * [RawKeyboard], the legacy API that dispatches key events containing raw |
| /// system data. |
| class HardwareKeyboard { |
| /// Provides convenient access to the current [HardwareKeyboard] singleton from |
| /// the [ServicesBinding] instance. |
| static HardwareKeyboard get instance => ServicesBinding.instance.keyboard; |
| |
| final Map<PhysicalKeyboardKey, LogicalKeyboardKey> _pressedKeys = <PhysicalKeyboardKey, LogicalKeyboardKey>{}; |
| |
| /// The set of [PhysicalKeyboardKey]s that are pressed. |
| /// |
| /// If called from a key event handler, the result will already include the effect |
| /// of the event. |
| /// |
| /// See also: |
| /// |
| /// * [logicalKeysPressed], which tells if a logical key is being pressed. |
| Set<PhysicalKeyboardKey> get physicalKeysPressed => _pressedKeys.keys.toSet(); |
| |
| /// The set of [LogicalKeyboardKey]s that are pressed. |
| /// |
| /// If called from a key event handler, the result will already include the effect |
| /// of the event. |
| /// |
| /// See also: |
| /// |
| /// * [physicalKeysPressed], which tells if a physical key is being pressed. |
| Set<LogicalKeyboardKey> get logicalKeysPressed => _pressedKeys.values.toSet(); |
| |
| /// Returns the logical key that corresponds to the given pressed physical key. |
| /// |
| /// Returns null if the physical key is not currently pressed. |
| LogicalKeyboardKey? lookUpLayout(PhysicalKeyboardKey physicalKey) => _pressedKeys[physicalKey]; |
| |
| final Set<KeyboardLockMode> _lockModes = <KeyboardLockMode>{}; |
| /// The set of [KeyboardLockMode] that are enabled. |
| /// |
| /// Lock keys, such as CapsLock, are logical keys that toggle their |
| /// respective boolean states on key down events. Such flags are usually used |
| /// as modifier to other keys or events. |
| /// |
| /// If called from a key event handler, the result will already include the effect |
| /// of the event. |
| Set<KeyboardLockMode> get lockModesEnabled => _lockModes; |
| |
| void _assertEventIsRegular(KeyEvent event) { |
| assert(() { |
| const String common = 'If this occurs in real application, please report this ' |
| 'bug to Flutter. If this occurs in unit tests, please ensure that ' |
| "simulated events follow Flutter's event model as documented in " |
| '`HardwareKeyboard`. This was the event: '; |
| if (event is KeyDownEvent) { |
| assert(!_pressedKeys.containsKey(event.physicalKey), |
| 'A ${event.runtimeType} is dispatched, but the state shows that the physical ' |
| 'key is already pressed. $common$event'); |
| } else if (event is KeyRepeatEvent || event is KeyUpEvent) { |
| assert(_pressedKeys.containsKey(event.physicalKey), |
| 'A ${event.runtimeType} is dispatched, but the state shows that the physical ' |
| 'key is not pressed. $common$event'); |
| assert(_pressedKeys[event.physicalKey] == event.logicalKey, |
| 'A ${event.runtimeType} is dispatched, but the state shows that the physical ' |
| 'key is pressed on a different logical key. $common$event ' |
| 'and the recorded logical key ${_pressedKeys[event.physicalKey]}'); |
| } else { |
| assert(false, 'Unexpected key event class ${event.runtimeType}'); |
| } |
| return true; |
| }()); |
| } |
| |
| List<KeyEventCallback> _handlers = <KeyEventCallback>[]; |
| bool _duringDispatch = false; |
| List<KeyEventCallback>? _modifiedHandlers; |
| |
| /// Register a listener that is called every time a hardware key event |
| /// occurs. |
| /// |
| /// All registered handlers will be invoked in order regardless of |
| /// their return value. The return value indicates whether Flutter |
| /// "handles" the event. If any handler returns true, the event |
| /// will not be propagated to other native components in the add-to-app |
| /// scenario. |
| /// |
| /// If an object added a handler, it must remove the handler before it is |
| /// disposed. |
| /// |
| /// If used during event dispatching, the addition will not take effect |
| /// until after the dispatching. |
| /// |
| /// See also: |
| /// |
| /// * [removeHandler], which removes the handler. |
| void addHandler(KeyEventCallback handler) { |
| if (_duringDispatch) { |
| _modifiedHandlers ??= <KeyEventCallback>[..._handlers]; |
| _modifiedHandlers!.add(handler); |
| } else { |
| _handlers.add(handler); |
| } |
| } |
| |
| /// Stop calling the given listener every time a hardware key event |
| /// occurs. |
| /// |
| /// The `handler` argument must be [identical] to the one used in |
| /// [addHandler]. If multiple exist, the first one will be removed. |
| /// If none is found, then this method is a no-op. |
| /// |
| /// If used during event dispatching, the removal will not take effect |
| /// until after the event has been dispatched. |
| void removeHandler(KeyEventCallback handler) { |
| if (_duringDispatch) { |
| _modifiedHandlers ??= <KeyEventCallback>[..._handlers]; |
| _modifiedHandlers!.remove(handler); |
| } else { |
| _handlers.remove(handler); |
| } |
| } |
| |
| /// Query the engine and update _pressedKeys accordingly to the engine answer. |
| // |
| /// Both the framework and the engine maintain a state of the current pressed |
| /// keys. There are edge cases, related to startup and restart, where the framework |
| /// needs to resynchronize its keyboard state. |
| Future<void> syncKeyboardState() async { |
| final Map<int, int>? keyboardState = await SystemChannels.keyboard.invokeMapMethod<int, int>( |
| 'getKeyboardState', |
| ); |
| if (keyboardState != null) { |
| for (final int key in keyboardState.keys) { |
| final PhysicalKeyboardKey physicalKey = PhysicalKeyboardKey(key); |
| final LogicalKeyboardKey logicalKey = LogicalKeyboardKey(keyboardState[key]!); |
| _pressedKeys[physicalKey] = logicalKey; |
| } |
| } |
| } |
| |
| bool _dispatchKeyEvent(KeyEvent event) { |
| // This dispatching could have used the same algorithm as [ChangeNotifier], |
| // but since 1) it shouldn't be necessary to support reentrantly |
| // dispatching, 2) there shouldn't be many handlers (most apps should use |
| // only 1, this function just uses a simpler algorithm. |
| assert(!_duringDispatch, 'Nested keyboard dispatching is not supported'); |
| _duringDispatch = true; |
| bool handled = false; |
| for (final KeyEventCallback handler in _handlers) { |
| try { |
| final bool thisResult = handler(event); |
| handled = handled || thisResult; |
| } catch (exception, stack) { |
| InformationCollector? collector; |
| assert(() { |
| collector = () => <DiagnosticsNode>[ |
| DiagnosticsProperty<KeyEvent>('Event', event), |
| ]; |
| return true; |
| }()); |
| FlutterError.reportError(FlutterErrorDetails( |
| exception: exception, |
| stack: stack, |
| library: 'services library', |
| context: ErrorDescription('while processing a key handler'), |
| informationCollector: collector, |
| )); |
| } |
| } |
| _duringDispatch = false; |
| if (_modifiedHandlers != null) { |
| _handlers = _modifiedHandlers!; |
| _modifiedHandlers = null; |
| } |
| return handled; |
| } |
| |
| List<String> _debugPressedKeysDetails() { |
| if (_pressedKeys.isEmpty) { |
| return <String>['Empty']; |
| } |
| final List<String> details = <String>[]; |
| for (final PhysicalKeyboardKey physicalKey in _pressedKeys.keys) { |
| details.add('$physicalKey: ${_pressedKeys[physicalKey]}'); |
| } |
| return details; |
| } |
| |
| /// Process a new [KeyEvent] by recording the state changes and dispatching |
| /// to handlers. |
| bool handleKeyEvent(KeyEvent event) { |
| assert(_keyboardDebug(() => 'Key event received: $event')); |
| assert(_keyboardDebug(() => 'Pressed state before processing the event:', _debugPressedKeysDetails)); |
| _assertEventIsRegular(event); |
| final PhysicalKeyboardKey physicalKey = event.physicalKey; |
| final LogicalKeyboardKey logicalKey = event.logicalKey; |
| if (event is KeyDownEvent) { |
| _pressedKeys[physicalKey] = logicalKey; |
| final KeyboardLockMode? lockMode = KeyboardLockMode.findLockByLogicalKey(event.logicalKey); |
| if (lockMode != null) { |
| if (_lockModes.contains(lockMode)) { |
| _lockModes.remove(lockMode); |
| } else { |
| _lockModes.add(lockMode); |
| } |
| } |
| } else if (event is KeyUpEvent) { |
| _pressedKeys.remove(physicalKey); |
| } else if (event is KeyRepeatEvent) { |
| // Empty |
| } |
| |
| assert(_keyboardDebug(() => 'Pressed state after processing the event:', _debugPressedKeysDetails)); |
| return _dispatchKeyEvent(event); |
| } |
| |
| /// Clear all keyboard states and additional handlers. |
| /// |
| /// All handlers are removed except for the first one, which is added by |
| /// [ServicesBinding]. |
| /// |
| /// This is used by the testing framework to make sure that tests are hermetic. |
| @visibleForTesting |
| void clearState() { |
| _pressedKeys.clear(); |
| _lockModes.clear(); |
| _handlers.clear(); |
| assert(_modifiedHandlers == null); |
| } |
| } |
| |
| /// The mode in which information of key messages is delivered. |
| /// |
| /// Different platforms use different methods, classes, and models to inform the |
| /// framework of native key events, which is called "transit mode". |
| /// |
| /// The framework must determine which transit mode the current platform |
| /// implements and behave accordingly (such as transforming and synthesizing |
| /// events if necessary). Unit tests related to keyboard might also want to |
| /// simulate platform of each transit mode. |
| /// |
| /// The transit mode of the current platform is inferred by [KeyEventManager] at |
| /// the start of the application. |
| /// |
| /// See also: |
| /// |
| /// * [KeyEventManager], which infers the transit mode of the current platform |
| /// and guides how key messages are dispatched. |
| /// * [debugKeyEventSimulatorTransitModeOverride], overrides the transit mode |
| /// used to simulate key events. |
| /// * [KeySimulatorTransitModeVariant], an easier way to set |
| /// [debugKeyEventSimulatorTransitModeOverride] in widget tests. |
| enum KeyDataTransitMode { |
| /// Key event information is delivered as raw key data. |
| /// |
| /// Raw key data is platform's native key event information sent in JSON |
| /// through a method channel, which is then interpreted as a platform subclass |
| /// of [RawKeyEventData]. |
| /// |
| /// If the current transit mode is [rawKeyData], the raw key data is converted |
| /// to both [KeyMessage.events] and [KeyMessage.rawEvent]. |
| rawKeyData, |
| |
| /// Key event information is delivered as converted key data, followed |
| /// by raw key data. |
| /// |
| /// Key data ([ui.KeyData]) is a standardized event stream converted from |
| /// platform's native key event information, sent through the embedder |
| /// API. Its event model is described in [HardwareKeyboard]. |
| /// |
| /// Raw key data is platform's native key event information sent in JSON |
| /// through a method channel. It is interpreted by subclasses of |
| /// [RawKeyEventData]. |
| /// |
| /// If the current transit mode is [rawKeyData], the key data is converted to |
| /// [KeyMessage.events], and the raw key data is converted to |
| /// [KeyMessage.rawEvent]. |
| keyDataThenRawKeyData, |
| } |
| |
| /// The assembled information converted from a native key message. |
| /// |
| /// Native key messages, produced by physically pressing or releasing |
| /// keyboard keys, are translated into two different event streams in Flutter: |
| /// |
| /// * The [KeyEvent] stream, represented by [KeyMessage.events] (recommended). |
| /// * The [RawKeyEvent] stream, represented by [KeyMessage.rawEvent] (legacy, |
| /// to be deprecated). |
| /// |
| /// Either the [KeyEvent] stream or the [RawKeyEvent] stream alone provides a |
| /// complete description of the keyboard messages, but in different event |
| /// models. Flutter is still transitioning from the legacy model to the new |
| /// model, therefore it dispatches both streams simultaneously until the |
| /// transition is completed. [KeyMessage] is used to bundle the |
| /// stream segments of both models from a native key message together for the |
| /// convenience of propagation. |
| /// |
| /// Typically, an application either processes [KeyMessage.events] |
| /// or [KeyMessage.rawEvent], not both. For example, handling a |
| /// [KeyMessage], means handling each event in [KeyMessage.events]. |
| /// |
| /// In advanced cases, a widget needs to process both streams at the same time. |
| /// For example, [FocusNode] has an `onKey` that dispatches [RawKeyEvent]s and |
| /// an `onKeyEvent` that dispatches [KeyEvent]s. To processes a [KeyMessage], |
| /// it first calls `onKeyEvent` with each [KeyEvent] of [events], and then |
| /// `onKey` with [rawEvent]. All callbacks are invoked regardless of their |
| /// [KeyEventResult]. Their results are combined into the result of the node |
| /// using [combineKeyEventResults]. |
| /// |
| /// ```dart |
| /// void handleMessage(FocusNode node, KeyMessage message) { |
| /// final List<KeyEventResult> results = <KeyEventResult>[]; |
| /// if (node.onKeyEvent != null) { |
| /// for (final KeyEvent event in message.events) { |
| /// results.add(node.onKeyEvent!(node, event)); |
| /// } |
| /// } |
| /// if (node.onKey != null && message.rawEvent != null) { |
| /// results.add(node.onKey!(node, message.rawEvent!)); |
| /// } |
| /// final KeyEventResult result = combineKeyEventResults(results); |
| /// // Progress based on `result`... |
| /// } |
| /// ``` |
| @immutable |
| class KeyMessage { |
| /// Create a [KeyMessage] by providing all information. |
| /// |
| /// The [events] might be empty. |
| const KeyMessage(this.events, this.rawEvent); |
| |
| /// The list of [KeyEvent]s converted from the native key message. |
| /// |
| /// A native key message is converted into multiple [KeyEvent]s in a regular |
| /// event model. The [events] might contain zero or any number of |
| /// [KeyEvent]s. |
| /// |
| /// See also: |
| /// |
| /// * [HardwareKeyboard], which describes the regular event model. |
| /// * [HardwareKeyboard.addHandler], [KeyboardListener], [Focus.onKeyEvent], |
| /// where [KeyEvent]s are commonly used. |
| final List<KeyEvent> events; |
| |
| /// The native key message in the form of a raw key event. |
| /// |
| /// A native key message is sent to the framework in JSON and represented |
| /// in a platform-specific form as [RawKeyEventData] and a platform-neutral |
| /// form as [RawKeyEvent]. Their stream is not as regular as [KeyEvent]'s, |
| /// but keeps as much native information and structure as possible. |
| /// |
| /// 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; |
| |
| @override |
| String toString() { |
| return 'KeyMessage($events)'; |
| } |
| } |
| |
| /// The signature for [KeyEventManager.keyMessageHandler]. |
| /// |
| /// A [KeyMessageHandler] processes a [KeyMessage] and returns whether |
| /// the message is considered handled. Handled messages should not be |
| /// propagated to other native components. |
| typedef KeyMessageHandler = bool Function(KeyMessage message); |
| |
| /// A singleton class that processes key messages from the platform and |
| /// dispatches converted messages accordingly. |
| /// |
| /// [KeyEventManager] receives platform key messages by [handleKeyData] |
| /// and [handleRawKeyMessage], sends converted events to [HardwareKeyboard] |
| /// and [RawKeyboard] for recording keeping, and then dispatches the [KeyMessage] |
| /// to [keyMessageHandler], the global message handler. |
| /// |
| /// [KeyEventManager] is typically created, owned, and invoked by |
| /// [ServicesBinding]. |
| /// |
| /// ## On embedder implementation |
| /// |
| /// Currently, Flutter has two sets of key event APIs running in parallel. |
| /// |
| /// * The legacy "raw key event" route receives messages from the |
| /// "flutter/keyevent" message channel ([SystemChannels.keyEvent]) and |
| /// dispatches [RawKeyEvent] to [RawKeyboard] and [Focus.onKey] as well as |
| /// similar methods. |
| /// * The newer "hardware key event" route receives messages from the |
| /// "flutter/keydata" message channel (embedder API |
| /// `FlutterEngineSendKeyEvent`) and dispatches [KeyEvent] to |
| /// [HardwareKeyboard] and some methods such as [Focus.onKeyEvent]. |
| /// |
| /// [KeyEventManager] resolves cross-platform compatibility of keyboard |
| /// implementations, since legacy platforms might have not implemented the new |
| /// key data API and only send raw key data on each key message. |
| /// [KeyEventManager] recognizes the platform support by detecting whether a |
| /// message comes from platform channel "flutter/keyevent" before one from |
| /// "flutter/keydata", or vice versa, at the beginning of the app. |
| /// |
| /// * If a "flutter/keyevent" message is received first, then this platform is |
| /// considered a legacy platform. The raw key event is transformed into a |
| /// hardware key event at best effort. No messages from "flutter/keydata" are |
| /// expected. |
| /// * If a "flutter/keydata" message is received first, then this platform is |
| /// considered a newer platform. The hardware key events are stored, and |
| /// dispatched only when a raw key message is received. |
| /// |
| /// Therefore, to correctly implement a platform that supports |
| /// `FlutterEngineSendKeyEvent`, the platform must ensure that |
| /// `FlutterEngineSendKeyEvent` is called before sending a message to |
| /// "flutter/keyevent" at the beginning of the app, and every physical key event |
| /// is ended with a "flutter/keyevent" message. |
| class KeyEventManager { |
| /// Create an instance. |
| /// |
| /// This is typically only called by [ServicesBinding]. |
| KeyEventManager(this._hardwareKeyboard, this._rawKeyboard); |
| |
| /// The global entrance which handles all key events sent to Flutter. |
| /// |
| /// Typical applications use [WidgetsBinding], where this field is |
| /// set by the focus system (see `FocusManager`) on startup to a function that |
| /// dispatches incoming events to the focus system, including |
| /// `FocusNode.onKey`, `FocusNode.onKeyEvent`, and `Shortcuts`. In this case, |
| /// the application does not need to read, assign, or invoke this value. |
| /// |
| /// For advanced uses, the application can "patch" this callback. See below |
| /// for details. |
| /// |
| /// ## Handlers and event results |
| /// |
| /// Roughly speaking, Flutter processes each native key event with the |
| /// following phases: |
| /// |
| /// 1. Platform-side pre-filtering, sometimes used for IME. |
| /// 2. The key event system. |
| /// 3. The text input system. |
| /// 4. Other native components (possibly non-Flutter). |
| /// |
| /// Each phase will conclude with a boolean called an "event result". If the |
| /// result is true, this phase _handles_ the event and prevents the event |
| /// from being propagated to the next phase. This mechanism allows shortcuts |
| /// such as "Ctrl-C" to not generate a text "C" in the text field, or |
| /// shortcuts that are not handled by any components to trigger special alerts |
| /// (such as the "bonk" noise on macOS). |
| /// |
| /// In the second phase, known as "the key event system", the event is dispatched |
| /// to several destinations: [RawKeyboard]'s listeners, |
| /// [HardwareKeyboard]'s handlers, and [keyMessageHandler]. |
| /// All destinations will always receive the event regardless of the handlers' |
| /// results. If any handler's result is true, then the overall result of the |
| /// second phase is true, and event propagation is stopped. |
| /// |
| /// See also: |
| /// |
| /// * [RawKeyboard.addListener], which adds a raw keyboard listener. |
| /// * [RawKeyboardListener], which is also implemented by adding a raw |
| /// keyboard listener. |
| /// * [HardwareKeyboard.addHandler], which adds a hardware keyboard handler. |
| /// |
| /// ## Advanced usages: Manual assignment or patching |
| /// |
| /// If you are not using the focus system to manage focus, set this |
| /// attribute to a [KeyMessageHandler] that returns true if the propagation |
| /// on the platform should not be continued. If this field is null, key events |
| /// will be assumed to not have been handled by Flutter, a result of "false". |
| /// |
| /// Even if you are using the focus system, you might also want to do more |
| /// than the focus system allows. In these cases, you can _patch_ |
| /// [keyMessageHandler] by setting it to a callback that performs your tasks |
| /// and calls the original callback in between (or not at all.) |
| /// |
| /// Patching [keyMessageHandler] can not be reverted. You should always assume |
| /// that another component might have patched it before you and after you. |
| /// This means that you might want to write your own global notification |
| /// manager, to which callbacks can be added and removed. |
| /// |
| /// You should not patch [keyMessageHandler] until the `FocusManager` has assigned |
| /// its callback. This is assured during any time within the widget lifecycle |
| /// (such as `initState`), or after calling `WidgetManager.instance`. |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to process key events that are not handled by any |
| /// focus handler (such as `Shortcuts`) by patching [keyMessageHandler]. |
| /// |
| /// The app prints out any key events that are not handled by the app body. |
| /// Try typing something in the first text field. These key presses are not |
| /// handled by `Shortcuts` and will be sent to the fallback handler and printed |
| /// out. Now try some text shortcuts, such as Ctrl+A. The KeyA press is |
| /// handled as a shortcut, and is not sent to the fallback handler and so is |
| /// not printed out. |
| /// |
| /// The key widget is `FallbackKeyEventRegistrar`, a necessity class to allow |
| /// reversible patching. `FallbackFocus` and `FallbackFocusNode` are also |
| /// useful to recognize the widget tree's structure. `FallbackDemo` is an |
| /// example of using them in an app. |
| /// |
| /// ** See code in examples/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [HardwareKeyboard.addHandler], which accepts multiple global handlers |
| /// to process [KeyEvent]s |
| KeyMessageHandler? keyMessageHandler; |
| |
| final HardwareKeyboard _hardwareKeyboard; |
| final RawKeyboard _rawKeyboard; |
| |
| // The [KeyDataTransitMode] of the current platform. |
| // |
| // The `_transitMode` is guaranteed to be non-null during key event callbacks. |
| // |
| // The `_transitMode` is null before the first event, after which it is inferred |
| // and will not change throughout the lifecycle of the application. |
| // |
| // The `_transitMode` can be reset to null in tests using [clearState]. |
| KeyDataTransitMode? _transitMode; |
| |
| // The accumulated [KeyEvent]s since the last time the message is dispatched. |
| // |
| // If _transitMode is [KeyDataTransitMode.keyDataThenRawKeyData], then [KeyEvent]s |
| // are directly received from [handleKeyData]. If _transitMode is |
| // [KeyDataTransitMode.rawKeyData], then [KeyEvent]s are converted from the raw |
| // key data of [handleRawKeyMessage]. Either way, the accumulated key |
| // events are only dispatched along with the next [KeyMessage] when a |
| // dispatchable [RawKeyEvent] is available. |
| final List<KeyEvent> _keyEventsSinceLastMessage = <KeyEvent>[]; |
| |
| // When a RawKeyDownEvent is skipped ([RawKeyEventData.shouldDispatchEvent] |
| // is false), its physical key will be recorded here, so that its up event |
| // can also be properly skipped. |
| final Set<PhysicalKeyboardKey> _skippedRawKeysPressed = <PhysicalKeyboardKey>{}; |
| |
| /// Dispatch a key data to global and leaf listeners. |
| /// |
| /// This method is the handler to the global `onKeyData` API. |
| bool handleKeyData(ui.KeyData data) { |
| _transitMode ??= KeyDataTransitMode.keyDataThenRawKeyData; |
| switch (_transitMode!) { |
| case KeyDataTransitMode.rawKeyData: |
| assert(false, 'Should never encounter KeyData when transitMode is rawKeyData.'); |
| return false; |
| case KeyDataTransitMode.keyDataThenRawKeyData: |
| // Having 0 as the physical and logical ID indicates an empty key data |
| // (the only occasion 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 |
| /// the JSON form of the native key message and returns the responds for the |
| /// channel. |
| Future<Map<String, dynamic>> handleRawKeyMessage(dynamic message) async { |
| if (_transitMode == null) { |
| _transitMode = KeyDataTransitMode.rawKeyData; |
| // Convert raw events using a listener so that conversion only occurs if |
| // the raw event should be dispatched. |
| _rawKeyboard.addListener(_convertRawEventAndStore); |
| } |
| final RawKeyEvent rawEvent = RawKeyEvent.fromMessage(message as Map<String, dynamic>); |
| |
| bool shouldDispatch = true; |
| if (rawEvent is RawKeyDownEvent) { |
| if (!rawEvent.data.shouldDispatchEvent()) { |
| shouldDispatch = false; |
| _skippedRawKeysPressed.add(rawEvent.physicalKey); |
| } else { |
| _skippedRawKeysPressed.remove(rawEvent.physicalKey); |
| } |
| } else if (rawEvent is RawKeyUpEvent) { |
| if (_skippedRawKeysPressed.contains(rawEvent.physicalKey)) { |
| _skippedRawKeysPressed.remove(rawEvent.physicalKey); |
| shouldDispatch = false; |
| } |
| } |
| |
| bool handled = true; |
| if (shouldDispatch) { |
| // The following `handleRawKeyEvent` will call `_convertRawEventAndStore` |
| // unless the event is not dispatched. |
| handled = _rawKeyboard.handleRawKeyEvent(rawEvent); |
| |
| for (final KeyEvent event in _keyEventsSinceLastMessage) { |
| handled = _hardwareKeyboard.handleKeyEvent(event) || handled; |
| } |
| if (_transitMode == KeyDataTransitMode.rawKeyData) { |
| assert(setEquals(_rawKeyboard.physicalKeysPressed, _hardwareKeyboard.physicalKeysPressed), |
| 'RawKeyboard reported ${_rawKeyboard.physicalKeysPressed}, ' |
| 'while HardwareKeyboard reported ${_hardwareKeyboard.physicalKeysPressed}'); |
| } |
| |
| handled = _dispatchKeyMessage(_keyEventsSinceLastMessage, rawEvent) || handled; |
| _keyEventsSinceLastMessage.clear(); |
| } |
| |
| return <String, dynamic>{ 'handled': handled }; |
| } |
| |
| // Convert the raw event to key events, including synthesizing events for |
| // modifiers, and store the key events in `_keyEventsSinceLastMessage`. |
| // |
| // This is only called when `_transitMode` is `rawKeyEvent` and if the raw |
| // event should be dispatched. |
| void _convertRawEventAndStore(RawKeyEvent rawEvent) { |
| final PhysicalKeyboardKey physicalKey = rawEvent.physicalKey; |
| final LogicalKeyboardKey logicalKey = rawEvent.logicalKey; |
| final Set<PhysicalKeyboardKey> physicalKeysPressed = _hardwareKeyboard.physicalKeysPressed; |
| final List<KeyEvent> eventAfterwards = <KeyEvent>[]; |
| final KeyEvent? mainEvent; |
| final LogicalKeyboardKey? recordedLogicalMain = _hardwareKeyboard.lookUpLayout(physicalKey); |
| final Duration timeStamp = ServicesBinding.instance.currentSystemFrameTimeStamp; |
| final String? character = rawEvent.character == '' ? null : rawEvent.character; |
| if (rawEvent is RawKeyDownEvent) { |
| if (recordedLogicalMain == null) { |
| mainEvent = KeyDownEvent( |
| physicalKey: physicalKey, |
| logicalKey: logicalKey, |
| character: character, |
| timeStamp: timeStamp, |
| ); |
| physicalKeysPressed.add(physicalKey); |
| } else { |
| assert(physicalKeysPressed.contains(physicalKey)); |
| mainEvent = KeyRepeatEvent( |
| physicalKey: physicalKey, |
| logicalKey: recordedLogicalMain, |
| character: character, |
| timeStamp: timeStamp, |
| ); |
| } |
| } else { |
| assert(rawEvent is RawKeyUpEvent, 'Unexpected subclass of RawKeyEvent: ${rawEvent.runtimeType}'); |
| if (recordedLogicalMain == null) { |
| mainEvent = null; |
| } else { |
| mainEvent = KeyUpEvent( |
| logicalKey: recordedLogicalMain, |
| physicalKey: physicalKey, |
| timeStamp: timeStamp, |
| ); |
| physicalKeysPressed.remove(physicalKey); |
| } |
| } |
| for (final PhysicalKeyboardKey key in physicalKeysPressed.difference(_rawKeyboard.physicalKeysPressed)) { |
| if (key == physicalKey) { |
| // Somehow, a down event is dispatched but the key is absent from |
| // keysPressed. Synthesize a up event for the key, but this event must |
| // be added after the main key down event. |
| eventAfterwards.add(KeyUpEvent( |
| physicalKey: key, |
| logicalKey: logicalKey, |
| timeStamp: timeStamp, |
| synthesized: true, |
| )); |
| } else { |
| _keyEventsSinceLastMessage.add(KeyUpEvent( |
| physicalKey: key, |
| logicalKey: _hardwareKeyboard.lookUpLayout(key)!, |
| timeStamp: timeStamp, |
| synthesized: true, |
| )); |
| } |
| } |
| for (final PhysicalKeyboardKey key in _rawKeyboard.physicalKeysPressed.difference(physicalKeysPressed)) { |
| _keyEventsSinceLastMessage.add(KeyDownEvent( |
| physicalKey: key, |
| logicalKey: _rawKeyboard.lookUpLayout(key)!, |
| timeStamp: timeStamp, |
| synthesized: true, |
| )); |
| } |
| if (mainEvent != null) { |
| _keyEventsSinceLastMessage.add(mainEvent); |
| } |
| _keyEventsSinceLastMessage.addAll(eventAfterwards); |
| } |
| |
| /// Reset the inferred platform transit mode and related states. |
| /// |
| /// This method is only used in unit tests. In release mode, this is a no-op. |
| @visibleForTesting |
| void clearState() { |
| assert(() { |
| _transitMode = null; |
| _rawKeyboard.removeListener(_convertRawEventAndStore); |
| _keyEventsSinceLastMessage.clear(); |
| return true; |
| }()); |
| } |
| |
| static KeyEvent _eventFromData(ui.KeyData keyData) { |
| final PhysicalKeyboardKey physicalKey = |
| PhysicalKeyboardKey.findKeyByCode(keyData.physical) ?? |
| PhysicalKeyboardKey(keyData.physical); |
| final LogicalKeyboardKey logicalKey = |
| LogicalKeyboardKey.findKeyByKeyId(keyData.logical) ?? |
| LogicalKeyboardKey(keyData.logical); |
| final Duration timeStamp = keyData.timeStamp; |
| switch (keyData.type) { |
| case ui.KeyEventType.down: |
| return KeyDownEvent( |
| physicalKey: physicalKey, |
| logicalKey: logicalKey, |
| timeStamp: timeStamp, |
| character: keyData.character, |
| synthesized: keyData.synthesized, |
| ); |
| case ui.KeyEventType.up: |
| assert(keyData.character == null); |
| return KeyUpEvent( |
| physicalKey: physicalKey, |
| logicalKey: logicalKey, |
| timeStamp: timeStamp, |
| synthesized: keyData.synthesized, |
| ); |
| case ui.KeyEventType.repeat: |
| return KeyRepeatEvent( |
| physicalKey: physicalKey, |
| logicalKey: logicalKey, |
| timeStamp: timeStamp, |
| character: keyData.character, |
| ); |
| } |
| } |
| } |