actions.dart 66.6 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/gestures.dart';
7
import 'package:flutter/rendering.dart';
8
import 'package:flutter/scheduler.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 46
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
47
  const Intent();
48

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

58 59 60 61 62 63
/// 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);

64 65 66 67 68 69 70 71 72 73 74
/// 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.
///
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
/// ### Action Overriding
///
/// When using a leaf widget to build a more specialized widget, it's sometimes
/// desirable to change the default handling of an [Intent] defined in the leaf
/// widget. For instance, [TextField]'s [SelectAllTextIntent] by default selects
/// the text it currently contains, but in a US phone number widget that
/// consists of 3 different [TextField]s (area code, prefix and line number),
/// [SelectAllTextIntent] should instead select the text within all 3
/// [TextField]s.
///
/// An overridable [Action] is a special kind of [Action] created using the
/// [Action.overridable] constructor. It has access to a default [Action], and a
/// nullable override [Action]. It has the same behavior as its override if that
/// exists, and mirrors the behavior of its `defaultAction` otherwise.
///
/// The [Action.overridable] constructor creates overridable [Action]s that use
/// a [BuildContext] to find a suitable override in its ancestor [Actions]
/// widget. This can be used to provide a default implementation when creating a
/// general purpose leaf widget, and later override it when building a more
/// specialized widget using that leaf widget. Using the [TextField] example
/// above, the [TextField] widget uses an overridable [Action] to provide a
/// sensible default for [SelectAllTextIntent], while still allowing app
/// developers to change that if they add an ancestor [Actions] widget that maps
/// [SelectAllTextIntent] to a different [Action].
///
100 101 102 103
/// See the article on [Using Actions and
/// Shortcuts](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts)
/// for a detailed explanation.
///
104 105
/// See also:
///
106
///  * [Shortcuts], which is a widget that contains a key map, in which it looks
107
///    up key combinations in order to invoke actions.
108
///  * [Actions], which is a widget that defines a map of [Intent] to [Action]
109
///    and allows redefining of actions for its descendants.
110 111
///  * [ActionDispatcher], a class that takes an [Action] and invokes it, passing
///    a given [Intent].
112 113
///  * [Action.overridable] for an example on how to make an [Action]
///    overridable.
114
abstract class Action<T extends Intent> with Diagnosticable {
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
  /// Creates an [Action].
  Action();

  /// Creates an [Action] that allows itself to be overridden by the closest
  /// ancestor [Action] in the given [context] that handles the same [Intent],
  /// if one exists.
  ///
  /// When invoked, the resulting [Action] tries to find the closest [Action] in
  /// the given `context` that handles the same type of [Intent] as the
  /// `defaultAction`, then calls its [Action.invoke] method. When no override
  /// [Action]s can be found, it invokes the `defaultAction`.
  ///
  /// An overridable action delegates everything to its override if one exists,
  /// and has the same behavior as its `defaultAction` otherwise. For this
  /// reason, the override has full control over whether and how an [Intent]
  /// should be handled, or a key event should be consumed. An override
  /// [Action]'s [callingAction] property will be set to the [Action] it
  /// currently overrides, giving it access to the default behavior. See the
  /// [callingAction] property for an example.
  ///
  /// The `context` argument is the [BuildContext] to find the override with. It
  /// is typically a [BuildContext] above the [Actions] widget that contains
  /// this overridable [Action].
  ///
  /// The `defaultAction` argument is the [Action] to be invoked where there's
  /// no ancestor [Action]s can't be found in `context` that handle the same
  /// type of [Intent].
  ///
  /// This is useful for providing a set of default [Action]s in a leaf widget
  /// to allow further overriding, or to allow the [Intent] to propagate to
  /// parent widgets that also support this [Intent].
  ///
147
  /// {@tool dartpad}
148 149 150 151 152 153 154 155 156 157
  /// This sample shows how to implement a rudimentary `CopyableText` widget
  /// that responds to Ctrl-C by copying its own content to the clipboard.
  ///
  /// if `CopyableText` is to be provided in a package, developers using the
  /// widget may want to change how copying is handled. As the author of the
  /// package, you can enable that by making the corresponding [Action]
  /// overridable. In the second part of the code sample, three `CopyableText`
  /// widgets are used to build a verification code widget which overrides the
  /// "copy" action by copying the combined numbers from all three `CopyableText`
  /// widgets.
158
  ///
159
  /// ** See code in examples/api/lib/widgets/actions/action.action_overridable.0.dart **
160 161 162 163 164 165 166 167
  /// {@end-tool}
  factory Action.overridable({
    required Action<T> defaultAction,
    required BuildContext context,
  }) {
    return defaultAction._makeOverridableAction(context);
  }

168 169
  final ObserverList<ActionListenerCallback> _listeners = ObserverList<ActionListenerCallback>();

170
  Action<T>? _currentCallingAction;
171
  // ignore: use_setters_to_change_properties, (code predates enabling of this lint)
172 173
  void _updateCallingAction(Action<T>? value) {
    _currentCallingAction = value;
174
  }
175

176 177 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 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
  /// The [Action] overridden by this [Action].
  ///
  /// The [Action.overridable] constructor creates an overridable [Action] that
  /// allows itself to be overridden by the closest ancestor [Action], and falls
  /// back to its own `defaultAction` when no overrides can be found. When an
  /// override is present, an overridable [Action] forwards all incoming
  /// method calls to the override, and allows the override to access the
  /// `defaultAction` via its [callingAction] property.
  ///
  /// Before forwarding the call to the override, the overridable [Action] is
  /// responsible for setting [callingAction] to its `defaultAction`, which is
  /// already taken care of by the overridable [Action] created using
  /// [Action.overridable].
  ///
  /// This property is only non-null when this [Action] is an override of the
  /// [callingAction], and is currently being invoked from [callingAction].
  ///
  /// Invoking [callingAction]'s methods, or accessing its properties, is
  /// allowed and does not introduce infinite loops or infinite recursions.
  ///
  /// {@tool snippet}
  /// An example `Action` that handles [PasteTextIntent] but has mostly the same
  /// behavior as the overridable action. It's OK to call
  /// `callingAction?.isActionEnabled` in the implementation of this `Action`.
  ///
  /// ```dart
  /// class MyPasteAction extends Action<PasteTextIntent> {
  ///   @override
  ///   Object? invoke(PasteTextIntent intent) {
  ///     print(intent);
  ///     return callingAction?.invoke(intent);
  ///   }
  ///
  ///   @override
  ///   bool get isActionEnabled => callingAction?.isActionEnabled ?? false;
  ///
  ///   @override
  ///   bool consumesKey(PasteTextIntent intent) => callingAction?.consumesKey(intent) ?? false;
  /// }
  /// ```
  /// {@end-tool}
  @protected
  Action<T>? get callingAction => _currentCallingAction;

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

223
  /// Returns true if the action is enabled and is ready to be invoked.
224
  ///
225 226
  /// This will be called by the [ActionDispatcher] before attempting to invoke
  /// the action.
227 228 229
  bool isEnabled(T intent) => isActionEnabled;

