Skip to content

Commit 1994027

Browse files
authored
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.
1 parent c248854 commit 1994027

File tree

3 files changed

+123
-82
lines changed

3 files changed

+123
-82
lines changed

packages/flutter/lib/src/widgets/actions.dart

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,7 +1296,34 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
12961296
}
12971297
}
12981298

1299-
/// An [Intent], that is bound to a [DoNothingAction].
1299+
/// An [Intent] that keeps a [VoidCallback] to be invoked by a
1300+
/// [VoidCallbackAction] when it receives this intent.
1301+
class VoidCallbackIntent extends Intent {
1302+
/// Creates a [VoidCallbackIntent].
1303+
const VoidCallbackIntent(this.callback);
1304+
1305+
/// The callback that is to be called by the [VoidCallbackAction] that
1306+
/// receives this intent.
1307+
final VoidCallback callback;
1308+
}
1309+
1310+
/// An [Action] that invokes the [VoidCallback] given to it in the
1311+
/// [VoidCallbackIntent] passed to it when invoked.
1312+
///
1313+
/// See also:
1314+
///
1315+
/// * [CallbackAction], which is an action that will invoke a callback with the
1316+
/// intent passed to the action's invoke method. The callback is configured
1317+
/// on the action, not the intent, like this class.
1318+
class VoidCallbackAction extends Action<VoidCallbackIntent> {
1319+
@override
1320+
Object? invoke(VoidCallbackIntent intent) {
1321+
intent.callback();
1322+
return null;
1323+
}
1324+
}
1325+
1326+
/// An [Intent] that is bound to a [DoNothingAction].
13001327
///
13011328
/// Attaching a [DoNothingIntent] to a [Shortcuts] mapping is one way to disable
13021329
/// a keyboard shortcut defined by a widget higher in the widget hierarchy and
@@ -1317,7 +1344,7 @@ class DoNothingIntent extends Intent {
13171344
const DoNothingIntent._();
13181345
}
13191346

1320-
/// An [Intent], that is bound to a [DoNothingAction], but, in addition to not
1347+
/// An [Intent] that is bound to a [DoNothingAction], but, in addition to not
13211348
/// performing an action, also stops the propagation of the key event bound to
13221349
/// this intent to other key event handlers in the focus chain.
13231350
///
@@ -1342,7 +1369,7 @@ class DoNothingAndStopPropagationIntent extends Intent {
13421369
const DoNothingAndStopPropagationIntent._();
13431370
}
13441371

1345-
/// An [Action], that doesn't perform any action when invoked.
1372+
/// An [Action] that doesn't perform any action when invoked.
13461373
///
13471374
/// Attaching a [DoNothingAction] to an [Actions.actions] mapping is a way to
13481375
/// disable an action defined by a widget higher in the widget hierarchy.
@@ -1411,15 +1438,15 @@ class ButtonActivateIntent extends Intent {
14111438
const ButtonActivateIntent();
14121439
}
14131440

1414-
/// An action that activates the currently focused control.
1441+
/// An [Action] that activates the currently focused control.
14151442
///
14161443
/// This is an abstract class that serves as a base class for actions that
14171444
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter],
14181445
/// [LogicalKeyboardKey.gameButtonA], and [LogicalKeyboardKey.space] in the
14191446
/// default keyboard map in [WidgetsApp].
14201447
abstract class ActivateAction extends Action<ActivateIntent> { }
14211448

1422-
/// An intent that selects the currently focused control.
1449+
/// An [Intent] that selects the currently focused control.
14231450
class SelectIntent extends Intent { }
14241451

14251452
/// An action that selects the currently focused control.
@@ -1441,7 +1468,7 @@ class DismissIntent extends Intent {
14411468
const DismissIntent();
14421469
}
14431470

1444-
/// An action that dismisses the focused widget.
1471+
/// An [Action] that dismisses the focused widget.
14451472
///
14461473
/// This is an abstract class that serves as a base class for dismiss actions.
14471474
abstract class DismissAction extends Action<DismissIntent> { }

packages/flutter/lib/src/widgets/app.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,7 @@ class WidgetsApp extends StatefulWidget {
12891289
DirectionalFocusIntent: DirectionalFocusAction(),
12901290
ScrollIntent: ScrollAction(),
12911291
PrioritizedIntents: PrioritizedAction(),
1292+
VoidCallbackIntent: VoidCallbackAction(),
12921293
};
12931294

12941295
@override

packages/flutter/test/widgets/actions_test.dart

Lines changed: 89 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,83 +9,7 @@ import 'package:flutter/rendering.dart';
99
import 'package:flutter/services.dart';
1010
import 'package:flutter_test/flutter_test.dart';
1111

