| // 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 'keyboard_key.dart'; |
| import 'raw_keyboard.dart'; |
| |
| /// 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. |
| @immutable |
| class KeyboardLockMode { |
| // 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; |
| |
| /// 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. |
| static const KeyboardLockMode numLock = KeyboardLockMode._(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. |
| static const KeyboardLockMode scrollLock = KeyboardLockMode._(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. |
| static const KeyboardLockMode capsLock = KeyboardLockMode._(LogicalKeyboardKey.capsLock); |
| |
| 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, because on most platforms Flutter has to map the |
| /// platform representation back to an HID code since the original HID |
| /// code is not provided. USB HID is simply a conveniently well-defined |
| /// standard to list possible keys that a Flutter app can 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), generats 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 |
| /// synthronize 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 PhysicalKeyboardKey physicalKey, |
| required LogicalKeyboardKey logicalKey, |
| String? character, |
| required Duration timeStamp, |
| bool synthesized = false, |
| }) : super( |
| physicalKey: physicalKey, |
| logicalKey: logicalKey, |
| character: character, |
| timeStamp: timeStamp, |
| synthesized: 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 PhysicalKeyboardKey physicalKey, |
| required LogicalKeyboardKey logicalKey, |
| required Duration timeStamp, |
| bool synthesized = false, |
| }) : super( |
| physicalKey: physicalKey, |
| logicalKey: logicalKey, |
| timeStamp: timeStamp, |
| synthesized: synthesized, |
| ); |
| } |
| |
| /// An event indicating that the user has been holding a key on the keyboard |
| /// and causing repeated events. |
| /// |
| /// 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 PhysicalKeyboardKey physicalKey, |
| required LogicalKeyboardKey logicalKey, |
| String? character, |
| required Duration timeStamp, |
| }) : super( |
| physicalKey: physicalKey, |
| logicalKey: logicalKey, |
| character: character, |
| timeStamp: timeStamp, |
| ); |
| } |
| |
| /// The signature for [HardwareKeyboard.addHandler], a callback to 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); |
| } |
| } |
| |
| 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) { |
| FlutterError.reportError(FlutterErrorDetails( |
| exception: exception, |
| stack: stack, |
| library: 'services library', |
| context: ErrorDescription('while dispatching notifications for $runtimeType'), |
| informationCollector: () sync* { |
| yield DiagnosticsProperty<HardwareKeyboard>( |
| 'The $runtimeType sending notification was', |
| this, |
| style: DiagnosticsTreeStyle.errorProperty, |
| ); |
| }, |
| )); |
| } |
| } |
| _duringDispatch = false; |
| if (_modifiedHandlers != null) { |
| _handlers = _modifiedHandlers!; |
| _modifiedHandlers = null; |
| } |
| return handled; |
| } |
| |
| /// Process a new [KeyEvent] by recording the state changes and dispatching |
| /// to handlers. |
| bool handleKeyEvent(KeyEvent event) { |
| _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 |
| } |
| |
| 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 assumbled information corresponding to a native key message. |
| /// |
| /// While Flutter's [KeyEvent]s are created from key messages from the native |
| /// 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]. |
| /// |
| /// 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 |
| /// convenience of cross-platform code. |
| @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] 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] also resolves cross-platform compatibility of keyboard |
| /// implementations. Legacy platforms might have not implemented the new key |
| /// data API and only send raw key data on each key message. [KeyEventManager] |
| /// recognize platform types as [KeyDataTransitMode] and dispatches events in |
| /// different ways accordingly. |
| /// |
| /// [KeyEventManager] is typically created, owned, and invoked by |
| /// [ServicesBinding]. |
| class KeyEventManager { |
| /// Create an instance. |
| /// |
| /// This is typically only called by [ServicesBinding]. |
| KeyEventManager(this._hardwareKeyboard, this._rawKeyboard); |
| |
| /// The global handler for all hardware key messages of Flutter. |
| /// |
| /// Key messages received from the platform are first sent to [RawKeyboard]'s |
| /// listeners and [HardwareKeyboard]'s handlers, then sent to |
| /// [keyMessageHandler], regardless of the results of [HardwareKeyboard]'s |
| /// handlers. The result from the handlers and [keyMessageHandler] are |
| /// combined and returned to the platform. The handler result is explained below. |
| /// |
| /// This handler is normally set by the [FocusManager] so that it can control |
| /// the key event propagation to focused widgets. Applications that use the |
| /// focus system (see [Focus] and [FocusManager]) to receive key events |
| /// do not need to set this field. |
| /// |
| /// ## Handler result |
| /// |
| /// Key messages on the platform are given to Flutter to be handled by the |
| /// engine. If they are not handled, then the platform will continue to |
| /// distribute the keys (i.e. propagate them) to other (possibly non-Flutter) |
| /// components in the application. The return value from this handler tells |
| /// the platform to either stop propagation (by returning true: "event |
| /// handled"), or pass the event on to other controls (false: "event not |
| /// handled"). Some platforms might trigger special alerts if the event |
| /// is not handled by other controls either (such as the "bonk" noise on |
| /// macOS). |
| /// |
| /// If you are not using the [FocusManager] to manage focus, set this |
| /// attribute to a [KeyMessageHandler] that returns true if the propagation |
| /// on the platform should not be continued. Otherwise, key events will be |
| /// assumed to not have been handled by Flutter, and will also be sent to |
| /// other (possibly non-Flutter) controls in the application. |
| /// |
| /// See also: |
| /// |
| /// * [Focus.onKeyEvent], a [Focus] callback attribute that will be given |
| /// key events distributed by the [FocusManager] based on the current |
| /// primary focus. |
| /// * [HardwareKeyboard.addHandler], which accepts multiple handlers to |
| /// control the handler result but only accepts [KeyEvent] instead of |
| /// [KeyMessage]. |
| 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>[]; |
| |
| /// 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: |
| 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)); |
| } |
| 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>); |
| // The following `handleRawKeyEvent` will call `_convertRawEventAndStore` |
| // unless the event is not dispatched. |
| bool 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}'); |
| } |
| |
| if (keyMessageHandler != null) { |
| handled = keyMessageHandler!(KeyMessage(_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 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)) { |
| _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); |
| } |
| |
| /// 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, |
| ); |
| } |
| } |
| } |