actions.dart 46.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Ian Hickson's avatar
Ian Hickson committed
4

5
import 'package:flutter/foundation.dart';
6
import 'package:flutter/scheduler.dart';
7
import 'package:flutter/gestures.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/services.dart';
10

11
import 'basic.dart';
12
import 'focus_manager.dart';
13
import 'focus_scope.dart';
14
import 'framework.dart';
15
import 'media_query.dart';
16
import 'shortcuts.dart';
17

18 19 20 21 22 23
// BuildContext/Element doesn't have a parent accessor, but it can be
// simulated with visitAncestorElements. _getParent is needed because
// context.getElementForInheritedWidgetOfExactType will return itself if it
// happens to be of the correct type. getParent should be O(1), since we
// always return false at the first ancestor.
BuildContext _getParent(BuildContext context) {
24
  late final BuildContext parent;
25 26 27 28 29 30
  context.visitAncestorElements((Element ancestor) {
    parent = ancestor;
    return false;
  });
  return parent;
}
31

32
/// An abstract class representing a particular configuration of an [Action].
33
///
34
/// This class is what the [Shortcuts.shortcuts] map has as values, and is used
35 36
/// by an [ActionDispatcher] to look up an action and invoke it, giving it this
/// object to extract configuration information from.
37 38 39 40 41 42
///
/// See also:
///
///  * [Actions.invoke], which invokes the action associated with a specified
///    [Intent] using the [Actions] widget that most tightly encloses the given
///    [BuildContext].
43
@immutable
44
abstract class Intent with Diagnosticable {
45
  /// A const constructor for an [Intent].
46
  const Intent();
47

48 49
  /// An intent that is mapped to a [DoNothingAction], which, as the name
  /// implies, does nothing.
50
  ///
51 52 53
  /// 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.
54
  static const DoNothingIntent doNothing = DoNothingIntent._();
55 56
}

57 58 59 60 61 62
/// The kind of callback that an [Action] uses to notify of changes to the
/// action's state.
///
/// To register an action listener, call [Action.addActionListener].
typedef ActionListenerCallback = void Function(Action<Intent> action);

63 64 65 66 67 68 69 70 71 72 73 74 75
/// Base class for actions.
///
/// As the name implies, an [Action] is an action or command to be performed.
/// They are typically invoked as a result of a user action, such as a keyboard
/// shortcut in a [Shortcuts] widget, which is used to look up an [Intent],
/// which is given to an [ActionDispatcher] to map the [Intent] to an [Action]
/// and invoke it.
///
/// The [ActionDispatcher] can invoke an [Action] on the primary focus, or
/// without regard for focus.
///
/// See also:
///
76
///  * [Shortcuts], which is a widget that contains a key map, in which it looks
77
///    up key combinations in order to invoke actions.
78
///  * [Actions], which is a widget that defines a map of [Intent] to [Action]
79
///    and allows redefining of actions for its descendants.
80 81 82 83 84 85 86
///  * [ActionDispatcher], a class that takes an [Action] and invokes it, passing
///    a given [Intent].
abstract class Action<T extends Intent> with Diagnosticable {
  final ObserverList<ActionListenerCallback> _listeners = ObserverList<ActionListenerCallback>();

  /// Gets the type of intent this action responds to.
  Type get intentType => T;
87

88
  /// Returns true if the action is enabled and is ready to be invoked.
89
  ///
90 91 92 93 94
  /// This will be called by the [ActionDispatcher] before attempting to invoke
  /// the action.
  ///
  /// If the enabled state changes, overriding subclasses must call
  /// [notifyActionListeners] to notify any listeners of the change.
95
  bool isEnabled(covariant T intent) => true;
96

97 98 99 100 101 102 103 104 105 106 107 108 109
  /// Indicates whether this action should treat key events mapped to this
  /// action as being "handled" when it is invoked via the key event.
  ///
  /// If the key is handled, then no other key event handlers in the focus chain
  /// will receive the event.
  ///
  /// If the key event is not handled, it will be passed back to the engine, and
  /// continue to be processed there, allowing text fields and non-Flutter
  /// widgets to receive the key event.
  ///
  /// The default implementation returns true.
  bool consumesKey(covariant T intent) => true;

110 111
  /// Called when the action is to be performed.
  ///
112 113 114
  /// This is called by the [ActionDispatcher] when an action is invoked via
  /// [Actions.invoke], or when an action is invoked using
  /// [ActionDispatcher.invokeAction] directly.
115 116
  ///
  /// This method is only meant to be invoked by an [ActionDispatcher], or by
117
  /// its subclasses, and only when [isEnabled] is true.
118 119 120 121 122 123 124
  ///
  /// When overriding this method, the returned value can be any Object, but
  /// changing the return type of the override to match the type of the returned
  /// value provides more type safety.
  ///
  /// For instance, if your override of `invoke` returns an `int`, then define
  /// it like so:
125
  ///
126 127 128 129 130 131 132 133 134 135 136 137 138 139
  /// ```dart
  /// class IncrementIntent extends Intent {
  ///   const IncrementIntent({this.index});
  ///
  ///   final int index;
  /// }
  ///
  /// class MyIncrementAction extends Action<IncrementIntent> {
  ///   @override
  ///   int invoke(IncrementIntent intent) {
  ///     return intent.index + 1;
  ///   }
  /// }
  /// ```
140
  @protected
141
  Object? invoke(covariant T intent);
142 143 144 145 146 147 148 149