  /// Whether this [Action] is inherently enabled.
230
  ///
231 232 233
  /// If [isActionEnabled] is false, then this [Action] is disabled for any
  /// given [Intent].
  //
234 235
  /// If the enabled state changes, overriding subclasses must call
  /// [notifyActionListeners] to notify any listeners of the change.
236 237 238 239
  ///
  /// In the case of an overridable `Action`, accessing this property creates
  /// an dependency on the overridable `Action`s `lookupContext`.
  bool get isActionEnabled => true;
240

241 242 243 244 245 246 247 248 249 250 251
  /// 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.
252
  bool consumesKey(T intent) => true;
253

254 255
  /// Called when the action is to be performed.
  ///
256 257 258
  /// This is called by the [ActionDispatcher] when an action is invoked via
  /// [Actions.invoke], or when an action is invoked using
  /// [ActionDispatcher.invokeAction] directly.
259 260
  ///
  /// This method is only meant to be invoked by an [ActionDispatcher], or by
261
  /// its subclasses, and only when [isEnabled] is true.
262 263 264 265 266
  ///
  /// 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.
  ///
267
  /// For instance, if an override of [invoke] returned an `int`, then it might
268
  /// be defined like so:
269
  ///
270 271
  /// ```dart
  /// class IncrementIntent extends Intent {
272
  ///   const IncrementIntent({required this.index});
273 274 275 276 277 278 279 280 281 282 283
  ///
  ///   final int index;
  /// }
  ///
  /// class MyIncrementAction extends Action<IncrementIntent> {
  ///   @override
  ///   int invoke(IncrementIntent intent) {
  ///     return intent.index + 1;
  ///   }
  /// }
  /// ```
284 285 286 287
  ///
  /// 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.
288
  @protected
289
  Object? invoke(T intent);
290 291 292 293 294 295 296 297

  /// 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.
  ///
298
  /// {@template flutter.widgets.Action.addActionListener}
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
  /// 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.
  ///
325
  /// {@macro flutter.widgets.Action.addActionListener}
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
  @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
344
  @pragma('vm:notify-debugger-on-exception')
345 346 347 348 349 350 351
  void notifyActionListeners() {
    if (_listeners.isEmpty) {
      return;
    }

    // Make a local copy so that a listener can unregister while the list is
    // being iterated over.
352
    final List<ActionListenerCallback> localListeners = List<ActionListenerCallback>.of(_listeners);
353
    for (final ActionListenerCallback listener in localListeners) {
354
      InformationCollector? collector;
355
      assert(() {
356 357
        collector = () => <DiagnosticsNode>[
          DiagnosticsProperty<Action<T>>(
358 359 360
            'The $runtimeType sending notification was',
            this,
            style: DiagnosticsTreeStyle.errorProperty,
361 362
          ),
        ];
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
        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,
        ));
      }
    }
  }
380 381 382 383

  Action<T> _makeOverridableAction(BuildContext context) {
    return _OverridableAction<T>(defaultAction: this, lookupContext: context);
  }
384 385 386 387 388 389 390 391 392 393 394 395 396
}

/// 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.
397
///
398
/// {@tool dartpad}
399 400 401
/// This example shows how ActionListener handles adding and removing of
/// the [listener] in the widget lifecycle.
///
402
/// ** See code in examples/api/lib/widgets/actions/action_listener.0.dart **
403 404
/// {@end-tool}
///
405 406 407 408 409 410
@immutable
class ActionListener extends StatefulWidget {
  /// Create a const [ActionListener].
  ///
  /// The [listener], [action], and [child] arguments must not be null.
  const ActionListener({
411
    super.key,
412 413 414
    required this.listener,
    required this.action,
    required this.child,
415 416
  })  : assert(listener != null),
        assert(action != null),
417
        assert(child != null);
418 419 420 421 422 423 424 425 426 427 428

  /// 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;

429
  /// {@macro flutter.widgets.ProxyWidget.child}
430
  final Widget child;
431 432

  @override
433
  State<ActionListener> createState() => _ActionListenerState();
434 435 436 437 438 439 440
}

class _ActionListenerState extends State<ActionListener> {
  @override
  void initState() {
    super.initState();
    widget.action.addActionListener(widget.listener);
441
  }
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475

  @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
476
  /// its subclasses, and only when [isEnabled] is true.
477 478
  ///
  /// The optional `context` parameter is the context of the invocation of the
479
  /// action, and in the case of an action invoked by a [ShortcutManager], via
480 481 482 483 484 485
  /// 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.
  ///
486
  /// For instance, if an override of [invoke] returned an `int`, then it might
487
  /// be defined like so:
488 489 490
  ///
  /// ```dart
  /// class IncrementIntent extends Intent {
491
  ///   const IncrementIntent({required this.index});
492 493 494 495 496 497
  ///
  ///   final int index;
  /// }
  ///
  /// class MyIncrementAction extends ContextAction<IncrementIntent> {
  ///   @override
498
  ///   int invoke(IncrementIntent intent, [BuildContext? context]) {
499 500 501 502 503 504
  ///     return intent.index + 1;
  ///   }
  /// }
  /// ```
  @protected
  @override
505 506 507 508 509 510
  Object? invoke(T intent, [BuildContext? context]);

