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

Shortcut activator (#78522)

This PR introduced a new class, ShortcutActivator, and refactored the the definition and lookup system of shortcuts, in order to solve a few issues of the previous algorithm.
parent 600e4893
......@@ -132,8 +132,8 @@ abstract class RawKeyEventData {
///
/// If the modifier key wasn't pressed at the time of this event, returns
/// null. If the given key only appears in one place on the keyboard, returns
/// [KeyboardSide.all] if pressed. Never returns [KeyboardSide.any], because
/// that doesn't make sense in this context.
/// [KeyboardSide.all] if pressed. If the given platform does not specify
/// the side, return [KeyboardSide.any].
KeyboardSide? getModifierSide(ModifierKey key);
/// Returns true if a CTRL modifier key was pressed at the time of this event,
......@@ -734,15 +734,38 @@ class RawKeyboard {
};
void _synchronizeModifiers(RawKeyEvent event) {
// Don't send any key events for these changes, since there *should* be
// separate events for each modifier key down/up that occurs while the app
// has focus. This is just to synchronize the modifier keys when they are
// pressed/released while the app doesn't have focus, to make sure that
// _keysPressed reflects reality at all times.
// Compare modifier states to the ground truth as specified by
// [RawKeyEvent.data.modifiersPressed] and update unsynchronized ones.
//
// This function will update the state of modifier keys in `_keysPressed` so
// that they match the ones given by [RawKeyEvent.data.modifiersPressed].
// For a `modifiersPressed` result of anything but [KeyboardSide.any], the
// states in `_keysPressed` will be updated to exactly match the result,
// i.e. exactly one of "no down", "left down", "right down" or "both down".
//
// If `modifiersPressed` returns [KeyboardSide.any], the states in
// `_keysPressed` will be updated to a rough match, i.e. "either side down"
// or "no down". If `_keysPressed` has no modifier down, a
// [KeyboardSide.any] will synchronize by forcing the left modifier down. If
// `_keysPressed` has any modifier down, a [KeyboardSide.any] will not cause
// a state change.
final Map<ModifierKey, KeyboardSide?> modifiersPressed = event.data.modifiersPressed;
final Map<PhysicalKeyboardKey, LogicalKeyboardKey> modifierKeys = <PhysicalKeyboardKey, LogicalKeyboardKey>{};
// Physical keys that whose modifiers are pressed at any side.
final Set<PhysicalKeyboardKey> anySideKeys = <PhysicalKeyboardKey>{};
final Set<PhysicalKeyboardKey> keysPressedAfterEvent = <PhysicalKeyboardKey>{
..._keysPressed.keys,
if (event is RawKeyDownEvent) event.physicalKey,
};
for (final ModifierKey key in modifiersPressed.keys) {
if (modifiersPressed[key] == KeyboardSide.any) {
final Set<PhysicalKeyboardKey>? thisModifierKeys = _modifierKeyMap[_ModifierSidePair(key, KeyboardSide.all)];
anySideKeys.addAll(thisModifierKeys!);
if (thisModifierKeys.any(keysPressedAfterEvent.contains)) {
continue;
}
}
final Set<PhysicalKeyboardKey>? mappedKeys = _modifierKeyMap[_ModifierSidePair(key, modifiersPressed[key])];
assert((){
if (mappedKeys == null) {
......@@ -764,7 +787,9 @@ class RawKeyboard {
modifierKeys[physicalModifier] = _allModifiers[physicalModifier]!;
}
}
_allModifiersExceptFn.keys.forEach(_keysPressed.remove);
_allModifiersExceptFn.keys
.where((PhysicalKeyboardKey key) => !anySideKeys.contains(key))
.forEach(_keysPressed.remove);
if (event.data is! RawKeyEventDataFuchsia && event.data is! RawKeyEventDataMacOs) {
// On Fuchsia and macOS, the Fn key is not considered a modifier key.
_keysPressed.remove(PhysicalKeyboardKey.fn);
......
......@@ -119,7 +119,7 @@ class RawKeyEventDataWeb extends RawKeyEventData {
//
// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState>
// for more information.
return KeyboardSide.all;
return KeyboardSide.any;
}
// Modifier key masks.
......
......@@ -32,8 +32,7 @@ class KeySet<T extends KeyboardKey> {
///
/// If you need a set of more than four keys, use [KeySet.fromSet].
///
/// The `key1` parameter must not be null. The same [KeyboardKey] may
/// not be appear more than once in the set.
/// The same [KeyboardKey] may not be appear more than once in the set.
KeySet(
T key1, [
T? key2,
......@@ -70,7 +69,7 @@ class KeySet<T extends KeyboardKey> {
///
/// Do not mutate the `keys` set after passing it to this object.
///
/// The `keys` set must not be null, contain nulls, or be empty.
/// The `keys` set must not be empty.
KeySet.fromSet(Set<T> keys)
: assert(keys != null),
assert(keys.isNotEmpty),
......@@ -140,24 +139,150 @@ class KeySet<T extends KeyboardKey> {
}
}
/// An interface to define the keyboard key combination to trigger a shortcut.
///
/// [ShortcutActivator]s are used by [Shortcuts] widgets, and are mapped to
/// [Intent]s, the intended behavior that the key combination should trigger.
/// When a [Shortcuts] widget receives a key event, its [ShortcutManager] looks
/// up the first matching [ShortcutActivator], and signals the corresponding
/// [Intent], which might trigger an action as defined by a hierarchy of
/// [Actions] widgets. For a detailed introduction on the mechanism and use of
/// the shortcut-action system, see [Actions].
///
/// The matching [ShortcutActivator] is looked up in the following way:
///
/// * Find the registered [ShortcutActivator]s whose [triggers] contain the
/// incoming event.
/// * Of the previous list, finds the first activator whose [accepts] returns
/// true in the order of insertion.
///
/// See also:
///
/// * [SingleActivator], an implementation that represents a single key combined
/// with modifiers (control, shift, alt, meta).
/// * [LogicalKeySet], an implementation that requires one or more
/// [LogicalKeyboardKey]s to be pressed at the same time. Prefer
/// [SingleActivator] when possible.
abstract class ShortcutActivator {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ShortcutActivator();
/// All the keys that might be the final event to trigger this shortcut.
///
/// For example, for `Ctrl-A`, the KeyA is the only trigger, while Ctrl is not,
/// because the shortcut should only work by pressing KeyA *after* Ctrl, but
/// not before. For `Ctrl-A-E`, on the other hand, both KeyA and KeyE should be
/// triggers, since either of them is allowed to trigger.
///
/// The trigger keys are used as the first-pass filter for incoming events, as
/// [Intent]s are stored in a [Map] and indexed by trigger keys. Subclasses
/// should make sure that the return value of this method does not change
/// throughout the lifespan of this object.
Iterable<LogicalKeyboardKey> get triggers;
/// Whether the triggering `event` and the keyboard `state` at the time of the
/// event meet required conditions, providing that the event is a triggering
/// event.
///
/// For example, for `Ctrl-A`, it has to check if the event is a
/// [RawKeyDownEvent], if either side of the Ctrl key is pressed, and none of
/// the Shift keys, Alt keys, or Meta keys are pressed; it doesn't have to
/// check if KeyA is pressed, since it's already guaranteed.
///
/// This method must not cause any side effects for the `state`. Typically
/// this is only used to query whether [RawKeyboard.keysPressed] contains
/// a key.
///
/// See also:
///
/// * [LogicalKeyboardKey.collapseSynonyms], which helps deciding whether a
/// modifier key is pressed when the side variation is not important.
bool accepts(RawKeyEvent event, RawKeyboard state);
/// Returns a description of the key set that is short and readable.
///
/// Intended to be used in debug mode for logging purposes.
String debugDescribeKeys();
}
/// A set of [LogicalKeyboardKey]s that can be used as the keys in a map.
///
/// A key set contains the keys that are down simultaneously to represent a
/// shortcut.
/// [LogicalKeySet] can be used as a [ShortcutActivator]. It is not recommended
/// to use [LogicalKeySet] for a common shortcut such as `Delete` or `Ctrl+C`,
/// prefer [SingleActivator] when possible, whose behavior more closely resembles
/// that of typical platforms.
///
/// This is mainly used by [ShortcutManager] and [Shortcuts] widget to allow the
/// definition of shortcut mappings.
/// When used as a [ShortcutActivator], [LogicalKeySet] will activate the intent
/// when all [keys] are pressed, and no others, except that modifier keys are
/// considered without considering sides (e.g. control left and control right are
/// considered the same).
///
/// This is a thin wrapper around a [Set], but changes the equality comparison
/// from an identity comparison to a contents comparison so that non-identical
/// sets with the same keys in them will compare as equal.
class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable {
/// {@tool dartpad --template=stateful_widget_scaffold_center}
/// In the following example, the counter is increased when the following key
/// sequences are pressed:
///
/// * Control left, then C.
/// * Control right, then C.
/// * C, then Control left.
///
/// But not when:
///
/// * Control left, then A, then C.
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```
///
/// ```dart preamble
/// class IncrementIntent extends Intent {
/// const IncrementIntent();
/// }
/// ```
///
/// ```dart
/// int count = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// return Shortcuts(
/// shortcuts: <ShortcutActivator, Intent>{
/// LogicalKeySet(LogicalKeyboardKey.keyC, LogicalKeyboardKey.controlLeft): const IncrementIntent(),
/// },
/// child: Actions(
/// actions: <Type, Action<Intent>>{
/// IncrementIntent: CallbackAction<IncrementIntent>(
/// onInvoke: (IncrementIntent intent) => setState(() {
/// count = count + 1;
/// }),
/// ),
/// },
/// child: Focus(
/// autofocus: true,
/// child: Column(
/// children: <Widget>[
/// const Text('Add to the counter by pressing Ctrl+C'),
/// Text('count: $count'),
/// ],
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// This is also a thin wrapper around a [Set], but changes the equality
/// comparison from an identity comparison to a contents comparison so that
/// non-identical sets with the same keys in them will compare as equal.
class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable
implements ShortcutActivator {
/// A constructor for making a [LogicalKeySet] of up to four keys.
///
/// If you need a set of more than four keys, use [LogicalKeySet.fromSet].
///
/// The `key1` parameter must not be null. The same [LogicalKeyboardKey] may
/// not be appear more than once in the set.
/// The same [LogicalKeyboardKey] may not be appear more than once in the set.
LogicalKeySet(
LogicalKeyboardKey key1, [
LogicalKeyboardKey? key2,
......@@ -168,20 +293,39 @@ class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable {
/// Create a [LogicalKeySet] from a set of [LogicalKeyboardKey]s.
///
/// Do not mutate the `keys` set after passing it to this object.
///
/// The `keys` must not be null.
LogicalKeySet.fromSet(Set<LogicalKeyboardKey> keys) : super.fromSet(keys);
@override
Iterable<LogicalKeyboardKey> get triggers => _triggers;
late final Set<LogicalKeyboardKey> _triggers = keys.expand(
(LogicalKeyboardKey key) => _unmapSynonyms[key] ?? <LogicalKeyboardKey>[key]).toSet();
@override
bool accepts(RawKeyEvent event, RawKeyboard state) {
final Set<LogicalKeyboardKey> collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys);
final Set<LogicalKeyboardKey> collapsedPressed = LogicalKeyboardKey.collapseSynonyms(state.keysPressed);
final bool keysEqual = collapsedRequired.difference(collapsedPressed).isEmpty
&& collapsedRequired.length == collapsedPressed.length;
return event is RawKeyDownEvent && keysEqual;
}
static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{
LogicalKeyboardKey.alt,
LogicalKeyboardKey.control,
LogicalKeyboardKey.meta,
LogicalKeyboardKey.shift,
};
static final Map<LogicalKeyboardKey, List<LogicalKeyboardKey>> _unmapSynonyms = <LogicalKeyboardKey, List<LogicalKeyboardKey>>{
LogicalKeyboardKey.control: <LogicalKeyboardKey>[LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight],
LogicalKeyboardKey.shift: <LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight],
LogicalKeyboardKey.alt: <LogicalKeyboardKey>[LogicalKeyboardKey.altLeft, LogicalKeyboardKey.altRight],
LogicalKeyboardKey.meta: <LogicalKeyboardKey>[LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight],
};
/// Returns a description of the key set that is short and readable.
///
/// Intended to be used in debug mode for logging purposes.
@override
String debugDescribeKeys() {
final List<LogicalKeyboardKey> sortedKeys = keys.toList()..sort(
(LogicalKeyboardKey a, LogicalKeyboardKey b) {
......@@ -210,14 +354,12 @@ class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable {
/// A [DiagnosticsProperty] which handles formatting a `Map<LogicalKeySet,
/// Intent>` (the same type as the [Shortcuts.shortcuts] property) so that its
/// diagnostic output is human-readable.
class ShortcutMapProperty extends DiagnosticsProperty<Map<LogicalKeySet, Intent>> {
class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Intent>> {
/// Create a diagnostics property for `Map<LogicalKeySet, Intent>` objects,
/// which are the same type as the [Shortcuts.shortcuts] property.
///
/// The [showName] and [level] arguments must not be null.
ShortcutMapProperty(
String name,
Map<LogicalKeySet, Intent> value, {
Map<ShortcutActivator, Intent> value, {
bool showName = true,
Object defaultValue = kNoDefaultValue,
DiagnosticLevel level = DiagnosticLevel.info,
......@@ -234,11 +376,222 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<LogicalKeySet, Intent>
);
@override
Map<LogicalKeySet, Intent> get value => super.value!;
Map<ShortcutActivator, Intent> get value => super.value!;
@override
String valueToString({ TextTreeConfiguration? parentConfiguration }) {
return '{${value.keys.map<String>((LogicalKeySet keySet) => '{${keySet.debugDescribeKeys()}}: ${value[keySet]}').join(', ')}}';
String valueToString({TextTreeConfiguration? parentConfiguration}) {
return '{${value.keys.map<String>((ShortcutActivator keySet) => '{${keySet.debugDescribeKeys()}}: ${value[keySet]}').join(', ')}}';
}
}
/// A shortcut key combination of a single key and modifiers.
///
/// This [ShortcutActivator] implements typical shortcuts such as:
///
/// * ArrowLeft
/// * Shift + Delete
/// * Control + Alt + Meta + Shift + A
///
/// More specifically, it creates shortcut key combinations that are composed of a
/// [trigger] key, and zero, some, or all of the four modifiers (control, shift,
/// alt, meta). The shortcut is activated when the following conditions are met:
///
/// * The incoming event is a down event for a [trigger] key.
/// * If [control] is true, then at least one control key must be held.
/// Otherwise, no control keys must be held.
/// * Similar conditions apply for the [alt], [shift], and [meta] keys.
///
/// This resembles the typical behavior of most operating systems, and handles
/// modifier keys differently from [LogicalKeySet] in the following way:
///
/// * [SingleActivator]s allow additional non-modifier keys being pressed in
/// order to activate the shortcut. For example, pressing key X while holding
/// ControlLeft *and key A* will be accepted by
/// `SingleActivator(LogicalKeyboardKey.keyX, control: true)`.
/// * [SingleActivator]s do not consider modifiers to be a trigger key. For
/// example, pressing ControlLeft while holding key X *will not* activate a
/// `SingleActivator(LogicalKeyboardKey.keyX, control: true)`.
class SingleActivator with Diagnosticable implements ShortcutActivator {
/// Create an activator of a trigger key and modifiers.
///
/// 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
/// a modifier key (sided or unsided).
///
/// The `control`, `shift`, `alt`, and `meta` flags represent whether
/// the respect modifier keys should be held (true) or released (false)
///
/// {@tool dartpad --template=stateful_widget_scaffold_center}
/// In the following example, the shortcut `Control + C` increases the counter:
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```
///
/// ```dart preamble
/// class IncrementIntent extends Intent {
/// const IncrementIntent();
/// }
/// ```
///
/// ```dart
/// int count = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// return Shortcuts(
/// shortcuts: const <ShortcutActivator, Intent>{
/// SingleActivator(LogicalKeyboardKey.keyC, control: true): IncrementIntent(),
/// },
/// child: Actions(
/// actions: <Type, Action<Intent>>{
/// IncrementIntent: CallbackAction<IncrementIntent>(
/// onInvoke: (IncrementIntent intent) => setState(() {
/// count = count + 1;
/// }),
/// ),
/// },
/// child: Focus(
/// autofocus: true,
/// child: Column(
/// children: <Widget>[
/// const Text('Add to the counter by pressing Ctrl+C'),
/// Text('count: $count'),
/// ],
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
const SingleActivator(
this.trigger, {
this.control = false,
this.shift = false,
this.alt = false,
this.meta = false,
}) : // The enumerated check with `identical` is cumbersome but the only way
// since const constructors can not call functions such as `==` or
// `Set.contains`. Checking with `identical` is sufficient since
// `LogicalKeyboardKey` only provides cached values.
assert(!identical(trigger, LogicalKeyboardKey.control)
&& !identical(trigger, LogicalKeyboardKey.controlLeft)
&& !identical(trigger, LogicalKeyboardKey.controlRight)
&& !identical(trigger, LogicalKeyboardKey.shift)
&& !identical(trigger, LogicalKeyboardKey.shiftLeft)
&& !identical(trigger, LogicalKeyboardKey.shiftRight)
&& !identical(trigger, LogicalKeyboardKey.alt)
&& !identical(trigger, LogicalKeyboardKey.altLeft)
&& !identical(trigger, LogicalKeyboardKey.altRight)
&& !identical(trigger, LogicalKeyboardKey.meta)
&& !identical(trigger, LogicalKeyboardKey.metaLeft)
&& !identical(trigger, LogicalKeyboardKey.metaRight));
/// The non-modifier key of the shortcut that is pressed after all modifiers
/// to activate the shortcut.
///
/// For example, for `Control + C`, [trigger] should be
/// [LogicalKeyboardKey.keyC].
final LogicalKeyboardKey trigger;
/// Whether either (or both) control keys should be held for [trigger] to
/// activate the shortcut.
///
/// If false, then all control keys must be released when the event is received
/// in order to activate the shortcut.
///
/// See also:
///
/// * [LogicalKeyboardKey.controlLeft], [LogicalKeyboardKey.controlRight].
final bool control;
/// Whether either (or both) shift keys should be held for [trigger] to
/// activate the shortcut.
///
/// If false, then all shift keys must be released when the event is received
/// in order to activate the shortcut.
///
/// See also:
///
/// * [LogicalKeyboardKey.shiftLeft], [LogicalKeyboardKey.shiftRight].
final bool shift;
/// Whether either (or both) alt keys should be held for [trigger] to
/// activate the shortcut.
///
/// If false, then all alt keys must be released when the event is received
/// in order to activate the shortcut.
///
/// See also:
///
/// * [LogicalKeyboardKey.altLeft], [LogicalKeyboardKey.altRight].
final bool alt;
/// Whether either (or both) meta keys should be held for [trigger] to
/// activate the shortcut.
///
/// If false, then all meta keys must be released when the event is received
/// in order to activate the shortcut.
///
/// See also:
///
/// * [LogicalKeyboardKey.metaLeft], [LogicalKeyboardKey.metaRight].
final bool meta;
@override
Iterable<LogicalKeyboardKey> get triggers sync* {
yield trigger;
}
@override
bool accepts(RawKeyEvent event, RawKeyboard state) {
final Set<LogicalKeyboardKey> pressed = state.keysPressed;
return event is RawKeyDownEvent
&& (control == (pressed.contains(LogicalKeyboardKey.controlLeft) || pressed.contains(LogicalKeyboardKey.controlRight)))
&& (shift == (pressed.contains(LogicalKeyboardKey.shiftLeft) || pressed.contains(LogicalKeyboardKey.shiftRight)))
&& (alt == (pressed.contains(LogicalKeyboardKey.altLeft) || pressed.contains(LogicalKeyboardKey.altRight)))
&& (meta == (pressed.contains(LogicalKeyboardKey.metaLeft) || pressed.contains(LogicalKeyboardKey.metaRight)));
}
/// Returns a short and readable description of the key combination.
///
/// Intended to be used in debug mode for logging purposes. In release mode,
/// [debugDescribeKeys] returns an empty string.
@override
String debugDescribeKeys() {
String result = '';
assert(() {
final List<String> keys = <String>[
if (control) 'Control',
if (alt) 'Alt',
if (meta) 'Meta',
if (shift) 'Shift',
trigger.debugName ?? trigger.toStringShort(),
];
result = keys.join(' + ');
return true;
}());
return result;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('keys', debugDescribeKeys()));
}
}
class _ActivatorIntentPair with Diagnosticable {
const _ActivatorIntentPair(this.activator, this.intent);
final ShortcutActivator activator;
final Intent intent;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('activator', activator.debugDescribeKeys()));
properties.add(DiagnosticsProperty<Intent>('intent', intent));
}
}
......@@ -248,10 +601,8 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<LogicalKeySet, Intent>
/// the widget that you want to find a manager for.
class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// Constructs a [ShortcutManager].
///
/// The [shortcuts] argument must not be null.
ShortcutManager({
Map<LogicalKeySet, Intent> shortcuts = const <LogicalKeySet, Intent>{},
Map<ShortcutActivator, Intent> shortcuts = const <ShortcutActivator, Intent>{},
this.modal = false,
}) : assert(shortcuts != null),
_shortcuts = shortcuts;
......@@ -274,16 +625,32 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// When the map is changed, listeners to this manager will be notified.
///
/// The returned map should not be modified.
Map<LogicalKeySet, Intent> get shortcuts => _shortcuts;
Map<LogicalKeySet, Intent> _shortcuts;
set shortcuts(Map<LogicalKeySet, Intent> value) {
Map<ShortcutActivator, Intent> get shortcuts => _shortcuts;
Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{};
set shortcuts(Map<ShortcutActivator, Intent> value) {
assert(value != null);
if (!mapEquals<LogicalKeySet, Intent>(_shortcuts, value)) {
if (!mapEquals<ShortcutActivator, Intent>(_shortcuts, value)) {
_shortcuts = value;
_indexedShortcutsCache = null;
notifyListeners();
}
}
static Map<LogicalKeyboardKey, List<_ActivatorIntentPair>> _indexShortcuts(Map<ShortcutActivator, Intent> source) {
final Map<LogicalKeyboardKey, List<_ActivatorIntentPair>> result = <LogicalKeyboardKey, List<_ActivatorIntentPair>>{};
source.forEach((ShortcutActivator activator, Intent intent) {
for (final LogicalKeyboardKey trigger in activator.triggers) {
result.putIfAbsent(trigger, () => <_ActivatorIntentPair>[])
.add(_ActivatorIntentPair(activator, intent));
}
});
return result;
}
Map<LogicalKeyboardKey, List<_ActivatorIntentPair>> get _indexedShortcuts {
return _indexedShortcutsCache ??= _indexShortcuts(_shortcuts);
}
Map<LogicalKeyboardKey, List<_ActivatorIntentPair>>? _indexedShortcutsCache;
/// Returns the [Intent], if any, that matches the current set of pressed
/// keys.
///
......@@ -291,43 +658,24 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
///
/// Defaults to a set derived from [RawKeyboard.keysPressed] if `keysPressed`
/// is not supplied.
Intent? _find({ LogicalKeySet? keysPressed }) {
if (keysPressed == null && RawKeyboard.instance.keysPressed.isEmpty) {
Intent? _find(RawKeyEvent event, RawKeyboard state) {
final List<_ActivatorIntentPair>? candidates = _indexedShortcuts[event.logicalKey];
if (candidates == null)
return null;
}
keysPressed ??= LogicalKeySet.fromSet(RawKeyboard.instance.keysPressed);
Intent? matchedIntent = _shortcuts[keysPressed];
if (matchedIntent == null) {
// If there's not a more specific match, We also look for any keys that
// have synonyms in the map. This is for things like left and right shift
// keys mapping to just the "shift" pseudo-key.
final Set<LogicalKeyboardKey> pseudoKeys = <LogicalKeyboardKey>{};
for (final KeyboardKey setKey in keysPressed.keys) {
if (setKey is LogicalKeyboardKey) {
final Set<LogicalKeyboardKey> synonyms = setKey.synonyms;
if (synonyms.isNotEmpty) {
// There currently aren't any synonyms that match more than one key.
assert(synonyms.length == 1, 'Unexpectedly encountered a key synonym with more than one key.');
pseudoKeys.add(synonyms.first);
} else {
pseudoKeys.add(setKey);
}
}
for (final _ActivatorIntentPair activatorIntent in candidates) {
if (activatorIntent.activator.accepts(event, state)) {
return activatorIntent.intent;
}
matchedIntent = _shortcuts[LogicalKeySet.fromSet(pseudoKeys)];
}
return matchedIntent;
return null;
}
/// Handles a key press `event` in the given `context`.
///
/// The optional `keysPressed` argument is used as the set of currently
/// pressed keys. Defaults to a set derived from [RawKeyboard.keysPressed] if
/// `keysPressed` is not supplied.
///
/// If a key mapping is found, then the associated action will be invoked
/// using the [Intent] that the `keysPressed` maps to, and the currently
/// focused widget's context (from [FocusManager.primaryFocus]).
/// If a key mapping is found, then the associated action will be invoked using
/// the [Intent] activated by the [ShortcutActivator] in the [shortcuts] map,
/// and the currently focused widget's context (from
/// [FocusManager.primaryFocus]).
///
/// Returns a [KeyEventResult.handled] if an action was invoked, otherwise a
/// [KeyEventResult.skipRemainingHandlers] if [modal] is true, or if it maps
......@@ -338,23 +686,19 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// returned), a pressed [KeySet] must be mapped to an [Intent], the [Intent]
/// must be mapped to an [Action], and the [Action] must be enabled.
@protected
KeyEventResult handleKeypress(
BuildContext context,
RawKeyEvent event, {
LogicalKeySet? keysPressed,
}) {
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored;
}
assert(context != null);
assert(
keysPressed != null || RawKeyboard.instance.keysPressed.isNotEmpty,
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(keysPressed: keysPressed);
final Intent? matchedIntent = _find(event, RawKeyboard.instance);
if (matchedIntent != null) {
final BuildContext? primaryContext = primaryFocus?.context;
if (primaryContext != null) {
......@@ -376,7 +720,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Map<LogicalKeySet, Intent>>('shortcuts', _shortcuts));
properties.add(DiagnosticsProperty<Map<ShortcutActivator, Intent>>('shortcuts', _shortcuts));
properties.add(FlagProperty('modal', value: modal, ifTrue: 'modal', defaultValue: false));
}
}
......@@ -418,7 +762,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// @override
/// Widget build(BuildContext context) {
/// return Shortcuts(
/// shortcuts: <LogicalKeySet, Intent>{
/// shortcuts: <ShortcutActivator, Intent>{
/// LogicalKeySet(LogicalKeyboardKey.arrowUp): const IncrementIntent(),
/// LogicalKeySet(LogicalKeyboardKey.arrowDown): const DecrementIntent(),
/// },
......@@ -525,7 +869,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// @override
/// Widget build(BuildContext context) {
/// return Shortcuts(
/// shortcuts: <LogicalKeySet, Intent>{
/// shortcuts: <ShortcutActivator, Intent>{
/// LogicalKeySet(LogicalKeyboardKey.arrowUp): const IncrementIntent(2),
/// LogicalKeySet(LogicalKeyboardKey.arrowDown): const DecrementIntent(2),
/// },
......@@ -565,7 +909,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
class Shortcuts extends StatefulWidget {
/// Creates a const [Shortcuts] widget.
///
/// The [child] and [shortcuts] arguments are required and must not be null.
/// The [child] and [shortcuts] arguments are required.
const Shortcuts({
Key? key,
this.manager,
......@@ -592,7 +936,7 @@ class Shortcuts extends StatefulWidget {
/// in here (e.g. a final variable from your widget class) instead of defining
/// it inline in the build function.
/// {@endtemplate}
final Map<LogicalKeySet, Intent> shortcuts;
final Map<ShortcutActivator, Intent> shortcuts;
/// The child widget for this [Shortcuts] widget.
///
......@@ -611,8 +955,6 @@ class Shortcuts extends StatefulWidget {
/// Returns the [ShortcutManager] that most tightly encloses the given
/// [BuildContext].
///
/// The [context] argument must not be null.
///
/// If no [Shortcuts] widget encloses the context given, will assert in debug
/// mode and throw an exception in release mode.
///
......@@ -643,8 +985,6 @@ class Shortcuts extends StatefulWidget {
/// Returns the [ShortcutManager] that most tightly encloses the given
/// [BuildContext].
///
/// The [context] argument must not be null.
///
/// If no [Shortcuts] widget encloses the context given, will return null.
///
/// See also:
......
......@@ -354,16 +354,15 @@ void main() {
testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async {
expect(RawKeyboard.instance.keysPressed, isEmpty);
// Generate the data for a regular key down event.
// Generate the data for a regular key down event. Change the modifiers so
// that they show the shift key as already down when this event is
// received, but it's not in keysPressed yet.
final Map<String, dynamic> data = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.keyA,
platform: 'web',
isDown: true,
);
// Change the modifiers so that they show the shift key as already down
// when this event is received, but it's not in keysPressed yet.
data['metaState'] |= RawKeyEventDataWeb.modifierShift;
// dispatch the modified data.
)..['metaState'] |= RawKeyEventDataWeb.modifierShift;
// Dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data),
......@@ -374,13 +373,71 @@ void main() {
equals(
<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
// Web doesn't distinguish between left and right keys, so they're
// all shown as down when either is pressed.
LogicalKeyboardKey.shiftRight,
LogicalKeyboardKey.keyA,
},
),
);
// Generate the data for a regular key up event. Don't set the modifiers
// for shift so that they show the shift key as already up when this event
// is received, and it's in keysPressed.
final Map<String, dynamic> data2 = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.keyA,
platform: 'web',
isDown: false,
)..['metaState'] = 0;
// Dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data2),
(ByteData? data) {},
);
expect(
RawKeyboard.instance.keysPressed,
equals(
<LogicalKeyboardKey>{},
),
);
// Press right modifier key
final Map<String, dynamic> data3 = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.shiftRight,
platform: 'web',
isDown: true,
)..['metaState'] |= RawKeyEventDataWeb.modifierShift;
// Dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data3),
(ByteData? data) {},
);
expect(
RawKeyboard.instance.keysPressed,
equals(
<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftRight,
},
),
);
// Release the key
final Map<String, dynamic> data4 = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.shiftRight,
platform: 'web',
isDown: false,
)..['metaState'] = 0;
// Dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data4),
(ByteData? data) {},
);
expect(
RawKeyboard.instance.keysPressed,
equals(
<LogicalKeyboardKey>{},
),
);
});
testWidgets('sided modifiers without a side set return all sides on Android', (WidgetTester tester) async {
......@@ -572,7 +629,7 @@ void main() {
);
}, skip: isBrowser); // This is a GLFW-specific test.
testWidgets('sided modifiers without a side set return all sides on web', (WidgetTester tester) async {
testWidgets('sided modifiers without a side set return left sides on web', (WidgetTester tester) async {
expect(RawKeyboard.instance.keysPressed, isEmpty);
// Generate the data for a regular key down event.
final Map<String, dynamic> data = KeyEventSimulator.getKeyData(
......@@ -597,13 +654,9 @@ void main() {
equals(
<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
LogicalKeyboardKey.altLeft,
LogicalKeyboardKey.altRight,
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.controlRight,
LogicalKeyboardKey.metaLeft,
LogicalKeyboardKey.metaRight,
LogicalKeyboardKey.keyA,
},
),
......
......@@ -41,14 +41,35 @@ class TestShortcutManager extends ShortcutManager {
List<LogicalKeyboardKey> keys;
@override
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event, {LogicalKeySet? keysPressed}) {
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
keys.add(event.logicalKey);
}
return super.handleKeypress(context, event, keysPressed: keysPressed);
return super.handleKeypress(context, event);
}
}
Widget activatorTester(ShortcutActivator activator, ValueSetter<Intent> onInvoke) {
return Actions(
key: GlobalKey(),
actions: <Type, Action<Intent>>{
TestIntent: TestAction(onInvoke: (Intent intent) {
onInvoke(intent);
return true;
}),
},
child: Shortcuts(
shortcuts: <ShortcutActivator, Intent>{
activator: const TestIntent(),
},
child: const Focus(
autofocus: true,
child: SizedBox(width: 100, height: 100),
),
),
);
}
void main() {
group(LogicalKeySet, () {
test('LogicalKeySet passes parameters correctly.', () {
......@@ -146,6 +167,71 @@ void main() {
})),
);
});
testWidgets('handles two keys', (WidgetTester tester) async {
int invoked = 0;
await tester.pumpWidget(activatorTester(
LogicalKeySet(
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.control,
),
(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.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 1);
invoked = 0;
// KeyC -> LCtrl: Accept
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 1);
invoked = 0;
// RCtrl -> KeyC: Accept
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
expect(invoked, 1);
invoked = 0;
// LCtrl -> LShift -> KeyC: Reject
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 0);
invoked = 0;
// LCtrl -> KeyA -> KeyC: Reject
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
expect(invoked, 0);
invoked = 0;
expect(RawKeyboard.instance.keysPressed, isEmpty);
});
test('LogicalKeySet.hashCode is stable', () {
final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA);
......@@ -196,6 +282,194 @@ void main() {
expect(description[0], equals('keys: Key A + Key B'));
});
});
group(SingleActivator, () {
testWidgets('handles Ctrl-C', (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.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 1);
invoked = 0;
// KeyC -> LCtrl: Reject
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
invoked = 0;
// LShift -> LCtrl -> KeyC: Reject
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
invoked = 0;
// With Ctrl-C pressed, KeyA -> Release KeyA: Reject
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
invoked = 0;
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
expect(invoked, 0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
invoked = 0;
// LCtrl -> KeyA -> KeyC: Accept
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
invoked = 0;
// RCtrl -> KeyC: Accept
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
expect(invoked, 1);
invoked = 0;
// LCtrl -> RCtrl -> KeyC: Accept
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
expect(invoked, 1);
invoked = 0;
// While holding Ctrl-C, press KeyA: Reject
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 1);
invoked = 0;
expect(RawKeyboard.instance.keysPressed, isEmpty);
});
testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async {
int invoked = 0;
await tester.pumpWidget(activatorTester(
const SingleActivator(
LogicalKeyboardKey.keyC,
shift: true,
control: true,
),
(Intent intent) { invoked += 1; },
));
await tester.pump();
// LShift -> LCtrl -> KeyC: Accept
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 1);
invoked = 0;
// LCtrl -> LShift -> KeyC: Accept
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 1);
invoked = 0;
// LCtrl -> KeyC -> LShift: Reject
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 0);
invoked = 0;
expect(RawKeyboard.instance.keysPressed, isEmpty);
});
test('diagnostics.', () {
{
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SingleActivator(
LogicalKeyboardKey.keyA,
).debugFillProperties(builder);
final List<String> description = builder.properties.where((DiagnosticsNode node) {
return !node.isFiltered(DiagnosticLevel.info);
}).map((DiagnosticsNode node) => node.toString()).toList();
expect(description.length, equals(1));
expect(description[0], equals('keys: Key A'));
}
{
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
shift: true,
alt: true,
meta: true,
).debugFillProperties(builder);
final List<String> description = builder.properties.where((DiagnosticsNode node) {
return !node.isFiltered(DiagnosticLevel.info);
}).map((DiagnosticsNode node) => node.toString()).toList();
expect(description.length, equals(1));
expect(description[0], equals('keys: Control + Alt + Meta + Shift + Key A'));
}
});
});
group(Shortcuts, () {
testWidgets('Default constructed Shortcuts has empty shortcuts', (WidgetTester tester) async {
final ShortcutManager manager = ShortcutManager();
......
......@@ -21,6 +21,8 @@ import 'test_pointer.dart';
/// This value must be greater than [kTouchSlop].
const double kDragSlopDefault = 20.0;
const String _defaultPlatform = kIsWeb ? 'web' : 'android';
/// Class that programmatically interacts with widgets.
///
/// For a variant of this class suited specifically for unit tests, see
......@@ -976,8 +978,8 @@ abstract class WidgetController {
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". Must not be null. Some platforms (e.g.
/// Windows, iOS) are not yet supported.
/// 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.
///
/// Keys that are down when the test completes are cleared after each test.
///
......@@ -991,7 +993,7 @@ abstract class WidgetController {
///
/// - [sendKeyDownEvent] to simulate only a key down event.
/// - [sendKeyUpEvent] to simulate only a key up event.
Future<bool> sendKeyEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
Future<bool> sendKeyEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
assert(platform != null);
final bool handled = await simulateKeyDownEvent(key, platform: platform);
// Internally wrapped in async guard.
......@@ -1006,8 +1008,8 @@ abstract class WidgetController {
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". Must not be null. Some platforms (e.g.
/// Windows, iOS) are not yet supported.
/// 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.
///
/// Keys that are down when the test completes are cleared after each test.
///
......@@ -1017,7 +1019,7 @@ abstract class WidgetController {
///
/// - [sendKeyUpEvent] to simulate the corresponding key up event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
assert(platform != null);
// Internally wrapped in async guard.
return simulateKeyDownEvent(key, platform: platform);
......@@ -1029,8 +1031,8 @@ abstract class WidgetController {
/// not from a soft keyboard.
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". May not be null.
/// [Platform.operatingSystem] to make the event appear to be from that type of
/// system. Defaults to "web" on web, and "android" everywhere else. May not be null.
///
/// Returns true if the key event was handled by the framework.
///
......@@ -1038,7 +1040,7 @@ abstract class WidgetController {
///
/// - [sendKeyDownEvent] to simulate the corresponding key down event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
assert(platform != null);
// Internally wrapped in async guard.
return simulateKeyUpEvent(key, platform: platform);
......
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