  /// Register a callback to listen for changes to the state of this action.
  ///
  /// If you call this, you must call [removeActionListener] a matching number
  /// of times, or memory leaks will occur. To help manage this and avoid memory
  /// leaks, use of the [ActionListener] widget to register and unregister your
  /// listener appropriately is highly recommended.
  ///
150
  /// {@template flutter.widgets.Action.addActionListener}
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
  /// If a listener had been added twice, and is removed once during an
  /// iteration (i.e. in response to a notification), it will still be called
  /// again. If, on the other hand, it is removed as many times as it was
  /// registered, then it will no longer be called. This odd behavior is the
  /// result of the [Action] not being able to determine which listener
  /// is being removed, since they are identical, and therefore conservatively
  /// still calling all the listeners when it knows that any are still
  /// registered.
  ///
  /// This surprising behavior can be unexpectedly observed when registering a
  /// listener on two separate objects which are both forwarding all
  /// registrations to a common upstream object.
  /// {@endtemplate}
  @mustCallSuper
  void addActionListener(ActionListenerCallback listener) => _listeners.add(listener);

  /// Remove a previously registered closure from the list of closures that are
  /// notified when the object changes.
  ///
  /// If the given listener is not registered, the call is ignored.
  ///
  /// If you call [addActionListener], you must call this method a matching
  /// number of times, or memory leaks will occur. To help manage this and avoid
  /// memory leaks, use of the [ActionListener] widget to register and
  /// unregister your listener appropriately is highly recommended.
  ///
177
  /// {@macro flutter.widgets.Action.addActionListener}
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
  @mustCallSuper
  void removeActionListener(ActionListenerCallback listener) => _listeners.remove(listener);

  /// Call all the registered listeners.
  ///
  /// Subclasses should call this method whenever the object changes, to notify
  /// any clients the object may have changed. Listeners that are added during this
  /// iteration will not be visited. Listeners that are removed during this
  /// iteration will not be visited after they are removed.
  ///
  /// Exceptions thrown by listeners will be caught and reported using
  /// [FlutterError.reportError].
  ///
  /// Surprising behavior can result when reentrantly removing a listener (i.e.
  /// in response to a notification) that has been registered multiple times.
  /// See the discussion at [removeActionListener].
  @protected
  @visibleForTesting
  void notifyActionListeners() {
    if (_listeners.isEmpty) {
      return;
    }

    // Make a local copy so that a listener can unregister while the list is
    // being iterated over.
    final List<ActionListenerCallback> localListeners = List<ActionListenerCallback>.from(_listeners);
    for (final ActionListenerCallback listener in localListeners) {
205
      InformationCollector? collector;
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
      assert(() {
        collector = () sync* {
          yield DiagnosticsProperty<Action<T>>(
            'The $runtimeType sending notification was',
            this,
            style: DiagnosticsTreeStyle.errorProperty,
          );
        };
        return true;
      }());
      try {
        if (_listeners.contains(listener)) {
          listener(this);
        }
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'widgets library',
          context: ErrorDescription('while dispatching notifications for $runtimeType'),
          informationCollector: collector,
        ));
      }
    }
  }
}

/// A helper widget for making sure that listeners on an action are removed properly.
///
/// Listeners on the [Action] class must have their listener callbacks removed
/// with [Action.removeActionListener] when the listener is disposed of. This widget
/// helps with that, by providing a lifetime for the connection between the
/// [listener] and the [Action], and by handling the adding and removing of
/// the [listener] at the right points in the widget lifecycle.
///
/// If you listen to an [Action] widget in a widget hierarchy, you should use
/// this widget. If you are using an [Action] outside of a widget context, then
/// you must call removeListener yourself.
@immutable
class ActionListener extends StatefulWidget {
  /// Create a const [ActionListener].
  ///
  /// The [listener], [action], and [child] arguments must not be null.
  const ActionListener({
250 251 252 253
    Key? key,
    required this.listener,
    required this.action,
    required this.child,
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
  })  : assert(listener != null),
        assert(action != null),
        assert(child != null),
        super(key: key);

  /// The [ActionListenerCallback] callback to register with the [action].
  ///
  /// Must not be null.
  final ActionListenerCallback listener;

  /// The [Action] that the callback will be registered with.
  ///
  /// Must not be null.
  final Action<Intent> action;

269
  /// {@macro flutter.widgets.ProxyWidget.child}
270
  final Widget child;
271 272

  @override
273 274 275 276 277 278 279 280
  _ActionListenerState createState() => _ActionListenerState();
}

class _ActionListenerState extends State<ActionListener> {
  @override
  void initState() {
    super.initState();
    widget.action.addActionListener(widget.listener);
281
  }
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315