  @override
  ContextAction<T> _makeOverridableAction(BuildContext context) {
    return _OverridableContextAction<T>(defaultAction: this, lookupContext: context);
  }
511 512 513
}

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

/// An [Action] that takes a callback in order to configure it without having to
517
/// create an explicit [Action] subclass just to call a callback.
518 519 520
///
/// See also:
///
521
///  * [Shortcuts], which is a widget that contains a key map, in which it looks
522
///    up key combinations in order to invoke actions.
523
///  * [Actions], which is a widget that defines a map of [Intent] to [Action]
524
///    and allows redefining of actions for its descendants.
525
///  * [ActionDispatcher], a class that takes an [Action] and invokes it using a
526
///    [FocusNode] for context.
527 528
class CallbackAction<T extends Intent> extends Action<T> {
  /// A constructor for a [CallbackAction].
529 530 531
  ///
  /// The `intentKey` and [onInvoke] parameters must not be null.
  /// The [onInvoke] parameter is required.
532
  CallbackAction({required this.onInvoke}) : assert(onInvoke != null);
533 534 535 536 537

  /// The callback to be called when invoked.
  ///
  /// Must not be null.
  @protected
538
  final OnInvokeCallback<T> onInvoke;
539 540

  @override
541
  Object? invoke(T intent) => onInvoke(intent);
542 543
}

544 545 546 547 548 549 550
/// An action dispatcher that invokes the actions given to it.
///
/// The [invokeAction] method on this class directly calls the [Action.invoke]
/// method on the [Action] object.
///
/// For [ContextAction] actions, if no `context` is provided, the
/// [BuildContext] of the [primaryFocus] is used instead.
551 552 553 554 555 556 557
///
/// 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].
558
class ActionDispatcher with Diagnosticable {
559
  /// Creates an action dispatcher that invokes actions directly.
560 561
  const ActionDispatcher();

562
  /// Invokes the given `action`, passing it the given `intent`.
563
  ///
564 565 566 567
  /// 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.
568
  ///
569 570 571 572 573 574 575 576 577 578
  /// 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,
  ]) {
579 580
    assert(action != null);
    assert(intent != null);
581 582 583 584 585 586
    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);
587 588 589 590 591 592 593
    }
  }
}

/// A widget that establishes an [ActionDispatcher] and a map of [Intent] to
/// [Action] to be used by its descendants when invoking an [Action].
///
594 595
/// {@youtube 560 315 https://www.youtube.com/watch?v=XawP1i314WM}
///
596 597 598
/// Actions are typically invoked using [Actions.invoke] with the context
/// containing the ambient [Actions] widget.
///
599
/// {@tool dartpad}
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
/// 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.
///
615
/// ** See code in examples/api/lib/widgets/actions/actions.0.dart **
616 617
/// {@end-tool}
///
618 619
/// See also:
///
620 621 622 623 624 625
///  * [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.
626
class Actions extends StatefulWidget {
627 628 629 630
  /// Creates an [Actions] widget.
  ///
  /// The [child], [actions], and [dispatcher] arguments must not be null.
  const Actions({
631
    super.key,
632
    this.dispatcher,
633 634
    required this.actions,
    required this.child,
635
  })  : assert(actions != null),
636
        assert(child != null);
637 638 639 640

  /// The [ActionDispatcher] object that invokes actions.
  ///
  /// This is what is returned from [Actions.of], and used by [Actions.invoke].
641 642 643 644 645
  ///
  /// 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].
646
  final ActionDispatcher? dispatcher;
647

648
  /// {@template flutter.widgets.actions.actions}
649 650
  /// A map of [Intent] keys to [Action<Intent>] objects that defines which
  /// actions this widget knows about.
651 652 653 654
  ///
  /// 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.
655
  /// {@endtemplate}
656 657
  final Map<Type, Action<Intent>> actions;

658
  /// {@macro flutter.widgets.ProxyWidget.child}
659 660 661 662 663
  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.
664
  static bool _visitActionsAncestors(BuildContext context, bool Function(InheritedElement element) visitor) {
665
    InheritedElement? actionsElement = context.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
666 667 668 669 670 671 672 673
    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);
674
      actionsElement = parent.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
675 676 677
    }
    return actionsElement != null;
  }
678

679 680
  // Finds the nearest valid ActionDispatcher, or creates a new one if it
  // doesn't find one.
681
  static ActionDispatcher _findDispatcher(BuildContext context) {
682
    ActionDispatcher? dispatcher;
683
    _visitActionsAncestors(context, (InheritedElement element) {
684
      final ActionDispatcher? found = (element.widget as _ActionsMarker).dispatcher;
685 686 687
      if (found != null) {
        dispatcher = found;
        return true;
688
      }
689 690 691 692
      return false;
    });
    return dispatcher ?? const ActionDispatcher();
  }
693

694 695
  /// 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
696
  /// not enabled, or no matching action is found.
697 698 699 700 701 702 703 704
  ///
  /// 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.
705 706
  static VoidCallback? handler<T extends Intent>(BuildContext context, T intent) {
    final Action<T>? action = Actions.maybeFind<T>(context);
707
    if (action != null && action.isEnabled(intent)) {
708
      return () {
709 710 711 712 713
        // 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);
        }
714
      };
715
    }
716 717 718 719 720 721 722 723
    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.
724 725 726 727
  ///
  /// 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.
728 729 730 731 732 733 734 735 736 737 738 739 740 741
  ///
  /// 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;
742 743 744 745 746 747 748 749 750 751
        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',
        );
752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775
      }
      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 }) {
776
    Action<T>? action;
777

778 779 780 781
    // 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;
782 783 784 785 786
    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 '
787
      '"intent" argument is passed.',
788
    );
789

