Unverified Commit 19940279 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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.
parent c248854d
...@@ -1296,7 +1296,34 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> { ...@@ -1296,7 +1296,34 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
} }
} }
/// 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<VoidCallbackIntent> {
@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 /// 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 /// a keyboard shortcut defined by a widget higher in the widget hierarchy and
...@@ -1317,7 +1344,7 @@ class DoNothingIntent extends Intent { ...@@ -1317,7 +1344,7 @@ class DoNothingIntent extends Intent {
const DoNothingIntent._(); 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 /// performing an action, also stops the propagation of the key event bound to
/// this intent to other key event handlers in the focus chain. /// this intent to other key event handlers in the focus chain.
/// ///
...@@ -1342,7 +1369,7 @@ class DoNothingAndStopPropagationIntent extends Intent { ...@@ -1342,7 +1369,7 @@ class DoNothingAndStopPropagationIntent extends Intent {
const DoNothingAndStopPropagationIntent._(); 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 /// Attaching a [DoNothingAction] to an [Actions.actions] mapping is a way to
/// disable an action defined by a widget higher in the widget hierarchy. /// disable an action defined by a widget higher in the widget hierarchy.
...@@ -1411,7 +1438,7 @@ class ButtonActivateIntent extends Intent { ...@@ -1411,7 +1438,7 @@ class ButtonActivateIntent extends Intent {
const ButtonActivateIntent(); 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 /// This is an abstract class that serves as a base class for actions that
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter], /// activate a control. By default, is bound to [LogicalKeyboardKey.enter],
...@@ -1419,7 +1446,7 @@ class ButtonActivateIntent extends Intent { ...@@ -1419,7 +1446,7 @@ class ButtonActivateIntent extends Intent {
/// default keyboard map in [WidgetsApp]. /// default keyboard map in [WidgetsApp].
abstract class ActivateAction extends Action<ActivateIntent> { } abstract class ActivateAction extends Action<ActivateIntent> { }
/// An intent that selects the currently focused control. /// An [Intent] that selects the currently focused control.
class SelectIntent extends Intent { } class SelectIntent extends Intent { }
/// An action that selects the currently focused control. /// An action that selects the currently focused control.
...@@ -1441,7 +1468,7 @@ class DismissIntent extends Intent { ...@@ -1441,7 +1468,7 @@ class DismissIntent extends Intent {
const DismissIntent(); 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. /// This is an abstract class that serves as a base class for dismiss actions.
abstract class DismissAction extends Action<DismissIntent> { } abstract class DismissAction extends Action<DismissIntent> { }
......
...@@ -1289,6 +1289,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -1289,6 +1289,7 @@ class WidgetsApp extends StatefulWidget {
DirectionalFocusIntent: DirectionalFocusAction(), DirectionalFocusIntent: DirectionalFocusAction(),
ScrollIntent: ScrollAction(), ScrollIntent: ScrollAction(),
PrioritizedIntents: PrioritizedAction(), PrioritizedIntents: PrioritizedAction(),
VoidCallbackIntent: VoidCallbackAction(),
}; };
@override @override
......
...@@ -9,83 +9,7 @@ import 'package:flutter/rendering.dart'; ...@@ -9,83 +9,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
typedef PostInvokeCallback = void Function({Action<Intent> 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<TestIntent> {
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<ActionListenerCallback> listeners = <ActionListenerCallback>[];
void _testInvoke(TestIntent intent) => invoke(intent);
}
class TestDispatcher extends ActionDispatcher {
const TestDispatcher({this.postInvoke});
final PostInvokeCallback? postInvoke;
@override
Object? invokeAction(Action<Intent> 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() { 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, () { group(ActionDispatcher, () {
testWidgets('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async { testWidgets('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async {
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
...@@ -1033,6 +957,29 @@ void main() { ...@@ -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', () { group('Diagnostics', () {
testWidgets('default Intent debugFillProperties', (WidgetTester tester) async { testWidgets('default Intent debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
...@@ -1766,6 +1713,72 @@ void main() { ...@@ -1766,6 +1713,72 @@ void main() {
}); });
} }
typedef PostInvokeCallback = void Function({Action<Intent> 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<TestIntent> {
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<ActionListenerCallback> listeners = <ActionListenerCallback>[];
void _testInvoke(TestIntent intent) => invoke(intent);
}
class TestDispatcher extends ActionDispatcher {
const TestDispatcher({this.postInvoke});
final PostInvokeCallback? postInvoke;
@override
Object? invokeAction(Action<Intent> 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<TestIntent> { class TestContextAction extends ContextAction<TestIntent> {
List<BuildContext?> capturedContexts = <BuildContext?>[]; List<BuildContext?> capturedContexts = <BuildContext?>[];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment