Prepare ShortcutActivator and ShortcutManager to migrate to KeyEvent from RawKeyEvent. (#136854)
This commit is contained in:
@@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'binding.dart';
|
||||
import 'debug.dart';
|
||||
import 'raw_keyboard.dart';
|
||||
import 'raw_keyboard_android.dart';
|
||||
import 'system_channels.dart';
|
||||
|
||||
export 'dart:ui' show KeyData;
|
||||
@@ -124,6 +125,7 @@ abstract class KeyEvent with Diagnosticable {
|
||||
required this.logicalKey,
|
||||
this.character,
|
||||
required this.timeStamp,
|
||||
this.deviceType = ui.KeyEventDeviceType.keyboard,
|
||||
this.synthesized = false,
|
||||
});
|
||||
|
||||
@@ -206,6 +208,13 @@ abstract class KeyEvent with Diagnosticable {
|
||||
/// All events share the same timeStamp origin.
|
||||
final Duration timeStamp;
|
||||
|
||||
/// The source device type for the key event.
|
||||
///
|
||||
/// Not all platforms supply an accurate type.
|
||||
///
|
||||
/// Defaults to [ui.KeyEventDeviceType.keyboard].
|
||||
final ui.KeyEventDeviceType deviceType;
|
||||
|
||||
/// Whether this event is synthesized by Flutter to synchronize key states.
|
||||
///
|
||||
/// An non-[synthesized] event is converted from a native event, and a native
|
||||
@@ -253,6 +262,7 @@ class KeyDownEvent extends KeyEvent {
|
||||
super.character,
|
||||
required super.timeStamp,
|
||||
super.synthesized,
|
||||
super.deviceType,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -272,6 +282,7 @@ class KeyUpEvent extends KeyEvent {
|
||||
required super.logicalKey,
|
||||
required super.timeStamp,
|
||||
super.synthesized,
|
||||
super.deviceType,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,6 +306,7 @@ class KeyRepeatEvent extends KeyEvent {
|
||||
required super.logicalKey,
|
||||
super.character,
|
||||
required super.timeStamp,
|
||||
super.deviceType,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -719,20 +731,20 @@ enum KeyDataTransitMode {
|
||||
/// to both [KeyMessage.events] and [KeyMessage.rawEvent].
|
||||
rawKeyData,
|
||||
|
||||
/// Key event information is delivered as converted key data, followed
|
||||
/// by raw key data.
|
||||
/// Key event information is delivered as converted key data, followed by raw
|
||||
/// key data.
|
||||
///
|
||||
/// Key data ([ui.KeyData]) is a standardized event stream converted from
|
||||
/// platform's native key event information, sent through the embedder
|
||||
/// API. Its event model is described in [HardwareKeyboard].
|
||||
/// platform's native key event information, sent through the embedder API.
|
||||
/// Its event model is described in [HardwareKeyboard].
|
||||
///
|
||||
/// Raw key data is platform's native key event information sent in JSON
|
||||
/// through a method channel. It is interpreted by subclasses of
|
||||
/// [RawKeyEventData].
|
||||
///
|
||||
/// If the current transit mode is [rawKeyData], the key data is converted to
|
||||
/// [KeyMessage.events], and the raw key data is converted to
|
||||
/// [KeyMessage.rawEvent].
|
||||
/// If the current transit mode is [keyDataThenRawKeyData], then the
|
||||
/// [KeyEventManager] will use the [ui.KeyData] for [KeyMessage.events], and
|
||||
/// the raw data for [KeyMessage.rawEvent].
|
||||
keyDataThenRawKeyData,
|
||||
}
|
||||
|
||||
@@ -1112,6 +1124,33 @@ class KeyEventManager {
|
||||
return <String, dynamic>{ 'handled': handled };
|
||||
}
|
||||
|
||||
ui.KeyEventDeviceType _convertDeviceType(RawKeyEvent rawEvent) {
|
||||
final RawKeyEventData data = rawEvent.data;
|
||||
// Device type is only available from Android.
|
||||
if (data is! RawKeyEventDataAndroid) {
|
||||
return ui.KeyEventDeviceType.keyboard;
|
||||
}
|
||||
|
||||
switch (data.eventSource) {
|
||||
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_KEYBOARD
|
||||
case 0x00000101:
|
||||
return ui.KeyEventDeviceType.keyboard;
|
||||
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_DPAD
|
||||
case 0x00000201:
|
||||
return ui.KeyEventDeviceType.directionalPad;
|
||||
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_GAMEPAD
|
||||
case 0x00000401:
|
||||
return ui.KeyEventDeviceType.gamepad;
|
||||
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_JOYSTICK
|
||||
case 0x01000010:
|
||||
return ui.KeyEventDeviceType.joystick;
|
||||
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_HDMI
|
||||
case 0x02000001:
|
||||
return ui.KeyEventDeviceType.hdmi;
|
||||
}
|
||||
return ui.KeyEventDeviceType.keyboard;
|
||||
}
|
||||
|
||||
// Convert the raw event to key events, including synthesizing events for
|
||||
// modifiers, and store the key events in `_keyEventsSinceLastMessage`.
|
||||
//
|
||||
@@ -1126,6 +1165,7 @@ class KeyEventManager {
|
||||
final LogicalKeyboardKey? recordedLogicalMain = _hardwareKeyboard.lookUpLayout(physicalKey);
|
||||
final Duration timeStamp = ServicesBinding.instance.currentSystemFrameTimeStamp;
|
||||
final String? character = rawEvent.character == '' ? null : rawEvent.character;
|
||||
final ui.KeyEventDeviceType deviceType = _convertDeviceType(rawEvent);
|
||||
if (rawEvent is RawKeyDownEvent) {
|
||||
if (recordedLogicalMain == null) {
|
||||
mainEvent = KeyDownEvent(
|
||||
@@ -1133,6 +1173,7 @@ class KeyEventManager {
|
||||
logicalKey: logicalKey,
|
||||
character: character,
|
||||
timeStamp: timeStamp,
|
||||
deviceType: deviceType,
|
||||
);
|
||||
physicalKeysPressed.add(physicalKey);
|
||||
} else {
|
||||
@@ -1142,6 +1183,7 @@ class KeyEventManager {
|
||||
logicalKey: recordedLogicalMain,
|
||||
character: character,
|
||||
timeStamp: timeStamp,
|
||||
deviceType: deviceType,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -1153,6 +1195,7 @@ class KeyEventManager {
|
||||
logicalKey: recordedLogicalMain,
|
||||
physicalKey: physicalKey,
|
||||
timeStamp: timeStamp,
|
||||
deviceType: deviceType,
|
||||
);
|
||||
physicalKeysPressed.remove(physicalKey);
|
||||
}
|
||||
@@ -1167,6 +1210,7 @@ class KeyEventManager {
|
||||
logicalKey: logicalKey,
|
||||
timeStamp: timeStamp,
|
||||
synthesized: true,
|
||||
deviceType: deviceType,
|
||||
));
|
||||
} else {
|
||||
_keyEventsSinceLastMessage.add(KeyUpEvent(
|
||||
@@ -1174,6 +1218,7 @@ class KeyEventManager {
|
||||
logicalKey: _hardwareKeyboard.lookUpLayout(key)!,
|
||||
timeStamp: timeStamp,
|
||||
synthesized: true,
|
||||
deviceType: deviceType,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1183,6 +1228,7 @@ class KeyEventManager {
|
||||
logicalKey: _rawKeyboard.lookUpLayout(key)!,
|
||||
timeStamp: timeStamp,
|
||||
synthesized: true,
|
||||
deviceType: deviceType,
|
||||
));
|
||||
}
|
||||
if (mainEvent != null) {
|
||||
@@ -1220,6 +1266,7 @@ class KeyEventManager {
|
||||
timeStamp: timeStamp,
|
||||
character: keyData.character,
|
||||
synthesized: keyData.synthesized,
|
||||
deviceType: keyData.deviceType,
|
||||
);
|
||||
case ui.KeyEventType.up:
|
||||
assert(keyData.character == null);
|
||||
@@ -1228,6 +1275,7 @@ class KeyEventManager {
|
||||
logicalKey: logicalKey,
|
||||
timeStamp: timeStamp,
|
||||
synthesized: keyData.synthesized,
|
||||
deviceType: keyData.deviceType,
|
||||
);
|
||||
case ui.KeyEventType.repeat:
|
||||
return KeyRepeatEvent(
|
||||
@@ -1235,6 +1283,7 @@ class KeyEventManager {
|
||||
logicalKey: logicalKey,
|
||||
timeStamp: timeStamp,
|
||||
character: keyData.character,
|
||||
deviceType: keyData.deviceType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ import 'focus_scope.dart';
|
||||
import 'framework.dart';
|
||||
import 'platform_menu_bar.dart';
|
||||
|
||||
final Set<LogicalKeyboardKey> _controlSynonyms = LogicalKeyboardKey.expandSynonyms(<LogicalKeyboardKey>{LogicalKeyboardKey.control});
|
||||
final Set<LogicalKeyboardKey> _shiftSynonyms = LogicalKeyboardKey.expandSynonyms(<LogicalKeyboardKey>{LogicalKeyboardKey.shift});
|
||||
final Set<LogicalKeyboardKey> _altSynonyms = LogicalKeyboardKey.expandSynonyms(<LogicalKeyboardKey>{LogicalKeyboardKey.alt});
|
||||
final Set<LogicalKeyboardKey> _metaSynonyms = LogicalKeyboardKey.expandSynonyms(<LogicalKeyboardKey>{LogicalKeyboardKey.meta});
|
||||
|
||||
/// A set of [KeyboardKey]s that can be used as the keys in a [Map].
|
||||
///
|
||||
/// A key set contains the keys that are down simultaneously to represent a
|
||||
@@ -168,55 +173,62 @@ abstract class ShortcutActivator {
|
||||
/// 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.
|
||||
/// An optional property to provide 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.
|
||||
/// For example, for `Ctrl-A`, [LogicalKeyboardKey.keyA] is the only trigger,
|
||||
/// while [LogicalKeyboardKey.control] 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.
|
||||
/// If provided, trigger keys can be used as a first-pass filter for incoming
|
||||
/// events in order to optimize lookups, as [Intent]s are stored in a [Map]
|
||||
/// and indexed by trigger keys. It is up to the individual implementors of
|
||||
/// this interface to decide if they ignore triggers or not.
|
||||
///
|
||||
/// Subclasses should make sure that the return value of this method does not
|
||||
/// change throughout the lifespan of this object.
|
||||
///
|
||||
/// This method might also return null, which means this activator declares
|
||||
/// all keys as the trigger key. All activators whose [triggers] returns null
|
||||
/// will be tested with [accepts] on every event. Since this becomes a
|
||||
/// linear search, and having too many might impact performance, it is
|
||||
/// preferred to return non-null [triggers] whenever possible.
|
||||
Iterable<LogicalKeyboardKey>? get triggers;
|
||||
/// all keys as trigger keys. Activators whose [triggers] return null will be
|
||||
/// tested with [accepts] on every event. Since this becomes a linear search,
|
||||
/// and having too many might impact performance, it is preferred to return
|
||||
/// non-null [triggers] whenever possible.
|
||||
Iterable<LogicalKeyboardKey>? get triggers => null;
|
||||
|
||||
/// 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
|
||||
/// [KeyDownEvent], if either side of the Ctrl key is pressed, and none of
|
||||
/// the Shift keys, Alt keys, or Meta keys are pressed; it doesn't have to
|
||||
/// check if KeyA is pressed, since it's already guaranteed.
|
||||
/// [KeyDownEvent], if either side of the Ctrl key is pressed, and none of the
|
||||
/// Shift keys, Alt keys, or Meta keys are pressed; it doesn't have to check
|
||||
/// if KeyA is pressed, since it's already guaranteed.
|
||||
///
|
||||
/// As a possible performance improvement, implementers of this function are
|
||||
/// encouraged (but not required) to check the [triggers] member, if it is
|
||||
/// non-null, to see if it contains the event's logical key before doing more
|
||||
/// complicated work.
|
||||
///
|
||||
/// This method must not cause any side effects for the `state`. Typically
|
||||
/// this is only used to query whether [HardwareKeyboard.logicalKeysPressed]
|
||||
/// contains a key.
|
||||
///
|
||||
/// Since [ShortcutActivator] accepts all event types, subclasses might want
|
||||
/// to check the event type in [accepts].
|
||||
///
|
||||
/// 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);
|
||||
/// * [LogicalKeyboardKey.collapseSynonyms], which helps deciding whether a
|
||||
/// modifier key is pressed when the side variation is not important.
|
||||
bool accepts(KeyEvent event, HardwareKeyboard state);
|
||||
|
||||
/// Returns true if the event and keyboard state would cause this
|
||||
/// [ShortcutActivator] to be activated.
|
||||
///
|
||||
/// If the keyboard `state` isn't supplied, then it defaults to using
|
||||
/// [RawKeyboard.instance].
|
||||
static bool isActivatedBy(ShortcutActivator activator, RawKeyEvent event) {
|
||||
return (activator.triggers?.contains(event.logicalKey) ?? true)
|
||||
&& activator.accepts(event, RawKeyboard.instance);
|
||||
/// Returns true if the event and current [HardwareKeyboard] state would cause
|
||||
/// this [ShortcutActivator] to be activated.
|
||||
@Deprecated(
|
||||
'Call accepts on the activator instead. '
|
||||
'This feature was deprecated after v3.16.0-15.0.pre.',
|
||||
)
|
||||
static bool isActivatedBy(ShortcutActivator activator, KeyEvent event) {
|
||||
return activator.accepts(event, HardwareKeyboard.instance);
|
||||
}
|
||||
|
||||
/// Returns a description of the key set that is short and readable.
|
||||
@@ -281,16 +293,20 @@ class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable
|
||||
(LogicalKeyboardKey key) => _unmapSynonyms[key] ?? <LogicalKeyboardKey>[key],
|
||||
).toSet();
|
||||
|
||||
bool _checkKeyRequirements(Set<LogicalKeyboardKey> pressed) {
|
||||
final Set<LogicalKeyboardKey> collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys);
|
||||
final Set<LogicalKeyboardKey> collapsedPressed = LogicalKeyboardKey.collapseSynonyms(pressed);
|
||||
return collapsedRequired.length == collapsedPressed.length
|
||||
&& collapsedRequired.difference(collapsedPressed).isEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
bool accepts(RawKeyEvent event, RawKeyboard state) {
|
||||
if (event is! RawKeyDownEvent) {
|
||||
bool accepts(KeyEvent event, HardwareKeyboard state) {
|
||||
if (event is! KeyDownEvent && event is! KeyRepeatEvent) {
|
||||
return false;
|
||||
}
|
||||
final Set<LogicalKeyboardKey> collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys);
|
||||
final Set<LogicalKeyboardKey> collapsedPressed = LogicalKeyboardKey.collapseSynonyms(state.keysPressed);
|
||||
final bool keysEqual = collapsedRequired.difference(collapsedPressed).isEmpty
|
||||
&& collapsedRequired.length == collapsedPressed.length;
|
||||
return keysEqual;
|
||||
return triggers.contains(event.logicalKey)
|
||||
&& _checkKeyRequirements(state.logicalKeysPressed);
|
||||
}
|
||||
|
||||
static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{
|
||||
@@ -391,19 +407,20 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
|
||||
/// Triggered when the [trigger] key is pressed while the modifiers are held.
|
||||
///
|
||||
/// The [trigger] should be the non-modifier key that is pressed after all the
|
||||
/// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not be
|
||||
/// a modifier key (sided or unsided).
|
||||
/// 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).
|
||||
/// They default to false.
|
||||
/// The [control], [shift], [alt], and [meta] flags represent whether the
|
||||
/// respective modifier keys should be held (true) or released (false). They
|
||||
/// default to false.
|
||||
///
|
||||
/// By default, the activator is checked on all [RawKeyDownEvent] events for
|
||||
/// the [trigger] key. If `includeRepeats` is false, only the [trigger] key
|
||||
/// events with a false [RawKeyDownEvent.repeat] attribute will be considered.
|
||||
/// By default, the activator is checked on all [KeyDownEvent] events for the
|
||||
/// [trigger] key. If [includeRepeats] is false, only [trigger] key events
|
||||
/// which are not [KeyRepeatEvent]s will be considered.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// In the following example, the shortcut `Control + C` increases the counter:
|
||||
/// In the following example, the shortcut `Control + C` increases the
|
||||
/// counter:
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/shortcuts/single_activator.single_activator.0.dart **
|
||||
/// {@end-tool}
|
||||
@@ -492,8 +509,8 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
|
||||
///
|
||||
/// If [includeRepeats] is true, the activator is checked on all
|
||||
/// [RawKeyDownEvent] events for the [trigger] key. If [includeRepeats] is
|
||||
/// false, only [trigger] key events with a false [RawKeyDownEvent.repeat]
|
||||
/// attribute will be considered.
|
||||
/// false, only [trigger] key events which are not [KeyRepeatEvent]s will be
|
||||
/// considered.
|
||||
final bool includeRepeats;
|
||||
|
||||
@override
|
||||
@@ -501,15 +518,18 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
|
||||
return <LogicalKeyboardKey>[trigger];
|
||||
}
|
||||
|
||||
bool _shouldAcceptModifiers(Set<LogicalKeyboardKey> pressed) {
|
||||
return control == pressed.intersection(_controlSynonyms).isNotEmpty
|
||||
&& shift == pressed.intersection(_shiftSynonyms).isNotEmpty
|
||||
&& alt == pressed.intersection(_altSynonyms).isNotEmpty
|
||||
&& meta == pressed.intersection(_metaSynonyms).isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
bool accepts(RawKeyEvent event, RawKeyboard state) {
|
||||
final Set<LogicalKeyboardKey> pressed = state.keysPressed;
|
||||
return event is RawKeyDownEvent
|
||||
&& (includeRepeats || !event.repeat)
|
||||
&& (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)));
|
||||
bool accepts(KeyEvent event, HardwareKeyboard state) {
|
||||
return (event is KeyDownEvent || (includeRepeats && event is KeyRepeatEvent))
|
||||
&& triggers.contains(event.logicalKey)
|
||||
&& _shouldAcceptModifiers(state.logicalKeysPressed);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -680,15 +700,19 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
|
||||
@override
|
||||
Iterable<LogicalKeyboardKey>? get triggers => null;
|
||||
|
||||
bool _shouldAcceptModifiers(Set<LogicalKeyboardKey> pressed) {
|
||||
// Doesn't look for shift, since the character will encode that.
|
||||
return control == pressed.intersection(_controlSynonyms).isNotEmpty
|
||||
&& alt == pressed.intersection(_altSynonyms).isNotEmpty
|
||||
&& meta == pressed.intersection(_metaSynonyms).isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
bool accepts(RawKeyEvent event, RawKeyboard state) {
|
||||
final Set<LogicalKeyboardKey> pressed = state.keysPressed;
|
||||
return event is RawKeyDownEvent
|
||||
&& event.character == character
|
||||
&& (includeRepeats || !event.repeat)
|
||||
&& (alt == (pressed.contains(LogicalKeyboardKey.altLeft) || pressed.contains(LogicalKeyboardKey.altRight)))
|
||||
&& (control == (pressed.contains(LogicalKeyboardKey.controlLeft) || pressed.contains(LogicalKeyboardKey.controlRight)))
|
||||
&& (meta == (pressed.contains(LogicalKeyboardKey.metaLeft) || pressed.contains(LogicalKeyboardKey.metaRight)));
|
||||
bool accepts(KeyEvent event, HardwareKeyboard state) {
|
||||
// Ignore triggers, since we're only interested in the character.
|
||||
return event.character == character
|
||||
&& (event is KeyDownEvent || (includeRepeats && event is KeyRepeatEvent))
|
||||
&& _shouldAcceptModifiers(state.logicalKeysPressed);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -800,21 +824,19 @@ class ShortcutManager with Diagnosticable, ChangeNotifier {
|
||||
|
||||
Map<LogicalKeyboardKey?, List<_ActivatorIntentPair>>? _indexedShortcutsCache;
|
||||
|
||||
Iterable<_ActivatorIntentPair> _getCandidates(LogicalKeyboardKey key) {
|
||||
return <_ActivatorIntentPair>[
|
||||
... _indexedShortcuts[key] ?? <_ActivatorIntentPair>[],
|
||||
... _indexedShortcuts[null] ?? <_ActivatorIntentPair>[],
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns the [Intent], if any, that matches the current set of pressed
|
||||
/// keys.
|
||||
///
|
||||
/// Returns null if no intent matches the current set of pressed keys.
|
||||
///
|
||||
/// Defaults to a set derived from [RawKeyboard.keysPressed] if `keysPressed`
|
||||
/// is not supplied.
|
||||
Intent? _find(RawKeyEvent event, RawKeyboard state) {
|
||||
final List<_ActivatorIntentPair>? candidatesByKey = _indexedShortcuts[event.logicalKey];
|
||||
final List<_ActivatorIntentPair>? candidatesByNull = _indexedShortcuts[null];
|
||||
final List<_ActivatorIntentPair> candidates = <_ActivatorIntentPair>[
|
||||
if (candidatesByKey != null) ...candidatesByKey,
|
||||
if (candidatesByNull != null) ...candidatesByNull,
|
||||
];
|
||||
for (final _ActivatorIntentPair activatorIntent in candidates) {
|
||||
Intent? _find(KeyEvent event, HardwareKeyboard state) {
|
||||
for (final _ActivatorIntentPair activatorIntent in _getCandidates(event.logicalKey)) {
|
||||
if (activatorIntent.activator.accepts(event, state)) {
|
||||
return activatorIntent.intent;
|
||||
}
|
||||
@@ -824,9 +846,9 @@ class ShortcutManager with Diagnosticable, ChangeNotifier {
|
||||
|
||||
/// Handles a key press `event` in the given `context`.
|
||||
///
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -835,11 +857,12 @@ class ShortcutManager with Diagnosticable, ChangeNotifier {
|
||||
/// and in all other cases returns [KeyEventResult.ignored].
|
||||
///
|
||||
/// In order for an action to be invoked (and [KeyEventResult.handled]
|
||||
/// returned), a pressed [KeySet] must be mapped to an [Intent], the [Intent]
|
||||
/// must be mapped to an [Action], and the [Action] must be enabled.
|
||||
/// returned), a [ShortcutActivator] must accept the given [KeyEvent], 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) {
|
||||
final Intent? matchedIntent = _find(event, RawKeyboard.instance);
|
||||
KeyEventResult handleKeypress(BuildContext context, KeyEvent event) {
|
||||
final Intent? matchedIntent = _find(event, HardwareKeyboard.instance);
|
||||
if (matchedIntent != null) {
|
||||
final BuildContext? primaryContext = primaryFocus?.context;
|
||||
if (primaryContext != null) {
|
||||
@@ -1035,7 +1058,7 @@ class _ShortcutsState extends State<Shortcuts> {
|
||||
_internalManager?.shortcuts = widget.shortcuts;
|
||||
}
|
||||
|
||||
KeyEventResult _handleOnKey(FocusNode node, RawKeyEvent event) {
|
||||
KeyEventResult _handleOnKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (node.context == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
@@ -1047,7 +1070,7 @@ class _ShortcutsState extends State<Shortcuts> {
|
||||
return Focus(
|
||||
debugLabel: '$Shortcuts',
|
||||
canRequestFocus: false,
|
||||
onKey: _handleOnKey,
|
||||
onKeyEvent: _handleOnKeyEvent,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
@@ -1123,8 +1146,8 @@ class CallbackShortcuts extends StatelessWidget {
|
||||
// A helper function to make the stack trace more useful if the callback
|
||||
// throws, by providing the activator and event as arguments that will appear
|
||||
// in the stack trace.
|
||||
bool _applyKeyBinding(ShortcutActivator activator, RawKeyEvent event) {
|
||||
if (ShortcutActivator.isActivatedBy(activator, event)) {
|
||||
bool _applyKeyEventBinding(ShortcutActivator activator, KeyEvent event) {
|
||||
if (activator.accepts(event, HardwareKeyboard.instance)) {
|
||||
bindings[activator]!.call();
|
||||
return true;
|
||||
}
|
||||
@@ -1136,11 +1159,11 @@ class CallbackShortcuts extends StatelessWidget {
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
skipTraversal: true,
|
||||
onKey: (FocusNode node, RawKeyEvent event) {
|
||||
onKeyEvent: (FocusNode node, KeyEvent event) {
|
||||
KeyEventResult result = KeyEventResult.ignored;
|
||||
// Activates all key bindings that match, returns "handled" if any handle it.
|
||||
for (final ShortcutActivator activator in bindings.keys) {
|
||||
result = _applyKeyBinding(activator, event) ? KeyEventResult.handled : result;
|
||||
result = _applyKeyEventBinding(activator, event) ? KeyEventResult.handled : result;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -213,11 +213,11 @@ void main() {
|
||||
|
||||
testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async {
|
||||
// Collect some key events to use for testing.
|
||||
final List<RawKeyEvent> events = <RawKeyEvent>[];
|
||||
final List<KeyEvent> events = <KeyEvent>[];
|
||||
await tester.pumpWidget(
|
||||
Focus(
|
||||
autofocus: true,
|
||||
onKey: (FocusNode node, RawKeyEvent event) {
|
||||
onKeyEvent: (FocusNode node, KeyEvent event) {
|
||||
events.add(event);
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
@@ -229,10 +229,13 @@ void main() {
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(set, events[0]), isTrue);
|
||||
expect(ShortcutActivator.isActivatedBy(set, events.last), isTrue);
|
||||
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(set, events.last), isTrue);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(set, events.last), isFalse);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
expect(ShortcutActivator.isActivatedBy(set, events[0]), isFalse);
|
||||
expect(ShortcutActivator.isActivatedBy(set, events.last), isFalse);
|
||||
});
|
||||
|
||||
test('LogicalKeySet diagnostics work.', () {
|
||||
@@ -457,11 +460,11 @@ void main() {
|
||||
|
||||
testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async {
|
||||
// Collect some key events to use for testing.
|
||||
final List<RawKeyEvent> events = <RawKeyEvent>[];
|
||||
final List<KeyEvent> events = <KeyEvent>[];
|
||||
await tester.pumpWidget(
|
||||
Focus(
|
||||
autofocus: true,
|
||||
onKey: (FocusNode node, RawKeyEvent event) {
|
||||
onKeyEvent: (FocusNode node, KeyEvent event) {
|
||||
events.add(event);
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
@@ -471,12 +474,27 @@ void main() {
|
||||
|
||||
const SingleActivator singleActivator = SingleActivator(LogicalKeyboardKey.keyA, control: true);
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue);
|
||||
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isFalse);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isFalse);
|
||||
|
||||
const SingleActivator noRepeatSingleActivator = SingleActivator(LogicalKeyboardKey.keyA, control: true, includeRepeats: false);
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isTrue);
|
||||
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isFalse);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(singleActivator, events[1]), isTrue);
|
||||
expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isFalse);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
expect(ShortcutActivator.isActivatedBy(singleActivator, events[1]), isFalse);
|
||||
expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isFalse);
|
||||
});
|
||||
|
||||
group('diagnostics.', () {
|
||||
@@ -1241,11 +1259,11 @@ void main() {
|
||||
|
||||
testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async {
|
||||
// Collect some key events to use for testing.
|
||||
final List<RawKeyEvent> events = <RawKeyEvent>[];
|
||||
final List<KeyEvent> events = <KeyEvent>[];
|
||||
await tester.pumpWidget(
|
||||
Focus(
|
||||
autofocus: true,
|
||||
onKey: (FocusNode node, RawKeyEvent event) {
|
||||
onKeyEvent: (FocusNode node, KeyEvent event) {
|
||||
events.add(event);
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
@@ -1256,8 +1274,20 @@ void main() {
|
||||
const CharacterActivator characterActivator = CharacterActivator('a');
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(characterActivator, events.last), isTrue);
|
||||
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(characterActivator, events.last), isTrue);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(characterActivator, events[0]), isTrue);
|
||||
expect(ShortcutActivator.isActivatedBy(characterActivator, events.last), isFalse);
|
||||
|
||||
const CharacterActivator noRepeatCharacterActivator = CharacterActivator('a', includeRepeats: false);
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(noRepeatCharacterActivator, events.last), isTrue);
|
||||
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(noRepeatCharacterActivator, events.last), isFalse);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
expect(ShortcutActivator.isActivatedBy(noRepeatCharacterActivator, events.last), isFalse);
|
||||
});
|
||||
|
||||
group('diagnostics.', () {
|
||||
@@ -1936,7 +1966,7 @@ class TestAction extends CallbackAction<Intent> {
|
||||
/// An activator that accepts down events that has [key] as the logical key.
|
||||
///
|
||||
/// This class is used only to tests. It is intentionally designed poorly by
|
||||
/// returning null in [triggers], and checks [key] in [accepts].
|
||||
/// returning null in [triggers], and checks [key] in [acceptsEvent].
|
||||
class DumbLogicalActivator extends ShortcutActivator {
|
||||
const DumbLogicalActivator(this.key);
|
||||
|
||||
@@ -1946,8 +1976,8 @@ class DumbLogicalActivator extends ShortcutActivator {
|
||||
Iterable<LogicalKeyboardKey>? get triggers => null;
|
||||
|
||||
@override
|
||||
bool accepts(RawKeyEvent event, RawKeyboard state) {
|
||||
return event is RawKeyDownEvent
|
||||
bool accepts(KeyEvent event, HardwareKeyboard state) {
|
||||
return (event is KeyDownEvent || event is KeyRepeatEvent)
|
||||
&& event.logicalKey == key;
|
||||
}
|
||||
|
||||
@@ -1980,8 +2010,8 @@ class TestShortcutManager extends ShortcutManager {
|
||||
List<LogicalKeyboardKey> keys;
|
||||
|
||||
@override
|
||||
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
|
||||
if (event is RawKeyDownEvent) {
|
||||
KeyEventResult handleKeypress(BuildContext context, KeyEvent event) {
|
||||
if (event is KeyDownEvent || event is KeyRepeatEvent) {
|
||||
keys.add(event.logicalKey);
|
||||
}
|
||||
return super.handleKeypress(context, event);
|
||||
|
||||
Reference in New Issue
Block a user