790 791
    _visitActionsAncestors(context, (InheritedElement element) {
      final _ActionsMarker actions = element.widget as _ActionsMarker;
792
      final Action<T>? result = _castAction(actions, intent: intent);
793 794 795 796 797 798 799 800 801
      if (result != null) {
        context.dependOnInheritedElement(element);
        action = result;
        return true;
      }
      return false;
    });

    return action;
802 803
  }

804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846
  static Action<T>? _maybeFindWithoutDependingOn<T extends Intent>(BuildContext context, { T? intent }) {
    Action<T>? action;

    // 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.',
    );

    _visitActionsAncestors(context, (InheritedElement element) {
      final _ActionsMarker actions = element.widget as _ActionsMarker;
      final Action<T>? result = _castAction(actions, intent: intent);
      if (result != null) {
        action = result;
        return true;
      }
      return false;
    });

    return action;
  }

  // Find the [Action] that handles the given `intent` in the given
  // `_ActionsMarker`, and verify it has the right type parameter.
  static Action<T>? _castAction<T extends Intent>(_ActionsMarker actionsMarker, { T? intent }) {
    final Action<Intent>? mappedAction = actionsMarker.actions[intent?.runtimeType ?? T];
    if (mappedAction is Action<T>?) {
      return mappedAction;
    } else {
      assert(
        false,
        '$T cannot be handled by an Action of runtime type ${mappedAction.runtimeType}.'
      );
      return null;
    }
  }

847 848 849
  /// Returns the [ActionDispatcher] associated with the [Actions] widget that
  /// most tightly encloses the given [BuildContext].
  ///
850 851 852
  /// Will return a newly created [ActionDispatcher] if no ambient [Actions]
  /// widget is found.
  static ActionDispatcher of(BuildContext context) {
853
    assert(context != null);
854
    final _ActionsMarker? marker = context.dependOnInheritedWidgetOfExactType<_ActionsMarker>();
855
    return marker?.dispatcher ?? _findDispatcher(context);
856 857
  }

Kate Lovett's avatar
Kate Lovett committed
858
  /// Invokes the action associated with the given [Intent] using the
859 860
  /// [Actions] widget that most tightly encloses the given [BuildContext].
  ///
861 862 863 864
  /// This method returns the result of invoking the action's [Action.invoke]
  /// method.
  ///
  /// The `context` and `intent` arguments must not be null.
865
  ///
866 867
  /// If the given `intent` doesn't map to an action, then it will look to the
  /// next ancestor [Actions] widget in the hierarchy until it reaches the root.
868
  ///
869
  /// This method will throw an exception if no ambient [Actions] widget is
870 871
  /// found, or when a suitable [Action] is found but it returns false for
  /// [Action.isEnabled].
872
  static Object? invoke<T extends Intent>(
873
    BuildContext context,
874 875
    T intent,
  ) {
876
    assert(intent != null);
877
    assert(context != null);
878
    Object? returnValue;
879

880
    final bool actionFound = _visitActionsAncestors(context, (InheritedElement element) {
881
      final _ActionsMarker actions = element.widget as _ActionsMarker;
882 883 884 885 886
      final Action<T>? result = _castAction(actions, intent: intent);
      if (result != null && result.isEnabled(intent)) {
        // Invoke the action we found using the relevant dispatcher from the Actions
        // Element we found.
        returnValue = _findDispatcher(element).invokeAction(result, intent, context);
887
      }
888
      return result != null;
889
    });
890 891

    assert(() {
892
      if (!actionFound) {
893 894 895 896 897 898 899 900 901 902 903 904
        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'
          'The context used was:\n'
          '  $context\n'
          'The intent type requested was:\n'
          '  ${intent.runtimeType}',
        );
905 906 907
      }
      return true;
    }());
908
    return returnValue;
909 910
  }

911 912 913 914 915
  /// Invokes the action associated with the given [Intent] using the
  /// [Actions] widget that most tightly encloses the given [BuildContext].
  ///
  /// This method returns the result of invoking the action's [Action.invoke]
  /// method. If no action mapping was found for the specified intent, or if the
916
  /// first action found was disabled, or the action itself returns null
917 918 919 920
  /// from [Action.invoke], then this method returns null.
  ///
  /// The `context` and `intent` arguments must not be null.
  ///
921 922 923 924
  /// If the given `intent` doesn't map to an action, then it will look to the
  /// next ancestor [Actions] widget in the hierarchy until it reaches the root.
  /// If a suitable [Action] is found but its [Action.isEnabled] returns false,
  /// the search will stop and this method will return null.
925 926 927 928 929 930
  static Object? maybeInvoke<T extends Intent>(
    BuildContext context,
    T intent,
  ) {
    assert(intent != null);
    assert(context != null);
931
    Object? returnValue;
932 933 934

    _visitActionsAncestors(context, (InheritedElement element) {
      final _ActionsMarker actions = element.widget as _ActionsMarker;
935 936 937 938 939
      final Action<T>? result = _castAction(actions, intent: intent);
      if (result != null && result.isEnabled(intent)) {
        // Invoke the action we found using the relevant dispatcher from the Actions
        // Element we found.
        returnValue = _findDispatcher(element).invokeAction(result, intent, context);
940
      }
941
      return result != null;
942
    });
943
    return returnValue;
944 945
  }

946
  @override
947
  State<Actions> createState() => _ActionsState();
948 949 950 951 952

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher));
953 954 955 956 957
    properties.add(DiagnosticsProperty<Map<Type, Action<Intent>>>('actions', actions));
  }
}

class _ActionsState extends State<Actions> {
958
  // The set of actions that this Actions widget is current listening to.
959
  Set<Action<Intent>>? listenedActions = <Action<Intent>>{};
960 961
  // Used to tell the marker to rebuild its dependencies when the state of an
  // action in the map changes.
962 963 964 965 966 967 968 969 970
  Object rebuildKey = Object();

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

  void _handleActionChanged(Action<Intent> action) {
971 972 973 974
    // Generate a new key so that the marker notifies dependents.
    setState(() {
      rebuildKey = Object();
    });
975 976 977
  }

