diff --git a/packages/flutter/lib/src/services/hardware_keyboard.dart b/packages/flutter/lib/src/services/hardware_keyboard.dart index d30e6c6e65..aa855d9005 100644 --- a/packages/flutter/lib/src/services/hardware_keyboard.dart +++ b/packages/flutter/lib/src/services/hardware_keyboard.dart @@ -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 { '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, ); } } diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart index d211817f3f..902be42453 100644 --- a/packages/flutter/lib/src/widgets/shortcuts.dart +++ b/packages/flutter/lib/src/widgets/shortcuts.dart @@ -14,6 +14,11 @@ import 'focus_scope.dart'; import 'framework.dart'; import 'platform_menu_bar.dart'; +final Set _controlSynonyms = LogicalKeyboardKey.expandSynonyms({LogicalKeyboardKey.control}); +final Set _shiftSynonyms = LogicalKeyboardKey.expandSynonyms({LogicalKeyboardKey.shift}); +final Set _altSynonyms = LogicalKeyboardKey.expandSynonyms({LogicalKeyboardKey.alt}); +final Set _metaSynonyms = LogicalKeyboardKey.expandSynonyms({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? 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? 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 with Diagnosticable (LogicalKeyboardKey key) => _unmapSynonyms[key] ?? [key], ).toSet(); + bool _checkKeyRequirements(Set pressed) { + final Set collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys); + final Set 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 collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys); - final Set 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 _modifiers = { @@ -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 [trigger]; } + bool _shouldAcceptModifiers(Set 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 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? get triggers => null; + bool _shouldAcceptModifiers(Set 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 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>? _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 { _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 { 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; }, diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart index 047f727a46..4e49f20032 100644 --- a/packages/flutter/test/widgets/shortcuts_test.dart +++ b/packages/flutter/test/widgets/shortcuts_test.dart @@ -213,11 +213,11 @@ void main() { testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. - final List events = []; + final List events = []; 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 events = []; + final List events = []; 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 events = []; + final List events = []; 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 { /// 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? 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 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);