  @override
  void didUpdateWidget(ActionListener oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.action == widget.action && oldWidget.listener == widget.listener) {
      return;
    }
    oldWidget.action.removeActionListener(oldWidget.listener);
    widget.action.addActionListener(widget.listener);
  }

  @override
  void dispose() {
    widget.action.removeActionListener(widget.listener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

/// An abstract [Action] subclass that adds an optional [BuildContext] to the
/// [invoke] method to be able to provide context to actions.
///
/// [ActionDispatcher.invokeAction] checks to see if the action it is invoking
/// is a [ContextAction], and if it is, supplies it with a context.
abstract class ContextAction<T extends Intent> extends Action<T> {
  /// Called when the action is to be performed.
  ///
  /// This is called by the [ActionDispatcher] when an action is invoked via
  /// [Actions.invoke], or when an action is invoked using
  /// [ActionDispatcher.invokeAction] directly.
  ///
  /// This method is only meant to be invoked by an [ActionDispatcher], or by
316
  /// its subclasses, and only when [isEnabled] is true.
317 318
  ///
  /// The optional `context` parameter is the context of the invocation of the
319
  /// action, and in the case of an action invoked by a [ShortcutManager], via
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
  /// a [Shortcuts] widget, will be the context of the [Shortcuts] widget.
  ///
  /// When overriding this method, the returned value can be any Object, but
  /// changing the return type of the override to match the type of the returned
  /// value provides more type safety.
  ///
  /// For instance, if your override of `invoke` returns an `int`, then define
  /// it like so:
  ///
  /// ```dart
  /// class IncrementIntent extends Intent {
  ///   const IncrementIntent({this.index});
  ///
  ///   final int index;
  /// }
  ///
  /// class MyIncrementAction extends ContextAction<IncrementIntent> {
  ///   @override
  ///   int invoke(IncrementIntent intent, [BuildContext context]) {
  ///     return intent.index + 1;
  ///   }
  /// }
  /// ```
  @protected
  @override
345
  Object? invoke(covariant T intent, [BuildContext? context]);
346 347 348
}

/// The signature of a callback accepted by [CallbackAction].
349
typedef OnInvokeCallback<T extends Intent> = Object? Function(T intent);
350 351

/// An [Action] that takes a callback in order to configure it without having to
352
/// create an explicit [Action] subclass just to call a callback.
353 354 355
///
/// See also:
///
356
///  * [Shortcuts], which is a widget that contains a key map, in which it looks
357
///    up key combinations in order to invoke actions.
358
///  * [Actions], which is a widget that defines a map of [Intent] to [Action]
359
///    and allows redefining of actions for its descendants.
360
///  * [ActionDispatcher], a class that takes an [Action] and invokes it using a
361
///    [FocusNode] for context.
362 363
class CallbackAction<T extends Intent> extends Action<T> {
  /// A constructor for a [CallbackAction].
364 365 366
  ///
  /// The `intentKey` and [onInvoke] parameters must not be null.
  /// The [onInvoke] parameter is required.
367
  CallbackAction({required this.onInvoke}) : assert(onInvoke != null);
368 369 370 371 372

  /// The callback to be called when invoked.
  ///
  /// Must not be null.
  @protected
373
  final OnInvokeCallback<T> onInvoke;
374 375

  @override
376
  Object? invoke(covariant T intent) => onInvoke(intent);
377 378
}

379 380 381 382 383 384 385 386
/// An action dispatcher that simply invokes the actions given to it.
///
/// See also:
///
///  - [ShortcutManager], that uses this class to invoke actions.
///  - [Shortcuts] widget, which defines key mappings to [Intent]s.
///  - [Actions] widget, which defines a mapping between a in [Intent] type and
///    an [Action].
387
class ActionDispatcher with Diagnosticable {
388
  /// Const constructor so that subclasses can be immutable.
389 390
  const ActionDispatcher();

391
  /// Invokes the given `action`, passing it the given `intent`.
392
  ///
393 394 395 396
  /// The action will be invoked with the given `context`, if given, but only if
  /// the action is a [ContextAction] subclass. If no `context` is given, and
  /// the action is a [ContextAction], then the context from the [primaryFocus]
  /// is used.
397
  ///
398 399 400 401 402 403 404 405 406 407
  /// Returns the object returned from [Action.invoke].
  ///
  /// The caller must receive a `true` result from [Action.isEnabled] before
  /// calling this function. This function will assert if the action is not
  /// enabled when called.
  Object? invokeAction(
    covariant Action<Intent> action,
    covariant Intent intent, [
    BuildContext? context,
  ]) {
408 409
    assert(action != null);
    assert(intent != null);
410 411 412 413 414 415
    assert(action.isEnabled(intent), 'Action must be enabled when calling invokeAction');
    if (action is ContextAction) {
      context ??= primaryFocus?.context;
      return action.invoke(intent, context);
    } else {
      return action.invoke(intent);
416 417 418 419 420 421 422 423 424 425 426 427
    }
  }
}

/// A widget that establishes an [ActionDispatcher] and a map of [Intent] to
/// [Action] to be used by its descendants when invoking an [Action].
///
/// Actions are typically invoked using [Actions.invoke] with the context
/// containing the ambient [Actions] widget.
///
/// See also:
///
428 429 430 431 432 433
///  * [ActionDispatcher], the object that this widget uses to manage actions.
///  * [Action], a class for containing and defining an invocation of a user
///    action.
///  * [Intent], a class that holds a unique [LocalKey] identifying an action,
///    as well as configuration information for running the [Action].
///  * [Shortcuts], a widget used to bind key combinations to [Intent]s.
434
class Actions extends StatefulWidget {
435 436 437 438
  /// Creates an [Actions] widget.
  ///
  /// The [child], [actions], and [dispatcher] arguments must not be null.
  const Actions({
439
    Key? key,
440
    this.dispatcher,
441 442
    required this.actions,
    required this.child,
443
  })  : assert(actions != null),
444 445
        assert(child != null),
        super(key: key);
446 447 448 449

  /// The [ActionDispatcher] object that invokes actions.
  ///
  /// This is what is returned from [Actions.of], and used by [Actions.invoke].
450 451 452 453 454
  ///
  /// If this [dispatcher] is null, then [Actions.of] and [Actions.invoke] will
  /// look up the tree until they find an Actions widget that has a dispatcher
  /// set. If not such widget is found, then they will return/use a
  /// default-constructed [ActionDispatcher].
455
  final ActionDispatcher? dispatcher;
456

457
  /// {@template flutter.widgets.actions.actions}
458 459
  /// A map of [Intent] keys to [Action<Intent>] objects that defines which
  /// actions this widget knows about.
460 461 462 463
  ///
  /// For performance reasons, it is recommended that a pre-built map is
  /// passed in here (e.g. a final variable from your widget class) instead of
  /// defining it inline in the build function.
464
  /// {@endtemplate}
465 466
  final Map<Type, Action<Intent>> actions;

467
  /// {@macro flutter.widgets.ProxyWidget.child}
468 469 470 471 472 473
  final Widget child;

  // Visits the Actions widget ancestors of the given element using
  // getElementForInheritedWidgetOfExactType. Returns true if the visitor found
  // what it was looking for.
  static bool _visitActionsAncestors(BuildContext context, bool visitor(InheritedElement element)) {
474
    InheritedElement? actionsElement = context.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
475 476 477 478 479 480 481 482
    while (actionsElement != null) {
      if (visitor(actionsElement) == true) {
        break;
      }
      // _getParent is needed here because
      // context.getElementForInheritedWidgetOfExactType will return itself if it
      // happens to be of the correct type.
      final BuildContext parent = _getParent(actionsElement);
483
      actionsElement = parent.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
484 485 486
    }
    return actionsElement != null;
  }
487

488 489
  // Finds the nearest valid ActionDispatcher, or creates a new one if it
  // doesn't find one.
490
  static ActionDispatcher _findDispatcher(BuildContext context) {
491
    ActionDispatcher? dispatcher;
492
    _visitActionsAncestors(context, (InheritedElement element) {
493
      final ActionDispatcher? found = (element.widget as _ActionsMarker).dispatcher;
494 495 496
      if (found != null) {
        dispatcher = found;
        return true;
497
      }
498 499 500 501
      return false;
    });
    return dispatcher ?? const ActionDispatcher();
  }
502

503 504
  /// Returns a [VoidCallback] handler that invokes the bound action for the
  /// given `intent` if the action is enabled, and returns null if the action is
505
  /// not enabled, or no matching action is found.
506 507 508 509 510 511 512 513
  ///
  /// This is intended to be used in widgets which have something similar to an
  /// `onTap` handler, which takes a `VoidCallback`, and can be set to the
  /// result of calling this function.
  ///
  /// Creates a dependency on the [Actions] widget that maps the bound action so
  /// that if the actions change, the context will be rebuilt and find the
  /// updated action.
514 515
  static VoidCallback? handler<T extends Intent>(BuildContext context, T intent) {
    final Action<T>? action = Actions.maybeFind<T>(context);
516
    if (action != null && action.isEnabled(intent)) {
517
      return () {
518 519 520 521 522
        // Could be that the action was enabled when the closure was created,
        // but is now no longer enabled, so check again.
        if (action.isEnabled(intent)) {
          Actions.of(context).invokeAction(action, intent, context);
        }
523
      };
524
    }
525 526 527 528 529 530 531 532
    return null;
  }

  /// Finds the [Action] bound to the given intent type `T` in the given `context`.
  ///
  /// Creates a dependency on the [Actions] widget that maps the bound action so
  /// that if the actions change, the context will be rebuilt and find the
  /// updated action.
533 534 535 536
  ///
  /// The optional `intent` argument supplies the type of the intent to look for
  /// if the concrete type of the intent sought isn't available. If not
  /// supplied, then `T` is used.
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
  ///
  /// If no [Actions] widget surrounds the given context, this function will
  /// assert in debug mode, and throw an exception in release mode.
  ///
  /// See also:
  ///
  ///  * [maybeFind], which is similar to this function, but will return null if
  ///    no [Actions] ancestor is found.
  static Action<T> find<T extends Intent>(BuildContext context, { T? intent }) {
    final Action<T>? action = maybeFind(context, intent: intent);

    assert(() {
      if (action == null) {
        final Type type = intent?.runtimeType ?? T;
        throw FlutterError('Unable to find an action for a $type in an $Actions widget '
            'in the given context.\n'
            "$Actions.find() was called on a context that doesn't contain an "
            '$Actions widget with a mapping for the given intent type.\n'
            'The context used was:\n'
            '  $context\n'
            'The intent type requested was:\n'
            '  $type');
      }
      return true;
    }());
    return action!;
  }

  /// Finds the [Action] bound to the given intent type `T` in the given `context`.
  ///
  /// Creates a dependency on the [Actions] widget that maps the bound action so
  /// that if the actions change, the context will be rebuilt and find the
  /// updated action.
  ///
  /// The optional `intent` argument supplies the type of the intent to look for
  /// if the concrete type of the intent sought isn't available. If not
  /// supplied, then `T` is used.
  ///
  /// If no [Actions] widget surrounds the given context, this function will
  /// return null.
  ///
  /// See also:
  ///
  ///  * [find], which is similar to this function, but will throw if
  ///    no [Actions] ancestor is found.
  static Action<T>? maybeFind<T extends Intent>(BuildContext context, { T? intent }) {
583
    Action<T>? action;
584

585 586 587 588 589 590 591 592 593 594
    // Specialize the type if a runtime example instance of the intent is given.
    // This allows this function to be called by code that doesn't know the
    // concrete type of the intent at compile time.
    final Type type = intent?.runtimeType ?? T;
    assert(type != Intent,
      'The type passed to "find" resolved to "Intent": either a non-Intent'
      'generic type argument or an example intent derived from Intent must be'
      'specified. Intent may be used as the generic type as long as the optional'
      '"intent" argument is passed.');

595 596
    _visitActionsAncestors(context, (InheritedElement element) {
      final _ActionsMarker actions = element.widget as _ActionsMarker;
597
      final Action<T>? result = actions.actions[type] as Action<T>?;
598 599 600 601 602 603 604 605 606
      if (result != null) {
        context.dependOnInheritedElement(element);
        action = result;
        return true;
      }
      return false;
    });

    return action;
607 608
  }

609 610 611
  /// Returns the [ActionDispatcher] associated with the [Actions] widget that
  /// most tightly encloses the given [BuildContext].
  ///
612 613 614
  /// Will return a newly created [ActionDispatcher] if no ambient [Actions]
  /// widget is found.
  static ActionDispatcher of(BuildContext context) {
615
    assert(context != null);
616
    final _ActionsMarker? marker = context.dependOnInheritedWidgetOfExactType<_ActionsMarker>();
617
    return marker?.dispatcher ?? _findDispatcher(context);
618 619
  }

Kate Lovett's avatar
Kate Lovett committed
620
  /// Invokes the action associated with the given [Intent] using the
621 622 623 624
  /// [Actions] widget that most tightly encloses the given [BuildContext].
  ///
  /// The `context`, `intent` and `nullOk` arguments must not be null.
  ///
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
  /// If the given `intent` doesn't map to an action, or doesn't map to one that
  /// returns true for [Action.isEnabled] in an [Actions.actions] map it finds,
  /// then it will look to the next ancestor [Actions] widget in the hierarchy
  /// until it reaches the root.
  ///
  /// In debug mode, if `nullOk` is false, this method will throw an exception
  /// if no ambient [Actions] widget is found, or if the given `intent` doesn't
  /// map to an action in any of the [Actions.actions] maps that are found. In
  /// release mode, this method will return null if no matching enabled action
  /// is found, regardless of the setting of `nullOk`.
  ///
  /// Setting `nullOk` to true indicates that if no ambient [Actions] widget is
  /// found, then in debug mode, this method should return null instead of
  /// throwing an exception.
  ///
  /// This method returns the result of invoking the action's [Action.invoke]
  /// method. If no action mapping was found for the specified intent (and
  /// `nullOk` is true), or if the actions that were found were disabled, or the
  /// action itself returns null from [Action.invoke], then this method returns
  /// null.
645
  static Object? invoke<T extends Intent>(
646
    BuildContext context,
647
    T intent, {
648 649 650
    bool nullOk = false,
  }) {
    assert(intent != null);
651 652
    assert(nullOk != null);
    assert(context != null);
653 654
    Action<T>? action;
    InheritedElement? actionElement;
655

656 657
    _visitActionsAncestors(context, (InheritedElement element) {
      final _ActionsMarker actions = element.widget as _ActionsMarker;
658
      final Action<T>? result = actions.actions[intent.runtimeType] as Action<T>?;
659 660
      if (result != null) {
        actionElement = element;
661 662 663 664
        if (result.isEnabled(intent)) {
          action = result;
          return true;
        }
665
      }
666 667
      return false;
    });
668 669

    assert(() {
670
      if (!nullOk && actionElement == null) {
671 672 673 674 675 676
        throw FlutterError('Unable to find an action for an Intent with type '
            '${intent.runtimeType} in an $Actions widget in the given context.\n'
            '$Actions.invoke() was unable to find an $Actions widget that '
            "contained a mapping for the given intent, or the intent type isn't the "
            'same as the type argument to invoke (which is $T - try supplying a '
            'type argument to invoke if one was not given)\n'
677 678
            'The context used was:\n'
            '  $context\n'
679 680
            'The intent type requested was:\n'
            '  ${intent.runtimeType}');
681 682 683
      }
      return true;
    }());
684 685 686
    if (actionElement == null || action == null) {
      return null;
    }
687 688 689
    // Invoke the action we found using the relevant dispatcher from the Actions
    // Element we found.
    return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
690 691 692
  }

  @override
693
  State<Actions> createState() => _ActionsState();
694 695 696 697 698

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher));
699 700 701 702 703
    properties.add(DiagnosticsProperty<Map<Type, Action<Intent>>>('actions', actions));
  }
}