  void _updateActionListeners() {
978
    final Set<Action<Intent>> widgetActions = widget.actions.values.toSet();
979 980
    final Set<Action<Intent>> removedActions = listenedActions!.difference(widgetActions);
    final Set<Action<Intent>> addedActions = widgetActions.difference(listenedActions!);
981 982 983

    for (final Action<Intent> action in removedActions) {
      action.removeActionListener(_handleActionChanged);
984
    }
985 986
    for (final Action<Intent> action in addedActions) {
      action.addActionListener(_handleActionChanged);
987
    }
988
    listenedActions = widgetActions;
989 990 991 992 993 994 995 996 997 998 999
  }

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

  @override
  void dispose() {
    super.dispose();
1000
    for (final Action<Intent> action in listenedActions!) {
1001 1002
      action.removeActionListener(_handleActionChanged);
    }
1003
    listenedActions = null;
1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
  }

  @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({
1021 1022 1023
    required this.dispatcher,
    required this.actions,
    required this.rebuildKey,
1024
    required super.child,
1025
  })  : assert(child != null),
1026
        assert(actions != null);
1027

1028
  final ActionDispatcher? dispatcher;
1029 1030 1031 1032 1033 1034 1035 1036
  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);
1037 1038
  }
}
1039

1040 1041 1042 1043 1044
/// 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.
///
1045 1046
/// {@youtube 560 315 https://www.youtube.com/watch?v=R84AGg0lKs8}
///
1047 1048 1049 1050 1051
/// 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.
///
1052
/// {@tool dartpad}
1053 1054
/// 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
1055 1056 1057
/// 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).
1058 1059 1060 1061
///
/// 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
1062
/// [CupertinoApp]), so the `ENTER` key will also activate the buttons.
1063
///
1064
/// ** See code in examples/api/lib/widgets/actions/focusable_action_detector.0.dart **
1065 1066 1067 1068 1069 1070 1071 1072 1073
/// {@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].
  ///
1074
  /// The [enabled], [autofocus], [mouseCursor], and [child] arguments must not be null.
1075
  const FocusableActionDetector({
1076
    super.key,
1077 1078 1079
    this.enabled = true,
    this.focusNode,
    this.autofocus = false,
1080
    this.descendantsAreFocusable = true,
1081
    this.descendantsAreTraversable = true,
1082 1083 1084 1085 1086
    this.shortcuts,
    this.actions,
    this.onShowFocusHighlight,
    this.onShowHoverHighlight,
    this.onFocusChange,
1087
    this.mouseCursor = MouseCursor.defer,
1088
    this.includeFocusSemantics = true,
1089
    required this.child,
1090 1091
  })  : assert(enabled != null),
        assert(autofocus != null),
1092
        assert(mouseCursor != null),
1093
        assert(child != null);
1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104

  /// 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}
1105
  final FocusNode? focusNode;
1106 1107 1108 1109

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

1110 1111 1112
  /// {@macro flutter.widgets.Focus.descendantsAreFocusable}
  final bool descendantsAreFocusable;

1113 1114 1115
  /// {@macro flutter.widgets.Focus.descendantsAreTraversable}
  final bool descendantsAreTraversable;

1116
  /// {@macro flutter.widgets.actions.actions}
1117
  final Map<Type, Action<Intent>>? actions;
1118 1119

  /// {@macro flutter.widgets.shortcuts.shortcuts}
1120
  final Map<ShortcutActivator, Intent>? shortcuts;
1121

1122 1123 1124 1125
  /// 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.
1126
  final ValueChanged<bool>? onShowFocusHighlight;
1127 1128

  /// A function that will be called when the hover highlight should be shown or hidden.
1129 1130
  ///
  /// This method is not triggered at the unmount of the widget.
1131
  final ValueChanged<bool>? onShowHoverHighlight;
1132 1133 1134 1135

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

1138 1139 1140
  /// The cursor for a mouse pointer when it enters or is hovering over the
  /// widget.
  ///
1141
  /// The [mouseCursor] defaults to [MouseCursor.defer], deferring the choice of
1142
  /// cursor to the next region behind it in hit-test order.
1143 1144
  final MouseCursor mouseCursor;

1145 1146 1147 1148 1149
  /// Whether to include semantics from [Focus].
  ///
  /// Defaults to true.
  final bool includeFocusSemantics;

1150 1151
  /// The child widget for this [FocusableActionDetector] widget.
  ///
1152
  /// {@macro flutter.widgets.ProxyWidget.child}
1153 1154 1155
  final Widget child;

  @override
1156
  State<FocusableActionDetector> createState() => _FocusableActionDetectorState();
1157 1158 1159 1160 1161 1162
}

class _FocusableActionDetectorState extends State<FocusableActionDetector> {
  @override
  void initState() {
    super.initState();
1163
    SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
1164 1165
      _updateHighlightMode(FocusManager.instance.highlightMode);
    });
1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176
    FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
  }

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

  bool _canShowHighlight = false;
  void _updateHighlightMode(FocusHighlightMode mode) {
1177 1178 1179 1180 1181 1182 1183 1184 1185 1186
    _mayTriggerCallback(task: () {
      switch (FocusManager.instance.highlightMode) {
        case FocusHighlightMode.touch:
          _canShowHighlight = false;
          break;
        case FocusHighlightMode.traditional:
          _canShowHighlight = true;
          break;
      }
    });
1187 1188
  }

1189 1190 1191 1192
  // 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.
1193 1194 1195 1196 1197 1198 1199 1200 1201 1202
  void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
    if (!mounted) {
      return;
    }
    _updateHighlightMode(mode);
  }

  bool _hovering = false;
  void _handleMouseEnter(PointerEnterEvent event) {
    if (!_hovering) {
1203 1204 1205
      _mayTriggerCallback(task: () {
        _hovering = true;
      });
1206 1207 1208 1209 1210
    }
  }

  void _handleMouseExit(PointerExitEvent event) {
    if (_hovering) {
1211 1212 1213
      _mayTriggerCallback(task: () {
        _hovering = false;
      });
1214 1215 1216 1217 1218 1219
    }
  }

  bool _focused = false;
  void _handleFocusChange(bool focused) {
    if (_focused != focused) {
1220
      _mayTriggerCallback(task: () {
1221 1222
        _focused = focused;
      });
1223
      widget.onFocusChange?.call(_focused);
1224 1225 1226
    }
  }

