From 1994027986cf59a4c307d6612706ce08c16b1ae8 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 17 May 2022 11:11:41 -0700 Subject: [PATCH] Add VoidCallbackAction and VoidCallbackIntent (#103518) This adds a simple VoidCallbackAction and VoidCallbackIntent that allows configuring an intent that will invoke a void callback when the intent is sent to the action subsystem. This allows binding a shortcut directly to a void callback in a Shortcuts widget. I also added an instance of VoidCallbackAction to the default actions so that simply binding a shortcut to a VoidCallbackIntent works anywhere in the app, and you don't need to add a VoidCallbackAction at the top of your app to make it work. --- packages/flutter/lib/src/widgets/actions.dart | 39 ++++- packages/flutter/lib/src/widgets/app.dart | 1 + .../flutter/test/widgets/actions_test.dart | 165 ++++++++++-------- 3 files changed, 123 insertions(+), 82 deletions(-) diff --git a/packages/flutter/lib/src/widgets/actions.dart b/packages/flutter/lib/src/widgets/actions.dart index 50a132315b48..0c9c519bff27 100644 --- a/packages/flutter/lib/src/widgets/actions.dart +++ b/packages/flutter/lib/src/widgets/actions.dart @@ -1296,7 +1296,34 @@ class _FocusableActionDetectorState extends State { } } -/// An [Intent], that is bound to a [DoNothingAction]. +/// An [Intent] that keeps a [VoidCallback] to be invoked by a +/// [VoidCallbackAction] when it receives this intent. +class VoidCallbackIntent extends Intent { + /// Creates a [VoidCallbackIntent]. + const VoidCallbackIntent(this.callback); + + /// The callback that is to be called by the [VoidCallbackAction] that + /// receives this intent. + final VoidCallback callback; +} + +/// An [Action] that invokes the [VoidCallback] given to it in the +/// [VoidCallbackIntent] passed to it when invoked. +/// +/// See also: +/// +/// * [CallbackAction], which is an action that will invoke a callback with the +/// intent passed to the action's invoke method. The callback is configured +/// on the action, not the intent, like this class. +class VoidCallbackAction extends Action { + @override + Object? invoke(VoidCallbackIntent intent) { + intent.callback(); + return null; + } +} + +/// An [Intent] that is bound to a [DoNothingAction]. /// /// Attaching a [DoNothingIntent] to a [Shortcuts] mapping is one way to disable /// a keyboard shortcut defined by a widget higher in the widget hierarchy and @@ -1317,7 +1344,7 @@ class DoNothingIntent extends Intent { const DoNothingIntent._(); } -/// An [Intent], that is bound to a [DoNothingAction], but, in addition to not +/// An [Intent] that is bound to a [DoNothingAction], but, in addition to not /// performing an action, also stops the propagation of the key event bound to /// this intent to other key event handlers in the focus chain. /// @@ -1342,7 +1369,7 @@ class DoNothingAndStopPropagationIntent extends Intent { const DoNothingAndStopPropagationIntent._(); } -/// An [Action], that doesn't perform any action when invoked. +/// An [Action] that doesn't perform any action when invoked. /// /// Attaching a [DoNothingAction] to an [Actions.actions] mapping is a way to /// disable an action defined by a widget higher in the widget hierarchy. @@ -1411,7 +1438,7 @@ class ButtonActivateIntent extends Intent { const ButtonActivateIntent(); } -/// An action that activates the currently focused control. +/// An [Action] that activates the currently focused control. /// /// This is an abstract class that serves as a base class for actions that /// activate a control. By default, is bound to [LogicalKeyboardKey.enter], @@ -1419,7 +1446,7 @@ class ButtonActivateIntent extends Intent { /// default keyboard map in [WidgetsApp]. abstract class ActivateAction extends Action { } -/// An intent that selects the currently focused control. +/// An [Intent] that selects the currently focused control. class SelectIntent extends Intent { } /// An action that selects the currently focused control. @@ -1441,7 +1468,7 @@ class DismissIntent extends Intent { const DismissIntent(); } -/// An action that dismisses the focused widget. +/// An [Action] that dismisses the focused widget. /// /// This is an abstract class that serves as a base class for dismiss actions. abstract class DismissAction extends Action { } diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 35c2864130dc..99d5eb904fca 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -1289,6 +1289,7 @@ class WidgetsApp extends StatefulWidget { DirectionalFocusIntent: DirectionalFocusAction(), ScrollIntent: ScrollAction(), PrioritizedIntents: PrioritizedAction(), + VoidCallbackIntent: VoidCallbackAction(), }; @override diff --git a/packages/flutter/test/widgets/actions_test.dart b/packages/flutter/test/widgets/actions_test.dart index 9f0401ba389a..f92dea40284b 100644 --- a/packages/flutter/test/widgets/actions_test.dart +++ b/packages/flutter/test/widgets/actions_test.dart @@ -9,83 +9,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -typedef PostInvokeCallback = void Function({Action action, Intent intent, ActionDispatcher dispatcher}); - -class TestIntent extends Intent { - const TestIntent(); -} - -class SecondTestIntent extends TestIntent { - const SecondTestIntent(); -} - -class ThirdTestIntent extends SecondTestIntent { - const ThirdTestIntent(); -} - -class TestAction extends CallbackAction { - TestAction({ - required OnInvokeCallback onInvoke, - }) : assert(onInvoke != null), - super(onInvoke: onInvoke); - - @override - bool isEnabled(TestIntent intent) => enabled; - - bool get enabled => _enabled; - bool _enabled = true; - set enabled(bool value) { - if (_enabled == value) { - return; - } - _enabled = value; - notifyActionListeners(); - } - - @override - void addActionListener(ActionListenerCallback listener) { - super.addActionListener(listener); - listeners.add(listener); - } - - @override - void removeActionListener(ActionListenerCallback listener) { - super.removeActionListener(listener); - listeners.remove(listener); - } - List listeners = []; - - void _testInvoke(TestIntent intent) => invoke(intent); -} - -class TestDispatcher extends ActionDispatcher { - const TestDispatcher({this.postInvoke}); - - final PostInvokeCallback? postInvoke; - - @override - Object? invokeAction(Action action, Intent intent, [BuildContext? context]) { - final Object? result = super.invokeAction(action, intent, context); - postInvoke?.call(action: action, intent: intent, dispatcher: this); - return result; - } -} - -class TestDispatcher1 extends TestDispatcher { - const TestDispatcher1({super.postInvoke}); -} - void main() { - testWidgets('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async { - late Intent passedIntent; - final TestAction action = TestAction(onInvoke: (Intent intent) { - passedIntent = intent; - return true; - }); - const TestIntent intent = TestIntent(); - action._testInvoke(intent); - expect(passedIntent, equals(intent)); - }); group(ActionDispatcher, () { testWidgets('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async { await tester.pumpWidget(Container()); @@ -1033,6 +957,29 @@ void main() { ); }); + group('Action subclasses', () { + testWidgets('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async { + late Intent passedIntent; + final TestAction action = TestAction(onInvoke: (Intent intent) { + passedIntent = intent; + return true; + }); + const TestIntent intent = TestIntent(); + action._testInvoke(intent); + expect(passedIntent, equals(intent)); + }); + testWidgets('VoidCallbackAction', (WidgetTester tester) async { + bool called = false; + void testCallback() { + called = true; + } + final VoidCallbackAction action = VoidCallbackAction(); + final VoidCallbackIntent intent = VoidCallbackIntent(testCallback); + action.invoke(intent); + expect(called, isTrue); + }); + }); + group('Diagnostics', () { testWidgets('default Intent debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); @@ -1766,6 +1713,72 @@ void main() { }); } +typedef PostInvokeCallback = void Function({Action action, Intent intent, ActionDispatcher dispatcher}); + +class TestIntent extends Intent { + const TestIntent(); +} + +class SecondTestIntent extends TestIntent { + const SecondTestIntent(); +} + +class ThirdTestIntent extends SecondTestIntent { + const ThirdTestIntent(); +} + +class TestAction extends CallbackAction { + TestAction({ + required OnInvokeCallback onInvoke, + }) : assert(onInvoke != null), + super(onInvoke: onInvoke); + + @override + bool isEnabled(TestIntent intent) => enabled; + + bool get enabled => _enabled; + bool _enabled = true; + set enabled(bool value) { + if (_enabled == value) { + return; + } + _enabled = value; + notifyActionListeners(); + } + + @override + void addActionListener(ActionListenerCallback listener) { + super.addActionListener(listener); + listeners.add(listener); + } + + @override + void removeActionListener(ActionListenerCallback listener) { + super.removeActionListener(listener); + listeners.remove(listener); + } + List listeners = []; + + void _testInvoke(TestIntent intent) => invoke(intent); +} + +class TestDispatcher extends ActionDispatcher { + const TestDispatcher({this.postInvoke}); + + final PostInvokeCallback? postInvoke; + + @override + Object? invokeAction(Action action, Intent intent, [BuildContext? context]) { + final Object? result = super.invokeAction(action, intent, context); + postInvoke?.call(action: action, intent: intent, dispatcher: this); + return result; + } +} + +class TestDispatcher1 extends TestDispatcher { + const TestDispatcher1({super.postInvoke}); +} + class TestContextAction extends ContextAction { List capturedContexts = [];