class _ActionsState extends State<Actions> {
704
  // The set of actions that this Actions widget is current listening to.
705
  Set<Action<Intent>>? listenedActions = <Action<Intent>>{};
706 707
  // Used to tell the marker to rebuild its dependencies when the state of an
  // action in the map changes.
708 709 710 711 712 713 714 715 716
  Object rebuildKey = Object();

  @override
  void initState() {
    super.initState();
    _updateActionListeners();
  }

  void _handleActionChanged(Action<Intent> action) {
717 718 719 720
    // Generate a new key so that the marker notifies dependents.
    setState(() {
      rebuildKey = Object();
    });
721 722 723
  }

  void _updateActionListeners() {
724
    final Set<Action<Intent>> widgetActions = widget.actions.values.toSet();
725 726
    final Set<Action<Intent>> removedActions = listenedActions!.difference(widgetActions);
    final Set<Action<Intent>> addedActions = widgetActions.difference(listenedActions!);
727 728 729

    for (final Action<Intent> action in removedActions) {
      action.removeActionListener(_handleActionChanged);
730
    }
731 732
    for (final Action<Intent> action in addedActions) {
      action.addActionListener(_handleActionChanged);
733
    }
734
    listenedActions = widgetActions;
735 736 737 738 739 740 741 742 743 744 745
  }