12-
typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, ActionDispatcher dispatcher});
13-
14-
class TestIntent extends Intent {
15-
const TestIntent();
16-
}
17-
18-
class SecondTestIntent extends TestIntent {
19-
const SecondTestIntent();
20-
}
21-
22-
class ThirdTestIntent extends SecondTestIntent {
23-
const ThirdTestIntent();
24-
}
25-
26-
class TestAction extends CallbackAction<TestIntent> {
27-
TestAction({
28-
required OnInvokeCallback onInvoke,
29-
}) : assert(onInvoke != null),
30-
super(onInvoke: onInvoke);
31-
32-
@override
33-
bool isEnabled(TestIntent intent) => enabled;
34-
35-
bool get enabled => _enabled;
36-
bool _enabled = true;
37-
set enabled(bool value) {
38-
if (_enabled == value) {
39-
return;
40-
}
41-
_enabled = value;
42-
notifyActionListeners();
43-
}
44-
45-
@override
46-
void addActionListener(ActionListenerCallback listener) {
47-
super.addActionListener(listener);
48-
listeners.add(listener);
49-
}
50-
51-
@override
52-
void removeActionListener(ActionListenerCallback listener) {
53-
super.removeActionListener(listener);
54-
listeners.remove(listener);
55-
}
56-
List<ActionListenerCallback> listeners = <ActionListenerCallback>[];
57-
58-
void _testInvoke(TestIntent intent) => invoke(intent);
59-
}
60-
61-
class TestDispatcher extends ActionDispatcher {
62-
const TestDispatcher({this.postInvoke});
63-
64-
final PostInvokeCallback? postInvoke;
65-
66-
@override
67-
Object? invokeAction(Action<Intent> action, Intent intent, [BuildContext? context]) {
68-
final Object? result = super.invokeAction(action, intent, context);
69-
postInvoke?.call(action: action, intent: intent, dispatcher: this);
70-
return result;
71-
}
72-
}
73-
74-
class TestDispatcher1 extends TestDispatcher {
75-
const TestDispatcher1({super.postInvoke});
76-
}
77-
7812
void main() {
79-
testWidgets('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async {
80-
late Intent passedIntent;
81-
final TestAction action = TestAction(onInvoke: (Intent intent) {
82-
passedIntent = intent;
83-
return true;
84-
});
85-
const TestIntent intent = TestIntent();
86-
action._testInvoke(intent);
87-
expect(passedIntent, equals(intent));
88-
});
8913
group(ActionDispatcher, () {
9014
testWidgets('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async {
9115
await tester.pumpWidget(Container());
@@ -1033,6 +957,29 @@ void main() {
1033957
);
1034958
});
1035959

960+
group('Action subclasses', () {
961+
testWidgets('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async {
962+
late Intent passedIntent;
963+
final TestAction action = TestAction(onInvoke: (Intent intent) {
964+
passedIntent = intent;
965+
return true;
966+
});
967+
const TestIntent intent = TestIntent();
968+
action._testInvoke(intent);
969+
expect(passedIntent, equals(intent));
970+
});
971+
testWidgets('VoidCallbackAction', (WidgetTester tester) async {
972+
bool called = false;
973+
void testCallback() {
974+
called = true;
975+
}
976+
final VoidCallbackAction action = VoidCallbackAction();
977+
final VoidCallbackIntent intent = VoidCallbackIntent(testCallback);
978+
action.invoke(intent);
979+
expect(called, isTrue);
980+
});
981+
});
982+
1036983
group('Diagnostics', () {
1037984
testWidgets('default Intent debugFillProperties', (WidgetTester tester) async {
1038985
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
@@ -1766,6 +1713,72 @@ void main() {
17661713
});
17671714
}
17681715

1716+
typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, ActionDispatcher dispatcher});
1717+
1718+
class TestIntent extends Intent {
1719+
const TestIntent();
1720+
}
1721+
1722+
class SecondTestIntent extends TestIntent {
1723+
const SecondTestIntent();
1724+
}
1725+
1726+
class ThirdTestIntent extends SecondTestIntent {
1727+
const ThirdTestIntent();
1728+
}
1729+
1730+
class TestAction extends CallbackAction<TestIntent> {
1731+
TestAction({
1732+
required OnInvokeCallback onInvoke,
1733+
}) : assert(onInvoke != null),
1734+
super(onInvoke: onInvoke);
1735+
1736+
@override
1737+
bool isEnabled(TestIntent intent) => enabled;
1738+
1739+
bool get enabled => _enabled;
1740+
bool _enabled = true;
1741+
set enabled(bool value) {
1742+
if (_enabled == value) {
1743+
return;
1744+
}
1745+
_enabled = value;
1746+
notifyActionListeners();
1747+
}
1748+
1749+
@override
1750+
void addActionListener(ActionListenerCallback listener) {
1751+
super.addActionListener(listener);
1752+
listeners.add(listener);
1753+
}
1754+
1755+
@override
1756+
void removeActionListener(ActionListenerCallback listener) {
1757+
super.removeActionListener(listener);
1758+
listeners.remove(listener);
1759+
}
1760+
List<ActionListenerCallback> listeners = <ActionListenerCallback>[];
1761+
1762+
void _testInvoke(TestIntent intent) => invoke(intent);
1763+
}
1764+
1765+
class TestDispatcher extends ActionDispatcher {
1766+
const TestDispatcher({this.postInvoke});
1767+
1768+
final PostInvokeCallback? postInvoke;
1769+
1770+
@override
1771+
Object? invokeAction(Action<Intent> action, Intent intent, [BuildContext? context]) {
1772+
final Object? result = super.invokeAction(action, intent, context);
1773+
postInvoke?.call(action: action, intent: intent, dispatcher: this);
1774+
return result;
1775+
}
1776+
}
1777+
1778+
class TestDispatcher1 extends TestDispatcher {
1779+
const TestDispatcher1({super.postInvoke});
1780+
}
1781+
17691782
class TestContextAction extends ContextAction<TestIntent> {
17701783
List<BuildContext?> capturedContexts = <BuildContext?>[];
17711784

0 commit comments

Comments
 (0)