Unverified Commit 2d0afe4d authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add some new examples to Actions and Shortcuts (#72163)

Adds a couple of new examples to the Actions and Shortcuts widgets, and updates some documentation.
parent e7772d0e
...@@ -137,6 +137,10 @@ abstract class Action<T extends Intent> with Diagnosticable { ...@@ -137,6 +137,10 @@ abstract class Action<T extends Intent> with Diagnosticable {
/// } /// }
/// } /// }
/// ``` /// ```
///
/// To receive the result of invoking an action, it must be invoked using
/// [Actions.invoke], or by invoking it using an [ActionDispatcher]. An action
/// invoked via a [Shortcuts] widget will have its return value ignored.
@protected @protected
Object? invoke(covariant T intent); Object? invoke(covariant T intent);
...@@ -519,6 +523,168 @@ class ActionDispatcher with Diagnosticable { ...@@ -519,6 +523,168 @@ class ActionDispatcher with Diagnosticable {
/// Actions are typically invoked using [Actions.invoke] with the context /// Actions are typically invoked using [Actions.invoke] with the context
/// containing the ambient [Actions] widget. /// containing the ambient [Actions] widget.
/// ///
/// {@tool dartpad --template=stateful_widget_scaffold_center}
///
/// This example creates a custom [Action] subclass `ModifyAction` for modifying
/// a model, and another, `SaveAction` for saving it.
///
/// This example demonstrates passing arguments to the [Intent] to be carried to
/// the [Action]. Actions can get data either from their own construction (like
/// the `model` in this example), or from the intent passed to them when invoked
/// (like the increment `amount` in this example).
///
/// This example also demonstrates how to use Intents to limit a widget's
/// dependencies on its surroundings. The `SaveButton` widget defined in this
/// example can invoke actions defined in its ancestor widgets, which can be
/// customized to match the part of the widget tree that it is in. It doesn't
/// need to know about the `SaveAction` class, only the `SaveIntent`, and it
/// only needs to know about a value notifier, not the entire model.
///
/// ```dart preamble
/// // A simple model class that notifies listeners when it changes.
/// class Model {
/// ValueNotifier<bool> isDirty = ValueNotifier<bool>(false);
/// ValueNotifier<int> data = ValueNotifier<int>(0);
///
/// int save() {
/// if (isDirty.value) {
/// print('Saved Data: ${data.value}');
/// isDirty.value = false;
/// }
/// return data.value;
/// }
///
/// void setValue(int newValue) {
/// isDirty.value = data.value != newValue;
/// data.value = newValue;
/// }
/// }
///
/// class ModifyIntent extends Intent {
/// const ModifyIntent(this.value);
///
/// final int value;
/// }
///
/// // An Action that modifies the model by setting it to the value that it gets
/// // from the Intent passed to it when invoked.
/// class ModifyAction extends Action<ModifyIntent> {
/// ModifyAction(this.model);
///
/// final Model model;
///
/// @override
/// void invoke(covariant ModifyIntent intent) {
/// model.setValue(intent.value);
/// }
/// }
///
/// // An intent for saving data.
/// class SaveIntent extends Intent {
/// const SaveIntent();
/// }
///
/// // An Action that saves the data in the model it is created with.
/// class SaveAction extends Action<SaveIntent> {
/// SaveAction(this.model);
///
/// final Model model;
///
/// @override
/// int invoke(covariant SaveIntent intent) => model.save();
/// }
///
/// class SaveButton extends StatefulWidget {
/// const SaveButton(this.valueNotifier);
///
/// final ValueNotifier<bool> valueNotifier;
///
/// @override
/// _SaveButtonState createState() => _SaveButtonState();
/// }
///
/// class _SaveButtonState extends State<SaveButton> {
/// int savedValue = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// return AnimatedBuilder(
/// animation: widget.valueNotifier,
/// builder: (BuildContext context, Widget? child) {
/// return TextButton.icon(
/// icon: const Icon(Icons.save),
/// label: Text('$savedValue'),
/// style: ButtonStyle(
/// foregroundColor: MaterialStateProperty.all<Color>(
/// widget.valueNotifier.value ? Colors.red : Colors.green,
/// ),
/// ),
/// onPressed: () {
/// setState(() {
/// savedValue = Actions.invoke(context, const SaveIntent()) as int;
/// });
/// },
/// );
/// },
/// );
/// }
/// }
/// ```
///
/// ```dart
/// Model model = Model();
/// int count = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// return Actions(
/// actions: <Type, Action<Intent>>{
/// ModifyIntent: ModifyAction(model),
/// SaveIntent: SaveAction(model),
/// },
/// child: Builder(
/// builder: (BuildContext context) {
/// return Row(
/// mainAxisAlignment: MainAxisAlignment.spaceAround,
/// children: <Widget>[
/// const Spacer(),
/// Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[
/// IconButton(
/// icon: const Icon(Icons.exposure_plus_1),
/// onPressed: () {
/// Actions.invoke(context, ModifyIntent(count++));
/// },
/// ),
/// AnimatedBuilder(
/// animation: model.data,
/// builder: (BuildContext context, Widget? child) {
/// return Padding(
/// padding: const EdgeInsets.all(8.0),
/// child: Text('${model.data.value}',
/// style: Theme.of(context).textTheme.headline4),
/// );
/// }),
/// IconButton(
/// icon: const Icon(Icons.exposure_minus_1),
/// onPressed: () {
/// Actions.invoke(context, ModifyIntent(count--));
/// },
/// ),
/// ],
/// ),
/// SaveButton(model.isDirty),
/// const Spacer(),
/// ],
/// );
/// },
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// See also: /// See also:
/// ///
/// * [ActionDispatcher], the object that this widget uses to manage actions. /// * [ActionDispatcher], the object that this widget uses to manage actions.
......
...@@ -151,8 +151,8 @@ class KeySet<T extends KeyboardKey> { ...@@ -151,8 +151,8 @@ class KeySet<T extends KeyboardKey> {
/// A key set contains the keys that are down simultaneously to represent a /// A key set contains the keys that are down simultaneously to represent a
/// shortcut. /// shortcut.
/// ///
/// This is mainly used by [ShortcutManager] to allow the definition of shortcut /// This is mainly used by [ShortcutManager] and [Shortcuts] widget to allow the
/// mappings. /// definition of shortcut mappings.
/// ///
/// This is a thin wrapper around a [Set], but changes the equality comparison /// This is a thin wrapper around a [Set], but changes the equality comparison
/// from an identity comparison to a contents comparison so that non-identical /// from an identity comparison to a contents comparison so that non-identical
...@@ -213,8 +213,9 @@ class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable { ...@@ -213,8 +213,9 @@ class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable {
} }
} }
/// Diagnostics property which handles formatting a `Map<LogicalKeySet, Intent>` /// A [DiagnosticsProperty] which handles formatting a `Map<LogicalKeySet,
/// (the same type as the [Shortcuts.shortcuts] property) so that it is human-readable. /// Intent>` (the same type as the [Shortcuts.shortcuts] property) so that its
/// diagnostic output is human-readable.
class ShortcutMapProperty extends DiagnosticsProperty<Map<LogicalKeySet, Intent>> { class ShortcutMapProperty extends DiagnosticsProperty<Map<LogicalKeySet, Intent>> {
/// Create a diagnostics property for `Map<LogicalKeySet, Intent>` objects, /// Create a diagnostics property for `Map<LogicalKeySet, Intent>` objects,
/// which are the same type as the [Shortcuts.shortcuts] property. /// which are the same type as the [Shortcuts.shortcuts] property.
...@@ -269,8 +270,9 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable { ...@@ -269,8 +270,9 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// in the [shortcuts] map. /// in the [shortcuts] map.
/// ///
/// The net effect of setting `modal` to true is to return /// The net effect of setting `modal` to true is to return
/// [KeyEventResult.skipRemainingHandlers] from [handleKeypress] if it does not /// [KeyEventResult.skipRemainingHandlers] from [handleKeypress] if it does
/// exist in the shortcut map, instead of returning [KeyEventResult.ignored]. /// not exist in the shortcut map, instead of returning
/// [KeyEventResult.ignored].
final bool modal; final bool modal;
/// Returns the shortcut map. /// Returns the shortcut map.
...@@ -293,8 +295,8 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable { ...@@ -293,8 +295,8 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// ///
/// Returns null if no intent 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 /// Defaults to a set derived from [RawKeyboard.keysPressed] if `keysPressed`
/// not supplied. /// is not supplied.
Intent? _find({ LogicalKeySet? keysPressed }) { Intent? _find({ LogicalKeySet? keysPressed }) {
if (keysPressed == null && RawKeyboard.instance.keysPressed.isEmpty) { if (keysPressed == null && RawKeyboard.instance.keysPressed.isEmpty) {
return null; return null;
...@@ -334,9 +336,9 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable { ...@@ -334,9 +336,9 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// focused widget's context (from [FocusManager.primaryFocus]). /// focused widget's context (from [FocusManager.primaryFocus]).
/// ///
/// Returns a [KeyEventResult.handled] if an action was invoked, otherwise a /// Returns a [KeyEventResult.handled] if an action was invoked, otherwise a
/// [KeyEventResult.skipRemainingHandlers] if [modal] is true, or if it maps to a /// [KeyEventResult.skipRemainingHandlers] if [modal] is true, or if it maps
/// [DoNothingAction] with [DoNothingAction.consumesKey] set to false, and /// to a [DoNothingAction] with [DoNothingAction.consumesKey] set to false,
/// in all other cases returns [KeyEventResult.ignored]. /// and in all other cases returns [KeyEventResult.ignored].
/// ///
/// In order for an action to be invoked (and [KeyEventResult.handled] /// In order for an action to be invoked (and [KeyEventResult.handled]
/// returned), a pressed [KeySet] must be mapped to an [Intent], the [Intent] /// returned), a pressed [KeySet] must be mapped to an [Intent], the [Intent]
...@@ -382,59 +384,171 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable { ...@@ -382,59 +384,171 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
} }
} }
/// A widget that establishes a [ShortcutManager] to be used by its descendants /// A widget to that creates key bindings to specific actions for its
/// descendants.
///
/// This widget establishes a [ShortcutManager] to be used by its descendants
/// when invoking an [Action] via a keyboard key combination that maps to an /// when invoking an [Action] via a keyboard key combination that maps to an
/// [Intent]. /// [Intent].
/// ///
/// {@tool dartpad --template=stateful_widget_scaffold_center} /// {@tool dartpad --template=stateful_widget_scaffold_center}
/// ///
/// Here, we will use a [Shortcuts] and [Actions] widget to add and remove from a counter. /// Here, we will use the [Shortcuts] and [Actions] widgets to add and subtract
/// This can be done by creating a child widget that is focused and pressing the logical key /// from a counter. When the child widget has keyboard focus, and a user presses
/// sets that have been defined in [Shortcuts] and defining the actions that each key set /// the keys that have been defined in [Shortcuts], the action that is bound
/// performs. /// to the appropriate [Intent] for the key is invoked.
///
/// It also shows the use of a [CallbackAction] to avoid creating a new [Action]
/// subclass.
/// ///
/// ```dart imports /// ```dart imports
/// import 'package:flutter/services.dart'; /// import 'package:flutter/services.dart';
/// ``` /// ```
/// ///
/// ```dart preamble /// ```dart preamble
/// class Increment extends Intent {} /// class IncrementIntent extends Intent {
/// const IncrementIntent();
/// }
/// ///
/// class Decrement extends Intent {} /// class DecrementIntent extends Intent {
/// const DecrementIntent();
/// }
/// ``` /// ```
/// ///
/// ```dart /// ```dart
/// int count = 0; /// int count = 0;
/// ///
/// @override
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return Shortcuts( /// return Shortcuts(
/// shortcuts: <LogicalKeySet, Intent> { /// shortcuts: <LogicalKeySet, Intent>{
/// LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.keyK): Increment(), /// LogicalKeySet(LogicalKeyboardKey.arrowUp): const IncrementIntent(),
/// LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.keyL): Decrement(), /// LogicalKeySet(LogicalKeyboardKey.arrowDown): const DecrementIntent(),
/// }, /// },
/// child: Actions( /// child: Actions(
/// actions: <Type, Action<Intent>> { /// actions: <Type, Action<Intent>>{
/// Increment: CallbackAction<Increment>( /// IncrementIntent: CallbackAction<IncrementIntent>(
/// onInvoke: (Increment intent) => setState(() { count = count + 1; }), /// onInvoke: (IncrementIntent intent) => setState(() {
/// count = count + 1;
/// }),
/// ), /// ),
/// Decrement: CallbackAction<Decrement>( /// DecrementIntent: CallbackAction<DecrementIntent>(
/// onInvoke: (Decrement intent) => setState(() { count = count - 1; }), /// onInvoke: (DecrementIntent intent) => setState(() {
/// count = count - 1;
/// }),
/// ), /// ),
/// }, /// },
/// child: Focus( /// child: Focus(
/// autofocus:true, /// autofocus: true,
/// child: Column(
/// children: <Widget>[
/// const Text('Add to the counter by pressing the up arrow key'),
/// const Text(
/// 'Subtract from the counter by pressing the down arrow key'),
/// Text('count: $count'),
/// ],
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@tool dartpad --template=stateful_widget_scaffold_center}
///
/// This slightly more complicated, but more flexible, example creates a custom
/// [Action] subclass to increment and decrement within a widget (a [Column])
/// that has keyboard focus. When the user presses the up and down arrow keys,
/// the counter will increment and decrement a data model using the custom
/// actions.
///
/// One thing that this demonstrates is passing arguments to the [Intent] to be
/// carried to the [Action]. This shows how actions can get data either from
/// their own construction (like the `model` in this example), or from the
/// intent passed to them when invoked (like the increment `amount` in this
/// example).
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```
///
/// ```dart preamble
/// class Model with ChangeNotifier {
/// int count = 0;
/// void incrementBy(int amount) {
/// count += amount;
/// notifyListeners();
/// }
///
/// void decrementBy(int amount) {
/// count -= amount;
/// notifyListeners();
/// }
/// }
///
/// class IncrementIntent extends Intent {
/// const IncrementIntent(this.amount);
///
/// final int amount;
/// }
///
/// class DecrementIntent extends Intent {
/// const DecrementIntent(this.amount);
///
/// final int amount;
/// }
///
/// class IncrementAction extends Action<IncrementIntent> {
/// IncrementAction(this.model);
///
/// final Model model;
///
/// @override
/// void invoke(covariant IncrementIntent intent) {
/// model.incrementBy(intent.amount);
/// }
/// }
///
/// class DecrementAction extends Action<DecrementIntent> {
/// DecrementAction(this.model);
///
/// final Model model;
///
/// @override
/// void invoke(covariant DecrementIntent intent) {
/// model.decrementBy(intent.amount);
/// }
/// }
/// ```
///
/// ```dart
/// Model model = Model();
///
/// @override
/// Widget build(BuildContext context) {
/// return Shortcuts(
/// shortcuts: <LogicalKeySet, Intent>{
/// LogicalKeySet(LogicalKeyboardKey.arrowUp): const IncrementIntent(2),
/// LogicalKeySet(LogicalKeyboardKey.arrowDown): const DecrementIntent(2),
/// },
/// child: Actions(
/// actions: <Type, Action<Intent>>{
/// IncrementIntent: IncrementAction(model),
/// DecrementIntent: DecrementAction(model),
/// },
/// child: Focus(
/// autofocus: true,
/// child: Column( /// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[ /// children: <Widget>[
/// Text('Add: keyboard Shift + "k"'), /// const Text('Add to the counter by pressing the up arrow key'),
/// Text('Subtract: keyboard Shift + "l"'), /// const Text(
/// SizedBox(height: 10.0), /// 'Subtract from the counter by pressing the down arrow key'),
/// ColoredBox( /// AnimatedBuilder(
/// color: Colors.yellow, /// animation: model,
/// child: Padding( /// builder: (BuildContext context, Widget? child) {
/// padding: EdgeInsets.all(4.0), /// return Text('count: ${model.count}');
/// child: Text('count: $count'), /// },
/// ),
/// ), /// ),
/// ], /// ],
/// ), /// ),
...@@ -450,6 +564,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable { ...@@ -450,6 +564,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// * [Intent], a class for containing a description of a user action to be /// * [Intent], a class for containing a description of a user action to be
/// invoked. /// invoked.
/// * [Action], a class for defining an invocation of a user action. /// * [Action], a class for defining an invocation of a user action.
/// * [CallbackAction], a class for creating an action from a callback.
class Shortcuts extends StatefulWidget { class Shortcuts extends StatefulWidget {
/// Creates a const [Shortcuts] widget. /// Creates a const [Shortcuts] widget.
/// ///
...@@ -493,10 +608,10 @@ class Shortcuts extends StatefulWidget { ...@@ -493,10 +608,10 @@ class Shortcuts extends StatefulWidget {
/// map when logged. /// map when logged.
/// ///
/// This allows simplifying the diagnostic output to avoid cluttering it /// This allows simplifying the diagnostic output to avoid cluttering it
/// unnecessarily with the default shortcut map. /// unnecessarily with large default shortcut maps.
final String? debugLabel; final String? debugLabel;
/// Returns the [ActionDispatcher] that most tightly encloses the given /// Returns the [ShortcutManager] that most tightly encloses the given
/// [BuildContext]. /// [BuildContext].
/// ///
/// The [context] argument must not be null. /// The [context] argument must not be null.
...@@ -526,7 +641,7 @@ class Shortcuts extends StatefulWidget { ...@@ -526,7 +641,7 @@ class Shortcuts extends StatefulWidget {
return inherited!.manager; return inherited!.manager;
} }
/// Returns the [ActionDispatcher] that most tightly encloses the given /// Returns the [ShortcutManager] that most tightly encloses the given
/// [BuildContext]. /// [BuildContext].
/// ///
/// The [context] argument must not be null. /// The [context] argument must not be null.
......
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