  @override
  void didUpdateWidget(Actions oldWidget) {
    super.didUpdateWidget(oldWidget);
    _updateActionListeners();
  }

  @override
  void dispose() {
    super.dispose();
746
    for (final Action<Intent> action in listenedActions!) {
747 748
      action.removeActionListener(_handleActionChanged);
    }
749
    listenedActions = null;
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
  }

  @override
  Widget build(BuildContext context) {
    return _ActionsMarker(
      actions: widget.actions,
      dispatcher: widget.dispatcher,
      rebuildKey: rebuildKey,
      child: widget.child,
    );
  }
}

// An inherited widget used by Actions widget for fast lookup of the Actions
// widget information.
class _ActionsMarker extends InheritedWidget {
  const _ActionsMarker({
767 768 769 770 771
    required this.dispatcher,
    required this.actions,
    required this.rebuildKey,
    Key? key,
    required Widget child,
772 773 774 775
  })  : assert(child != null),
        assert(actions != null),
        super(key: key, child: child);

776
  final ActionDispatcher? dispatcher;
777 778 779 780 781 782 783 784
  final Map<Type, Action<Intent>> actions;
  final Object rebuildKey;

  @override
  bool updateShouldNotify(_ActionsMarker oldWidget) {
    return rebuildKey != oldWidget.rebuildKey
        || oldWidget.dispatcher != dispatcher
        || !mapEquals<Type, Action<Intent>>(oldWidget.actions, actions);
785 786
  }
}
787

788 789 790 791 792 793 794 795 796 797
/// A widget that combines the functionality of [Actions], [Shortcuts],
/// [MouseRegion] and a [Focus] widget to create a detector that defines actions
/// and key bindings, and provides callbacks for handling focus and hover
/// highlights.
///
/// This widget can be used to give a control the required detection modes for
/// focus and hover handling. It is most often used when authoring a new control
/// widget, and the new control should be enabled for keyboard traversal and
/// activation.
///
798
/// {@tool dartpad --template=stateful_widget_material}
799 800
/// This example shows how keyboard interaction can be added to a custom control
/// that changes color when hovered and focused, and can toggle a light when
801 802 803
/// activated, either by touch or by hitting the `X` key on the keyboard when
/// the "And Me" button has the keyboard focus (be sure to use TAB to move the
/// focus to the "And Me" button before trying it out).
804 805 806 807
///
/// This example defines its own key binding for the `X` key, but in this case,
/// there is also a default key binding for [ActivateAction] in the default key
/// bindings created by [WidgetsApp] (the parent for [MaterialApp], and
808
/// [CupertinoApp]), so the `ENTER` key will also activate the buttons.
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```
///
/// ```dart preamble
/// class FadButton extends StatefulWidget {
///   const FadButton({Key key, this.onPressed, this.child}) : super(key: key);
///
///   final VoidCallback onPressed;
///   final Widget child;
///
///   @override
///   _FadButtonState createState() => _FadButtonState();
/// }
///
/// class _FadButtonState extends State<FadButton> {
///   bool _focused = false;
///   bool _hovering = false;
///   bool _on = false;
829
///   Map<Type, Action<Intent>> _actionMap;
830 831 832 833 834
///   Map<LogicalKeySet, Intent> _shortcutMap;
///
///   @override
///   void initState() {
///     super.initState();
835 836 837 838
///     _actionMap = <Type, Action<Intent>>{
///       ActivateIntent: CallbackAction(
///         onInvoke: (Intent intent) => _toggleState(),
///       ),
839 840
///     };
///     _shortcutMap = <LogicalKeySet, Intent>{
841
///       LogicalKeySet(LogicalKeyboardKey.keyX): const ActivateIntent(),
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915
///     };
///   }
///
///   Color get color {
///     Color baseColor = Colors.lightBlue;
///     if (_focused) {
///       baseColor = Color.alphaBlend(Colors.black.withOpacity(0.25), baseColor);
///     }
///     if (_hovering) {
///       baseColor = Color.alphaBlend(Colors.black.withOpacity(0.1), baseColor);
///     }
///     return baseColor;
///   }
///
///   void _toggleState() {
///     setState(() {
///       _on = !_on;
///     });
///   }
///
///   void _handleFocusHighlight(bool value) {
///     setState(() {
///       _focused = value;
///     });
///   }
///
///   void _handleHoveHighlight(bool value) {
///     setState(() {
///       _hovering = value;
///     });
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return GestureDetector(
///       onTap: _toggleState,
///       child: FocusableActionDetector(
///         actions: _actionMap,
///         shortcuts: _shortcutMap,
///         onShowFocusHighlight: _handleFocusHighlight,
///         onShowHoverHighlight: _handleHoveHighlight,
///         child: Row(
///           children: <Widget>[
///             Container(
///               padding: EdgeInsets.all(10.0),
///               color: color,
///               child: widget.child,
///             ),
///             Container(
///               width: 30,
///               height: 30,
///               margin: EdgeInsets.all(10.0),
///               color: _on ? Colors.red : Colors.transparent,
///             ),
///           ],
///         ),
///       ),
///     );
///   }
/// }
/// ```
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Scaffold(
///     appBar: AppBar(
///       title: Text('FocusableActionDetector Example'),
///     ),
///     body: Center(
///       child: Row(
///         mainAxisAlignment: MainAxisAlignment.center,
///         children: <Widget>[
///           Padding(
///             padding: const EdgeInsets.all(8.0),
916
///             child: TextButton(onPressed: () {}, child: Text('Press Me')),
917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936
///           ),
///           Padding(
///             padding: const EdgeInsets.all(8.0),
///             child: FadButton(onPressed: () {}, child: Text('And Me')),
///           ),
///         ],
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
/// This widget doesn't have any visual representation, it is just a detector that
/// provides focus and hover capabilities.
///
/// It hosts its own [FocusNode] or uses [focusNode], if given.
class FocusableActionDetector extends StatefulWidget {
  /// Create a const [FocusableActionDetector].
  ///
937
  /// The [enabled], [autofocus], [mouseCursor], and [child] arguments must not be null.
938
  const FocusableActionDetector({
939
    Key? key,
940 941 942 943 944 945 946 947
    this.enabled = true,
    this.focusNode,
    this.autofocus = false,
    this.shortcuts,
    this.actions,
    this.onShowFocusHighlight,
    this.onShowHoverHighlight,
    this.onFocusChange,
948
    this.mouseCursor = MouseCursor.defer,
949
    required this.child,
950 951
  })  : assert(enabled != null),
        assert(autofocus != null),
952
        assert(mouseCursor != null),
953 954 955 956 957 958 959 960 961 962 963 964 965
        assert(child != null),
        super(key: key);

  /// Is this widget enabled or not.
  ///
  /// If disabled, will not send any notifications needed to update highlight or
  /// focus state, and will not define or respond to any actions or shortcuts.
  ///
  /// When disabled, adds [Focus] to the widget tree, but sets
  /// [Focus.canRequestFocus] to false.
  final bool enabled;

  /// {@macro flutter.widgets.Focus.focusNode}
966
  final FocusNode? focusNode;
967 968 969 970 971

  /// {@macro flutter.widgets.Focus.autofocus}
  final bool autofocus;

  /// {@macro flutter.widgets.actions.actions}
972
  final Map<Type, Action<Intent>>? actions;
973 974

  /// {@macro flutter.widgets.shortcuts.shortcuts}
975
  final Map<LogicalKeySet, Intent>? shortcuts;
976

977 978 979 980
  /// A function that will be called when the focus highlight should be shown or
  /// hidden.
  ///
  /// This method is not triggered at the unmount of the widget.
981
  final ValueChanged<bool>? onShowFocusHighlight;
982 983

  /// A function that will be called when the hover highlight should be shown or hidden.
984 985
  ///
  /// This method is not triggered at the unmount of the widget.
986
  final ValueChanged<bool>? onShowHoverHighlight;
987 988 989 990

  /// A function that will be called when the focus changes.
  ///
  /// Called with true if the [focusNode] has primary focus.
991
  final ValueChanged<bool>? onFocusChange;
992

993 994 995
  /// The cursor for a mouse pointer when it enters or is hovering over the
  /// widget.
  ///
996
  /// The [mouseCursor] defaults to [MouseCursor.defer], deferring the choice of
997
  /// cursor to the next region behind it in hit-test order.
998 999
  final MouseCursor mouseCursor;

1000 1001
  /// The child widget for this [FocusableActionDetector] widget.
  ///
1002
  /// {@macro flutter.widgets.ProxyWidget.child}
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
  final Widget child;

  @override
  _FocusableActionDetectorState createState() => _FocusableActionDetectorState();
}

class _FocusableActionDetectorState extends State<FocusableActionDetector> {
  @override
  void initState() {
    super.initState();
1013
    SchedulerBinding.instance!.addPostFrameCallback((Duration duration) {
1014 1015
      _updateHighlightMode(FocusManager.instance.highlightMode);
    });
1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026
    FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
  }

  @override
  void dispose() {
    FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange);
    super.dispose();
  }

  bool _canShowHighlight = false;
  void _updateHighlightMode(FocusHighlightMode mode) {
1027 1028 1029 1030 1031 1032 1033 1034 1035 1036
    _mayTriggerCallback(task: () {
      switch (FocusManager.instance.highlightMode) {
        case FocusHighlightMode.touch:
          _canShowHighlight = false;
          break;
        case FocusHighlightMode.traditional:
          _canShowHighlight = true;
          break;
      }
    });
1037 1038
  }

1039 1040 1041 1042
  // Have to have this separate from the _updateHighlightMode because it gets
  // called in initState, where things aren't mounted yet.
  // Since this method is a highlight mode listener, it is only called
  // immediately following pointer events.
1043 1044 1045 1046 1047 1048 1049 1050 1051 1052
  void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
    if (!mounted) {
      return;
    }
    _updateHighlightMode(mode);
  }

  bool _hovering = false;
  void _handleMouseEnter(PointerEnterEvent event) {
    if (!_hovering) {
1053 1054 1055
      _mayTriggerCallback(task: () {
        _hovering = true;
      });
1056 1057 1058 1059 1060
    }
  }

  void _handleMouseExit(PointerExitEvent event) {
    if (_hovering) {
1061 1062 1063
      _mayTriggerCallback(task: () {
        _hovering = false;
      });
1064 1065 1066 1067 1068 1069
    }
  }

  bool _focused = false;
  void _handleFocusChange(bool focused) {
    if (_focused != focused) {
1070
      _mayTriggerCallback(task: () {
1071 1072
        _focused = focused;
      });
1073
      widget.onFocusChange?.call(_focused);
1074 1075 1076
    }
  }

1077 1078 1079 1080 1081 1082
  // Record old states, do `task` if not null, then compare old states with the
  // new states, and trigger callbacks if necessary.
  //
  // The old states are collected from `oldWidget` if it is provided, or the
  // current widget (before doing `task`) otherwise. The new states are always
  // collected from the current widget.
1083
  void _mayTriggerCallback({VoidCallback? task, FocusableActionDetector? oldWidget}) {
1084 1085 1086
    bool shouldShowHoverHighlight(FocusableActionDetector target) {
      return _hovering && target.enabled && _canShowHighlight;
    }
1087

1088
    bool canRequestFocus(FocusableActionDetector target) {
1089
      final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
1090 1091 1092 1093 1094 1095 1096 1097
      switch (mode) {
        case NavigationMode.traditional:
          return target.enabled;
        case NavigationMode.directional:
          return true;
      }
    }

1098
    bool shouldShowFocusHighlight(FocusableActionDetector target) {
1099
      return _focused && _canShowHighlight && canRequestFocus(target);
1100 1101
    }

1102
    assert(SchedulerBinding.instance!.schedulerPhase != SchedulerPhase.persistentCallbacks);
1103 1104 1105
    final FocusableActionDetector oldTarget = oldWidget ?? widget;
    final bool didShowHoverHighlight = shouldShowHoverHighlight(oldTarget);
    final bool didShowFocusHighlight = shouldShowFocusHighlight(oldTarget);
1106
    if (task != null) {
1107
      task();
1108
    }
1109 1110
    final bool doShowHoverHighlight = shouldShowHoverHighlight(widget);
    final bool doShowFocusHighlight = shouldShowFocusHighlight(widget);
1111
    if (didShowFocusHighlight != doShowFocusHighlight) {
1112
      widget.onShowFocusHighlight?.call(doShowFocusHighlight);
1113 1114
    }
    if (didShowHoverHighlight != doShowHoverHighlight) {
1115
      widget.onShowHoverHighlight?.call(doShowHoverHighlight);
1116
    }
1117 1118
  }

1119 1120 1121 1122
  @override
  void didUpdateWidget(FocusableActionDetector oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.enabled != oldWidget.enabled) {
1123
      SchedulerBinding.instance!.addPostFrameCallback((Duration duration) {
1124 1125 1126
        _mayTriggerCallback(oldWidget: oldWidget);
      });
    }
1127 1128
  }

1129
  bool get _canRequestFocus {
1130
    final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
1131 1132 1133 1134 1135 1136 1137 1138
    switch (mode) {
      case NavigationMode.traditional:
        return widget.enabled;
      case NavigationMode.directional:
        return true;
    }
  }

1139 1140 1141 1142 1143 1144 1145
  // This global key is needed to keep only the necessary widgets in the tree
  // while maintaining the subtree's state.
  //
  // See https://github.com/flutter/flutter/issues/64058 for an explanation of
  // why using a global key over keeping the shape of the tree.
  final GlobalKey _mouseRegionKey = GlobalKey();

1146 1147
  @override
  Widget build(BuildContext context) {
1148
    Widget child = MouseRegion(
1149
      key: _mouseRegionKey,
1150 1151 1152 1153 1154 1155 1156 1157 1158
      onEnter: _handleMouseEnter,
      onExit: _handleMouseExit,
      cursor: widget.mouseCursor,
      child: Focus(
        focusNode: widget.focusNode,
        autofocus: widget.autofocus,
        canRequestFocus: _canRequestFocus,
        onFocusChange: _handleFocusChange,
        child: widget.child,
1159 1160
      ),
    );
1161 1162
    if (widget.enabled && widget.actions != null && widget.actions!.isNotEmpty) {
      child = Actions(actions: widget.actions!, child: child);
1163
    }
1164 1165
    if (widget.enabled && widget.shortcuts != null && widget.shortcuts!.isNotEmpty) {
      child = Shortcuts(shortcuts: widget.shortcuts!, child: child);
1166 1167
    }
    return child;
1168 1169 1170
  }
}

