Unverified Commit caf876cf authored by Tong Mu's avatar Tong Mu Committed by GitHub

Reland: Keyboard events (#87174)

parent a82255b5
...@@ -327,6 +327,9 @@ class LogicalKeyboardKey extends KeyboardKey { ...@@ -327,6 +327,9 @@ class LogicalKeyboardKey extends KeyboardKey {
@@@LOGICAL_KEY_DEFINITIONS@@@ @@@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 // A list of all predefined constant LogicalKeyboardKeys so they can be
// searched. // searched.
static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{ static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{
...@@ -489,6 +492,9 @@ class PhysicalKeyboardKey extends KeyboardKey { ...@@ -489,6 +492,9 @@ class PhysicalKeyboardKey extends KeyboardKey {
@@@PHYSICAL_KEY_DEFINITIONS@@@ @@@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 // A list of all the predefined constant PhysicalKeyboardKeys so that they
// can be searched. // can be searched.
static const Map<int, PhysicalKeyboardKey> _knownPhysicalKeys = <int, PhysicalKeyboardKey>{ static const Map<int, PhysicalKeyboardKey> _knownPhysicalKeys = <int, PhysicalKeyboardKey>{
......
...@@ -176,41 +176,5 @@ ...@@ -176,41 +176,5 @@
"Zoom": ["ZOOM"], "Zoom": ["ZOOM"],
"Noname": ["NONAME"], "Noname": ["NONAME"],
"Pa1": ["PA1"], "Pa1": ["PA1"],
"OemClear": ["OEM_CLEAR"], "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"]
} }
...@@ -24,6 +24,16 @@ bool _isAsciiLetter(String? char) { ...@@ -24,6 +24,16 @@ bool _isAsciiLetter(String? char) {
|| (charCode >= charLowerA && charCode <= charLowerZ); || (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 /// Generates the keyboard_maps.dart files, based on the information in the key
/// data structure given to it. /// data structure given to it.
class KeyboardMapsCodeGenerator extends BaseCodeGenerator { class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
...@@ -171,7 +181,9 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator { ...@@ -171,7 +181,9 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
// because they are not used by the embedding. Add them manually. // because they are not used by the embedding. Add them manually.
final List<int>? keyCodes = entry.windowsValues.isNotEmpty final List<int>? keyCodes = entry.windowsValues.isNotEmpty
? entry.windowsValues ? 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) { if (keyCodes != null) {
for (final int code in keyCodes) { for (final int code in keyCodes) {
lines.add(code, ' $code: LogicalKeyboardKey.${entry.constantName},'); lines.add(code, ' $code: LogicalKeyboardKey.${entry.constantName},');
......
...@@ -15,9 +15,11 @@ export 'src/services/autofill.dart'; ...@@ -15,9 +15,11 @@ export 'src/services/autofill.dart';
export 'src/services/binary_messenger.dart'; export 'src/services/binary_messenger.dart';
export 'src/services/binding.dart'; export 'src/services/binding.dart';
export 'src/services/clipboard.dart'; export 'src/services/clipboard.dart';
export 'src/services/debug.dart';
export 'src/services/deferred_component.dart'; export 'src/services/deferred_component.dart';
export 'src/services/font_loader.dart'; export 'src/services/font_loader.dart';
export 'src/services/haptic_feedback.dart'; export 'src/services/haptic_feedback.dart';
export 'src/services/hardware_keyboard.dart';
export 'src/services/keyboard_key.dart'; export 'src/services/keyboard_key.dart';
export 'src/services/keyboard_maps.dart'; export 'src/services/keyboard_maps.dart';
export 'src/services/message_codec.dart'; export 'src/services/message_codec.dart';
......
...@@ -14,7 +14,9 @@ import 'package:flutter/scheduler.dart'; ...@@ -14,7 +14,9 @@ import 'package:flutter/scheduler.dart';
import 'asset_bundle.dart'; import 'asset_bundle.dart';
import 'binary_messenger.dart'; import 'binary_messenger.dart';
import 'hardware_keyboard.dart';
import 'message_codec.dart'; import 'message_codec.dart';
import 'raw_keyboard.dart';
import 'restoration.dart'; import 'restoration.dart';
import 'system_channels.dart'; import 'system_channels.dart';
...@@ -31,6 +33,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { ...@@ -31,6 +33,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
_instance = this; _instance = this;
_defaultBinaryMessenger = createBinaryMessenger(); _defaultBinaryMessenger = createBinaryMessenger();
_restorationManager = createRestorationManager(); _restorationManager = createRestorationManager();
_initKeyboard();
initLicenses(); initLicenses();
SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object)); SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object));
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage); SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
...@@ -42,6 +45,23 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { ...@@ -42,6 +45,23 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
static ServicesBinding? get instance => _instance; static ServicesBinding? get instance => _instance;
static ServicesBinding? _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]. /// The default instance of [BinaryMessenger].
/// ///
/// This is used to send messages from the application to the platform, and /// This is used to send messages from the application to the platform, and
......
// 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;
}
This diff is collapsed.
...@@ -2624,6 +2624,9 @@ class LogicalKeyboardKey extends KeyboardKey { ...@@ -2624,6 +2624,9 @@ class LogicalKeyboardKey extends KeyboardKey {
/// See the function [RawKeyEvent.logicalKey] for more information. /// See the function [RawKeyEvent.logicalKey] for more information.
static const LogicalKeyboardKey gameButtonZ = LogicalKeyboardKey(0x0020000031f); 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 // A list of all predefined constant LogicalKeyboardKeys so they can be
// searched. // searched.
static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{ static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{
...@@ -5136,6 +5139,9 @@ class PhysicalKeyboardKey extends KeyboardKey { ...@@ -5136,6 +5139,9 @@ class PhysicalKeyboardKey extends KeyboardKey {
/// See the function [RawKeyEvent.physicalKey] for more information. /// See the function [RawKeyEvent.physicalKey] for more information.
static const PhysicalKeyboardKey showAllWindows = PhysicalKeyboardKey(0x000c029f); 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 // A list of all the predefined constant PhysicalKeyboardKeys so that they
// can be searched. // can be searched.
static const Map<int, PhysicalKeyboardKey> _knownPhysicalKeys = <int, PhysicalKeyboardKey>{ static const Map<int, PhysicalKeyboardKey> _knownPhysicalKeys = <int, PhysicalKeyboardKey>{
......
...@@ -2953,6 +2953,16 @@ const Map<int, LogicalKeyboardKey> kWindowsToLogicalKey = <int, LogicalKeyboardK ...@@ -2953,6 +2953,16 @@ const Map<int, LogicalKeyboardKey> kWindowsToLogicalKey = <int, LogicalKeyboardK
45: LogicalKeyboardKey.insert, 45: LogicalKeyboardKey.insert,
46: LogicalKeyboardKey.delete, 46: LogicalKeyboardKey.delete,
47: LogicalKeyboardKey.help, 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, 65: LogicalKeyboardKey.keyA,
66: LogicalKeyboardKey.keyB, 66: LogicalKeyboardKey.keyB,
67: LogicalKeyboardKey.keyC, 67: LogicalKeyboardKey.keyC,
......
...@@ -3,10 +3,12 @@ ...@@ -3,10 +3,12 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'binding.dart';
import 'hardware_keyboard.dart';
import 'keyboard_key.dart'; import 'keyboard_key.dart';
import 'raw_keyboard_android.dart'; import 'raw_keyboard_android.dart';
import 'raw_keyboard_fuchsia.dart'; import 'raw_keyboard_fuchsia.dart';
...@@ -286,17 +288,20 @@ abstract class RawKeyEvent with Diagnosticable { ...@@ -286,17 +288,20 @@ abstract class RawKeyEvent with Diagnosticable {
final RawKeyEventData data; final RawKeyEventData data;
String? character; String? character;
if (kIsWeb) { RawKeyEventData _dataFromWeb() {
final String? key = message['key'] as String?; final String? key = message['key'] as String?;
data = RawKeyEventDataWeb( if (key != null && key.isNotEmpty) {
character = key;
}
return RawKeyEventDataWeb(
code: message['code'] as String? ?? '', code: message['code'] as String? ?? '',
key: key ?? '', key: key ?? '',
location: message['location'] as int? ?? 0, location: message['location'] as int? ?? 0,
metaState: message['metaState'] as int? ?? 0, metaState: message['metaState'] as int? ?? 0,
); );
if (key != null && key.isNotEmpty) { }
character = key; if (kIsWeb) {
} data = _dataFromWeb();
} else { } else {
final String keymap = message['keymap'] as String; final String keymap = message['keymap'] as String;
switch (keymap) { switch (keymap) {
...@@ -373,16 +378,7 @@ abstract class RawKeyEvent with Diagnosticable { ...@@ -373,16 +378,7 @@ abstract class RawKeyEvent with Diagnosticable {
} }
break; break;
case 'web': case 'web':
final String? key = message['key'] as String?; data = _dataFromWeb();
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;
}
break; break;
default: default:
/// This exception would only be hit on platforms that haven't yet /// This exception would only be hit on platforms that haven't yet
...@@ -573,9 +569,7 @@ typedef RawKeyEventHandler = bool Function(RawKeyEvent event); ...@@ -573,9 +569,7 @@ typedef RawKeyEventHandler = bool Function(RawKeyEvent event);
/// * [SystemChannels.keyEvent], the low-level channel used for receiving /// * [SystemChannels.keyEvent], the low-level channel used for receiving
/// events from the system. /// events from the system.
class RawKeyboard { class RawKeyboard {
RawKeyboard._() { RawKeyboard._();
SystemChannels.keyEvent.setMessageHandler(_handleKeyEvent);
}
/// The shared instance of [RawKeyboard]. /// The shared instance of [RawKeyboard].
static final RawKeyboard instance = RawKeyboard._(); static final RawKeyboard instance = RawKeyboard._();
...@@ -606,39 +600,45 @@ class RawKeyboard { ...@@ -606,39 +600,45 @@ class RawKeyboard {
_listeners.remove(listener); _listeners.remove(listener);
} }
/// A handler for hardware keyboard events that will stop propagation if the /// A handler for raw hardware keyboard events that will stop propagation if
/// handler returns true. /// the handler returns true.
/// ///
/// Key events on the platform are given to Flutter to be handled by the /// This property is only a wrapper over [KeyEventManager.keyMessageHandler],
/// engine. If they are not handled, then the platform will continue to /// and is kept only for backward compatibility. New code should use
/// distribute the keys (i.e. propagate them) to other (possibly non-Flutter) /// [KeyEventManager.keyMessageHandler] to set custom global key event
/// components in the application. The return value from this handler tells /// handler. Setting [keyEventHandler] will cause
/// the platform to either stop propagation (by returning true: "event /// [KeyEventManager.keyMessageHandler] to be set with a converted handler.
/// handled"), or pass the event on to other controls (false: "event not /// If [KeyEventManager.keyMessageHandler] is set by [FocusManager] (the most
/// handled"). /// common situation), then the exact value of [keyEventHandler] is a dummy
/// /// callback and must not be invoked.
/// This handler is normally set by the [FocusManager] so that it can control RawKeyEventHandler? get keyEventHandler {
/// the key event propagation to focused widgets. if (ServicesBinding.instance!.keyEventManager.keyMessageHandler != _cachedKeyMessageHandler) {
/// _cachedKeyMessageHandler = ServicesBinding.instance!.keyEventManager.keyMessageHandler;
/// Most applications can use the focus system (see [Focus] and _cachedKeyEventHandler = _cachedKeyMessageHandler == null ?
/// [FocusManager]) to receive key events. If you are not using the null :
/// [FocusManager] to manage focus, then to be able to stop propagation of the (RawKeyEvent event) {
/// event by indicating that the event was handled, set this attribute to a assert(false,
/// [RawKeyEventHandler]. Otherwise, key events will be assumed to not have 'The RawKeyboard.instance.keyEventHandler assigned by Flutter is a dummy '
/// been handled by Flutter, and will also be sent to other (possibly 'callback kept for compatibility and should not be directly called. Use '
/// non-Flutter) controls in the application. 'ServicesBinding.instance!.keyMessageHandler instead.');
/// return true;
/// See also: };
/// }
/// * [Focus.onKey], a [Focus] callback attribute that will be given key return _cachedKeyEventHandler;
/// events distributed by the [FocusManager] based on the current primary }
/// focus. RawKeyEventHandler? _cachedKeyEventHandler;
/// * [addListener], to add passive key event listeners that do not stop event KeyMessageHandler? _cachedKeyMessageHandler;
/// propagation. set keyEventHandler(RawKeyEventHandler? handler) {
RawKeyEventHandler? keyEventHandler; _cachedKeyEventHandler = handler;
_cachedKeyMessageHandler = handler == null ?
null :
(KeyMessage message) => handler(message.rawEvent);
ServicesBinding.instance!.keyEventManager.keyMessageHandler = _cachedKeyMessageHandler;
}
Future<dynamic> _handleKeyEvent(dynamic message) async { /// Process a new [RawKeyEvent] by recording the state changes and
final RawKeyEvent event = RawKeyEvent.fromMessage(message as Map<String, dynamic>); /// dispatching to listeners.
bool handleRawKeyEvent(RawKeyEvent event) {
bool shouldDispatch = true; bool shouldDispatch = true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {
if (event.data.shouldDispatchEvent()) { if (event.data.shouldDispatchEvent()) {
...@@ -659,7 +659,7 @@ class RawKeyboard { ...@@ -659,7 +659,7 @@ class RawKeyboard {
} }
} }
if (!shouldDispatch) { if (!shouldDispatch) {
return <String, dynamic>{ 'handled': true }; return true;
} }
// Make sure that the modifiers reflect reality, in case a modifier key was // Make sure that the modifiers reflect reality, in case a modifier key was
// pressed/released while the app didn't have focus. // pressed/released while the app didn't have focus.
...@@ -678,12 +678,7 @@ class RawKeyboard { ...@@ -678,12 +678,7 @@ class RawKeyboard {
} }
} }
// Send the key event to the keyEventHandler, then send the appropriate return false;
// 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 };
} }
static final Map<_ModifierSidePair, Set<PhysicalKeyboardKey>> _modifierKeyMap = <_ModifierSidePair, Set<PhysicalKeyboardKey>>{ static final Map<_ModifierSidePair, Set<PhysicalKeyboardKey>> _modifierKeyMap = <_ModifierSidePair, Set<PhysicalKeyboardKey>>{
...@@ -808,6 +803,11 @@ class RawKeyboard { ...@@ -808,6 +803,11 @@ class RawKeyboard {
/// Returns the set of physical keys currently pressed. /// Returns the set of physical keys currently pressed.
Set<PhysicalKeyboardKey> get physicalKeysPressed => _keysPressed.keys.toSet(); 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]. /// Clears the list of keys returned from [keysPressed].
/// ///
/// This is used by the testing framework to make sure tests are hermetic. /// This is used by the testing framework to make sure tests are hermetic.
...@@ -832,5 +832,5 @@ class _ModifierSidePair { ...@@ -832,5 +832,5 @@ class _ModifierSidePair {
} }
@override @override
int get hashCode => hashValues(modifier, side); int get hashCode => ui.hashValues(modifier, side);
} }
...@@ -83,13 +83,11 @@ class RawKeyEventDataWeb extends RawKeyEventData { ...@@ -83,13 +83,11 @@ class RawKeyEventDataWeb extends RawKeyEventData {
@override @override
LogicalKeyboardKey get logicalKey { LogicalKeyboardKey get logicalKey {
// Look to see if the keyCode is a printable number pad key, so that a // Look to see if the keyCode is a key based on location. Typically they are
// difference between regular keys (e.g. ".") and the number pad version // numpad keys (versus main area keys) and left/right modifiers.
// (e.g. the "." on the number pad) can be determined. final LogicalKeyboardKey? maybeLocationKey = kWebLocationMap[key]?[location];
final LogicalKeyboardKey? numPadKey = kWebNumPadMap[code]; if (maybeLocationKey != null)
if (numPadKey != null) { return maybeLocationKey;
return numPadKey;
}
// Look to see if the [code] is one we know about and have a mapping for. // Look to see if the [code] is one we know about and have a mapping for.
final LogicalKeyboardKey? newKey = kWebToLogicalKey[code]; final LogicalKeyboardKey? newKey = kWebToLogicalKey[code];
......
...@@ -36,7 +36,7 @@ bool _focusDebug(String message, [Iterable<String>? details]) { ...@@ -36,7 +36,7 @@ bool _focusDebug(String message, [Iterable<String>? details]) {
} }
/// An enum that describes how to handle a key event handled by a /// An enum that describes how to handle a key event handled by a
/// [FocusOnKeyCallback]. /// [FocusOnKeyCallback] or [FocusOnKeyEventCallback].
enum KeyEventResult { enum KeyEventResult {
/// The key event has been handled, and the event should not be propagated to /// The key event has been handled, and the event should not be propagated to
/// other key event handlers. /// other key event handlers.
...@@ -52,6 +52,32 @@ enum KeyEventResult { ...@@ -52,6 +52,32 @@ enum KeyEventResult {
skipRemainingHandlers, 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] /// Signature of a callback used by [Focus.onKey] and [FocusScope.onKey]
/// to receive key events. /// to receive key events.
/// ///
...@@ -61,6 +87,15 @@ enum KeyEventResult { ...@@ -61,6 +87,15 @@ enum KeyEventResult {
/// was handled. /// was handled.
typedef FocusOnKeyCallback = KeyEventResult Function(FocusNode node, RawKeyEvent event); 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]. /// An attachment point for a [FocusNode].
/// ///
/// Using a [FocusAttachment] is rarely needed, unless you are building /// Using a [FocusAttachment] is rarely needed, unless you are building
...@@ -271,12 +306,13 @@ enum UnfocusDisposition { ...@@ -271,12 +306,13 @@ enum UnfocusDisposition {
/// {@template flutter.widgets.FocusNode.keyEvents} /// {@template flutter.widgets.FocusNode.keyEvents}
/// ## Key Event Propagation /// ## Key Event Propagation
/// ///
/// The [FocusManager] receives key events from [RawKeyboard] and will pass them /// The [FocusManager] receives key events from [RawKeyboard] and
/// to the focused nodes. It starts with the node with the primary focus, and /// [HardwareKeyboard] and will pass them to the focused nodes. It starts with
/// will call the [onKey] callback for that node. If the callback returns false, /// the node with the primary focus, and will call the [onKey] or [onKeyEvent]
/// indicating that it did not handle the event, the [FocusManager] will move to /// callback for that node. If the callback returns false, indicating that it did
/// the parent of that node and call its [onKey]. If that [onKey] returns true, /// not handle the event, the [FocusManager] will move to the parent of that node
/// then it will stop propagating the event. If it reaches the root /// 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. /// [FocusScopeNode], [FocusManager.rootScope], the event is discarded.
/// {@endtemplate} /// {@endtemplate}
/// ///
...@@ -433,9 +469,14 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -433,9 +469,14 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// ///
/// The [skipTraversal], [descendantsAreFocusable], and [canRequestFocus] /// The [skipTraversal], [descendantsAreFocusable], and [canRequestFocus]
/// arguments must not be null. /// 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({ FocusNode({
String? debugLabel, String? debugLabel,
this.onKey, this.onKey,
this.onKeyEvent,
bool skipTraversal = false, bool skipTraversal = false,
bool canRequestFocus = true, bool canRequestFocus = true,
bool descendantsAreFocusable = true, bool descendantsAreFocusable = true,
...@@ -574,9 +615,18 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -574,9 +615,18 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// Called if this focus node receives a key event while focused (i.e. when /// Called if this focus node receives a key event while focused (i.e. when
/// [hasFocus] returns true). /// [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} /// {@macro flutter.widgets.FocusNode.keyEvents}
FocusOnKeyCallback? onKey; 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; FocusManager? _manager;
List<FocusNode>? _ancestors; List<FocusNode>? _ancestors;
List<FocusNode>? _descendants; List<FocusNode>? _descendants;
...@@ -1028,10 +1078,19 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -1028,10 +1078,19 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// need to be attached. [FocusAttachment.detach] should be called on the old /// 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 /// node, and then [attach] called on the new node. This typically happens in
/// the [State.didUpdateWidget] method. /// 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 @mustCallSuper
FocusAttachment attach(BuildContext? context, {FocusOnKeyCallback? onKey}) { FocusAttachment attach(
BuildContext? context, {
FocusOnKeyEventCallback? onKeyEvent,
FocusOnKeyCallback? onKey,
}) {
_context = context; _context = context;
this.onKey = onKey ?? this.onKey; this.onKey = onKey ?? this.onKey;
this.onKeyEvent = onKeyEvent ?? this.onKeyEvent;
_attachment = FocusAttachment._(this); _attachment = FocusAttachment._(this);
return _attachment!; return _attachment!;
} }
...@@ -1225,6 +1284,7 @@ class FocusScopeNode extends FocusNode { ...@@ -1225,6 +1284,7 @@ class FocusScopeNode extends FocusNode {
/// All parameters are optional. /// All parameters are optional.
FocusScopeNode({ FocusScopeNode({
String? debugLabel, String? debugLabel,
FocusOnKeyEventCallback? onKeyEvent,
FocusOnKeyCallback? onKey, FocusOnKeyCallback? onKey,
bool skipTraversal = false, bool skipTraversal = false,
bool canRequestFocus = true, bool canRequestFocus = true,
...@@ -1232,6 +1292,7 @@ class FocusScopeNode extends FocusNode { ...@@ -1232,6 +1292,7 @@ class FocusScopeNode extends FocusNode {
assert(canRequestFocus != null), assert(canRequestFocus != null),
super( super(
debugLabel: debugLabel, debugLabel: debugLabel,
onKeyEvent: onKeyEvent,
onKey: onKey, onKey: onKey,
canRequestFocus: canRequestFocus, canRequestFocus: canRequestFocus,
descendantsAreFocusable: true, descendantsAreFocusable: true,
...@@ -1463,15 +1524,15 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -1463,15 +1524,15 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
/// When this focus manager is no longer needed, calling [dispose] on it will /// When this focus manager is no longer needed, calling [dispose] on it will
/// unregister these handlers. /// unregister these handlers.
void registerGlobalHandlers() { void registerGlobalHandlers() {
assert(RawKeyboard.instance.keyEventHandler == null); assert(ServicesBinding.instance!.keyEventManager.keyMessageHandler == null);
RawKeyboard.instance.keyEventHandler = _handleRawKeyEvent; ServicesBinding.instance!.keyEventManager.keyMessageHandler = _handleKeyMessage;
GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent); GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent);
} }
@override @override
void dispose() { void dispose() {
if (RawKeyboard.instance.keyEventHandler == _handleRawKeyEvent) { if (ServicesBinding.instance!.keyEventManager.keyMessageHandler == _handleKeyMessage) {
RawKeyboard.instance.keyEventHandler = null; ServicesBinding.instance!.keyEventManager.keyMessageHandler = null;
GestureBinding.instance!.pointerRouter.removeGlobalRoute(_handlePointerEvent); GestureBinding.instance!.pointerRouter.removeGlobalRoute(_handlePointerEvent);
} }
super.dispose(); super.dispose();
...@@ -1660,15 +1721,15 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -1660,15 +1721,15 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
} }
} }
bool _handleRawKeyEvent(RawKeyEvent event) { bool _handleKeyMessage(KeyMessage message) {
// Update highlightMode first, since things responding to the keys might // Update highlightMode first, since things responding to the keys might
// look at the highlight mode, and it should be accurate. // look at the highlight mode, and it should be accurate.
_lastInteractionWasTouch = false; _lastInteractionWasTouch = false;
_updateHighlightMode(); _updateHighlightMode();
assert(_focusDebug('Received key event ${event.logicalKey}')); assert(_focusDebug('Received key event $message'));
if (_primaryFocus == null) { 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; return false;
} }
...@@ -1677,25 +1738,35 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -1677,25 +1738,35 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
// stop propagation, stop. // stop propagation, stop.
bool handled = false; bool handled = false;
for (final FocusNode node in <FocusNode>[_primaryFocus!, ..._primaryFocus!.ancestors]) { for (final FocusNode node in <FocusNode>[_primaryFocus!, ..._primaryFocus!.ancestors]) {
if (node.onKey != null) { final List<KeyEventResult> results = <KeyEventResult>[];
final KeyEventResult result = node.onKey!(node, event); if (node.onKeyEvent != null) {
switch (result) { for (final KeyEvent event in message.events) {
case KeyEventResult.handled: results.add(node.onKeyEvent!(node, event));
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;
} }
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) { if (!handled) {
assert(_focusDebug('Key event not handled by anyone: $event.')); assert(_focusDebug('Key event not handled by anyone: $message.'));
} }
return handled; return handled;
} }
......
...@@ -283,6 +283,7 @@ class Focus extends StatefulWidget { ...@@ -283,6 +283,7 @@ class Focus extends StatefulWidget {
this.autofocus = false, this.autofocus = false,
this.onFocusChange, this.onFocusChange,
this.onKey, this.onKey,
this.onKeyEvent,
this.debugLabel, this.debugLabel,
this.canRequestFocus, this.canRequestFocus,
this.descendantsAreFocusable = true, this.descendantsAreFocusable = true,
...@@ -315,6 +316,24 @@ class Focus extends StatefulWidget { ...@@ -315,6 +316,24 @@ class Focus extends StatefulWidget {
/// focus. /// focus.
/// ///
/// Key events are first given to the [FocusNode] that has primary focus, and /// 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 /// 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 /// node up the focus hierarchy in turn. If an event reaches the root of the
/// hierarchy, it is discarded. /// hierarchy, it is discarded.
...@@ -540,7 +559,7 @@ class Focus extends StatefulWidget { ...@@ -540,7 +559,7 @@ class Focus extends StatefulWidget {
} }
@override @override
State<Focus> createState() => _FocusState(); State<Focus> createState() => _FocusState();
} }
class _FocusState extends State<Focus> { class _FocusState extends State<Focus> {
...@@ -575,7 +594,7 @@ class _FocusState extends State<Focus> { ...@@ -575,7 +594,7 @@ class _FocusState extends State<Focus> {
_canRequestFocus = focusNode.canRequestFocus; _canRequestFocus = focusNode.canRequestFocus;
_descendantsAreFocusable = focusNode.descendantsAreFocusable; _descendantsAreFocusable = focusNode.descendantsAreFocusable;
_hasPrimaryFocus = focusNode.hasPrimaryFocus; _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 // 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 // not be listening now if we're re-using a previous one because it should
...@@ -914,6 +933,7 @@ class FocusScope extends Focus { ...@@ -914,6 +933,7 @@ class FocusScope extends Focus {
ValueChanged<bool>? onFocusChange, ValueChanged<bool>? onFocusChange,
bool? canRequestFocus, bool? canRequestFocus,
bool? skipTraversal, bool? skipTraversal,
FocusOnKeyEventCallback? onKeyEvent,
FocusOnKeyCallback? onKey, FocusOnKeyCallback? onKey,
String? debugLabel, String? debugLabel,
}) : assert(child != null), }) : assert(child != null),
...@@ -926,6 +946,7 @@ class FocusScope extends Focus { ...@@ -926,6 +946,7 @@ class FocusScope extends Focus {
onFocusChange: onFocusChange, onFocusChange: onFocusChange,
canRequestFocus: canRequestFocus, canRequestFocus: canRequestFocus,
skipTraversal: skipTraversal, skipTraversal: skipTraversal,
onKeyEvent: onKeyEvent,
onKey: onKey, onKey: onKey,
debugLabel: debugLabel, debugLabel: debugLabel,
); );
......
// 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));
}
}
...@@ -21,10 +21,16 @@ export 'package:flutter/services.dart' show RawKeyEvent; ...@@ -21,10 +21,16 @@ export 'package:flutter/services.dart' show RawKeyEvent;
/// For text entry, consider using a [EditableText], which integrates with /// For text entry, consider using a [EditableText], which integrates with
/// on-screen keyboards and input method editors (IMEs). /// 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: /// See also:
/// ///
/// * [EditableText], which should be used instead of this widget for text /// * [EditableText], which should be used instead of this widget for text
/// entry. /// entry.
/// * [KeyboardListener], a similar widget based on the newer
/// [HardwareKeyboard] API.
class RawKeyboardListener extends StatefulWidget { class RawKeyboardListener extends StatefulWidget {
/// Creates a widget that receives raw keyboard events. /// Creates a widget that receives raw keyboard events.
/// ///
......
...@@ -194,13 +194,13 @@ abstract class ShortcutActivator { ...@@ -194,13 +194,13 @@ abstract class ShortcutActivator {
/// event. /// event.
/// ///
/// For example, for `Ctrl-A`, it has to check if the event is a /// 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 /// 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. /// check if KeyA is pressed, since it's already guaranteed.
/// ///
/// This method must not cause any side effects for the `state`. Typically /// This method must not cause any side effects for the `state`. Typically
/// this is only used to query whether [RawKeyboard.keysPressed] contains /// this is only used to query whether [HardwareKeyboard.logicalKeysPressed]
/// a key. /// contains a key.
/// ///
/// Since [ShortcutActivator] accepts all event types, subclasses might want /// Since [ShortcutActivator] accepts all event types, subclasses might want
/// to check the event type in [accepts]. /// to check the event type in [accepts].
...@@ -314,11 +314,13 @@ class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable ...@@ -314,11 +314,13 @@ class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable
@override @override
bool accepts(RawKeyEvent event, RawKeyboard state) { bool accepts(RawKeyEvent event, RawKeyboard state) {
if (event is! RawKeyDownEvent)
return false;
final Set<LogicalKeyboardKey> collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys); final Set<LogicalKeyboardKey> collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys);
final Set<LogicalKeyboardKey> collapsedPressed = LogicalKeyboardKey.collapseSynonyms(state.keysPressed); final Set<LogicalKeyboardKey> collapsedPressed = LogicalKeyboardKey.collapseSynonyms(state.keysPressed);
final bool keysEqual = collapsedRequired.difference(collapsedPressed).isEmpty final bool keysEqual = collapsedRequired.difference(collapsedPressed).isEmpty
&& collapsedRequired.length == collapsedPressed.length; && collapsedRequired.length == collapsedPressed.length;
return event is RawKeyDownEvent && keysEqual; return keysEqual;
} }
static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{ static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{
...@@ -425,7 +427,8 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Int ...@@ -425,7 +427,8 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Int
/// * [CharacterActivator], an activator that represents key combinations /// * [CharacterActivator], an activator that represents key combinations
/// that result in the specified character, such as question mark. /// that result in the specified character, such as question mark.
class SingleActivator with Diagnosticable implements ShortcutActivator { 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 /// 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 /// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not be
...@@ -434,6 +437,9 @@ class SingleActivator with Diagnosticable implements ShortcutActivator { ...@@ -434,6 +437,9 @@ class SingleActivator with Diagnosticable implements ShortcutActivator {
/// The `control`, `shift`, `alt`, and `meta` flags represent whether /// The `control`, `shift`, `alt`, and `meta` flags represent whether
/// the respect modifier keys should be held (true) or released (false) /// 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} /// {@tool dartpad --template=stateful_widget_scaffold_center}
/// In the following example, the shortcut `Control + C` increases the counter: /// In the following example, the shortcut `Control + C` increases the counter:
/// ///
...@@ -811,17 +817,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable { ...@@ -811,17 +817,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// must be mapped to an [Action], and the [Action] must be enabled. /// must be mapped to an [Action], and the [Action] must be enabled.
@protected @protected
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) { KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored;
}
assert(context != null); 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); final Intent? matchedIntent = _find(event, RawKeyboard.instance);
if (matchedIntent != null) { if (matchedIntent != null) {
final BuildContext? primaryContext = primaryFocus?.context; final BuildContext? primaryContext = primaryFocus?.context;
...@@ -1285,4 +1281,4 @@ class CallbackShortcuts extends StatelessWidget { ...@@ -1285,4 +1281,4 @@ class CallbackShortcuts extends StatelessWidget {
child: child, child: child,
); );
} }
} }
\ No newline at end of file
...@@ -63,6 +63,7 @@ export 'src/widgets/inherited_model.dart'; ...@@ -63,6 +63,7 @@ export 'src/widgets/inherited_model.dart';
export 'src/widgets/inherited_notifier.dart'; export 'src/widgets/inherited_notifier.dart';
export 'src/widgets/inherited_theme.dart'; export 'src/widgets/inherited_theme.dart';
export 'src/widgets/interactive_viewer.dart'; export 'src/widgets/interactive_viewer.dart';
export 'src/widgets/keyboard_listener.dart';
export 'src/widgets/layout_builder.dart'; export 'src/widgets/layout_builder.dart';
export 'src/widgets/list_wheel_scroll_view.dart'; export 'src/widgets/list_wheel_scroll_view.dart';
export 'src/widgets/localizations.dart'; export 'src/widgets/localizations.dart';
......
...@@ -4348,7 +4348,7 @@ void main() { ...@@ -4348,7 +4348,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump(); await tester.pump();
expect(focusNode3.hasPrimaryFocus, isTrue); expect(focusNode3.hasPrimaryFocus, isTrue);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async { testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async {
bool scrollInvoked = false; bool scrollInvoked = false;
...@@ -4381,7 +4381,7 @@ void main() { ...@@ -4381,7 +4381,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(scrollInvoked, isFalse); expect(scrollInvoked, isFalse);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cupertino text field semantics', (WidgetTester tester) async { testWidgets('Cupertino text field semantics', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -4731,7 +4731,7 @@ void main() { ...@@ -4731,7 +4731,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1); expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Shift test 2', (WidgetTester tester) async { testWidgets('Shift test 2', (WidgetTester tester) async {
await setupWidget(tester); await setupWidget(tester);
...@@ -4749,7 +4749,7 @@ void main() { ...@@ -4749,7 +4749,7 @@ void main() {
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1); expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Control Shift test', (WidgetTester tester) async { testWidgets('Control Shift test', (WidgetTester tester) async {
await setupWidget(tester); await setupWidget(tester);
...@@ -4766,7 +4766,7 @@ void main() { ...@@ -4766,7 +4766,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 5); expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test', (WidgetTester tester) async { testWidgets('Down and up test', (WidgetTester tester) async {
await setupWidget(tester); await setupWidget(tester);
...@@ -4793,7 +4793,7 @@ void main() { ...@@ -4793,7 +4793,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test 2', (WidgetTester tester) async { testWidgets('Down and up test 2', (WidgetTester tester) async {
await setupWidget(tester); await setupWidget(tester);
...@@ -4849,7 +4849,7 @@ void main() { ...@@ -4849,7 +4849,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5); expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Read only keyboard selection test', (WidgetTester tester) async { testWidgets('Read only keyboard selection test', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'readonly'); final TextEditingController controller = TextEditingController(text: 'readonly');
...@@ -4869,7 +4869,7 @@ void main() { ...@@ -4869,7 +4869,7 @@ void main() {
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1); expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
}); }, variant: KeySimulatorTransitModeVariant.all());
}, skip: areKeyEventsHandledByPlatform); }, skip: areKeyEventsHandledByPlatform);
testWidgets('Copy paste test', (WidgetTester tester) async { testWidgets('Copy paste test', (WidgetTester tester) async {
...@@ -4944,7 +4944,7 @@ void main() { ...@@ -4944,7 +4944,7 @@ void main() {
const String expected = 'a biga big house\njumped over a mouse'; const String expected = 'a biga big house\njumped over a mouse';
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}'); 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 { testWidgets('Copy paste obscured text test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -5018,7 +5018,7 @@ void main() { ...@@ -5018,7 +5018,7 @@ void main() {
const String expected = 'a biga big house jumped over a mouse'; const String expected = 'a biga big house jumped over a mouse';
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}'); 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 // Regressing test for https://github.com/flutter/flutter/issues/78219
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async { testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
...@@ -5069,7 +5069,7 @@ void main() { ...@@ -5069,7 +5069,7 @@ void main() {
// Do nothing. // Do nothing.
expect(find.text(clipboardContent), findsNothing); expect(find.text(clipboardContent), findsNothing);
expect(controller.selection, const TextSelection.collapsed(offset: -1)); expect(controller.selection, const TextSelection.collapsed(offset: -1));
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cut test', (WidgetTester tester) async { testWidgets('Cut test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -5145,7 +5145,7 @@ void main() { ...@@ -5145,7 +5145,7 @@ void main() {
const String expected = ' housa bige\njumped over a mouse'; const String expected = ' housa bige\njumped over a mouse';
expect(find.text(expected), findsOneWidget); expect(find.text(expected), findsOneWidget);
}, skip: areKeyEventsHandledByPlatform); }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Cut obscured text test', (WidgetTester tester) async { testWidgets('Cut obscured text test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -5220,7 +5220,7 @@ void main() { ...@@ -5220,7 +5220,7 @@ void main() {
const String expected = ' housa bige jumped over a mouse'; const String expected = ' housa bige jumped over a mouse';
expect(find.text(expected), findsOneWidget); expect(find.text(expected), findsOneWidget);
}, skip: areKeyEventsHandledByPlatform); }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Select all test', (WidgetTester tester) async { testWidgets('Select all test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -5269,7 +5269,7 @@ void main() { ...@@ -5269,7 +5269,7 @@ void main() {
const String expected = ''; const String expected = '';
expect(find.text(expected), findsOneWidget); expect(find.text(expected), findsOneWidget);
}, skip: areKeyEventsHandledByPlatform); }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Delete test', (WidgetTester tester) async { testWidgets('Delete test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -5321,7 +5321,7 @@ void main() { ...@@ -5321,7 +5321,7 @@ void main() {
const String expected2 = ''; const String expected2 = '';
expect(find.text(expected2), findsOneWidget); expect(find.text(expected2), findsOneWidget);
}, skip: areKeyEventsHandledByPlatform); }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing positions of text fields', (WidgetTester tester) async { testWidgets('Changing positions of text fields', (WidgetTester tester) async {
...@@ -5413,7 +5413,7 @@ void main() { ...@@ -5413,7 +5413,7 @@ void main() {
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(c1.selection.extentOffset - c1.selection.baseOffset, -10); expect(c1.selection.extentOffset - c1.selection.baseOffset, -10);
}, skip: areKeyEventsHandledByPlatform); }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing focus test', (WidgetTester tester) async { testWidgets('Changing focus test', (WidgetTester tester) async {
...@@ -5488,7 +5488,7 @@ void main() { ...@@ -5488,7 +5488,7 @@ void main() {
expect(c1.selection.extentOffset - c1.selection.baseOffset, 0); expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5); 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 { testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
......
...@@ -16,6 +16,11 @@ import 'package:flutter/material.dart'; ...@@ -16,6 +16,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.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/mock_canvas.dart';
import '../rendering/recording_canvas.dart'; import '../rendering/recording_canvas.dart';
...@@ -35,6 +40,38 @@ class FakeEditableTextState with TextSelectionDelegate { ...@@ -35,6 +40,38 @@ class FakeEditableTextState with TextSelectionDelegate {
void bringIntoView(TextPosition position) { } 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() { void main() {
test('RenderEditable respects clipBehavior', () { test('RenderEditable respects clipBehavior', () {
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0); const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
...@@ -1343,7 +1380,7 @@ void main() { ...@@ -1343,7 +1380,7 @@ void main() {
expect(currentSelection.extentOffset, 1); expect(currentSelection.extentOffset, 1);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
test('respects enableInteractiveSelection', () async { testVariants('respects enableInteractiveSelection', () async {
const String text = '012345'; const String text = '012345';
final TextSelectionDelegate delegate = FakeEditableTextState() final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(text: text); ..textEditingValue = const TextEditingValue(text: text);
...@@ -1403,7 +1440,7 @@ void main() { ...@@ -1403,7 +1440,7 @@ void main() {
await simulateKeyUpEvent(wordModifier); await simulateKeyUpEvent(wordModifier);
await simulateKeyUpEvent(LogicalKeyboardKey.shift); 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', () { group('delete', () {
test('when as a non-collapsed selection, it should delete a selection', () async { test('when as a non-collapsed selection, it should delete a selection', () async {
......
// 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());
}
...@@ -703,6 +703,70 @@ void main() { ...@@ -703,6 +703,70 @@ void main() {
)), )),
); );
}); });
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', () { group('RawKeyEventDataAndroid', () {
...@@ -955,6 +1019,7 @@ void main() { ...@@ -955,6 +1019,7 @@ void main() {
}, },
); );
expect(message, equals(<String, dynamic>{ 'handled': false })); expect(message, equals(<String, dynamic>{ 'handled': false }));
message = null;
// Set up a widget that will receive focused text events. // Set up a widget that will receive focused text events.
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
......
...@@ -148,7 +148,7 @@ void main() { ...@@ -148,7 +148,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonA); await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonA);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(checked, isTrue); expect(checked, isTrue);
}); }, variant: KeySimulatorTransitModeVariant.all());
group('error control test', () { group('error control test', () {
Future<void> expectFlutterError({ Future<void> expectFlutterError({
......
...@@ -114,5 +114,5 @@ void main() { ...@@ -114,5 +114,5 @@ void main() {
await tester.pump(); await tester.pump();
expect(leftCalled, isFalse); expect(leftCalled, isFalse);
expect(rightCalled, isTrue); expect(rightCalled, isTrue);
}); }, variant: KeySimulatorTransitModeVariant.all());
} }
...@@ -328,6 +328,8 @@ void main() { ...@@ -328,6 +328,8 @@ void main() {
}); });
testWidgets('Cursor animation restarts when it is moved using keys on desktop', (WidgetTester tester) async { 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'; const String testText = 'Some text long enough to move the cursor around';
final TextEditingController controller = TextEditingController(text: testText); final TextEditingController controller = TextEditingController(text: testText);
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
...@@ -400,7 +402,9 @@ void main() { ...@@ -400,7 +402,9 @@ void main() {
await tester.pump(const Duration(milliseconds: 1)); await tester.pump(const Duration(milliseconds: 1));
expect(renderEditable.cursorColor!.alpha, 0); expect(renderEditable.cursorColor!.alpha, 0);
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 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 { testWidgets('Cursor does not show when showCursor set to false', (WidgetTester tester) async {
const Widget widget = MaterialApp( const Widget widget = MaterialApp(
......
...@@ -4864,8 +4864,23 @@ void main() { ...@@ -4864,8 +4864,23 @@ void main() {
expect(controller.text, isEmpty, reason: 'on $platform'); 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); 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. // On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb, variant: TargetPlatformVariant.all()); }, skip: kIsWeb, variant: TargetPlatformVariant.all());
...@@ -7675,7 +7690,7 @@ void main() { ...@@ -7675,7 +7690,7 @@ void main() {
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 1); expect(controller.selection.baseOffset, 1);
} }
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('the toolbar is disposed when selection changes and there is no selectionControls', (WidgetTester tester) async { testWidgets('the toolbar is disposed when selection changes and there is no selectionControls', (WidgetTester tester) async {
late StateSetter setState; late StateSetter setState;
......
...@@ -165,7 +165,101 @@ void main() { ...@@ -165,7 +165,101 @@ void main() {
'hasPrimaryFocus: false', '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, () { group(FocusScopeNode, () {
testWidgets('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async { testWidgets('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async {
...@@ -935,7 +1029,7 @@ void main() { ...@@ -935,7 +1029,7 @@ void main() {
// Since none of the focused nodes handle this event, nothing should // Since none of the focused nodes handle this event, nothing should
// receive it. // receive it.
expect(receivedAnEvent, isEmpty); expect(receivedAnEvent, isEmpty);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Initial highlight mode guesses correctly.', (WidgetTester tester) async { testWidgets('Initial highlight mode guesses correctly.', (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic; FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic;
......
...@@ -1678,7 +1678,7 @@ void main() { ...@@ -1678,7 +1678,7 @@ void main() {
expect(Focus.of(lowerLeftKey.currentContext!).hasPrimaryFocus, isTrue); expect(Focus.of(lowerLeftKey.currentContext!).hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue); 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 { 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(); final List<int> items = List<int>.generate(11, (int index) => index).toList();
...@@ -1776,7 +1776,7 @@ void main() { ...@@ -1776,7 +1776,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(topNode.hasPrimaryFocus, isTrue); expect(topNode.hasPrimaryFocus, isTrue);
expect(controller.offset, equals(0.0)); 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 { 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(); final List<int> items = List<int>.generate(11, (int index) => index).toList();
...@@ -1874,7 +1874,7 @@ void main() { ...@@ -1874,7 +1874,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(leftNode.hasPrimaryFocus, isTrue); expect(leftNode.hasPrimaryFocus, isTrue);
expect(controller.offset, equals(0.0)); 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 { testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async {
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
...@@ -1997,10 +1997,10 @@ void main() { ...@@ -1997,10 +1997,10 @@ void main() {
expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Focus traversal does not break when no focusable is available on a MaterialApp', (WidgetTester tester) async { 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())); await tester.pumpWidget(MaterialApp(home: Container()));
...@@ -2013,7 +2013,7 @@ void main() { ...@@ -2013,7 +2013,7 @@ void main() {
await tester.idle(); await tester.idle();
expect(events.length, 2); expect(events.length, 2);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Focus traversal does not throw when no focusable is available in a group', (WidgetTester tester) async { 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'))))); await tester.pumpWidget(const MaterialApp(home: Scaffold(body: ListTile(title: Text('title')))));
...@@ -2047,7 +2047,7 @@ void main() { ...@@ -2047,7 +2047,7 @@ void main() {
await tester.idle(); await tester.idle();
expect(events.length, 2); expect(events.length, 2);
}); }, variant: KeySimulatorTransitModeVariant.all());
}); });
group(FocusTraversalGroup, () { group(FocusTraversalGroup, () {
testWidgets("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async { testWidgets("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async {
......
// 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();
});
}
...@@ -520,7 +520,7 @@ void main() { ...@@ -520,7 +520,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.position.pixels, equals(0.0)); 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))); 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 { testWidgets('Vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
...@@ -571,7 +571,7 @@ void main() { ...@@ -571,7 +571,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.pageUp); await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
await tester.pumpAndSettle(); 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))); 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 { testWidgets('Horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
...@@ -617,7 +617,7 @@ void main() { ...@@ -617,7 +617,7 @@ void main() {
await tester.sendKeyUpEvent(modifierKey); await tester.sendKeyUpEvent(modifierKey);
await tester.pumpAndSettle(); 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))); 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 { testWidgets('Horizontal scrollables are scrolled the correct direction in RTL locales.', (WidgetTester tester) async {
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
...@@ -666,7 +666,7 @@ void main() { ...@@ -666,7 +666,7 @@ void main() {
await tester.sendKeyUpEvent(modifierKey); await tester.sendKeyUpEvent(modifierKey);
await tester.pumpAndSettle(); 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))); 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 { testWidgets('Reversed vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
...@@ -720,7 +720,7 @@ void main() { ...@@ -720,7 +720,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.pageDown); await tester.sendKeyEvent(LogicalKeyboardKey.pageDown);
await tester.pumpAndSettle(); 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))); 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 { testWidgets('Reversed horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
...@@ -768,7 +768,7 @@ void main() { ...@@ -768,7 +768,7 @@ void main() {
if (!kIsWeb) if (!kIsWeb)
await tester.sendKeyUpEvent(modifierKey); await tester.sendKeyUpEvent(modifierKey);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Custom scrollables with a center sliver are scrolled when activated via keyboard.', (WidgetTester tester) async { testWidgets('Custom scrollables with a center sliver are scrolled when activated via keyboard.', (WidgetTester tester) async {
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
...@@ -828,7 +828,7 @@ void main() { ...@@ -828,7 +828,7 @@ void main() {
// Goes up two past "center" where it started, so negative. // Goes up two past "center" where it started, so negative.
expect(controller.position.pixels, equals(-100.0)); 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))); 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 { testWidgets('Can recommendDeferredLoadingForContext - animation', (WidgetTester tester) async {
final List<String> widgetTracker = <String>[]; final List<String> widgetTracker = <String>[];
......
...@@ -1570,7 +1570,7 @@ void main() { ...@@ -1570,7 +1570,7 @@ void main() {
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1); expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Shift test 2', (WidgetTester tester) async { testWidgets('Shift test 2', (WidgetTester tester) async {
await setupWidget(tester, 'abcdefghi'); await setupWidget(tester, 'abcdefghi');
...@@ -1582,7 +1582,7 @@ void main() { ...@@ -1582,7 +1582,7 @@ void main() {
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1); expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Control Shift test', (WidgetTester tester) async { testWidgets('Control Shift test', (WidgetTester tester) async {
await setupWidget(tester, 'their big house'); await setupWidget(tester, 'their big house');
...@@ -1594,7 +1594,7 @@ void main() { ...@@ -1594,7 +1594,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5); expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test', (WidgetTester tester) async { testWidgets('Down and up test', (WidgetTester tester) async {
await setupWidget(tester, 'a big house'); await setupWidget(tester, 'a big house');
...@@ -1612,7 +1612,7 @@ void main() { ...@@ -1612,7 +1612,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test 2', (WidgetTester tester) async { testWidgets('Down and up test 2', (WidgetTester tester) async {
await setupWidget(tester, 'a big house\njumped over a mouse\nOne more line yay'); await setupWidget(tester, 'a big house\njumped over a mouse\nOne more line yay');
...@@ -1663,7 +1663,7 @@ void main() { ...@@ -1663,7 +1663,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5); expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
}); }, variant: KeySimulatorTransitModeVariant.all());
}); });
testWidgets('Copy test', (WidgetTester tester) async { testWidgets('Copy test', (WidgetTester tester) async {
...@@ -1722,7 +1722,7 @@ void main() { ...@@ -1722,7 +1722,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Select all test', (WidgetTester tester) async { testWidgets('Select all test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -1757,7 +1757,7 @@ void main() { ...@@ -1757,7 +1757,7 @@ void main() {
expect(controller.selection.baseOffset, 0); expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 31); expect(controller.selection.extentOffset, 31);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('keyboard selection should call onSelectionChanged', (WidgetTester tester) async { testWidgets('keyboard selection should call onSelectionChanged', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -1804,7 +1804,7 @@ void main() { ...@@ -1804,7 +1804,7 @@ void main() {
expect(newSelection!.extentOffset, i + 1); expect(newSelection!.extentOffset, i + 1);
newSelection = null; newSelection = null;
} }
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing positions of selectable text', (WidgetTester tester) async { testWidgets('Changing positions of selectable text', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -1894,7 +1894,7 @@ void main() { ...@@ -1894,7 +1894,7 @@ void main() {
c1 = editableTextWidget.controller; c1 = editableTextWidget.controller;
expect(c1.selection.extentOffset - c1.selection.baseOffset, -6); expect(c1.selection.extentOffset - c1.selection.baseOffset, -6);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing focus test', (WidgetTester tester) async { testWidgets('Changing focus test', (WidgetTester tester) async {
...@@ -1964,7 +1964,7 @@ void main() { ...@@ -1964,7 +1964,7 @@ void main() {
expect(c1.selection.extentOffset - c1.selection.baseOffset, 0); expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5); expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
}); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Caret works when maxLines is null', (WidgetTester tester) async { testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -435,7 +435,33 @@ void main() { ...@@ -435,7 +435,33 @@ void main() {
invoked = 0; invoked = 0;
expect(RawKeyboard.instance.keysPressed, isEmpty); 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 { testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async {
int invoked = 0; int invoked = 0;
...@@ -1075,7 +1101,27 @@ void main() { ...@@ -1075,7 +1101,27 @@ void main() {
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 1); expect(invoked, 1);
invoked = 0; 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', () { group('CallbackShortcuts', () {
......
...@@ -828,6 +828,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -828,6 +828,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
assert(debugAssertAllSchedulerVarsUnset( assert(debugAssertAllSchedulerVarsUnset(
'The value of a scheduler debug variable was changed by the test.', '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) { void _verifyAutoUpdateGoldensUnset(bool valueBeforeTest) {
...@@ -894,6 +897,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -894,6 +897,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// tests. // tests.
// ignore: invalid_use_of_visible_for_testing_member // ignore: invalid_use_of_visible_for_testing_member
RawKeyboard.instance.clearKeysPressed(); 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, assert(!RendererBinding.instance!.mouseTracker.mouseIsConnected,
'The MouseTracker thinks that there is still a mouse connected, which indicates that a ' '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 ' 'test has not removed the mouse pointer which it added. Call removePointer on the '
......
...@@ -973,7 +973,7 @@ abstract class WidgetController { ...@@ -973,7 +973,7 @@ abstract class WidgetController {
return box.size; 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 /// This only simulates key events coming from a physical keyboard, not from a
/// soft keyboard. /// soft keyboard.
...@@ -984,6 +984,9 @@ abstract class WidgetController { ...@@ -984,6 +984,9 @@ abstract class WidgetController {
/// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet /// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet
/// supported. /// 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. /// 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 /// This method sends both the key down and the key up events, to simulate a
...@@ -1004,7 +1007,7 @@ abstract class WidgetController { ...@@ -1004,7 +1007,7 @@ abstract class WidgetController {
return handled; 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 /// This only simulates key down events coming from a physical keyboard, not
/// from a soft keyboard. /// from a soft keyboard.
...@@ -1015,13 +1018,17 @@ abstract class WidgetController { ...@@ -1015,13 +1018,17 @@ abstract class WidgetController {
/// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet /// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet
/// supported. /// 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. /// Keys that are down when the test completes are cleared after each test.
/// ///
/// Returns true if the key event was handled by the framework. /// Returns true if the key event was handled by the framework.
/// ///
/// See also: /// 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. /// - [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 { Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
assert(platform != null); assert(platform != null);
...@@ -1039,11 +1046,15 @@ abstract class WidgetController { ...@@ -1039,11 +1046,15 @@ abstract class WidgetController {
/// that type of system. Defaults to "web" on web, and "android" everywhere /// that type of system. Defaults to "web" on web, and "android" everywhere
/// else. May not be null. /// 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. /// Returns true if the key event was handled by the framework.
/// ///
/// See also: /// 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. /// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async { Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
assert(platform != null); assert(platform != null);
...@@ -1051,6 +1062,35 @@ abstract class WidgetController { ...@@ -1051,6 +1062,35 @@ abstract class WidgetController {
return simulateKeyUpEvent(key, platform: platform); 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 /// Returns the rect of the given widget. This is only valid once
/// the widget's render object has been laid out at least once. /// the widget's render object has been laid out at least once.
Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder); Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder);
......
...@@ -230,6 +230,7 @@ class TestPointer { ...@@ -230,6 +230,7 @@ class TestPointer {
timeStamp: timeStamp, timeStamp: timeStamp,
kind: kind, kind: kind,
device: _device, device: _device,
pointer: pointer,
position: _location ?? Offset.zero, position: _location ?? Offset.zero,
); );
} }
...@@ -255,6 +256,7 @@ class TestPointer { ...@@ -255,6 +256,7 @@ class TestPointer {
timeStamp: timeStamp, timeStamp: timeStamp,
kind: kind, kind: kind,
device: _device, device: _device,
pointer: pointer,
position: newLocation, position: newLocation,
delta: delta, delta: delta,
); );
......
...@@ -510,7 +510,7 @@ Future<void> expectLater( ...@@ -510,7 +510,7 @@ Future<void> expectLater(
/// Class that programmatically interacts with widgets and the test environment. /// Class that programmatically interacts with widgets and the test environment.
/// ///
/// For convenience, instances of this class (such as the one provided by /// 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 { class WidgetTester extends WidgetController implements HitTestDispatcher, TickerProvider {
WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) { WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
if (binding is LiveTestWidgetsFlutterBinding) if (binding is LiveTestWidgetsFlutterBinding)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment