diff --git a/dev/manual_tests/lib/actions.dart b/dev/manual_tests/lib/actions.dart index aa62cdf0aaa..a09d75450e6 100644 --- a/dev/manual_tests/lib/actions.dart +++ b/dev/manual_tests/lib/actions.dart @@ -235,9 +235,9 @@ abstract class UndoableAction extends Action { @override @mustCallSuper - void invoke(FocusNode node, Intent tag) { + void invoke(FocusNode node, Intent intent) { invocationNode = node; - invocationIntent = tag; + invocationIntent = intent; } @override @@ -253,8 +253,8 @@ class SetFocusActionBase extends UndoableAction { FocusNode _previousFocus; @override - void invoke(FocusNode node, Intent tag) { - super.invoke(node, tag); + void invoke(FocusNode node, Intent intent) { + super.invoke(node, intent); _previousFocus = WidgetsBinding.instance.focusManager.primaryFocus; node.requestFocus(); } @@ -292,8 +292,8 @@ class SetFocusAction extends SetFocusActionBase { static const LocalKey key = ValueKey(SetFocusAction); @override - void invoke(FocusNode node, Intent tag) { - super.invoke(node, tag); + void invoke(FocusNode node, Intent intent) { + super.invoke(node, intent); node.requestFocus(); } } @@ -305,8 +305,8 @@ class NextFocusAction extends SetFocusActionBase { static const LocalKey key = ValueKey(NextFocusAction); @override - void invoke(FocusNode node, Intent tag) { - super.invoke(node, tag); + void invoke(FocusNode node, Intent intent) { + super.invoke(node, intent); node.nextFocus(); } } @@ -317,8 +317,8 @@ class PreviousFocusAction extends SetFocusActionBase { static const LocalKey key = ValueKey(PreviousFocusAction); @override - void invoke(FocusNode node, Intent tag) { - super.invoke(node, tag); + void invoke(FocusNode node, Intent intent) { + super.invoke(node, intent); node.previousFocus(); } } @@ -337,9 +337,9 @@ class DirectionalFocusAction extends SetFocusActionBase { TraversalDirection direction; @override - void invoke(FocusNode node, DirectionalFocusIntent tag) { - super.invoke(node, tag); - final DirectionalFocusIntent args = tag; + void invoke(FocusNode node, DirectionalFocusIntent intent) { + super.invoke(node, intent); + final DirectionalFocusIntent args = intent; node.focusInDirection(args.direction); } } diff --git a/packages/flutter/lib/src/widgets/actions.dart b/packages/flutter/lib/src/widgets/actions.dart index cfe668158de..cd79369e2bb 100644 --- a/packages/flutter/lib/src/widgets/actions.dart +++ b/packages/flutter/lib/src/widgets/actions.dart @@ -29,11 +29,10 @@ class Intent extends Diagnosticable { /// An intent that can't be mapped to an action. /// - /// This Intent is prevented from being mapped to an action in the - /// [ActionDispatcher], and as such can be used to indicate that a shortcut - /// should not do anything, allowing a shortcut defined at a higher level to - /// be disabled at a deeper level in the widget hierarchy. - static const Intent doNothing = Intent(ValueKey(Intent)); + /// This Intent is mapped to an action in the [WidgetsApp] that does nothing, + /// so that it can be bound to a key in a [Shortcuts] widget in order to + /// disable a key binding made above it in the hierarchy. + static const Intent doNothing = DoNothingIntent(); /// The key for the action this intent is associated with. final LocalKey key; @@ -95,7 +94,7 @@ abstract class Action extends Diagnosticable { /// needed in the action, use [ActionDispatcher.invokeFocusedAction] instead. @protected @mustCallSuper - void invoke(FocusNode node, covariant Intent tag); + void invoke(FocusNode node, covariant Intent intent); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -134,7 +133,7 @@ class CallbackAction extends Action { final OnInvokeCallback onInvoke; @override - void invoke(FocusNode node, Intent tag) => onInvoke.call(node, tag); + void invoke(FocusNode node, Intent intent) => onInvoke.call(node, intent); } /// An action manager that simply invokes the actions given to it. @@ -224,6 +223,7 @@ class Actions extends InheritedWidget { // Stop visiting. return false; } + element.visitAncestorElements(visitAncestorElement); } return dispatcher ?? const ActionDispatcher(); @@ -278,9 +278,6 @@ class Actions extends InheritedWidget { /// /// Setting `nullOk` to true means that if no ambient [Actions] widget is /// found, then this method will return false instead of throwing. - /// - /// If the `intent` argument is [Intent.doNothing], then this function will - /// return false, without looking for a matching action. static bool invoke( BuildContext context, Intent intent, { @@ -292,10 +289,6 @@ class Actions extends InheritedWidget { Element actionsElement; Action action; - if (identical(intent, Intent.doNothing)) { - return false; - } - bool visitAncestorElement(Element element) { if (element.widget is! Actions) { // Continue visiting. @@ -358,3 +351,29 @@ class Actions extends InheritedWidget { properties.add(DiagnosticsProperty>('actions', actions)); } } + +/// An [Action], that, as the name implies, does nothing. +/// +/// This action is bound to the [Intent.doNothing] intent inside of +/// [WidgetsApp.build] so that a [Shortcuts] widget can bind a key to it to +/// override another shortcut binding defined above it in the hierarchy. +class DoNothingAction extends Action { + /// Const constructor for [DoNothingAction]. + const DoNothingAction() : super(key); + + /// The Key used for the [DoNothingIntent] intent, and registered at the top + /// level actions in [WidgetsApp.build]. + static const LocalKey key = ValueKey(DoNothingAction); + + @override + void invoke(FocusNode node, Intent intent) { } +} + +/// An [Intent] that can be used to disable [Shortcuts] key bindings defined +/// above a widget in the hierarchy. +/// +/// The [Intent.doNothing] intent is of this type. +class DoNothingIntent extends Intent { + /// Const constructor for [DoNothingIntent]. + const DoNothingIntent() : super(DoNothingAction.key); +} diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 19b9681d236..9b1e990d24e 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -8,6 +8,7 @@ import 'dart:collection' show HashMap; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'actions.dart'; import 'banner.dart'; import 'basic.dart'; import 'binding.dart'; @@ -1194,14 +1195,19 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv assert(_debugCheckLocalizations(appLocale)); - return DefaultFocusTraversal( - policy: ReadingOrderTraversalPolicy(), - child: MediaQuery( - data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), - child: Localizations( - locale: appLocale, - delegates: _localizationsDelegates.toList(), - child: title, + return Actions( + actions: { + DoNothingAction.key: () => const DoNothingAction(), + }, + child: DefaultFocusTraversal( + policy: ReadingOrderTraversalPolicy(), + child: MediaQuery( + data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), + child: Localizations( + locale: appLocale, + delegates: _localizationsDelegates.toList(), + child: title, + ), ), ), ); diff --git a/packages/flutter/test/widgets/actions_test.dart b/packages/flutter/test/widgets/actions_test.dart index 01e4137f3d5..1a7454f0548 100644 --- a/packages/flutter/test/widgets/actions_test.dart +++ b/packages/flutter/test/widgets/actions_test.dart @@ -302,11 +302,11 @@ void main() { ).debugFillProperties(builder); final List description = builder.properties - .where((DiagnosticsNode node) { - return !node.isFiltered(DiagnosticLevel.info); - }) - .map((DiagnosticsNode node) => node.toString()) - .toList(); + .where((DiagnosticsNode node) { + return !node.isFiltered(DiagnosticLevel.info); + }) + .map((DiagnosticsNode node) => node.toString()) + .toList(); expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000')); expect(description[1], equals('actions: {}')); diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart index 8a618f71935..ba38462bf99 100644 --- a/packages/flutter/test/widgets/shortcuts_test.dart +++ b/packages/flutter/test/widgets/shortcuts_test.dart @@ -37,22 +37,6 @@ class TestIntent extends Intent { const TestIntent() : super(TestAction.key); } -class DoNothingAction extends Action { - const DoNothingAction({ - @required OnInvokeCallback onInvoke, - }) : assert(onInvoke != null), - super(key); - - static const LocalKey key = ValueKey(DoNothingAction); - - @override - void invoke(FocusNode node, Intent invocation) {} -} - -class DoNothingIntent extends Intent { - const DoNothingIntent() : super(DoNothingAction.key); -} - class TestShortcutManager extends ShortcutManager { TestShortcutManager(this.keys); @@ -143,8 +127,8 @@ void main() { LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, - LogicalKeyboardKey.keyA,} - ); + LogicalKeyboardKey.keyA, + }); final Map map = {set1: 'one'}; expect(set2 == set3, isTrue); expect(set2 == set4, isTrue); @@ -192,10 +176,12 @@ void main() { await tester.pumpWidget( Actions( actions: { - TestAction.key: () => TestAction(onInvoke: (FocusNode node, Intent intent) { - invoked = true; - return true; - }), + TestAction.key: () => TestAction( + onInvoke: (FocusNode node, Intent intent) { + invoked = true; + return true; + }, + ), }, child: Shortcuts( manager: testManager, @@ -228,14 +214,16 @@ void main() { }, child: Actions( actions: { - TestAction.key: () => TestAction(onInvoke: (FocusNode node, Intent intent) { - invoked = true; - return true; - }), + TestAction.key: () => TestAction( + onInvoke: (FocusNode node, Intent intent) { + invoked = true; + return true; + }, + ), }, child: Shortcuts( shortcuts: { - LogicalKeySet(LogicalKeyboardKey.keyA): const DoNothingIntent(), + LogicalKeySet(LogicalKeyboardKey.keyA): Intent.doNothing, }, child: Focus( autofocus: true, @@ -251,5 +239,45 @@ void main() { expect(invoked, isTrue); expect(pressedKeys, equals([LogicalKeyboardKey.shiftLeft])); }); + testWidgets('$Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async { + final GlobalKey containerKey = GlobalKey(); + final List pressedKeys = []; + final TestShortcutManager testManager = TestShortcutManager(pressedKeys); + bool invoked = false; + await tester.pumpWidget( + MaterialApp( + home: Shortcuts( + manager: testManager, + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), + }, + child: Actions( + actions: { + TestAction.key: () => TestAction( + onInvoke: (FocusNode node, Intent intent) { + invoked = true; + return true; + }, + ), + }, + child: Shortcuts( + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.shift): Intent.doNothing, + }, + child: Focus( + autofocus: true, + child: Container(key: containerKey, width: 100, height: 100), + ), + ), + ), + ), + ), + ); + await tester.pump(); + expect(Shortcuts.of(containerKey.currentContext), isNotNull); + await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); + expect(invoked, isFalse); + expect(pressedKeys, isEmpty); + }); }); }