1171
/// An [Intent], that is bound to a [DoNothingAction].
1172 1173
///
/// Attaching a [DoNothingIntent] to a [Shortcuts] mapping is one way to disable
1174 1175
/// a keyboard shortcut defined by a widget higher in the widget hierarchy and
/// consume any key event that triggers it via a shortcut.
1176
///
1177
/// This intent cannot be subclassed.
1178 1179 1180 1181 1182 1183
///
/// See also:
///
///  * [DoNothingAndStopPropagationIntent], a similar intent that will not
///    handle the key event, but will still keep it from being passed to other key
///    handlers in the focus chain.
1184 1185 1186
class DoNothingIntent extends Intent {
  /// Creates a const [DoNothingIntent].
  factory DoNothingIntent() => const DoNothingIntent._();
1187

1188 1189 1190
  // Make DoNothingIntent constructor private so it can't be subclassed.
  const DoNothingIntent._();
}
1191

1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217
/// 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.
///
/// Attaching a [DoNothingAndStopPropagationIntent] to a [Shortcuts.shortcuts]
/// mapping is one way to disable a keyboard shortcut defined by a widget higher
/// in the widget hierarchy. In addition, the bound [DoNothingAction] will
/// return false from [DoNothingAction.consumesKey], causing the key bound to
/// this intent to be passed on to the platform embedding as "not handled" with
/// out passing it to other key handlers in the focus chain (e.g. parent
/// `Shortcuts` widgets higher up in the chain).
///
/// This intent cannot be subclassed.
///
/// See also:
///
///  * [DoNothingIntent], a similar intent that will handle the key event.
class DoNothingAndStopPropagationIntent extends Intent {
  /// Creates a const [DoNothingAndStopPropagationIntent].
  factory DoNothingAndStopPropagationIntent() => const DoNothingAndStopPropagationIntent._();

  // Make DoNothingAndStopPropagationIntent constructor private so it can't be subclassed.
  const DoNothingAndStopPropagationIntent._();
}

/// An [Action], that doesn't perform any action when invoked.
1218
///
1219 1220
/// Attaching a [DoNothingAction] to an [Actions.actions] mapping is a way to
/// disable an action defined by a widget higher in the widget hierarchy.
1221
///
1222 1223 1224 1225 1226 1227 1228 1229
/// If [consumesKey] returns false, then not only will this action do nothing,
/// but it will stop the propagation of the key event used to trigger it to
/// other widgets in the focus chain and tell the embedding that the key wasn't
/// handled, allowing text input fields or other non-Flutter elements to receive
/// that key event. The return value of [consumesKey] can be set via the
/// `consumesKey` argument to the constructor.
///
/// This action can be bound to any [Intent].
1230 1231
///
/// See also:
1232
///  - [DoNothingIntent], which is an intent that can be bound to a [KeySet] in
1233
///    a [Shortcuts] widget to do nothing.
1234 1235 1236
///  - [DoNothingAndStopPropagationIntent], which is an intent that can be bound
///    to a [KeySet] in a [Shortcuts] widget to do nothing and also stop key event
///    propagation to other key handlers in the focus chain.
1237
class DoNothingAction extends Action<Intent> {
1238 1239 1240 1241 1242 1243 1244 1245 1246
  /// Creates a [DoNothingAction].
  ///
  /// The optional [consumesKey] argument defaults to true.
  DoNothingAction({bool consumesKey = true}) : _consumesKey = consumesKey;

  @override
  bool consumesKey(Intent intent) => _consumesKey;
  final bool _consumesKey;

1247
  @override
1248 1249 1250 1251 1252 1253 1254
  void invoke(Intent intent) {}
}

/// An intent that activates the currently focused control.
class ActivateIntent extends Intent {
  /// Creates a const [ActivateIntent] so subclasses can be const.
  const ActivateIntent();
1255
}
1256

1257
/// An action that activates the currently focused control.
1258 1259
///
/// This is an abstract class that serves as a base class for actions that
1260 1261 1262 1263 1264 1265 1266
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter],
/// [LogicalKeyboardKey.gameButtonA], and [LogicalKeyboardKey.space] in the
/// default keyboard map in [WidgetsApp].
abstract class ActivateAction extends Action<ActivateIntent> {}

/// An intent that selects the currently focused control.
class SelectIntent extends Intent {}
1267 1268 1269 1270

/// An action that selects the currently focused control.
///
/// This is an abstract class that serves as a base class for actions that
1271
/// select something. It is not bound to any key by default.
1272
abstract class SelectAction extends Action<SelectIntent> {}
1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290

/// An [Intent] that dismisses the currently focused widget.
///
/// The [WidgetsApp.defaultShortcuts] binds this intent to the
/// [LogicalKeyboardKey.escape] and [LogicalKeyboardKey.gameButtonB] keys.
///
/// See also:
///  - [ModalRoute] which listens for this intent to dismiss modal routes
///    (dialogs, pop-up menus, drawers, etc).
class DismissIntent extends Intent {
  /// Creates a const [DismissIntent].
  const DismissIntent();
}

/// 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<DismissIntent> {}