1227 1228 1229 1230 1231 1232
  // 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.
1233
  void _mayTriggerCallback({VoidCallback? task, FocusableActionDetector? oldWidget}) {
1234 1235 1236
    bool shouldShowHoverHighlight(FocusableActionDetector target) {
      return _hovering && target.enabled && _canShowHighlight;
    }
1237

1238
    bool canRequestFocus(FocusableActionDetector target) {
1239
      final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
1240 1241 1242 1243 1244 1245 1246 1247
      switch (mode) {
        case NavigationMode.traditional:
          return target.enabled;
        case NavigationMode.directional:
          return true;
      }
    }

1248
    bool shouldShowFocusHighlight(FocusableActionDetector target) {
1249
      return _focused && _canShowHighlight && canRequestFocus(target);
1250 1251
    }

1252
    assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks);
1253 1254 1255
    final FocusableActionDetector oldTarget = oldWidget ?? widget;
    final bool didShowHoverHighlight = shouldShowHoverHighlight(oldTarget);
    final bool didShowFocusHighlight = shouldShowFocusHighlight(oldTarget);
1256
    if (task != null) {
1257
      task();
1258
    }
1259 1260
    final bool doShowHoverHighlight = shouldShowHoverHighlight(widget);
    final bool doShowFocusHighlight = shouldShowFocusHighlight(widget);
1261
    if (didShowFocusHighlight != doShowFocusHighlight) {
1262
      widget.onShowFocusHighlight?.call(doShowFocusHighlight);
1263 1264
    }
    if (didShowHoverHighlight != doShowHoverHighlight) {
1265
      widget.onShowHoverHighlight?.call(doShowHoverHighlight);
1266
    }
1267 1268
  }

1269 1270 1271 1272
  @override
  void didUpdateWidget(FocusableActionDetector oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.enabled != oldWidget.enabled) {
1273
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
1274 1275 1276
        _mayTriggerCallback(oldWidget: oldWidget);
      });
    }
1277 1278
  }

1279
  bool get _canRequestFocus {
1280
    final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
1281 1282 1283 1284 1285 1286 1287 1288
    switch (mode) {
      case NavigationMode.traditional:
        return widget.enabled;
      case NavigationMode.directional:
        return true;
    }
  }

1289 1290 1291 1292 1293 1294 1295
  // 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();

1296 1297
  @override
  Widget build(BuildContext context) {
1298
    Widget child = MouseRegion(
1299
      key: _mouseRegionKey,
1300 1301 1302 1303 1304 1305
      onEnter: _handleMouseEnter,
      onExit: _handleMouseExit,
      cursor: widget.mouseCursor,
      child: Focus(
        focusNode: widget.focusNode,
        autofocus: widget.autofocus,
1306
        descendantsAreFocusable: widget.descendantsAreFocusable,
1307
        descendantsAreTraversable: widget.descendantsAreTraversable,
1308 1309
        canRequestFocus: _canRequestFocus,
        onFocusChange: _handleFocusChange,
1310
        includeSemantics: widget.includeFocusSemantics,
1311
        child: widget.child,
1312 1313
      ),
    );
1314 1315
    if (widget.enabled && widget.actions != null && widget.actions!.isNotEmpty) {
      child = Actions(actions: widget.actions!, child: child);
1316
    }
1317 1318
    if (widget.enabled && widget.shortcuts != null && widget.shortcuts!.isNotEmpty) {
      child = Shortcuts(shortcuts: widget.shortcuts!, child: child);
1319 1320
    }
    return child;
1321 1322 1323
  }
}

1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351
/// 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].
1352 1353
///
/// Attaching a [DoNothingIntent] to a [Shortcuts] mapping is one way to disable
1354 1355
/// a keyboard shortcut defined by a widget higher in the widget hierarchy and
/// consume any key event that triggers it via a shortcut.
1356
///
1357
/// This intent cannot be subclassed.
1358 1359 1360 1361 1362 1363
///
/// 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.
1364 1365
class DoNothingIntent extends Intent {
  /// Creates a const [DoNothingIntent].
1366
  const factory DoNothingIntent() = DoNothingIntent._;
1367

1368 1369 1370
  // Make DoNothingIntent constructor private so it can't be subclassed.
  const DoNothingIntent._();
}
1371

1372
/// An [Intent] that is bound to a [DoNothingAction], but, in addition to not
1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390
/// 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].
1391
  const factory DoNothingAndStopPropagationIntent() = DoNothingAndStopPropagationIntent._;
1392 1393 1394 1395 1396

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

1397
/// An [Action] that doesn't perform any action when invoked.
1398
///
1399 1400
/// Attaching a [DoNothingAction] to an [Actions.actions] mapping is a way to
/// disable an action defined by a widget higher in the widget hierarchy.
1401
///
1402 1403 1404 1405 1406 1407 1408 1409
/// 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].
1410 1411
///
/// See also:
1412
///  - [DoNothingIntent], which is an intent that can be bound to a [KeySet] in
1413
///    a [Shortcuts] widget to do nothing.
1414 1415 1416
///  - [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.
1417
class DoNothingAction extends Action<Intent> {
1418 1419 1420 1421 1422 1423 1424 1425 1426
  /// 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;

1427
  @override
1428 1429 1430
  void invoke(Intent intent) {}
}

1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443
/// An [Intent] that activates the currently focused control.
///
/// This intent is bound by default to the [LogicalKeyboardKey.space] key on all
/// platforms, and also to the [LogicalKeyboardKey.enter] key on all platforms
/// except the web, where ENTER doesn't toggle selection. On the web, ENTER is
/// bound to [ButtonActivateIntent] instead.
///
/// See also:
///
///  * [WidgetsApp.defaultShortcuts], which contains the default shortcuts used
///    in apps.
///  * [WidgetsApp.shortcuts], which defines the shortcuts to use in an
///    application (and defaults to [WidgetsApp.defaultShortcuts]).
1444
class ActivateIntent extends Intent {
1445
  /// Creates an intent that activates the currently focused control.
1446
  const ActivateIntent();
1447
}
1448

1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461
/// An [Intent] that activates the currently focused button.
///
/// This intent is bound by default to the [LogicalKeyboardKey.enter] key on the
/// web, where ENTER can be used to activate buttons, but not toggle selection.
/// All other platforms bind [LogicalKeyboardKey.enter] to [ActivateIntent].
///
/// See also:
///
///  * [WidgetsApp.defaultShortcuts], which contains the default shortcuts used
///    in apps.
///  * [WidgetsApp.shortcuts], which defines the shortcuts to use in an
///    application (and defaults to [WidgetsApp.defaultShortcuts]).
class ButtonActivateIntent extends Intent {
1462
  /// Creates an intent that the currently focused control, if it's a button.
1463 1464 1465
  const ButtonActivateIntent();
}

1466
/// An [Action] that activates the currently focused control.
1467 1468
///
/// This is an abstract class that serves as a base class for actions that
1469 1470 1471
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter],
/// [LogicalKeyboardKey.gameButtonA], and [LogicalKeyboardKey.space] in the
/// default keyboard map in [WidgetsApp].
1472
abstract class ActivateAction extends Action<ActivateIntent> { }
1473

1474
/// An [Intent] that selects the currently focused control.
1475 1476 1477 1478
class SelectIntent extends Intent {
  /// Creates an intent that selects the currently focused control.
  const SelectIntent();
}
1479 1480 1481 1482

/// An action that selects the currently focused control.
///
/// This is an abstract class that serves as a base class for actions that
1483
/// select something. It is not bound to any key by default.
1484
abstract class SelectAction extends Action<SelectIntent> { }
1485 1486 1487 1488 1489 1490 1491 1492 1493 1494

/// 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 {
1495
  /// Creates an intent that dismisses the currently focused widget.
1496 1497 1498
  const DismissIntent();
}

1499
/// An [Action] that dismisses the focused widget.
1500 1501
///
/// This is an abstract class that serves as a base class for dismiss actions.
1502
abstract class DismissAction extends Action<DismissIntent> { }
1503 1504 1505

/// An [Intent] that evaluates a series of specified [orderedIntents] for
/// execution.
1506 1507
///
/// The first intent that matches an enabled action is used.
1508
class PrioritizedIntents extends Intent {
1509 1510
  /// Creates an intent that is used with [PrioritizedAction] to specify a list
  /// of intents, the first available of which will be used.
1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529
  const PrioritizedIntents({
    required this.orderedIntents,
  })  : assert(orderedIntents != null);

  /// List of intents to be evaluated in order for execution. When an
  /// [Action.isEnabled] returns true, that action will be invoked and
  /// progression through the ordered intents stops.
  final List<Intent> orderedIntents;
}

/// An [Action] that iterates through a list of [Intent]s, invoking the first
/// that is enabled.
class PrioritizedAction extends Action<PrioritizedIntents> {
  late Action<dynamic> _selectedAction;
  late Intent _selectedIntent;

  @override
  bool isEnabled(PrioritizedIntents intent) {
    final FocusNode? focus = primaryFocus;
1530
    if  (focus == null || focus.context == null) {
1531
      return false;
1532
    }
1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547
    for (final Intent candidateIntent in intent.orderedIntents) {
      final Action<Intent>? candidateAction = Actions.maybeFind<Intent>(
        focus.context!,
        intent: candidateIntent,
      );
      if (candidateAction != null && candidateAction.isEnabled(candidateIntent)) {
        _selectedAction = candidateAction;
        _selectedIntent = candidateIntent;
        return true;
      }
    }
    return false;
  }

  @override
1548
  void invoke(PrioritizedIntents intent) {
1549 1550 1551 1552 1553
    assert(_selectedAction != null);
    assert(_selectedIntent != null);
    _selectedAction.invoke(_selectedIntent);
  }
}
1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582

mixin _OverridableActionMixin<T extends Intent> on Action<T> {
  // When debugAssertMutuallyRecursive is true, this action will throw an
  // assertion error when the override calls this action's "invoke" method and
  // the override is already being invoked from within the "invoke" method.
  bool debugAssertMutuallyRecursive = false;
  bool debugAssertIsActionEnabledMutuallyRecursive = false;
  bool debugAssertIsEnabledMutuallyRecursive = false;
  bool debugAssertConsumeKeyMutuallyRecursive = false;

  // The default action to invoke if an enabled override Action can't be found
  // using [lookupContext];
  Action<T> get defaultAction;

  // The [BuildContext] used to find the override of this [Action].
  BuildContext get lookupContext;

  // How to invoke [defaultAction], given the caller [fromAction].
  Object? invokeDefaultAction(T intent, Action<T>? fromAction, BuildContext? context);

  Action<T>? getOverrideAction({ bool declareDependency = false }) {
    final Action<T>? override = declareDependency
     ? Actions.maybeFind(lookupContext)
     : Actions._maybeFindWithoutDependingOn(lookupContext);
    assert(!identical(override, this));
    return override;
  }

  @override
1583 1584 1585
  void _updateCallingAction(Action<T>? value) {
    super._updateCallingAction(value);
    defaultAction._updateCallingAction(value);
1586 1587 1588 1589 1590 1591 1592 1593
  }

  Object? _invokeOverride(Action<T> overrideAction, T intent, BuildContext? context) {
    assert(!debugAssertMutuallyRecursive);
    assert(() {
      debugAssertMutuallyRecursive = true;
      return true;
    }());
1594
    overrideAction._updateCallingAction(defaultAction);
1595 1596 1597
    final Object? returnValue = overrideAction is ContextAction<T>
      ? overrideAction.invoke(intent, context)
      : overrideAction.invoke(intent);
1598
    overrideAction._updateCallingAction(null);
1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620
    assert(() {
      debugAssertMutuallyRecursive = false;
      return true;
    }());
    return returnValue;
  }

  @override
  Object? invoke(T intent, [BuildContext? context]) {
    final Action<T>? overrideAction = getOverrideAction();
    final Object? returnValue = overrideAction == null
      ? invokeDefaultAction(intent, callingAction, context)
      : _invokeOverride(overrideAction, intent, context);
    return returnValue;
  }

  bool isOverrideActionEnabled(Action<T> overrideAction) {
    assert(!debugAssertIsActionEnabledMutuallyRecursive);
    assert(() {
      debugAssertIsActionEnabledMutuallyRecursive = true;
      return true;
    }());
1621
    overrideAction._updateCallingAction(defaultAction);
1622
    final bool isOverrideEnabled = overrideAction.isActionEnabled;
1623
    overrideAction._updateCallingAction(null);
1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648
    assert(() {
      debugAssertIsActionEnabledMutuallyRecursive = false;
      return true;
    }());
    return isOverrideEnabled;
  }

  @override
  bool get isActionEnabled {
    final Action<T>? overrideAction = getOverrideAction(declareDependency: true);
    final bool returnValue = overrideAction != null
      ? isOverrideActionEnabled(overrideAction)
      : defaultAction.isActionEnabled;
    return returnValue;
  }

  @override
  bool isEnabled(T intent) {
    assert(!debugAssertIsEnabledMutuallyRecursive);
    assert(() {
      debugAssertIsEnabledMutuallyRecursive = true;
      return true;
    }());

    final Action<T>? overrideAction = getOverrideAction();
1649
    overrideAction?._updateCallingAction(defaultAction);
1650
    final bool returnValue = (overrideAction ?? defaultAction).isEnabled(intent);
1651
    overrideAction?._updateCallingAction(null);
1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666
    assert(() {
      debugAssertIsEnabledMutuallyRecursive = false;
      return true;
    }());
    return returnValue;
  }

  @override
  bool consumesKey(T intent) {
    assert(!debugAssertConsumeKeyMutuallyRecursive);
    assert(() {
      debugAssertConsumeKeyMutuallyRecursive = true;
      return true;
    }());
    final Action<T>? overrideAction = getOverrideAction();
1667
    overrideAction?._updateCallingAction(defaultAction);
1668
    final bool isEnabled = (overrideAction ?? defaultAction).consumesKey(intent);
1669
    overrideAction?._updateCallingAction(null);
1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730
    assert(() {
      debugAssertConsumeKeyMutuallyRecursive = false;
      return true;
    }());
    return isEnabled;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Action<T>>('defaultAction', defaultAction));
  }
}

class _OverridableAction<T extends Intent> extends ContextAction<T> with _OverridableActionMixin<T> {
  _OverridableAction({ required this.defaultAction, required this.lookupContext }) ;

  @override
  final Action<T> defaultAction;

  @override
  final BuildContext lookupContext;

  @override
  Object? invokeDefaultAction(T intent, Action<T>? fromAction, BuildContext? context) {
    if (fromAction == null) {
      return defaultAction.invoke(intent);
    } else {
      final Object? returnValue = defaultAction.invoke(intent);
      return returnValue;
    }
  }

  @override
  ContextAction<T> _makeOverridableAction(BuildContext context) {
    return _OverridableAction<T>(defaultAction: defaultAction, lookupContext: context);
  }
}

class _OverridableContextAction<T extends Intent> extends ContextAction<T> with _OverridableActionMixin<T> {
  _OverridableContextAction({ required this.defaultAction, required this.lookupContext });

  @override
  final ContextAction<T> defaultAction;

  @override
  final BuildContext lookupContext;

  @override
  Object? _invokeOverride(Action<T> overrideAction, T intent, BuildContext? context) {
    assert(context != null);
    assert(!debugAssertMutuallyRecursive);
    assert(() {
      debugAssertMutuallyRecursive = true;
      return true;
    }());

    // Wrap the default Action together with the calling context in case
    // overrideAction is not a ContextAction and thus have no access to the
    // calling BuildContext.
    final Action<T> wrappedDefault = _ContextActionToActionAdapter<T>(invokeContext: context!, action: defaultAction);
1731
    overrideAction._updateCallingAction(wrappedDefault);
1732 1733 1734
    final Object? returnValue = overrideAction is ContextAction<T>
      ? overrideAction.invoke(intent, context)
      : overrideAction.invoke(intent);
1735
    overrideAction._updateCallingAction(null);
1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766

    assert(() {
      debugAssertMutuallyRecursive = false;
      return true;
    }());
    return returnValue;
  }

  @override
  Object? invokeDefaultAction(T intent, Action<T>? fromAction, BuildContext? context) {
    if (fromAction == null) {
      return defaultAction.invoke(intent, context);
    } else {
      final Object? returnValue = defaultAction.invoke(intent, context);
      return returnValue;
    }
  }

  @override
  ContextAction<T> _makeOverridableAction(BuildContext context) {
    return _OverridableContextAction<T>(defaultAction: defaultAction, lookupContext: context);
  }
}

class _ContextActionToActionAdapter<T extends Intent> extends Action<T> {
  _ContextActionToActionAdapter({required this.invokeContext, required this.action});

  final BuildContext invokeContext;
  final ContextAction<T> action;

  @override
1767 1768
  void _updateCallingAction(Action<T>? value) {
    action._updateCallingAction(value);
1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801
  }

  @override
  Action<T>? get callingAction => action.callingAction;

  @override
  bool isEnabled(T intent) => action.isEnabled(intent);

  @override
  bool get isActionEnabled => action.isActionEnabled;

  @override
  bool consumesKey(T intent) => action.consumesKey(intent);

  @override
  void addActionListener(ActionListenerCallback listener) {
    super.addActionListener(listener);
    action.addActionListener(listener);
  }

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

  @override
  @protected
  void notifyActionListeners() => action.notifyActionListeners();

  @override
  Object? invoke(T intent) => action.invoke(intent, invokeContext);
}