Reland: Keyboard events (#87174)
diff --git a/dev/tools/gen_keycodes/data/keyboard_key.tmpl b/dev/tools/gen_keycodes/data/keyboard_key.tmpl
index eaed68c..e0b8f0f 100644
--- a/dev/tools/gen_keycodes/data/keyboard_key.tmpl
+++ b/dev/tools/gen_keycodes/data/keyboard_key.tmpl
@@ -327,6 +327,9 @@
@@@LOGICAL_KEY_DEFINITIONS@@@
+ /// A list of all predefined constant [LogicalKeyboardKey]s.
+ static Iterable<LogicalKeyboardKey> get knownLogicalKeys => _knownLogicalKeys.values;
+
// A list of all predefined constant LogicalKeyboardKeys so they can be
// searched.
static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{
@@ -489,6 +492,9 @@
@@@PHYSICAL_KEY_DEFINITIONS@@@
+ /// A list of all predefined constant [PhysicalKeyboardKey]s.
+ static Iterable<PhysicalKeyboardKey> get knownPhysicalKeys => _knownPhysicalKeys.values;
+
// A list of all the predefined constant PhysicalKeyboardKeys so that they
// can be searched.
static const Map<int, PhysicalKeyboardKey> _knownPhysicalKeys = <int, PhysicalKeyboardKey>{
diff --git a/dev/tools/gen_keycodes/data/windows_logical_to_window_vk.json b/dev/tools/gen_keycodes/data/windows_logical_to_window_vk.json
index e4803cb..5d8b68d 100644
--- a/dev/tools/gen_keycodes/data/windows_logical_to_window_vk.json
+++ b/dev/tools/gen_keycodes/data/windows_logical_to_window_vk.json
@@ -176,41 +176,5 @@
"Zoom": ["ZOOM"],
"Noname": ["NONAME"],
"Pa1": ["PA1"],
- "OemClear": ["OEM_CLEAR"],
- "0": ["0"],
- "1": ["1"],
- "2": ["2"],
- "3": ["3"],
- "4": ["4"],
- "5": ["5"],
- "6": ["6"],
- "7": ["7"],
- "8": ["8"],
- "9": ["9"],
- "KeyA": ["A"],
- "KeyB": ["B"],
- "KeyC": ["C"],
- "KeyD": ["D"],
- "KeyE": ["E"],
- "KeyF": ["F"],
- "KeyG": ["G"],
- "KeyH": ["H"],
- "KeyI": ["I"],
- "KeyJ": ["J"],
- "KeyK": ["K"],
- "KeyL": ["L"],
- "KeyM": ["M"],
- "KeyN": ["N"],
- "KeyO": ["O"],
- "KeyP": ["P"],
- "KeyQ": ["Q"],
- "KeyR": ["R"],
- "KeyS": ["S"],
- "KeyT": ["T"],
- "KeyU": ["U"],
- "KeyV": ["V"],
- "KeyW": ["W"],
- "KeyX": ["X"],
- "KeyY": ["Y"],
- "KeyZ": ["Z"]
+ "OemClear": ["OEM_CLEAR"]
}
diff --git a/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart b/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
index 6a21b99..27eee82 100644
--- a/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
+++ b/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
@@ -24,6 +24,16 @@
|| (charCode >= charLowerA && charCode <= charLowerZ);
}
+bool _isDigit(String? char) {
+ if (char == null)
+ return false;
+ final int charDigit0 = '0'.codeUnitAt(0);
+ final int charDigit9 = '9'.codeUnitAt(0);
+ assert(char.length == 1);
+ final int charCode = char.codeUnitAt(0);
+ return charCode >= charDigit0 && charCode <= charDigit9;
+}
+
/// Generates the keyboard_maps.dart files, based on the information in the key
/// data structure given to it.
class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
@@ -171,7 +181,9 @@
// because they are not used by the embedding. Add them manually.
final List<int>? keyCodes = entry.windowsValues.isNotEmpty
? entry.windowsValues
- : (_isAsciiLetter(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] : null);
+ : (_isAsciiLetter(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] :
+ _isDigit(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] :
+ null);
if (keyCodes != null) {
for (final int code in keyCodes) {
lines.add(code, ' $code: LogicalKeyboardKey.${entry.constantName},');
diff --git a/packages/flutter/lib/services.dart b/packages/flutter/lib/services.dart
index e6a266f..0d7ec01 100644
--- a/packages/flutter/lib/services.dart
+++ b/packages/flutter/lib/services.dart
@@ -15,9 +15,11 @@
export 'src/services/binary_messenger.dart';
export 'src/services/binding.dart';
export 'src/services/clipboard.dart';
+export 'src/services/debug.dart';
export 'src/services/deferred_component.dart';
export 'src/services/font_loader.dart';
export 'src/services/haptic_feedback.dart';
+export 'src/services/hardware_keyboard.dart';
export 'src/services/keyboard_key.dart';
export 'src/services/keyboard_maps.dart';
export 'src/services/message_codec.dart';
diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart
index b5d2c39..c7858db 100644
--- a/packages/flutter/lib/src/services/binding.dart
+++ b/packages/flutter/lib/src/services/binding.dart
@@ -14,7 +14,9 @@
import 'asset_bundle.dart';
import 'binary_messenger.dart';
+import 'hardware_keyboard.dart';
import 'message_codec.dart';
+import 'raw_keyboard.dart';
import 'restoration.dart';
import 'system_channels.dart';
@@ -31,6 +33,7 @@
_instance = this;
_defaultBinaryMessenger = createBinaryMessenger();
_restorationManager = createRestorationManager();
+ _initKeyboard();
initLicenses();
SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object));
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
@@ -42,6 +45,23 @@
static ServicesBinding? get instance => _instance;
static ServicesBinding? _instance;
+ /// The global singleton instance of [HardwareKeyboard], which can be used to
+ /// query keyboard states.
+ HardwareKeyboard get keyboard => _keyboard;
+ late final HardwareKeyboard _keyboard;
+
+ /// The global singleton instance of [KeyEventManager], which is used
+ /// internally to dispatch key messages.
+ KeyEventManager get keyEventManager => _keyEventManager;
+ late final KeyEventManager _keyEventManager;
+
+ void _initKeyboard() {
+ _keyboard = HardwareKeyboard();
+ _keyEventManager = KeyEventManager(_keyboard, RawKeyboard.instance);
+ window.onKeyData = _keyEventManager.handleKeyData;
+ SystemChannels.keyEvent.setMessageHandler(_keyEventManager.handleRawKeyMessage);
+ }
+
/// The default instance of [BinaryMessenger].
///
/// This is used to send messages from the application to the platform, and
diff --git a/packages/flutter/lib/src/services/debug.dart b/packages/flutter/lib/src/services/debug.dart
new file mode 100644
index 0000000..3e0421a
--- /dev/null
+++ b/packages/flutter/lib/src/services/debug.dart
@@ -0,0 +1,30 @@
+// 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 'package:flutter/foundation.dart';
+
+import 'hardware_keyboard.dart';
+
+/// Override the transit mode with which key events are simulated.
+///
+/// Setting [debugKeyEventSimulatorTransitModeOverride] is a good way to make
+/// certain tests simulate the behavior of different type of platforms in terms
+/// of their extent of support for keyboard API.
+KeyDataTransitMode? debugKeyEventSimulatorTransitModeOverride;
+
+/// Returns true if none of the widget library debug variables have been changed.
+///
+/// This function is used by the test framework to ensure that debug variables
+/// haven't been inadvertently changed.
+///
+/// See [the services library](services/services-library.html) for a complete list.
+bool debugAssertAllServicesVarsUnset(String reason) {
+ assert(() {
+ if (debugKeyEventSimulatorTransitModeOverride != null) {
+ throw FlutterError(reason);
+ }
+ return true;
+ }());
+ return true;
+}
diff --git a/packages/flutter/lib/src/services/hardware_keyboard.dart b/packages/flutter/lib/src/services/hardware_keyboard.dart
new file mode 100644
index 0000000..dc57992
--- /dev/null
+++ b/packages/flutter/lib/src/services/hardware_keyboard.dart
@@ -0,0 +1,941 @@
+// 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:
+ // Postpone key event dispatching until the handleRawKeyMessage.
+ _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(physicalKey)!,
+ timeStamp: timeStamp,
+ synthesized: true,
+ ));
+ }
+ for (final PhysicalKeyboardKey key in _rawKeyboard.physicalKeysPressed.difference(physicalKeysPressed)) {
+ _keyEventsSinceLastMessage.add(KeyDownEvent(
+ physicalKey: key,
+ logicalKey: _rawKeyboard.lookUpLayout(physicalKey)!,
+ 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,
+ );
+ }
+ }
+}
diff --git a/packages/flutter/lib/src/services/keyboard_key.dart b/packages/flutter/lib/src/services/keyboard_key.dart
index de4d6cd..7262413 100644
--- a/packages/flutter/lib/src/services/keyboard_key.dart
+++ b/packages/flutter/lib/src/services/keyboard_key.dart
@@ -2624,6 +2624,9 @@
/// See the function [RawKeyEvent.logicalKey] for more information.
static const LogicalKeyboardKey gameButtonZ = LogicalKeyboardKey(0x0020000031f);
+ /// A list of all predefined constant [LogicalKeyboardKey]s.
+ static Iterable<LogicalKeyboardKey> get knownLogicalKeys => _knownLogicalKeys.values;
+
// A list of all predefined constant LogicalKeyboardKeys so they can be
// searched.
static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{
@@ -5136,6 +5139,9 @@
/// See the function [RawKeyEvent.physicalKey] for more information.
static const PhysicalKeyboardKey showAllWindows = PhysicalKeyboardKey(0x000c029f);
+ /// A list of all predefined constant [PhysicalKeyboardKey]s.
+ static Iterable<PhysicalKeyboardKey> get knownPhysicalKeys => _knownPhysicalKeys.values;
+
// A list of all the predefined constant PhysicalKeyboardKeys so that they
// can be searched.
static const Map<int, PhysicalKeyboardKey> _knownPhysicalKeys = <int, PhysicalKeyboardKey>{
diff --git a/packages/flutter/lib/src/services/keyboard_maps.dart b/packages/flutter/lib/src/services/keyboard_maps.dart
index d8940f1..74cea74 100644
--- a/packages/flutter/lib/src/services/keyboard_maps.dart
+++ b/packages/flutter/lib/src/services/keyboard_maps.dart
@@ -2953,6 +2953,16 @@
45: LogicalKeyboardKey.insert,
46: LogicalKeyboardKey.delete,
47: LogicalKeyboardKey.help,
+ 48: LogicalKeyboardKey.digit0,
+ 49: LogicalKeyboardKey.digit1,
+ 50: LogicalKeyboardKey.digit2,
+ 51: LogicalKeyboardKey.digit3,
+ 52: LogicalKeyboardKey.digit4,
+ 53: LogicalKeyboardKey.digit5,
+ 54: LogicalKeyboardKey.digit6,
+ 55: LogicalKeyboardKey.digit7,
+ 56: LogicalKeyboardKey.digit8,
+ 57: LogicalKeyboardKey.digit9,
65: LogicalKeyboardKey.keyA,
66: LogicalKeyboardKey.keyB,
67: LogicalKeyboardKey.keyC,
diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart
index ccacfea..ba67416 100644
--- a/packages/flutter/lib/src/services/raw_keyboard.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard.dart
@@ -3,10 +3,12 @@
// found in the LICENSE file.
import 'dart:io';
-import 'dart:ui';
+import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
+import 'binding.dart';
+import 'hardware_keyboard.dart';
import 'keyboard_key.dart';
import 'raw_keyboard_android.dart';
import 'raw_keyboard_fuchsia.dart';
@@ -286,17 +288,20 @@
final RawKeyEventData data;
String? character;
- if (kIsWeb) {
+ RawKeyEventData _dataFromWeb() {
final String? key = message['key'] as String?;
- data = RawKeyEventDataWeb(
+ if (key != null && key.isNotEmpty) {
+ character = key;
+ }
+ return RawKeyEventDataWeb(
code: message['code'] as String? ?? '',
key: key ?? '',
location: message['location'] as int? ?? 0,
metaState: message['metaState'] as int? ?? 0,
);
- if (key != null && key.isNotEmpty) {
- character = key;
- }
+ }
+ if (kIsWeb) {
+ data = _dataFromWeb();
} else {
final String keymap = message['keymap'] as String;
switch (keymap) {
@@ -373,16 +378,7 @@
}
break;
case 'web':
- final String? key = message['key'] as String?;
- data = RawKeyEventDataWeb(
- code: message['code'] as String? ?? '',
- key: key ?? '',
- location: message['location'] as int? ?? 0,
- metaState: message['metaState'] as int? ?? 0,
- );
- if (key != null && key.isNotEmpty) {
- character = key;
- }
+ data = _dataFromWeb();
break;
default:
/// This exception would only be hit on platforms that haven't yet
@@ -573,9 +569,7 @@
/// * [SystemChannels.keyEvent], the low-level channel used for receiving
/// events from the system.
class RawKeyboard {
- RawKeyboard._() {
- SystemChannels.keyEvent.setMessageHandler(_handleKeyEvent);
- }
+ RawKeyboard._();
/// The shared instance of [RawKeyboard].
static final RawKeyboard instance = RawKeyboard._();
@@ -606,39 +600,45 @@
_listeners.remove(listener);
}
- /// A handler for hardware keyboard events that will stop propagation if the
- /// handler returns true.
+ /// A handler for raw hardware keyboard events that will stop propagation if
+ /// the handler returns true.
///
- /// Key events 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").
- ///
- /// This handler is normally set by the [FocusManager] so that it can control
- /// the key event propagation to focused widgets.
- ///
- /// Most applications can use the focus system (see [Focus] and
- /// [FocusManager]) to receive key events. If you are not using the
- /// [FocusManager] to manage focus, then to be able to stop propagation of the
- /// event by indicating that the event was handled, set this attribute to a
- /// [RawKeyEventHandler]. 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.onKey], a [Focus] callback attribute that will be given key
- /// events distributed by the [FocusManager] based on the current primary
- /// focus.
- /// * [addListener], to add passive key event listeners that do not stop event
- /// propagation.
- RawKeyEventHandler? keyEventHandler;
+ /// This property is only a wrapper over [KeyEventManager.keyMessageHandler],
+ /// and is kept only for backward compatibility. New code should use
+ /// [KeyEventManager.keyMessageHandler] to set custom global key event
+ /// handler. Setting [keyEventHandler] will cause
+ /// [KeyEventManager.keyMessageHandler] to be set with a converted handler.
+ /// If [KeyEventManager.keyMessageHandler] is set by [FocusManager] (the most
+ /// common situation), then the exact value of [keyEventHandler] is a dummy
+ /// callback and must not be invoked.
+ RawKeyEventHandler? get keyEventHandler {
+ if (ServicesBinding.instance!.keyEventManager.keyMessageHandler != _cachedKeyMessageHandler) {
+ _cachedKeyMessageHandler = ServicesBinding.instance!.keyEventManager.keyMessageHandler;
+ _cachedKeyEventHandler = _cachedKeyMessageHandler == null ?
+ null :
+ (RawKeyEvent event) {
+ assert(false,
+ 'The RawKeyboard.instance.keyEventHandler assigned by Flutter is a dummy '
+ 'callback kept for compatibility and should not be directly called. Use '
+ 'ServicesBinding.instance!.keyMessageHandler instead.');
+ return true;
+ };
+ }
+ return _cachedKeyEventHandler;
+ }
+ RawKeyEventHandler? _cachedKeyEventHandler;
+ KeyMessageHandler? _cachedKeyMessageHandler;
+ set keyEventHandler(RawKeyEventHandler? handler) {
+ _cachedKeyEventHandler = handler;
+ _cachedKeyMessageHandler = handler == null ?
+ null :
+ (KeyMessage message) => handler(message.rawEvent);
+ ServicesBinding.instance!.keyEventManager.keyMessageHandler = _cachedKeyMessageHandler;
+ }
- Future<dynamic> _handleKeyEvent(dynamic message) async {
- final RawKeyEvent event = RawKeyEvent.fromMessage(message as Map<String, dynamic>);
+ /// Process a new [RawKeyEvent] by recording the state changes and
+ /// dispatching to listeners.
+ bool handleRawKeyEvent(RawKeyEvent event) {
bool shouldDispatch = true;
if (event is RawKeyDownEvent) {
if (event.data.shouldDispatchEvent()) {
@@ -659,7 +659,7 @@
}
}
if (!shouldDispatch) {
- return <String, dynamic>{ 'handled': true };
+ return true;
}
// Make sure that the modifiers reflect reality, in case a modifier key was
// pressed/released while the app didn't have focus.
@@ -678,12 +678,7 @@
}
}
- // Send the key event to the keyEventHandler, then send the appropriate
- // response to the platform so that it can resolve the event's handling.
- // Defaults to false if keyEventHandler is null.
- final bool handled = keyEventHandler != null && keyEventHandler!(event);
- assert(handled != null, 'keyEventHandler returned null, which is not allowed');
- return <String, dynamic>{ 'handled': handled };
+ return false;
}
static final Map<_ModifierSidePair, Set<PhysicalKeyboardKey>> _modifierKeyMap = <_ModifierSidePair, Set<PhysicalKeyboardKey>>{
@@ -808,6 +803,11 @@
/// Returns the set of physical keys currently pressed.
Set<PhysicalKeyboardKey> get physicalKeysPressed => _keysPressed.keys.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) => _keysPressed[physicalKey];
+
/// Clears the list of keys returned from [keysPressed].
///
/// This is used by the testing framework to make sure tests are hermetic.
@@ -832,5 +832,5 @@
}
@override
- int get hashCode => hashValues(modifier, side);
+ int get hashCode => ui.hashValues(modifier, side);
}
diff --git a/packages/flutter/lib/src/services/raw_keyboard_web.dart b/packages/flutter/lib/src/services/raw_keyboard_web.dart
index 257346d..70e2b94 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_web.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_web.dart
@@ -83,13 +83,11 @@
@override
LogicalKeyboardKey get logicalKey {
- // Look to see if the keyCode is a printable number pad key, so that a
- // difference between regular keys (e.g. ".") and the number pad version
- // (e.g. the "." on the number pad) can be determined.
- final LogicalKeyboardKey? numPadKey = kWebNumPadMap[code];
- if (numPadKey != null) {
- return numPadKey;
- }
+ // Look to see if the keyCode is a key based on location. Typically they are
+ // numpad keys (versus main area keys) and left/right modifiers.
+ final LogicalKeyboardKey? maybeLocationKey = kWebLocationMap[key]?[location];
+ if (maybeLocationKey != null)
+ return maybeLocationKey;
// Look to see if the [code] is one we know about and have a mapping for.
final LogicalKeyboardKey? newKey = kWebToLogicalKey[code];
diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart
index 6c442f7..cfd2100 100644
--- a/packages/flutter/lib/src/widgets/focus_manager.dart
+++ b/packages/flutter/lib/src/widgets/focus_manager.dart
@@ -36,7 +36,7 @@
}
/// An enum that describes how to handle a key event handled by a
-/// [FocusOnKeyCallback].
+/// [FocusOnKeyCallback] or [FocusOnKeyEventCallback].
enum KeyEventResult {
/// The key event has been handled, and the event should not be propagated to
/// other key event handlers.
@@ -52,6 +52,32 @@
skipRemainingHandlers,
}
+/// Combine the results returned by multiple [FocusOnKeyCallback]s or
+/// [FocusOnKeyEventCallback]s.
+///
+/// If any callback returns [KeyEventResult.handled], the node considers the
+/// message handled; otherwise, if any callback returns
+/// [KeyEventResult.skipRemainingHandlers], the node skips the remaining
+/// handlers without preventing the platform to handle; otherwise the node is
+/// ignored.
+KeyEventResult combineKeyEventResults(Iterable<KeyEventResult> results) {
+ bool hasSkipRemainingHandlers = false;
+ for (final KeyEventResult result in results) {
+ switch (result) {
+ case KeyEventResult.handled:
+ return KeyEventResult.handled;
+ case KeyEventResult.skipRemainingHandlers:
+ hasSkipRemainingHandlers = true;
+ break;
+ default:
+ break;
+ }
+ }
+ return hasSkipRemainingHandlers ?
+ KeyEventResult.skipRemainingHandlers :
+ KeyEventResult.ignored;
+}
+
/// Signature of a callback used by [Focus.onKey] and [FocusScope.onKey]
/// to receive key events.
///
@@ -61,6 +87,15 @@
/// was handled.
typedef FocusOnKeyCallback = KeyEventResult Function(FocusNode node, RawKeyEvent event);
+/// Signature of a callback used by [Focus.onKeyEvent] and [FocusScope.onKeyEvent]
+/// to receive key events.
+///
+/// The [node] is the node that received the event.
+///
+/// Returns a [KeyEventResult] that describes how, and whether, the key event
+/// was handled.
+typedef FocusOnKeyEventCallback = KeyEventResult Function(FocusNode node, KeyEvent event);
+
/// An attachment point for a [FocusNode].
///
/// Using a [FocusAttachment] is rarely needed, unless you are building
@@ -271,12 +306,13 @@
/// {@template flutter.widgets.FocusNode.keyEvents}
/// ## Key Event Propagation
///
-/// The [FocusManager] receives key events from [RawKeyboard] and will pass them
-/// to the focused nodes. It starts with the node with the primary focus, and
-/// will call the [onKey] callback for that node. If the callback returns false,
-/// indicating that it did not handle the event, the [FocusManager] will move to
-/// the parent of that node and call its [onKey]. If that [onKey] returns true,
-/// then it will stop propagating the event. If it reaches the root
+/// The [FocusManager] receives key events from [RawKeyboard] and
+/// [HardwareKeyboard] and will pass them to the focused nodes. It starts with
+/// the node with the primary focus, and will call the [onKey] or [onKeyEvent]
+/// callback for that node. If the callback returns false, indicating that it did
+/// not handle the event, the [FocusManager] will move to the parent of that node
+/// and call its [onKey] or [onKeyEvent]. If that [onKey] or [onKeyEvent] returns
+/// true, then it will stop propagating the event. If it reaches the root
/// [FocusScopeNode], [FocusManager.rootScope], the event is discarded.
/// {@endtemplate}
///
@@ -433,9 +469,14 @@
///
/// The [skipTraversal], [descendantsAreFocusable], and [canRequestFocus]
/// arguments must not be null.
+ ///
+ /// To receive key events that focuses on this node, pass a listener to `onKeyEvent`.
+ /// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated
+ /// in the future.
FocusNode({
String? debugLabel,
this.onKey,
+ this.onKeyEvent,
bool skipTraversal = false,
bool canRequestFocus = true,
bool descendantsAreFocusable = true,
@@ -574,9 +615,18 @@
/// Called if this focus node receives a key event while focused (i.e. when
/// [hasFocus] returns true).
///
+ /// This is a legacy API based on [RawKeyEvent] and will be deprecated in the
+ /// future. Prefer [onKeyEvent] instead.
+ ///
/// {@macro flutter.widgets.FocusNode.keyEvents}
FocusOnKeyCallback? onKey;
+ /// Called if this focus node receives a key event while focused (i.e. when
+ /// [hasFocus] returns true).
+ ///
+ /// {@macro flutter.widgets.FocusNode.keyEvents}
+ FocusOnKeyEventCallback? onKeyEvent;
+
FocusManager? _manager;
List<FocusNode>? _ancestors;
List<FocusNode>? _descendants;
@@ -1028,10 +1078,19 @@
/// need to be attached. [FocusAttachment.detach] should be called on the old
/// node, and then [attach] called on the new node. This typically happens in
/// the [State.didUpdateWidget] method.
+ ///
+ /// To receive key events that focuses on this node, pass a listener to `onKeyEvent`.
+ /// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated
+ /// in the future.
@mustCallSuper
- FocusAttachment attach(BuildContext? context, {FocusOnKeyCallback? onKey}) {
+ FocusAttachment attach(
+ BuildContext? context, {
+ FocusOnKeyEventCallback? onKeyEvent,
+ FocusOnKeyCallback? onKey,
+ }) {
_context = context;
this.onKey = onKey ?? this.onKey;
+ this.onKeyEvent = onKeyEvent ?? this.onKeyEvent;
_attachment = FocusAttachment._(this);
return _attachment!;
}
@@ -1225,6 +1284,7 @@
/// All parameters are optional.
FocusScopeNode({
String? debugLabel,
+ FocusOnKeyEventCallback? onKeyEvent,
FocusOnKeyCallback? onKey,
bool skipTraversal = false,
bool canRequestFocus = true,
@@ -1232,6 +1292,7 @@
assert(canRequestFocus != null),
super(
debugLabel: debugLabel,
+ onKeyEvent: onKeyEvent,
onKey: onKey,
canRequestFocus: canRequestFocus,
descendantsAreFocusable: true,
@@ -1463,15 +1524,15 @@
/// When this focus manager is no longer needed, calling [dispose] on it will
/// unregister these handlers.
void registerGlobalHandlers() {
- assert(RawKeyboard.instance.keyEventHandler == null);
- RawKeyboard.instance.keyEventHandler = _handleRawKeyEvent;
+ assert(ServicesBinding.instance!.keyEventManager.keyMessageHandler == null);
+ ServicesBinding.instance!.keyEventManager.keyMessageHandler = _handleKeyMessage;
GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent);
}
@override
void dispose() {
- if (RawKeyboard.instance.keyEventHandler == _handleRawKeyEvent) {
- RawKeyboard.instance.keyEventHandler = null;
+ if (ServicesBinding.instance!.keyEventManager.keyMessageHandler == _handleKeyMessage) {
+ ServicesBinding.instance!.keyEventManager.keyMessageHandler = null;
GestureBinding.instance!.pointerRouter.removeGlobalRoute(_handlePointerEvent);
}
super.dispose();
@@ -1660,15 +1721,15 @@
}
}
- bool _handleRawKeyEvent(RawKeyEvent event) {
+ bool _handleKeyMessage(KeyMessage message) {
// Update highlightMode first, since things responding to the keys might
// look at the highlight mode, and it should be accurate.
_lastInteractionWasTouch = false;
_updateHighlightMode();
- assert(_focusDebug('Received key event ${event.logicalKey}'));
+ assert(_focusDebug('Received key event $message'));
if (_primaryFocus == null) {
- assert(_focusDebug('No primary focus for key event, ignored: $event'));
+ assert(_focusDebug('No primary focus for key event, ignored: $message'));
return false;
}
@@ -1677,25 +1738,35 @@
// stop propagation, stop.
bool handled = false;
for (final FocusNode node in <FocusNode>[_primaryFocus!, ..._primaryFocus!.ancestors]) {
- if (node.onKey != null) {
- final KeyEventResult result = node.onKey!(node, event);
- switch (result) {
- case KeyEventResult.handled:
- assert(_focusDebug('Node $node handled key event $event.'));
- handled = true;
- break;
- case KeyEventResult.skipRemainingHandlers:
- assert(_focusDebug('Node $node stopped key event propagation: $event.'));
- handled = false;
- break;
- case KeyEventResult.ignored:
- continue;
+ final List<KeyEventResult> results = <KeyEventResult>[];
+ if (node.onKeyEvent != null) {
+ for (final KeyEvent event in message.events) {
+ results.add(node.onKeyEvent!(node, event));
}
- break;
}
+ if (node.onKey != null) {
+ results.add(node.onKey!(node, message.rawEvent));
+ }
+ final KeyEventResult result = combineKeyEventResults(results);
+ switch (result) {
+ case KeyEventResult.ignored:
+ continue;
+ case KeyEventResult.handled:
+ assert(_focusDebug('Node $node handled key event $message.'));
+ handled = true;
+ break;
+ case KeyEventResult.skipRemainingHandlers:
+ assert(_focusDebug('Node $node stopped key event propagation: $message.'));
+ handled = false;
+ break;
+ }
+ // Only KeyEventResult.ignored will continue the for loop. All other
+ // options will stop the event propagation.
+ assert(result != KeyEventResult.ignored);
+ break;
}
if (!handled) {
- assert(_focusDebug('Key event not handled by anyone: $event.'));
+ assert(_focusDebug('Key event not handled by anyone: $message.'));
}
return handled;
}
diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart
index 25c171e..673429a 100644
--- a/packages/flutter/lib/src/widgets/focus_scope.dart
+++ b/packages/flutter/lib/src/widgets/focus_scope.dart
@@ -283,6 +283,7 @@
this.autofocus = false,
this.onFocusChange,
this.onKey,
+ this.onKeyEvent,
this.debugLabel,
this.canRequestFocus,
this.descendantsAreFocusable = true,
@@ -315,6 +316,24 @@
/// focus.
///
/// Key events are first given to the [FocusNode] that has primary focus, and
+ /// if its [onKeyEvent] method return false, then they are given to each
+ /// ancestor node up the focus hierarchy in turn. If an event reaches the root
+ /// of the hierarchy, it is discarded.
+ ///
+ /// This is not the way to get text input in the manner of a text field: it
+ /// leaves out support for input method editors, and doesn't support soft
+ /// keyboards in general. For text input, consider [TextField],
+ /// [EditableText], or [CupertinoTextField] instead, which do support these
+ /// things.
+ final FocusOnKeyEventCallback? onKeyEvent;
+
+ /// Handler for keys pressed when this object or one of its children has
+ /// focus.
+ ///
+ /// This is a legacy API based on [RawKeyEvent] and will be deprecated in the
+ /// future. Prefer [onKeyEvent] instead.
+ ///
+ /// Key events are first given to the [FocusNode] that has primary focus, and
/// if its [onKey] method return false, then they are given to each ancestor
/// node up the focus hierarchy in turn. If an event reaches the root of the
/// hierarchy, it is discarded.
@@ -540,7 +559,7 @@
}
@override
- State<Focus> createState() => _FocusState();
+ State<Focus> createState() => _FocusState();
}
class _FocusState extends State<Focus> {
@@ -575,7 +594,7 @@
_canRequestFocus = focusNode.canRequestFocus;
_descendantsAreFocusable = focusNode.descendantsAreFocusable;
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
- _focusAttachment = focusNode.attach(context, onKey: widget.onKey);
+ _focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey);
// Add listener even if the _internalNode existed before, since it should
// not be listening now if we're re-using a previous one because it should
@@ -914,6 +933,7 @@
ValueChanged<bool>? onFocusChange,
bool? canRequestFocus,
bool? skipTraversal,
+ FocusOnKeyEventCallback? onKeyEvent,
FocusOnKeyCallback? onKey,
String? debugLabel,
}) : assert(child != null),
@@ -926,6 +946,7 @@
onFocusChange: onFocusChange,
canRequestFocus: canRequestFocus,
skipTraversal: skipTraversal,
+ onKeyEvent: onKeyEvent,
onKey: onKey,
debugLabel: debugLabel,
);
diff --git a/packages/flutter/lib/src/widgets/keyboard_listener.dart b/packages/flutter/lib/src/widgets/keyboard_listener.dart
new file mode 100644
index 0000000..6c1bb82
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/keyboard_listener.dart
@@ -0,0 +1,95 @@
+// 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 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+import 'focus_manager.dart';
+import 'focus_scope.dart';
+import 'framework.dart';
+
+export 'package:flutter/services.dart' show KeyEvent;
+
+/// A widget that calls a callback whenever the user presses or releases a key
+/// on a keyboard.
+///
+/// A [KeyboardListener] is useful for listening to key events and
+/// hardware buttons that are represented as keys. Typically used by games and
+/// other apps that use keyboards for purposes other than text entry.
+///
+/// For text entry, consider using a [EditableText], which integrates with
+/// on-screen keyboards and input method editors (IMEs).
+///
+/// The [KeyboardListener] is different from [RawKeyboardListener] in that
+/// [KeyboardListener] uses the newer [HardwareKeyboard] API, which is
+/// preferrable.
+///
+/// See also:
+///
+/// * [EditableText], which should be used instead of this widget for text
+/// entry.
+/// * [RawKeyboardListener], a similar widget based on the old [RawKeyboard]
+/// API.
+class KeyboardListener extends StatelessWidget {
+ /// Creates a widget that receives keyboard events.
+ ///
+ /// For text entry, consider using a [EditableText], which integrates with
+ /// on-screen keyboards and input method editors (IMEs).
+ ///
+ /// The [focusNode] and [child] arguments are required and must not be null.
+ ///
+ /// The [autofocus] argument must not be null.
+ ///
+ /// The `key` is an identifier for widgets, and is unrelated to keyboards.
+ /// See [Widget.key].
+ const KeyboardListener({
+ Key? key,
+ required this.focusNode,
+ this.autofocus = false,
+ this.includeSemantics = true,
+ this.onKeyEvent,
+ required this.child,
+ }) : assert(focusNode != null),
+ assert(autofocus != null),
+ assert(includeSemantics != null),
+ assert(child != null),
+ super(key: key);
+
+ /// Controls whether this widget has keyboard focus.
+ final FocusNode focusNode;
+
+ /// {@macro flutter.widgets.Focus.autofocus}
+ final bool autofocus;
+
+ /// {@macro flutter.widgets.Focus.includeSemantics}
+ final bool includeSemantics;
+
+ /// Called whenever this widget receives a keyboard event.
+ final ValueChanged<KeyEvent>? onKeyEvent;
+
+ /// The widget below this widget in the tree.
+ ///
+ /// {@macro flutter.widgets.ProxyWidget.child}
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ return Focus(
+ focusNode: focusNode,
+ autofocus: autofocus,
+ includeSemantics: includeSemantics,
+ onKeyEvent: (FocusNode node, KeyEvent event) {
+ onKeyEvent?.call(event);
+ return KeyEventResult.ignored;
+ },
+ child: child,
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
+ }
+}
diff --git a/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart b/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
index 21d7e33..db8f5be 100644
--- a/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
+++ b/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
@@ -21,10 +21,16 @@
/// For text entry, consider using a [EditableText], which integrates with
/// on-screen keyboards and input method editors (IMEs).
///
+/// The [RawKeyboardListener] is different from [KeyboardListener] in that
+/// [RawKeyboardListener] uses the legacy [RawKeyboard] API. Use
+/// [KeyboardListener] if possible.
+///
/// See also:
///
/// * [EditableText], which should be used instead of this widget for text
/// entry.
+/// * [KeyboardListener], a similar widget based on the newer
+/// [HardwareKeyboard] API.
class RawKeyboardListener extends StatefulWidget {
/// Creates a widget that receives raw keyboard events.
///
diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart
index 36297c8..941f7e9 100644
--- a/packages/flutter/lib/src/widgets/shortcuts.dart
+++ b/packages/flutter/lib/src/widgets/shortcuts.dart
@@ -194,13 +194,13 @@
/// event.
///
/// For example, for `Ctrl-A`, it has to check if the event is a
- /// [RawKeyDownEvent], if either side of the Ctrl key is pressed, and none of
+ /// [KeyDownEvent], if either side of the Ctrl key is pressed, and none of
/// the Shift keys, Alt keys, or Meta keys are pressed; it doesn't have to
/// check if KeyA is pressed, since it's already guaranteed.
///
/// This method must not cause any side effects for the `state`. Typically
- /// this is only used to query whether [RawKeyboard.keysPressed] contains
- /// a key.
+ /// this is only used to query whether [HardwareKeyboard.logicalKeysPressed]
+ /// contains a key.
///
/// Since [ShortcutActivator] accepts all event types, subclasses might want
/// to check the event type in [accepts].
@@ -314,11 +314,13 @@
@override
bool accepts(RawKeyEvent event, RawKeyboard state) {
+ if (event is! RawKeyDownEvent)
+ return false;
final Set<LogicalKeyboardKey> collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys);
final Set<LogicalKeyboardKey> collapsedPressed = LogicalKeyboardKey.collapseSynonyms(state.keysPressed);
final bool keysEqual = collapsedRequired.difference(collapsedPressed).isEmpty
&& collapsedRequired.length == collapsedPressed.length;
- return event is RawKeyDownEvent && keysEqual;
+ return keysEqual;
}
static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{
@@ -425,7 +427,8 @@
/// * [CharacterActivator], an activator that represents key combinations
/// that result in the specified character, such as question mark.
class SingleActivator with Diagnosticable implements ShortcutActivator {
- /// Create an activator of a trigger key and modifiers.
+ /// Triggered when the [trigger] key is pressed or repeated when the
+ /// modifiers are held.
///
/// The `trigger` should be the non-modifier key that is pressed after all the
/// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not be
@@ -434,6 +437,9 @@
/// The `control`, `shift`, `alt`, and `meta` flags represent whether
/// the respect modifier keys should be held (true) or released (false)
///
+ /// On each [RawKeyDownEvent] of the [trigger] key, this activator checks
+ /// whether the specified modifier conditions are met.
+ ///
/// {@tool dartpad --template=stateful_widget_scaffold_center}
/// In the following example, the shortcut `Control + C` increases the counter:
///
@@ -811,17 +817,7 @@
/// must be mapped to an [Action], and the [Action] must be enabled.
@protected
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
- if (event is! RawKeyDownEvent) {
- return KeyEventResult.ignored;
- }
assert(context != null);
- assert(
- RawKeyboard.instance.keysPressed.isNotEmpty,
- 'Received a key down event when no keys are in keysPressed. '
- "This state can occur if the key event being sent doesn't properly "
- 'set its modifier flags. This was the event: $event and its data: '
- '${event.data}',
- );
final Intent? matchedIntent = _find(event, RawKeyboard.instance);
if (matchedIntent != null) {
final BuildContext? primaryContext = primaryFocus?.context;
@@ -1285,4 +1281,4 @@
child: child,
);
}
-}
\ No newline at end of file
+}
diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart
index c4205df..ed09312 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -63,6 +63,7 @@
export 'src/widgets/inherited_notifier.dart';
export 'src/widgets/inherited_theme.dart';
export 'src/widgets/interactive_viewer.dart';
+export 'src/widgets/keyboard_listener.dart';
export 'src/widgets/layout_builder.dart';
export 'src/widgets/list_wheel_scroll_view.dart';
export 'src/widgets/localizations.dart';
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart
index 085a180..ed2c875 100644
--- a/packages/flutter/test/cupertino/text_field_test.dart
+++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -4348,7 +4348,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusNode3.hasPrimaryFocus, isTrue);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async {
bool scrollInvoked = false;
@@ -4381,7 +4381,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(scrollInvoked, isFalse);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cupertino text field semantics', (WidgetTester tester) async {
await tester.pumpWidget(
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index d1e5255..176094c 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -4731,7 +4731,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Shift test 2', (WidgetTester tester) async {
await setupWidget(tester);
@@ -4749,7 +4749,7 @@
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Control Shift test', (WidgetTester tester) async {
await setupWidget(tester);
@@ -4766,7 +4766,7 @@
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test', (WidgetTester tester) async {
await setupWidget(tester);
@@ -4793,7 +4793,7 @@
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test 2', (WidgetTester tester) async {
await setupWidget(tester);
@@ -4849,7 +4849,7 @@
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Read only keyboard selection test', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'readonly');
@@ -4869,7 +4869,7 @@
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
}, skip: areKeyEventsHandledByPlatform);
testWidgets('Copy paste test', (WidgetTester tester) async {
@@ -4944,7 +4944,7 @@
const String expected = 'a biga big house\njumped over a mouse';
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
- }, skip: areKeyEventsHandledByPlatform);
+ }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Copy paste obscured text test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
@@ -5018,7 +5018,7 @@
const String expected = 'a biga big house jumped over a mouse';
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
- }, skip: areKeyEventsHandledByPlatform);
+ }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
// Regressing test for https://github.com/flutter/flutter/issues/78219
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
@@ -5069,7 +5069,7 @@
// Do nothing.
expect(find.text(clipboardContent), findsNothing);
expect(controller.selection, const TextSelection.collapsed(offset: -1));
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cut test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
@@ -5145,7 +5145,7 @@
const String expected = ' housa bige\njumped over a mouse';
expect(find.text(expected), findsOneWidget);
- }, skip: areKeyEventsHandledByPlatform);
+ }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cut obscured text test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
@@ -5220,7 +5220,7 @@
const String expected = ' housa bige jumped over a mouse';
expect(find.text(expected), findsOneWidget);
- }, skip: areKeyEventsHandledByPlatform);
+ }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Select all test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
@@ -5269,7 +5269,7 @@
const String expected = '';
expect(find.text(expected), findsOneWidget);
- }, skip: areKeyEventsHandledByPlatform);
+ }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Delete test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
@@ -5321,7 +5321,7 @@
const String expected2 = '';
expect(find.text(expected2), findsOneWidget);
- }, skip: areKeyEventsHandledByPlatform);
+ }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing positions of text fields', (WidgetTester tester) async {
@@ -5413,7 +5413,7 @@
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(c1.selection.extentOffset - c1.selection.baseOffset, -10);
- }, skip: areKeyEventsHandledByPlatform);
+ }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing focus test', (WidgetTester tester) async {
@@ -5488,7 +5488,7 @@
expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
- }, skip: areKeyEventsHandledByPlatform);
+ }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart
index ff11a53..d68bcae 100644
--- a/packages/flutter/test/rendering/editable_test.dart
+++ b/packages/flutter/test/rendering/editable_test.dart
@@ -16,6 +16,11 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:meta/meta.dart';
+
+// The test_api package is not for general use... it's literally for our use.
+// ignore: deprecated_member_use
+import 'package:test_api/test_api.dart' as test_package;
import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart';
@@ -35,6 +40,38 @@
void bringIntoView(TextPosition position) { }
}
+@isTest
+void testVariants(
+ String description,
+ AsyncValueGetter<void> callback, {
+ bool? skip,
+ test_package.Timeout? timeout,
+ TestVariant<Object?> variant = const DefaultTestVariant(),
+ dynamic tags,
+}) {
+ assert(variant != null);
+ assert(variant.values.isNotEmpty, 'There must be at least one value to test in the testing variant.');
+ for (final dynamic value in variant.values) {
+ final String variationDescription = variant.describeValue(value);
+ final String combinedDescription = variationDescription.isNotEmpty ? '$description ($variationDescription)' : description;
+ test(
+ combinedDescription,
+ () async {
+ Object? memento;
+ try {
+ memento = await variant.setUp(value);
+ await callback();
+ } finally {
+ await variant.tearDown(value, memento);
+ }
+ },
+ skip: skip,
+ timeout: timeout,
+ tags: tags,
+ );
+ }
+}
+
void main() {
test('RenderEditable respects clipBehavior', () {
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
@@ -1343,7 +1380,7 @@
expect(currentSelection.extentOffset, 1);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
- test('respects enableInteractiveSelection', () async {
+ testVariants('respects enableInteractiveSelection', () async {
const String text = '012345';
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(text: text);
@@ -1403,7 +1440,7 @@
await simulateKeyUpEvent(wordModifier);
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
- }, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
+ }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/58068
group('delete', () {
test('when as a non-collapsed selection, it should delete a selection', () async {
diff --git a/packages/flutter/test/services/hardware_keyboard_test.dart b/packages/flutter/test/services/hardware_keyboard_test.dart
new file mode 100644
index 0000000..634a289
--- /dev/null
+++ b/packages/flutter/test/services/hardware_keyboard_test.dart
@@ -0,0 +1,165 @@
+// 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 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('HardwareKeyboard records pressed keys and enabled locks', (WidgetTester tester) async {
+ await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');
+ expect(HardwareKeyboard.instance.physicalKeysPressed,
+ equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed,
+ equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.lockModesEnabled,
+ equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+
+ await simulateKeyDownEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
+ expect(HardwareKeyboard.instance.physicalKeysPressed,
+ equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed,
+ equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.lockModesEnabled,
+ equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+
+ await simulateKeyRepeatEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
+ expect(HardwareKeyboard.instance.physicalKeysPressed,
+ equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed,
+ equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.lockModesEnabled,
+ equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+
+ await simulateKeyUpEvent(LogicalKeyboardKey.numLock);
+ expect(HardwareKeyboard.instance.physicalKeysPressed,
+ equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed,
+ equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.lockModesEnabled,
+ equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+
+ await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');
+ expect(HardwareKeyboard.instance.physicalKeysPressed,
+ equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed,
+ equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.lockModesEnabled,
+ equals(<KeyboardLockMode>{}));
+
+ await simulateKeyUpEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
+ expect(HardwareKeyboard.instance.physicalKeysPressed,
+ equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed,
+ equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.lockModesEnabled,
+ equals(<KeyboardLockMode>{}));
+
+ await simulateKeyUpEvent(LogicalKeyboardKey.numLock, platform: 'windows');
+ expect(HardwareKeyboard.instance.physicalKeysPressed,
+ equals(<PhysicalKeyboardKey>{}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed,
+ equals(<LogicalKeyboardKey>{}));
+ expect(HardwareKeyboard.instance.lockModesEnabled,
+ equals(<KeyboardLockMode>{}));
+ }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());
+
+ testWidgets('Dispatch events to all handlers', (WidgetTester tester) async {
+ final FocusNode focusNode = FocusNode();
+ final List<int> logs = <int>[];
+
+ await tester.pumpWidget(
+ KeyboardListener(
+ autofocus: true,
+ focusNode: focusNode,
+ child: Container(),
+ onKeyEvent: (KeyEvent event) {
+ logs.add(1);
+ },
+ ),
+ );
+
+ // Only the Service binding handler.
+
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[1]);
+ logs.clear();
+
+ // Add a handler.
+
+ bool handler2Result = false;
+ bool handler2(KeyEvent event) {
+ logs.add(2);
+ return handler2Result;
+ }
+ HardwareKeyboard.instance.addHandler(handler2);
+
+ expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[2, 1]);
+ logs.clear();
+
+ handler2Result = true;
+
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+ true);
+ expect(logs, <int>[2, 1]);
+ logs.clear();
+
+ // Add another handler.
+
+ handler2Result = false;
+ bool handler3Result = false;
+ bool handler3(KeyEvent event) {
+ logs.add(3);
+ return handler3Result;
+ }
+ HardwareKeyboard.instance.addHandler(handler3);
+
+ expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[2, 3, 1]);
+ logs.clear();
+
+ handler2Result = true;
+
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+ true);
+ expect(logs, <int>[2, 3, 1]);
+ logs.clear();
+
+ handler3Result = true;
+
+ expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+ true);
+ expect(logs, <int>[2, 3, 1]);
+ logs.clear();
+
+ // Add handler2 again.
+
+ HardwareKeyboard.instance.addHandler(handler2);
+
+ handler3Result = false;
+ handler2Result = false;
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[2, 3, 2, 1]);
+ logs.clear();
+
+ handler2Result = true;
+ expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+ true);
+ expect(logs, <int>[2, 3, 2, 1]);
+ logs.clear();
+
+ // Remove handler2 once.
+
+ HardwareKeyboard.instance.removeHandler(handler2);
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+ true);
+ expect(logs, <int>[3, 2, 1]);
+ logs.clear();
+ }, variant: KeySimulatorTransitModeVariant.all());
+}
diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart
index 44dc576..f5c227c 100644
--- a/packages/flutter/test/services/raw_keyboard_test.dart
+++ b/packages/flutter/test/services/raw_keyboard_test.dart
@@ -703,6 +703,70 @@
)),
);
});
+
+ testWidgets('Dispatch events to all handlers', (WidgetTester tester) async {
+ final FocusNode focusNode = FocusNode();
+ final List<int> logs = <int>[];
+
+ await tester.pumpWidget(
+ RawKeyboardListener(
+ autofocus: true,
+ focusNode: focusNode,
+ child: Container(),
+ onKey: (RawKeyEvent event) {
+ logs.add(1);
+ },
+ ),
+ );
+
+ // Only the Service binding handler.
+
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[1]);
+ logs.clear();
+
+ // Add a handler.
+
+ void handler2(RawKeyEvent event) {
+ logs.add(2);
+ }
+ RawKeyboard.instance.addListener(handler2);
+
+ expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[1, 2]);
+ logs.clear();
+
+ // Add another handler.
+
+ void handler3(RawKeyEvent event) {
+ logs.add(3);
+ }
+ RawKeyboard.instance.addListener(handler3);
+
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[1, 2, 3]);
+ logs.clear();
+
+ // Add handler2 again.
+
+ RawKeyboard.instance.addListener(handler2);
+
+ expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[1, 2, 3, 2]);
+ logs.clear();
+
+ // Remove handler2 once.
+
+ RawKeyboard.instance.removeListener(handler2);
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+ false);
+ expect(logs, <int>[1, 3, 2]);
+ logs.clear();
+ }, variant: KeySimulatorTransitModeVariant.all());
});
group('RawKeyEventDataAndroid', () {
@@ -955,6 +1019,7 @@
},
);
expect(message, equals(<String, dynamic>{ 'handled': false }));
+ message = null;
// Set up a widget that will receive focused text events.
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
diff --git a/packages/flutter/test/widgets/app_test.dart b/packages/flutter/test/widgets/app_test.dart
index 5fa16ab..a2403ec 100644
--- a/packages/flutter/test/widgets/app_test.dart
+++ b/packages/flutter/test/widgets/app_test.dart
@@ -148,7 +148,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonA);
await tester.pumpAndSettle();
expect(checked, isTrue);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
group('error control test', () {
Future<void> expectFlutterError({
diff --git a/packages/flutter/test/widgets/default_text_editing_actions_test.dart b/packages/flutter/test/widgets/default_text_editing_actions_test.dart
index 91ca5a8..8718d71 100644
--- a/packages/flutter/test/widgets/default_text_editing_actions_test.dart
+++ b/packages/flutter/test/widgets/default_text_editing_actions_test.dart
@@ -114,5 +114,5 @@
await tester.pump();
expect(leftCalled, isFalse);
expect(rightCalled, isTrue);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
}
diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart
index b069b4e..e7e44f8 100644
--- a/packages/flutter/test/widgets/editable_text_cursor_test.dart
+++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart
@@ -328,6 +328,8 @@
});
testWidgets('Cursor animation restarts when it is moved using keys on desktop', (WidgetTester tester) async {
+ debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
+
const String testText = 'Some text long enough to move the cursor around';
final TextEditingController controller = TextEditingController(text: testText);
final Widget widget = MaterialApp(
@@ -400,7 +402,9 @@
await tester.pump(const Duration(milliseconds: 1));
expect(renderEditable.cursorColor!.alpha, 0);
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
- }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }));
+
+ debugDefaultTargetPlatformOverride = null;
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cursor does not show when showCursor set to false', (WidgetTester tester) async {
const Widget widget = MaterialApp(
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 0bce261..63b55d7 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -4864,8 +4864,23 @@
expect(controller.text, isEmpty, reason: 'on $platform');
}
- testWidgets('keyboard text selection works', (WidgetTester tester) async {
+ testWidgets('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async {
+ debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;
+
await testTextEditing(tester, targetPlatform: defaultTargetPlatform);
+
+ debugKeyEventSimulatorTransitModeOverride = null;
+
+ // On web, using keyboard for selection is handled by the browser.
+ }, skip: kIsWeb, variant: TargetPlatformVariant.all());
+
+ testWidgets('keyboard text selection works (ui.KeyData then RawKeyEvent)', (WidgetTester tester) async {
+ debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
+
+ await testTextEditing(tester, targetPlatform: defaultTargetPlatform);
+
+ debugKeyEventSimulatorTransitModeOverride = null;
+
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb, variant: TargetPlatformVariant.all());
@@ -7675,7 +7690,7 @@
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 1);
}
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('the toolbar is disposed when selection changes and there is no selectionControls', (WidgetTester tester) async {
late StateSetter setState;
diff --git a/packages/flutter/test/widgets/focus_manager_test.dart b/packages/flutter/test/widgets/focus_manager_test.dart
index 881c203..949cd23 100644
--- a/packages/flutter/test/widgets/focus_manager_test.dart
+++ b/packages/flutter/test/widgets/focus_manager_test.dart
@@ -165,7 +165,101 @@
'hasPrimaryFocus: false',
]);
});
+
+ testWidgets('onKeyEvent and onKey correctly cooperate', (WidgetTester tester) async {
+ final FocusNode focusNode = FocusNode(debugLabel: 'Test Node 3');
+ List<List<KeyEventResult>> results = <List<KeyEventResult>>[
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ ];
+ final List<int> logs = <int>[];
+
+ await tester.pumpWidget(
+ Focus(
+ focusNode: FocusNode(debugLabel: 'Test Node 1'),
+ onKeyEvent: (_, KeyEvent event) {
+ logs.add(0);
+ return results[0][0];
+ },
+ onKey: (_, RawKeyEvent event) {
+ logs.add(1);
+ return results[0][1];
+ },
+ child: Focus(
+ focusNode: FocusNode(debugLabel: 'Test Node 2'),
+ onKeyEvent: (_, KeyEvent event) {
+ logs.add(10);
+ return results[1][0];
+ },
+ onKey: (_, RawKeyEvent event) {
+ logs.add(11);
+ return results[1][1];
+ },
+ child: Focus(
+ focusNode: focusNode,
+ onKeyEvent: (_, KeyEvent event) {
+ logs.add(20);
+ return results[2][0];
+ },
+ onKey: (_, RawKeyEvent event) {
+ logs.add(21);
+ return results[2][1];
+ },
+ child: const SizedBox(width: 200, height: 100),
+ ),
+ ),
+ ),
+ );
+ focusNode.requestFocus();
+ await tester.pump();
+
+ // All ignored.
+ results = <List<KeyEventResult>>[
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ ];
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1),
+ false);
+ expect(logs, <int>[20, 21, 10, 11, 0, 1]);
+ logs.clear();
+
+ // The onKeyEvent should be able to stop propagation.
+ results = <List<KeyEventResult>>[
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.handled, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ ];
+ expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1),
+ true);
+ expect(logs, <int>[20, 21, 10, 11]);
+ logs.clear();
+
+ // The onKey should be able to stop propagation.
+ results = <List<KeyEventResult>>[
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.handled],
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ ];
+ expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1),
+ true);
+ expect(logs, <int>[20, 21, 10, 11]);
+ logs.clear();
+
+ // KeyEventResult.skipRemainingHandlers works.
+ results = <List<KeyEventResult>>[
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.skipRemainingHandlers, KeyEventResult.ignored],
+ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+ ];
+ expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1),
+ false);
+ expect(logs, <int>[20, 21, 10, 11]);
+ logs.clear();
+ }, variant: KeySimulatorTransitModeVariant.all());
});
+
group(FocusScopeNode, () {
testWidgets('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async {
@@ -935,7 +1029,7 @@
// Since none of the focused nodes handle this event, nothing should
// receive it.
expect(receivedAnEvent, isEmpty);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Initial highlight mode guesses correctly.', (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic;
diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart
index d514039..8fc023d 100644
--- a/packages/flutter/test/widgets/focus_traversal_test.dart
+++ b/packages/flutter/test/widgets/focus_traversal_test.dart
@@ -1678,7 +1678,7 @@
expect(Focus.of(lowerLeftKey.currentContext!).hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue);
- }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+ }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Focus traversal inside a vertical scrollable scrolls to stay visible.', (WidgetTester tester) async {
final List<int> items = List<int>.generate(11, (int index) => index).toList();
@@ -1776,7 +1776,7 @@
await tester.pump();
expect(topNode.hasPrimaryFocus, isTrue);
expect(controller.offset, equals(0.0));
- }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+ }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Focus traversal inside a horizontal scrollable scrolls to stay visible.', (WidgetTester tester) async {
final List<int> items = List<int>.generate(11, (int index) => index).toList();
@@ -1874,7 +1874,7 @@
await tester.pump();
expect(leftNode.hasPrimaryFocus, isTrue);
expect(controller.offset, equals(0.0));
- }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+ }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async {
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
@@ -1997,10 +1997,10 @@
expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Focus traversal does not break when no focusable is available on a MaterialApp', (WidgetTester tester) async {
- final List<RawKeyEvent> events = <RawKeyEvent>[];
+ final List<Object> events = <Object>[];
await tester.pumpWidget(MaterialApp(home: Container()));
@@ -2013,7 +2013,7 @@
await tester.idle();
expect(events.length, 2);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Focus traversal does not throw when no focusable is available in a group', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: ListTile(title: Text('title')))));
@@ -2047,7 +2047,7 @@
await tester.idle();
expect(events.length, 2);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
});
group(FocusTraversalGroup, () {
testWidgets("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async {
diff --git a/packages/flutter/test/widgets/keyboard_listener_test.dart b/packages/flutter/test/widgets/keyboard_listener_test.dart
new file mode 100644
index 0000000..7164077
--- /dev/null
+++ b/packages/flutter/test/widgets/keyboard_listener_test.dart
@@ -0,0 +1,106 @@
+// 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 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Can dispose without keyboard', (WidgetTester tester) async {
+ final FocusNode focusNode = FocusNode();
+ await tester.pumpWidget(KeyboardListener(focusNode: focusNode, onKeyEvent: null, child: Container()));
+ await tester.pumpWidget(KeyboardListener(focusNode: focusNode, onKeyEvent: null, child: Container()));
+ await tester.pumpWidget(Container());
+ });
+
+ testWidgets('Fuchsia key event', (WidgetTester tester) async {
+ final List<KeyEvent> events = <KeyEvent>[];
+
+ final FocusNode focusNode = FocusNode();
+
+ await tester.pumpWidget(
+ KeyboardListener(
+ focusNode: focusNode,
+ onKeyEvent: events.add,
+ child: Container(),
+ ),
+ );
+
+ focusNode.requestFocus();
+ await tester.idle();
+
+ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
+ await tester.idle();
+
+ expect(events.length, 2);
+ expect(events[0], isA<KeyDownEvent>());
+ expect(events[0].physicalKey, PhysicalKeyboardKey.metaLeft);
+ expect(events[0].logicalKey, LogicalKeyboardKey.metaLeft);
+
+ await tester.pumpWidget(Container());
+ focusNode.dispose();
+ }, skip: isBrowser); // This is a Fuchsia-specific test.
+
+ testWidgets('Web key event', (WidgetTester tester) async {
+ final List<KeyEvent> events = <KeyEvent>[];
+
+ final FocusNode focusNode = FocusNode();
+
+ await tester.pumpWidget(
+ KeyboardListener(
+ focusNode: focusNode,
+ onKeyEvent: events.add,
+ child: Container(),
+ ),
+ );
+
+ focusNode.requestFocus();
+ await tester.idle();
+
+ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'web');
+ await tester.idle();
+
+ expect(events.length, 2);
+ expect(events[0], isA<KeyDownEvent>());
+ expect(events[0].physicalKey, PhysicalKeyboardKey.metaLeft);
+ expect(events[0].logicalKey, LogicalKeyboardKey.metaLeft);
+
+ await tester.pumpWidget(Container());
+ focusNode.dispose();
+ });
+
+ testWidgets('Defunct listeners do not receive events', (WidgetTester tester) async {
+ final List<KeyEvent> events = <KeyEvent>[];
+
+ final FocusNode focusNode = FocusNode();
+
+ await tester.pumpWidget(
+ KeyboardListener(
+ focusNode: focusNode,
+ onKeyEvent: events.add,
+ child: Container(),
+ ),
+ );
+
+ focusNode.requestFocus();
+ await tester.idle();
+
+ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
+ await tester.idle();
+
+ expect(events.length, 2);
+ events.clear();
+
+ await tester.pumpWidget(Container());
+
+ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
+
+ await tester.idle();
+
+ expect(events.length, 0);
+
+ await tester.pumpWidget(Container());
+ focusNode.dispose();
+ });
+}
diff --git a/packages/flutter/test/widgets/scrollable_test.dart b/packages/flutter/test/widgets/scrollable_test.dart
index 9df01fc..082fe70 100644
--- a/packages/flutter/test/widgets/scrollable_test.dart
+++ b/packages/flutter/test/widgets/scrollable_test.dart
@@ -520,7 +520,7 @@
await tester.pumpAndSettle();
expect(controller.position.pixels, equals(0.0));
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
@@ -571,7 +571,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
await tester.pumpAndSettle();
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
@@ -617,7 +617,7 @@
await tester.sendKeyUpEvent(modifierKey);
await tester.pumpAndSettle();
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 50.0, 600.0)));
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Horizontal scrollables are scrolled the correct direction in RTL locales.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
@@ -666,7 +666,7 @@
await tester.sendKeyUpEvent(modifierKey);
await tester.pumpAndSettle();
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(750.0, 0.0, 800.0, 600.0)));
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Reversed vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
@@ -720,7 +720,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.pageDown);
await tester.pumpAndSettle();
expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 550.0, 800.0, 600.0)));
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Reversed horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
@@ -768,7 +768,7 @@
if (!kIsWeb)
await tester.sendKeyUpEvent(modifierKey);
await tester.pumpAndSettle();
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Custom scrollables with a center sliver are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
@@ -828,7 +828,7 @@
// Goes up two past "center" where it started, so negative.
expect(controller.position.pixels, equals(-100.0));
expect(tester.getRect(find.byKey(const ValueKey<String>('Item 10'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 100.0, 800.0, 200.0)));
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Can recommendDeferredLoadingForContext - animation', (WidgetTester tester) async {
final List<String> widgetTracker = <String>[];
diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart
index e58f680..f039c68 100644
--- a/packages/flutter/test/widgets/selectable_text_test.dart
+++ b/packages/flutter/test/widgets/selectable_text_test.dart
@@ -1570,7 +1570,7 @@
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Shift test 2', (WidgetTester tester) async {
await setupWidget(tester, 'abcdefghi');
@@ -1582,7 +1582,7 @@
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Control Shift test', (WidgetTester tester) async {
await setupWidget(tester, 'their big house');
@@ -1594,7 +1594,7 @@
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test', (WidgetTester tester) async {
await setupWidget(tester, 'a big house');
@@ -1612,7 +1612,7 @@
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test 2', (WidgetTester tester) async {
await setupWidget(tester, 'a big house\njumped over a mouse\nOne more line yay');
@@ -1663,7 +1663,7 @@
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
});
testWidgets('Copy test', (WidgetTester tester) async {
@@ -1722,7 +1722,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Select all test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
@@ -1757,7 +1757,7 @@
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 31);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('keyboard selection should call onSelectionChanged', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
@@ -1804,7 +1804,7 @@
expect(newSelection!.extentOffset, i + 1);
newSelection = null;
}
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing positions of selectable text', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
@@ -1894,7 +1894,7 @@
c1 = editableTextWidget.controller;
expect(c1.selection.extentOffset - c1.selection.baseOffset, -6);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing focus test', (WidgetTester tester) async {
@@ -1964,7 +1964,7 @@
expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
await tester.pumpWidget(
diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart
index f52c03b..42c3c2d 100644
--- a/packages/flutter/test/widgets/shortcuts_test.dart
+++ b/packages/flutter/test/widgets/shortcuts_test.dart
@@ -435,7 +435,33 @@
invoked = 0;
expect(RawKeyboard.instance.keysPressed, isEmpty);
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
+
+ testWidgets('handles repeated events', (WidgetTester tester) async {
+ int invoked = 0;
+ await tester.pumpWidget(activatorTester(
+ const SingleActivator(
+ LogicalKeyboardKey.keyC,
+ control: true,
+ ),
+ (Intent intent) { invoked += 1; },
+ ));
+ await tester.pump();
+
+ // LCtrl -> KeyC: Accept
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+ expect(invoked, 0);
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
+ expect(invoked, 1);
+ await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC);
+ expect(invoked, 2);
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+ expect(invoked, 2);
+ invoked = 0;
+
+ expect(RawKeyboard.instance.keysPressed, isEmpty);
+ }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async {
int invoked = 0;
@@ -1075,7 +1101,27 @@
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 1);
invoked = 0;
- });
+ }, variant: KeySimulatorTransitModeVariant.all());
+
+ testWidgets('handles repeated events', (WidgetTester tester) async {
+ int invoked = 0;
+ await tester.pumpWidget(activatorTester(
+ const CharacterActivator('?'),
+ (Intent intent) { invoked += 1; },
+ ));
+ await tester.pump();
+
+ // Press KeyC: Accepted by DumbLogicalActivator
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
+ expect(invoked, 1);
+ await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?');
+ expect(invoked, 2);
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
+ expect(invoked, 2);
+ invoked = 0;
+ }, variant: KeySimulatorTransitModeVariant.all());
});
group('CallbackShortcuts', () {
diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart
index 8c34a61..16f2440 100644
--- a/packages/flutter_test/lib/src/binding.dart
+++ b/packages/flutter_test/lib/src/binding.dart
@@ -828,6 +828,9 @@
assert(debugAssertAllSchedulerVarsUnset(
'The value of a scheduler debug variable was changed by the test.',
));
+ assert(debugAssertAllServicesVarsUnset(
+ 'The value of a services debug variable was changed by the test.',
+ ));
}
void _verifyAutoUpdateGoldensUnset(bool valueBeforeTest) {
@@ -894,6 +897,10 @@
// tests.
// ignore: invalid_use_of_visible_for_testing_member
RawKeyboard.instance.clearKeysPressed();
+ // ignore: invalid_use_of_visible_for_testing_member
+ HardwareKeyboard.instance.clearState();
+ // ignore: invalid_use_of_visible_for_testing_member
+ keyEventManager.clearState();
assert(!RendererBinding.instance!.mouseTracker.mouseIsConnected,
'The MouseTracker thinks that there is still a mouse connected, which indicates that a '
'test has not removed the mouse pointer which it added. Call removePointer on the '
diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart
index 65d702b..a6466a3 100644
--- a/packages/flutter_test/lib/src/controller.dart
+++ b/packages/flutter_test/lib/src/controller.dart
@@ -973,7 +973,7 @@
return box.size;
}
- /// Simulates sending physical key down and up events through the system channel.
+ /// Simulates sending physical key down and up events.
///
/// This only simulates key events coming from a physical keyboard, not from a
/// soft keyboard.
@@ -984,6 +984,9 @@
/// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet
/// supported.
///
+ /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
+ /// controlled by [debugKeyEventSimulatorTransitModeOverride].
+ ///
/// Keys that are down when the test completes are cleared after each test.
///
/// This method sends both the key down and the key up events, to simulate a
@@ -1004,7 +1007,7 @@
return handled;
}
- /// Simulates sending a physical key down event through the system channel.
+ /// Simulates sending a physical key down event.
///
/// This only simulates key down events coming from a physical keyboard, not
/// from a soft keyboard.
@@ -1015,13 +1018,17 @@
/// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet
/// supported.
///
+ /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
+ /// controlled by [debugKeyEventSimulatorTransitModeOverride].
+ ///
/// Keys that are down when the test completes are cleared after each test.
///
/// Returns true if the key event was handled by the framework.
///
/// See also:
///
- /// - [sendKeyUpEvent] to simulate the corresponding key up event.
+ /// - [sendKeyUpEvent] and [sendKeyRepeatEvent] to simulate the corresponding
+ /// key up and repeat event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
assert(platform != null);
@@ -1039,11 +1046,15 @@
/// that type of system. Defaults to "web" on web, and "android" everywhere
/// else. May not be null.
///
+ /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
+ /// controlled by [debugKeyEventSimulatorTransitModeOverride].
+ ///
/// Returns true if the key event was handled by the framework.
///
/// See also:
///
- /// - [sendKeyDownEvent] to simulate the corresponding key down event.
+ /// - [sendKeyDownEvent] and [sendKeyRepeatEvent] to simulate the
+ /// corresponding key down and repeat event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
assert(platform != null);
@@ -1051,6 +1062,35 @@
return simulateKeyUpEvent(key, platform: platform);
}
+ /// Simulates sending a physical key repeat event.
+ ///
+ /// This only simulates key repeat events coming from a physical keyboard, not
+ /// from a soft keyboard.
+ ///
+ /// Specify `platform` as one of the platforms allowed in
+ /// [platform.Platform.operatingSystem] to make the event appear to be from that type
+ /// of system. Defaults to "web" on web, and "android" everywhere else. Must not be
+ /// null. Some platforms (e.g. Windows, iOS) are not yet supported.
+ ///
+ /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
+ /// controlled by [debugKeyEventSimulatorTransitModeOverride]. If through [RawKeyEvent],
+ /// this method is equivalent to [sendKeyDownEvent].
+ ///
+ /// Keys that are down when the test completes are cleared after each test.
+ ///
+ /// Returns true if the key event was handled by the framework.
+ ///
+ /// See also:
+ ///
+ /// - [sendKeyDownEvent] and [sendKeyUpEvent] to simulate the corresponding
+ /// key down and up event.
+ /// - [sendKeyEvent] to simulate both the key up and key down in the same call.
+ Future<bool> sendKeyRepeatEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
+ assert(platform != null);
+ // Internally wrapped in async guard.
+ return simulateKeyRepeatEvent(key, character: character, platform: platform);
+ }
+
/// Returns the rect of the given widget. This is only valid once
/// the widget's render object has been laid out at least once.
Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder);
diff --git a/packages/flutter_test/lib/src/event_simulation.dart b/packages/flutter_test/lib/src/event_simulation.dart
index 4b897e8..32f81dc 100644
--- a/packages/flutter_test/lib/src/event_simulation.dart
+++ b/packages/flutter_test/lib/src/event_simulation.dart
@@ -4,9 +4,12 @@
import 'dart:async';
import 'dart:io';
+import 'dart:ui' as ui;
-import 'package:flutter/foundation.dart' show kIsWeb;
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
import 'binding.dart';
import 'test_async_utils.dart';
@@ -16,13 +19,14 @@
// https://github.com/flutter/flutter/issues/33521
// This code can only simulate keys which appear in the key maps.
-String _keyLabel(LogicalKeyboardKey key) {
+String? _keyLabel(LogicalKeyboardKey key) {
final String keyLabel = key.keyLabel;
if (keyLabel.length == 1)
return keyLabel.toLowerCase();
- return '';
+ return null;
}
+// ignore: avoid_classes_with_only_static_members
/// A class that serves as a namespace for a bunch of keyboard-key generation
/// utilities.
class KeyEventSimulator {
@@ -153,7 +157,7 @@
return result!;
}
- static PhysicalKeyboardKey _findPhysicalKey(LogicalKeyboardKey key, String platform) {
+ static PhysicalKeyboardKey _findPhysicalKeyByPlatform(LogicalKeyboardKey key, String platform) {
assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation');
late Map<dynamic, PhysicalKeyboardKey> map;
if (kIsWeb) {
@@ -208,7 +212,7 @@
key = _getKeySynonym(key);
// Find a suitable physical key if none was supplied.
- physicalKey ??= _findPhysicalKey(key, platform);
+ physicalKey ??= _findPhysicalKeyByPlatform(key, platform);
assert(key.debugName != null);
final int keyCode = _getKeyCode(key, platform);
@@ -219,7 +223,7 @@
'keymap': platform,
};
- final String resultCharacter = character ?? _keyLabel(key);
+ final String resultCharacter = character ?? _keyLabel(key) ?? '';
void assignWeb() {
result['code'] = _getWebKeyCode(key);
result['key'] = resultCharacter;
@@ -625,7 +629,64 @@
return result;
}
- /// Simulates sending a hardware key down event through the system channel.
+ static Future<bool> _simulateKeyEventByRawEvent(ValueGetter<Map<String, dynamic>> buildKeyData) async {
+ return TestAsyncUtils.guard<bool>(() async {
+ final Completer<bool> result = Completer<bool>();
+ await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
+ SystemChannels.keyEvent.name,
+ SystemChannels.keyEvent.codec.encodeMessage(buildKeyData()),
+ (ByteData? data) {
+ if (data == null) {
+ result.complete(false);
+ return;
+ }
+ final Map<String, dynamic> decoded = SystemChannels.keyEvent.codec.decodeMessage(data) as Map<String, dynamic>;
+ result.complete(decoded['handled'] as bool);
+ }
+ );
+ return result.future;
+ });
+ }
+
+ static late final Map<String, PhysicalKeyboardKey> _debugNameToPhysicalKey = (() {
+ final Map<String, PhysicalKeyboardKey> result = <String, PhysicalKeyboardKey>{};
+ for (final PhysicalKeyboardKey key in PhysicalKeyboardKey.knownPhysicalKeys) {
+ final String? debugName = key.debugName;
+ if (debugName != null)
+ result[debugName] = key;
+ }
+ return result;
+ })();
+ static PhysicalKeyboardKey _findPhysicalKey(LogicalKeyboardKey key) {
+ final PhysicalKeyboardKey? result = _debugNameToPhysicalKey[key.debugName];
+ assert(result != null, 'Physical key for $key not found in known physical keys');
+ return result!;
+ }
+
+ static const KeyDataTransitMode _defaultTransitMode = KeyDataTransitMode.rawKeyData;
+
+ // The simulation transit mode for [simulateKeyDownEvent], [simulateKeyUpEvent],
+ // and [simulateKeyRepeatEvent].
+ //
+ // Simulation transit mode is the mode that simulated key events are constructed
+ // and delivered. For detailed introduction, see [KeyDataTransitMode] and
+ // its values.
+ //
+ // The `_transitMode` defaults to [KeyDataTransitMode.rawKeyEvent], and can be
+ // overridden with [debugKeyEventSimulatorTransitModeOverride]. In widget tests, it
+ // is often set with [KeySimulationModeVariant].
+ static KeyDataTransitMode get _transitMode {
+ KeyDataTransitMode? result;
+ assert(() {
+ result = debugKeyEventSimulatorTransitModeOverride;
+ return true;
+ }());
+ return result ?? _defaultTransitMode;
+ }
+
+ static String get _defaultPlatform => kIsWeb ? 'web' : Platform.operatingSystem;
+
+ /// Simulates sending a hardware key down event.
///
/// This only simulates key presses coming from a physical keyboard, not from a
/// soft keyboard.
@@ -641,29 +702,36 @@
///
/// See also:
///
- /// - [simulateKeyUpEvent] to simulate the corresponding key up event.
- static Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey, String? character}) async {
- return TestAsyncUtils.guard<bool>(() async {
- platform ??= Platform.operatingSystem;
- assert(_osIsSupported(platform!), 'Platform $platform not supported for key simulation');
-
-
- final Map<String, dynamic> data = getKeyData(key, platform: platform!, isDown: true, physicalKey: physicalKey, character: character);
- final Completer<bool> result = Completer<bool>();
- await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
- SystemChannels.keyEvent.name,
- SystemChannels.keyEvent.codec.encodeMessage(data),
- (ByteData? data) {
- if (data == null) {
- result.complete(false);
- return;
- }
- final Map<String, dynamic> decoded = SystemChannels.keyEvent.codec.decodeMessage(data) as Map<String, dynamic>;
- result.complete(decoded['handled'] as bool);
- }
- );
- return result.future;
- });
+ /// * [simulateKeyUpEvent] to simulate the corresponding key up event.
+ static Future<bool> simulateKeyDownEvent(
+ LogicalKeyboardKey key, {
+ String? platform,
+ PhysicalKeyboardKey? physicalKey,
+ String? character,
+ }) async {
+ Future<bool> _simulateByRawEvent() {
+ return _simulateKeyEventByRawEvent(() {
+ platform ??= _defaultPlatform;
+ return getKeyData(key, platform: platform!, isDown: true, physicalKey: physicalKey, character: character);
+ });
+ }
+ switch (_transitMode) {
+ case KeyDataTransitMode.rawKeyData:
+ return _simulateByRawEvent();
+ case KeyDataTransitMode.keyDataThenRawKeyData:
+ final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
+ final bool resultByKeyEvent = ServicesBinding.instance!.keyEventManager.handleKeyData(
+ ui.KeyData(
+ type: ui.KeyEventType.down,
+ physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
+ logical: logicalKey.keyId,
+ timeStamp: Duration.zero,
+ character: character ?? _keyLabel(key),
+ synthesized: false,
+ ),
+ );
+ return (await _simulateByRawEvent()) || resultByKeyEvent;
+ }
}
/// Simulates sending a hardware key up event through the system channel.
@@ -680,29 +748,81 @@
///
/// See also:
///
- /// - [simulateKeyDownEvent] to simulate the corresponding key down event.
- static Future<bool> simulateKeyUpEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey}) async {
- return TestAsyncUtils.guard<bool>(() async {
- platform ??= Platform.operatingSystem;
- assert(_osIsSupported(platform!), 'Platform $platform not supported for key simulation');
+ /// * [simulateKeyDownEvent] to simulate the corresponding key down event.
+ static Future<bool> simulateKeyUpEvent(
+ LogicalKeyboardKey key, {
+ String? platform,
+ PhysicalKeyboardKey? physicalKey,
+ }) async {
+ Future<bool> _simulateByRawEvent() {
+ return _simulateKeyEventByRawEvent(() {
+ platform ??= _defaultPlatform;
+ return getKeyData(key, platform: platform!, isDown: false, physicalKey: physicalKey);
+ });
+ }
+ switch (_transitMode) {
+ case KeyDataTransitMode.rawKeyData:
+ return _simulateByRawEvent();
+ case KeyDataTransitMode.keyDataThenRawKeyData:
+ final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
+ final bool resultByKeyEvent = ServicesBinding.instance!.keyEventManager.handleKeyData(
+ ui.KeyData(
+ type: ui.KeyEventType.up,
+ physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
+ logical: logicalKey.keyId,
+ timeStamp: Duration.zero,
+ character: null,
+ synthesized: false,
+ ),
+ );
+ return (await _simulateByRawEvent()) || resultByKeyEvent;
+ }
+ }
- final Map<String, dynamic> data = getKeyData(key, platform: platform!, isDown: false, physicalKey: physicalKey);
- bool result = false;
- await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
- SystemChannels.keyEvent.name,
- SystemChannels.keyEvent.codec.encodeMessage(data),
- (ByteData? data) {
- if (data == null) {
- return;
- }
- final Map<String, dynamic> decoded = SystemChannels.keyEvent.codec.decodeMessage(data) as Map<String, dynamic>;
- if (decoded['handled'] as bool) {
- result = true;
- }
- }
- );
- return result;
- });
+ /// Simulates sending a hardware key repeat event through the system channel.
+ ///
+ /// This only simulates key presses coming from a physical keyboard, not from a
+ /// soft keyboard.
+ ///
+ /// Specify `platform` as one of the platforms allowed in
+ /// [Platform.operatingSystem] to make the event appear to be from that type of
+ /// system. Defaults to the operating system that the test is running on. Some
+ /// platforms (e.g. Windows, iOS) are not yet supported.
+ ///
+ /// Returns true if the key event was handled by the framework.
+ ///
+ /// See also:
+ ///
+ /// * [simulateKeyDownEvent] to simulate the corresponding key down event.
+ static Future<bool> simulateKeyRepeatEvent(
+ LogicalKeyboardKey key, {
+ String? platform,
+ PhysicalKeyboardKey? physicalKey,
+ String? character,
+ }) async {
+ Future<bool> _simulateByRawEvent() {
+ return _simulateKeyEventByRawEvent(() {
+ platform ??= _defaultPlatform;
+ return getKeyData(key, platform: platform!, isDown: true, physicalKey: physicalKey, character: character);
+ });
+ }
+ switch (_transitMode) {
+ case KeyDataTransitMode.rawKeyData:
+ return _simulateByRawEvent();
+ case KeyDataTransitMode.keyDataThenRawKeyData:
+ final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
+ final bool resultByKeyEvent = ServicesBinding.instance!.keyEventManager.handleKeyData(
+ ui.KeyData(
+ type: ui.KeyEventType.repeat,
+ physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
+ logical: logicalKey.keyId,
+ timeStamp: Duration.zero,
+ character: character ?? _keyLabel(key),
+ synthesized: false,
+ ),
+ );
+ return (await _simulateByRawEvent()) || resultByKeyEvent;
+ }
}
}
@@ -725,8 +845,14 @@
///
/// See also:
///
-/// - [simulateKeyUpEvent] to simulate the corresponding key up event.
-Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey, String? character}) {
+/// * [simulateKeyUpEvent] and [simulateKeyRepeatEvent] to simulate the
+/// corresponding key up and repeat event.
+Future<bool> simulateKeyDownEvent(
+ LogicalKeyboardKey key, {
+ String? platform,
+ PhysicalKeyboardKey? physicalKey,
+ String? character,
+}) {
return KeyEventSimulator.simulateKeyDownEvent(key, platform: platform, physicalKey: physicalKey, character: character);
}
@@ -747,7 +873,85 @@
///
/// See also:
///
-/// - [simulateKeyDownEvent] to simulate the corresponding key down event.
-Future<bool> simulateKeyUpEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey}) {
+/// * [simulateKeyDownEvent] and [simulateKeyRepeatEvent] to simulate the
+/// corresponding key down and repeat event.
+Future<bool> simulateKeyUpEvent(
+ LogicalKeyboardKey key, {
+ String? platform,
+ PhysicalKeyboardKey? physicalKey,
+}) {
return KeyEventSimulator.simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
}
+
+/// Simulates sending a hardware key repeat event through the system channel.
+///
+/// This only simulates key presses coming from a physical keyboard, not from a
+/// soft keyboard.
+///
+/// Specify `platform` as one of the platforms allowed in
+/// [Platform.operatingSystem] to make the event appear to be from that type of
+/// system. Defaults to the operating system that the test is running on. Some
+/// platforms (e.g. Windows, iOS) are not yet supported.
+///
+/// Returns true if the key event was handled by the framework.
+///
+/// See also:
+///
+/// - [simulateKeyDownEvent] and [simulateKeyUpEvent] to simulate the
+/// corresponding key down and up event.
+Future<bool> simulateKeyRepeatEvent(
+ LogicalKeyboardKey key, {
+ String? platform,
+ PhysicalKeyboardKey? physicalKey,
+ String? character,
+}) {
+ return KeyEventSimulator.simulateKeyRepeatEvent(key, platform: platform, physicalKey: physicalKey, character: character);
+}
+
+/// A [TestVariant] that runs tests with transit modes set to different values
+/// of [KeyDataTransitMode].
+class KeySimulatorTransitModeVariant extends TestVariant<KeyDataTransitMode> {
+ /// Creates a [KeySimulatorTransitModeVariant] that tests the given [values].
+ const KeySimulatorTransitModeVariant(this.values);
+
+ /// Creates a [KeySimulatorTransitModeVariant] for each value option of
+ /// [KeyDataTransitMode].
+ KeySimulatorTransitModeVariant.all()
+ : this(KeyDataTransitMode.values.toSet());
+
+ /// Creates a [KeySimulatorTransitModeVariant] that only contains
+ /// [KeyDataTransitMode.keyDataThenRawKeyData].
+ KeySimulatorTransitModeVariant.keyDataThenRawKeyData()
+ : this(<KeyDataTransitMode>{KeyDataTransitMode.keyDataThenRawKeyData});
+
+ @override
+ final Set<KeyDataTransitMode> values;
+
+ @override
+ String describeValue(KeyDataTransitMode value) {
+ switch (value) {
+ case KeyDataTransitMode.rawKeyData:
+ return 'RawKeyEvent';
+ case KeyDataTransitMode.keyDataThenRawKeyData:
+ return 'ui.KeyData then RawKeyEvent';
+ }
+ }
+
+ @override
+ Future<KeyDataTransitMode?> setUp(KeyDataTransitMode value) async {
+ final KeyDataTransitMode? previousSetting = debugKeyEventSimulatorTransitModeOverride;
+ debugKeyEventSimulatorTransitModeOverride = value;
+ return previousSetting;
+ }
+
+ @override
+ Future<void> tearDown(KeyDataTransitMode value, KeyDataTransitMode? memento) async {
+ // ignore: invalid_use_of_visible_for_testing_member
+ RawKeyboard.instance.clearKeysPressed();
+ // ignore: invalid_use_of_visible_for_testing_member
+ HardwareKeyboard.instance.clearState();
+ // ignore: invalid_use_of_visible_for_testing_member
+ ServicesBinding.instance!.keyEventManager.clearState();
+ debugKeyEventSimulatorTransitModeOverride = memento;
+ }
+}
diff --git a/packages/flutter_test/lib/src/test_pointer.dart b/packages/flutter_test/lib/src/test_pointer.dart
index 77bd8bd..7ded5c0 100644
--- a/packages/flutter_test/lib/src/test_pointer.dart
+++ b/packages/flutter_test/lib/src/test_pointer.dart
@@ -230,6 +230,7 @@
timeStamp: timeStamp,
kind: kind,
device: _device,
+ pointer: pointer,
position: _location ?? Offset.zero,
);
}
@@ -255,6 +256,7 @@
timeStamp: timeStamp,
kind: kind,
device: _device,
+ pointer: pointer,
position: newLocation,
delta: delta,
);
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 9651fda..eb86947 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -510,7 +510,7 @@
/// Class that programmatically interacts with widgets and the test environment.
///
/// For convenience, instances of this class (such as the one provided by
-/// `testWidget`) can be used as the `vsync` for `AnimationController` objects.
+/// `testWidgets`) can be used as the `vsync` for `AnimationController` objects.
class WidgetTester extends WidgetController implements HitTestDispatcher, TickerProvider {
WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
if (binding is LiveTestWidgetsFlutterBinding)
diff --git a/packages/flutter_test/test/event_simulation_test.dart b/packages/flutter_test/test/event_simulation_test.dart
index 38cdd8f..0dfc774 100644
--- a/packages/flutter_test/test/event_simulation_test.dart
+++ b/packages/flutter_test/test/event_simulation_test.dart
@@ -2,14 +2,44 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
const List<String> platforms = <String>['linux', 'macos', 'android', 'fuchsia'];
+void _verifyKeyEvent<T extends KeyEvent>(KeyEvent event, PhysicalKeyboardKey physical, LogicalKeyboardKey logical, String? character) {
+ expect(event, isA<T>());
+ expect(event.physicalKey, physical);
+ expect(event.logicalKey, logical);
+ expect(event.character, character);
+ expect(event.synthesized, false);
+}
+
+void _verifyRawKeyEvent<T extends RawKeyEvent>(RawKeyEvent event, PhysicalKeyboardKey physical, LogicalKeyboardKey logical, String? character) {
+ expect(event, isA<T>());
+ expect(event.physicalKey, physical);
+ expect(event.logicalKey, logical);
+ expect(event.character, character);
+}
+
+Future<void> _shouldThrow<T extends Error>(AsyncValueGetter<void> func) async {
+ bool hasError = false;
+ try {
+ await func();
+ } catch (e) {
+ expect(e, isA<T>());
+ hasError = true;
+ } finally {
+ expect(hasError, true);
+ }
+}
+
void main() {
- testWidgets('simulates keyboard events', (WidgetTester tester) async {
+ testWidgets('simulates keyboard events (RawEvent)', (WidgetTester tester) async {
+ debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;
+
final List<RawKeyEvent> events = <RawKeyEvent>[];
final FocusNode focusNode = FocusNode();
@@ -51,5 +81,247 @@
await tester.pumpWidget(Container());
focusNode.dispose();
+
+ debugKeyEventSimulatorTransitModeOverride = null;
+ });
+
+ testWidgets('simulates keyboard events (KeyData then RawKeyEvent)', (WidgetTester tester) async {
+ debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
+
+ final List<KeyEvent> events = <KeyEvent>[];
+
+ final FocusNode focusNode = FocusNode();
+
+ await tester.pumpWidget(
+ KeyboardListener(
+ focusNode: focusNode,
+ onKeyEvent: events.add,
+ child: Container(),
+ ),
+ );
+
+ focusNode.requestFocus();
+ await tester.idle();
+
+ // Key press shiftLeft
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
+ expect(events.length, 1);
+ _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.shiftLeft}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.sendKeyRepeatEvent(LogicalKeyboardKey.shiftLeft);
+ expect(events.length, 1);
+ _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.shiftLeft}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
+ expect(events.length, 1);
+ _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ // Key press keyA
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
+ expect(events.length, 1);
+ _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyA}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
+ _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyA}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
+ _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ // Key press numpad1
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad1);
+ _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numpad1);
+ _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad1);
+ _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ // Key press numLock (1st time)
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.numLock);
+ _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+ events.clear();
+
+ await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numLock);
+ _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+ events.clear();
+
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.numLock);
+ _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+ events.clear();
+
+ // Key press numLock (2nd time)
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.numLock);
+ _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numLock);
+ _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.numLock);
+ _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+ expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+ expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+ events.clear();
+
+ await tester.idle();
+
+ await tester.pumpWidget(Container());
+ focusNode.dispose();
+
+ debugKeyEventSimulatorTransitModeOverride = null;
+ });
+
+ testWidgets('simulates using the correct transit mode: rawKeyData', (WidgetTester tester) async {
+ debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;
+
+ final List<Object> events = <Object>[];
+
+ final FocusNode focusNode = FocusNode();
+ await tester.pumpWidget(
+ Focus(
+ focusNode: focusNode,
+ onKey: (FocusNode node, RawKeyEvent event) {
+ events.add(event);
+ return KeyEventResult.ignored;
+ },
+ onKeyEvent: (FocusNode node, KeyEvent event) {
+ events.add(event);
+ return KeyEventResult.ignored;
+ },
+ child: Container(),
+ ),
+ );
+
+ focusNode.requestFocus();
+ await tester.idle();
+
+ // A (physical keyA, logical keyA) is pressed.
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
+ expect(events.length, 2);
+ expect(events[0], isA<KeyEvent>());
+ _verifyKeyEvent<KeyDownEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+ expect(events[1], isA<RawKeyEvent>());
+ _verifyRawKeyEvent<RawKeyDownEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+ events.clear();
+
+ // A (physical keyA, logical keyB) is released.
+ //
+ // Since this event was synthesized and regularized before being sent to
+ // HardwareKeyboard, this event will be accepted.
+ await simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA);
+ expect(events.length, 2);
+ expect(events[0], isA<KeyEvent>());
+ _verifyKeyEvent<KeyUpEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, null);
+ expect(events[1], isA<RawKeyEvent>());
+ _verifyRawKeyEvent<RawKeyUpEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, null);
+ events.clear();
+
+ // Manually switch the transit mode to `keyDataThenRawKeyData`. This will
+ // never happen in real applications so the assertion error can verify that
+ // the transit mode is correctly applied.
+ debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
+
+ await _shouldThrow<AssertionError>(() =>
+ simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA));
+
+ debugKeyEventSimulatorTransitModeOverride = null;
+ });
+
+ testWidgets('simulates using the correct transit mode: keyDataThenRawKeyData', (WidgetTester tester) async {
+ debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
+
+ final List<Object> events = <Object>[];
+
+ final FocusNode focusNode = FocusNode();
+ await tester.pumpWidget(
+ Focus(
+ focusNode: focusNode,
+ onKey: (FocusNode node, RawKeyEvent event) {
+ events.add(event);
+ return KeyEventResult.ignored;
+ },
+ onKeyEvent: (FocusNode node, KeyEvent event) {
+ events.add(event);
+ return KeyEventResult.ignored;
+ },
+ child: Container(),
+ ),
+ );
+
+ focusNode.requestFocus();
+ await tester.idle();
+
+ // A (physical keyA, logical keyA) is pressed.
+ await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
+ expect(events.length, 2);
+ expect(events[0], isA<KeyEvent>());
+ _verifyKeyEvent<KeyDownEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+ expect(events[1], isA<RawKeyEvent>());
+ _verifyRawKeyEvent<RawKeyDownEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+ events.clear();
+
+ // A (physical keyA, logical keyB) is released.
+ //
+ // Since this event is transmitted to HardwareKeyboard as-is, it will be rejected due to
+ // inconsistent logical key. This does not indicate behaviral difference,
+ // since KeyData is will never send malformed data sequence in real applications.
+ await _shouldThrow<AssertionError>(() =>
+ simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA));
+
+ debugKeyEventSimulatorTransitModeOverride = null;
});
}