actions.dart 59.1 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 75 76
/// 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:
///
77
///  * [Shortcuts], which is a widget that contains a key map, in which it looks
78
///    up key combinations in order to invoke actions.
79
///  * [Actions], which is a widget that defines a map of [Intent] to [Action]
80
///    and allows redefining of actions for its descendants.
81 82 83 84 85 86 87
///  * [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;
88

89
  /// Returns true if the action is enabled and is ready to be invoked.
90
  ///
91 92 93 94 95
  /// 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.
96
  bool isEnabled(covariant T intent) => true;
97

98 99 100 101 102 103 104 105 106 107 108 109 110
  /// 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;

111 112
  /// Called when the action is to be performed.
  ///
113 114 115
  /// This is called by the [ActionDispatcher] when an action is invoked via
  /// [Actions.invoke], or when an action is invoked using
  /// [ActionDispatcher.invokeAction] directly.
116 117
  ///
  /// This method is only meant to be invoked by an [ActionDispatcher], or by
118
  /// its subclasses, and only when [isEnabled] is true.
119 120 121 122 123 124 125
  ///
  /// 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:
126
  ///
127 128 129 130 131 132 133 134 135 136 137 138 139 140
  /// ```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;
  ///   }
  /// }
  /// ```
141 142 143 144
  ///
  /// 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.
145
  @protected
146
  Object? invoke(covariant T intent);
147 148 149 150 151 152 153 154

  /// 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.
  ///
155
  /// {@template flutter.widgets.Action.addActionListener}
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
  /// 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.
  ///
182
  /// {@macro flutter.widgets.Action.addActionListener}
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
  @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
201
  @pragma('vm:notify-debugger-on-exception')
202 203 204 205 206 207 208 209 210
  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) {
211
      InformationCollector? collector;
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.
250 251 252 253 254 255 256
///
/// {@tool dartpad --template=stateful_widget_scaffold_center}
/// This example shows how ActionListener handles adding and removing of
/// the [listener] in the widget lifecycle.
///
/// ```dart preamble
/// class ActionListenerExample extends StatefulWidget {
257 258
///   const ActionListenerExample({Key? key}) : super(key: key);
///
259
///   @override
260
///   State<ActionListenerExample> createState() => _ActionListenerExampleState();
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
/// }
///
/// class _ActionListenerExampleState extends State<ActionListenerExample> {
///   bool _on = false;
///   late final MyAction _myAction;
///
///   @override
///   void initState() {
///     super.initState();
///     _myAction = MyAction();
///   }
///
///   void _toggleState() {
///     setState(() {
///       _on = !_on;
///     });
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return Row(
///       crossAxisAlignment: CrossAxisAlignment.center,
///       mainAxisAlignment: MainAxisAlignment.center,
///       children: <Widget>[
///         Padding(
///           padding: const EdgeInsets.all(8.0),
///           child: OutlinedButton(
///             onPressed: _toggleState,
///             child: Text(_on ? 'Disable' : 'Enable'),
///           ),
///         ),
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
///         if (_on)
///           Padding(
///             padding: const EdgeInsets.all(8.0),
///             child: ActionListener(
///               listener: (Action<Intent> action) {
///                 if (action.intentType == MyIntent) {
///                   ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
///                     content: Text('Action Listener Called'),
///                   ));
///                 }
///               },
///               action: _myAction,
///               child: ElevatedButton(
///                 onPressed: () => const ActionDispatcher()
///                     .invokeAction(_myAction, const MyIntent()),
///                 child: const Text('Call Action Listener'),
///               ),
///             ),
///           ),
///         if (!_on) Container(),
312 313 314 315 316 317 318
///       ],
///     );
///   }
/// }
///
/// class MyAction extends Action<MyIntent> {
///   @override
319
///   void addActionListener(ActionListenerCallback listener) {
320 321 322 323 324
///     super.addActionListener(listener);
///     print('Action Listener was added');
///   }
///
///   @override
325
///   void removeActionListener(ActionListenerCallback listener) {
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
///     super.removeActionListener(listener);
///     print('Action Listener was removed');
///   }
///
///   @override
///   void invoke(covariant MyIntent intent) {
///     notifyActionListeners();
///   }
/// }
///
/// class MyIntent extends Intent {
///   const MyIntent();
/// }
/// ```
///
/// ```dart
342
/// @override
343
/// Widget build(BuildContext context) {
344
///   return const ActionListenerExample();
345 346 347 348
/// }
/// ```
/// {@end-tool}
///
349 350 351 352 353 354
@immutable
class ActionListener extends StatefulWidget {
  /// Create a const [ActionListener].
  ///
  /// The [listener], [action], and [child] arguments must not be null.
  const ActionListener({
355 356 357 358
    Key? key,
    required this.listener,
    required this.action,
    required this.child,
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
  })  : 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;

374
  /// {@macro flutter.widgets.ProxyWidget.child}
375
  final Widget child;
376 377

  @override
378
  State<ActionListener> createState() => _ActionListenerState();
379 380 381 382 383 384 385
}

class _ActionListenerState extends State<ActionListener> {
  @override
  void initState() {
    super.initState();
    widget.action.addActionListener(widget.listener);
386
  }
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420

  @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
421
  /// its subclasses, and only when [isEnabled] is true.
422 423
  ///
  /// The optional `context` parameter is the context of the invocation of the
424
  /// action, and in the case of an action invoked by a [ShortcutManager], via
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
  /// 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
450
  Object? invoke(covariant T intent, [BuildContext? context]);
451 452 453
}

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

/// An [Action] that takes a callback in order to configure it without having to
457
/// create an explicit [Action] subclass just to call a callback.
458 459 460
///
/// See also:
///
461
///  * [Shortcuts], which is a widget that contains a key map, in which it looks
462
///    up key combinations in order to invoke actions.
463
///  * [Actions], which is a widget that defines a map of [Intent] to [Action]
464
///    and allows redefining of actions for its descendants.
465
///  * [ActionDispatcher], a class that takes an [Action] and invokes it using a
466
///    [FocusNode] for context.
467 468
class CallbackAction<T extends Intent> extends Action<T> {
  /// A constructor for a [CallbackAction].
469 470 471
  ///
  /// The `intentKey` and [onInvoke] parameters must not be null.
  /// The [onInvoke] parameter is required.
472
  CallbackAction({required this.onInvoke}) : assert(onInvoke != null);
473 474 475 476 477

  /// The callback to be called when invoked.
  ///
  /// Must not be null.
  @protected
478
  final OnInvokeCallback<T> onInvoke;
479 480

  @override
481
  Object? invoke(covariant T intent) => onInvoke(intent);
482 483
}

484 485 486 487 488 489 490 491
/// 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].
492
class ActionDispatcher with Diagnosticable {
493
  /// Creates an action dispatcher that invokes actions directly.
494 495
  const ActionDispatcher();

496
  /// Invokes the given `action`, passing it the given `intent`.
497
  ///
498 499 500 501
  /// 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.
502
  ///
503 504 505 506 507 508 509 510 511 512
  /// 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,
  ]) {
513 514
    assert(action != null);
    assert(intent != null);
515 516 517 518 519 520
    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);
521 522 523 524 525 526 527 528 529 530
    }
  }
}

/// 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.
///
531 532 533 534 535 536 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 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602
/// {@tool dartpad --template=stateful_widget_scaffold_center}
///
/// This example creates a custom [Action] subclass `ModifyAction` for modifying
/// a model, and another, `SaveAction` for saving it.
///
/// This example demonstrates passing arguments to the [Intent] to be carried to
/// the [Action]. Actions can get data either from their own construction (like
/// the `model` in this example), or from the intent passed to them when invoked
/// (like the increment `amount` in this example).
///
/// This example also demonstrates how to use Intents to limit a widget's
/// dependencies on its surroundings. The `SaveButton` widget defined in this
/// example can invoke actions defined in its ancestor widgets, which can be
/// customized to match the part of the widget tree that it is in. It doesn't
/// need to know about the `SaveAction` class, only the `SaveIntent`, and it
/// only needs to know about a value notifier, not the entire model.
///
/// ```dart preamble
/// // A simple model class that notifies listeners when it changes.
/// class Model {
///   ValueNotifier<bool> isDirty = ValueNotifier<bool>(false);
///   ValueNotifier<int> data = ValueNotifier<int>(0);
///
///   int save() {
///     if (isDirty.value) {
///       print('Saved Data: ${data.value}');
///       isDirty.value = false;
///     }
///     return data.value;
///   }
///
///   void setValue(int newValue) {
///     isDirty.value = data.value != newValue;
///     data.value = newValue;
///   }
/// }
///
/// class ModifyIntent extends Intent {
///   const ModifyIntent(this.value);
///
///   final int value;
/// }
///
/// // An Action that modifies the model by setting it to the value that it gets
/// // from the Intent passed to it when invoked.
/// class ModifyAction extends Action<ModifyIntent> {
///   ModifyAction(this.model);
///
///   final Model model;
///
///   @override
///   void invoke(covariant ModifyIntent intent) {
///     model.setValue(intent.value);
///   }
/// }
///
/// // An intent for saving data.
/// class SaveIntent extends Intent {
///   const SaveIntent();
/// }
///
/// // An Action that saves the data in the model it is created with.
/// class SaveAction extends Action<SaveIntent> {
///   SaveAction(this.model);
///
///   final Model model;
///
///   @override
///   int invoke(covariant SaveIntent intent) => model.save();
/// }
///
/// class SaveButton extends StatefulWidget {
603
///   const SaveButton(this.valueNotifier, {Key? key}) : super(key: key);
604 605 606 607
///
///   final ValueNotifier<bool> valueNotifier;
///
///   @override
608
///   State<SaveButton> createState() => _SaveButtonState();
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
/// }
///
/// class _SaveButtonState extends State<SaveButton> {
///   int savedValue = 0;
///
///   @override
///   Widget build(BuildContext context) {
///     return AnimatedBuilder(
///       animation: widget.valueNotifier,
///       builder: (BuildContext context, Widget? child) {
///         return TextButton.icon(
///           icon: const Icon(Icons.save),
///           label: Text('$savedValue'),
///           style: ButtonStyle(
///             foregroundColor: MaterialStateProperty.all<Color>(
///               widget.valueNotifier.value ? Colors.red : Colors.green,
///             ),
///           ),
///           onPressed: () {
///             setState(() {
629
///               savedValue = Actions.invoke(context, const SaveIntent())! as int;
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
///             });
///           },
///         );
///       },
///     );
///   }
/// }
/// ```
///
/// ```dart
/// Model model = Model();
/// int count = 0;
///
/// @override
/// Widget build(BuildContext context) {
///   return Actions(
///     actions: <Type, Action<Intent>>{
///       ModifyIntent: ModifyAction(model),
///       SaveIntent: SaveAction(model),
///     },
///     child: Builder(
///       builder: (BuildContext context) {
///         return Row(
///           mainAxisAlignment: MainAxisAlignment.spaceAround,
///           children: <Widget>[
///             const Spacer(),
///             Column(
///               mainAxisAlignment: MainAxisAlignment.center,
///               children: <Widget>[
///                 IconButton(
///                   icon: const Icon(Icons.exposure_plus_1),
///                   onPressed: () {
662
///                     Actions.invoke(context, ModifyIntent(++count));
663 664 665 666 667 668 669 670 671 672 673 674 675 676
///                   },
///                 ),
///                 AnimatedBuilder(
///                   animation: model.data,
///                   builder: (BuildContext context, Widget? child) {
///                     return Padding(
///                       padding: const EdgeInsets.all(8.0),
///                       child: Text('${model.data.value}',
///                           style: Theme.of(context).textTheme.headline4),
///                     );
///                   }),
///                 IconButton(
///                   icon: const Icon(Icons.exposure_minus_1),
///                   onPressed: () {
677
///                     Actions.invoke(context, ModifyIntent(--count));
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692
///                   },
///                 ),
///               ],
///             ),
///             SaveButton(model.isDirty),
///             const Spacer(),
///           ],
///         );
///       },
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
693 694
/// See also:
///
695 696 697 698 699 700
///  * [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.
701
class Actions extends StatefulWidget {
702 703 704 705
  /// Creates an [Actions] widget.
  ///
  /// The [child], [actions], and [dispatcher] arguments must not be null.
  const Actions({
706
    Key? key,
707
    this.dispatcher,
708 709
    required this.actions,
    required this.child,
710
  })  : assert(actions != null),
711 712
        assert(child != null),
        super(key: key);
713 714 715 716

  /// The [ActionDispatcher] object that invokes actions.
  ///
  /// This is what is returned from [Actions.of], and used by [Actions.invoke].
717 718 719 720 721
  ///
  /// 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].
722
  final ActionDispatcher? dispatcher;
723

724
  /// {@template flutter.widgets.actions.actions}
725 726
  /// A map of [Intent] keys to [Action<Intent>] objects that defines which
  /// actions this widget knows about.
727 728 729 730
  ///
  /// 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.
731
  /// {@endtemplate}
732 733
  final Map<Type, Action<Intent>> actions;

734
  /// {@macro flutter.widgets.ProxyWidget.child}
735 736 737 738 739
  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.
740
  static bool _visitActionsAncestors(BuildContext context, bool Function(InheritedElement element) visitor) {
741
    InheritedElement? actionsElement = context.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
742 743 744 745 746 747 748 749
    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);
750
      actionsElement = parent.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
751 752 753
    }
    return actionsElement != null;
  }
754

755 756
  // Finds the nearest valid ActionDispatcher, or creates a new one if it
  // doesn't find one.
757
  static ActionDispatcher _findDispatcher(BuildContext context) {
758
    ActionDispatcher? dispatcher;
759
    _visitActionsAncestors(context, (InheritedElement element) {
760
      final ActionDispatcher? found = (element.widget as _ActionsMarker).dispatcher;
761 762 763
      if (found != null) {
        dispatcher = found;
        return true;
764
      }
765 766 767 768
      return false;
    });
    return dispatcher ?? const ActionDispatcher();
  }
769

770 771
  /// 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
772
  /// not enabled, or no matching action is found.
773 774 775 776 777 778 779 780
  ///
  /// 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.
781 782
  static VoidCallback? handler<T extends Intent>(BuildContext context, T intent) {
    final Action<T>? action = Actions.maybeFind<T>(context);
783
    if (action != null && action.isEnabled(intent)) {
784
      return () {
785 786 787 788 789
        // 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);
        }
790
      };
791
    }
792 793 794 795 796 797 798 799
    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.
800 801 802 803
  ///
  /// 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.
804 805 806 807 808 809 810 811 812 813 814 815 816 817
  ///
  /// 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;
818 819 820 821 822 823 824 825 826 827
        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',
        );
828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851
      }
      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 }) {
852
    Action<T>? action;
853

854 855 856 857
    // 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;
858 859 860 861 862
    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 '
863
      '"intent" argument is passed.',
864
    );
865

866 867
    _visitActionsAncestors(context, (InheritedElement element) {
      final _ActionsMarker actions = element.widget as _ActionsMarker;
868
      final Action<T>? result = actions.actions[type] as Action<T>?;
869 870 871 872 873 874 875 876 877
      if (result != null) {
        context.dependOnInheritedElement(element);
        action = result;
        return true;
      }
      return false;
    });

    return action;
878 879
  }

880 881 882
  /// Returns the [ActionDispatcher] associated with the [Actions] widget that
  /// most tightly encloses the given [BuildContext].
  ///
883 884 885
  /// Will return a newly created [ActionDispatcher] if no ambient [Actions]
  /// widget is found.
  static ActionDispatcher of(BuildContext context) {
886
    assert(context != null);
887
    final _ActionsMarker? marker = context.dependOnInheritedWidgetOfExactType<_ActionsMarker>();
888
    return marker?.dispatcher ?? _findDispatcher(context);
889 890
  }

Kate Lovett's avatar
Kate Lovett committed
891
  /// Invokes the action associated with the given [Intent] using the
892 893
  /// [Actions] widget that most tightly encloses the given [BuildContext].
  ///
894 895 896 897
  /// This method returns the result of invoking the action's [Action.invoke]
  /// method.
  ///
  /// The `context` and `intent` arguments must not be null.
898
  ///
899 900 901 902 903
  /// 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.
  ///
904 905 906
  /// This method will throw an exception if no ambient [Actions] widget is
  /// found, or if the given `intent` doesn't map to an enabled action in any of
  /// the [Actions.actions] maps that are found.
907
  static Object? invoke<T extends Intent>(
908
    BuildContext context,
909 910
    T intent,
  ) {
911
    assert(intent != null);
912
    assert(context != null);
913 914
    Action<T>? action;
    InheritedElement? actionElement;
915

916 917
    _visitActionsAncestors(context, (InheritedElement element) {
      final _ActionsMarker actions = element.widget as _ActionsMarker;
918
      final Action<T>? result = actions.actions[intent.runtimeType] as Action<T>?;
919 920
      if (result != null) {
        actionElement = element;
921 922 923 924
        if (result.isEnabled(intent)) {
          action = result;
          return true;
        }
925
      }
926 927
      return false;
    });
928 929

    assert(() {
930
      if (actionElement == null) {
931 932 933 934 935 936 937 938 939 940 941 942
        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}',
        );
943 944 945
      }
      return true;
    }());
946 947 948
    if (actionElement == null || action == null) {
      return null;
    }
949 950 951
    // Invoke the action we found using the relevant dispatcher from the Actions
    // Element we found.
    return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
952 953
  }

954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997
  /// 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
  /// actions that were found were disabled, or the action itself returns null
  /// from [Action.invoke], then this method returns null.
  ///
  /// The `context` and `intent` arguments must not be null.
  ///
  /// 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.
  static Object? maybeInvoke<T extends Intent>(
    BuildContext context,
    T intent,
  ) {
    assert(intent != null);
    assert(context != null);
    Action<T>? action;
    InheritedElement? actionElement;

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

    if (actionElement == null || action == null) {
      return null;
    }
    // Invoke the action we found using the relevant dispatcher from the Actions
    // Element we found.
    return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
  }

998
  @override
999
  State<Actions> createState() => _ActionsState();
1000 1001 1002 1003 1004

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher));
1005 1006 1007 1008 1009
    properties.add(DiagnosticsProperty<Map<Type, Action<Intent>>>('actions', actions));
  }
}

class _ActionsState extends State<Actions> {
1010
  // The set of actions that this Actions widget is current listening to.
1011
  Set<Action<Intent>>? listenedActions = <Action<Intent>>{};
1012 1013
  // Used to tell the marker to rebuild its dependencies when the state of an
  // action in the map changes.
1014 1015 1016 1017 1018 1019 1020 1021 1022
  Object rebuildKey = Object();

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

  void _handleActionChanged(Action<Intent> action) {
1023 1024 1025 1026
    // Generate a new key so that the marker notifies dependents.
    setState(() {
      rebuildKey = Object();
    });
1027 1028 1029
  }

  void _updateActionListeners() {
1030
    final Set<Action<Intent>> widgetActions = widget.actions.values.toSet();
1031 1032
    final Set<Action<Intent>> removedActions = listenedActions!.difference(widgetActions);
    final Set<Action<Intent>> addedActions = widgetActions.difference(listenedActions!);
1033 1034 1035

    for (final Action<Intent> action in removedActions) {
      action.removeActionListener(_handleActionChanged);
1036
    }
1037 1038
    for (final Action<Intent> action in addedActions) {
      action.addActionListener(_handleActionChanged);
1039
    }
1040
    listenedActions = widgetActions;
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
  }

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

  @override
  void dispose() {
    super.dispose();
1052
    for (final Action<Intent> action in listenedActions!) {
1053 1054
      action.removeActionListener(_handleActionChanged);
    }
1055
    listenedActions = null;
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072
  }

  @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({
1073 1074 1075 1076 1077
    required this.dispatcher,
    required this.actions,
    required this.rebuildKey,
    Key? key,
    required Widget child,
1078 1079 1080 1081
  })  : assert(child != null),
        assert(actions != null),
        super(key: key, child: child);

1082
  final ActionDispatcher? dispatcher;
1083 1084 1085 1086 1087 1088 1089 1090
  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);
1091 1092
  }
}
1093

1094 1095 1096 1097 1098 1099 1100 1101 1102 1103
/// 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.
///
1104
/// {@tool dartpad --template=stateful_widget_material}
1105 1106
/// 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
1107 1108 1109
/// 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).
1110 1111 1112 1113
///
/// 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
1114
/// [CupertinoApp]), so the `ENTER` key will also activate the buttons.
1115 1116 1117 1118 1119 1120 1121
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```
///
/// ```dart preamble
/// class FadButton extends StatefulWidget {
1122 1123 1124 1125 1126
///   const FadButton({
///     Key? key,
///     required this.onPressed,
///     required this.child,
///   }) : super(key: key);
1127 1128 1129 1130 1131
///
///   final VoidCallback onPressed;
///   final Widget child;
///
///   @override
1132
///   State<FadButton> createState() => _FadButtonState();
1133 1134 1135 1136 1137 1138
/// }
///
/// class _FadButtonState extends State<FadButton> {
///   bool _focused = false;
///   bool _hovering = false;
///   bool _on = false;
1139 1140 1141 1142
///   late final Map<Type, Action<Intent>> _actionMap;
///   final Map<ShortcutActivator, Intent> _shortcutMap = const <ShortcutActivator, Intent>{
///     SingleActivator(LogicalKeyboardKey.keyX): ActivateIntent(),
///   };
1143 1144 1145 1146
///
///   @override
///   void initState() {
///     super.initState();
1147
///     _actionMap = <Type, Action<Intent>>{
1148
///       ActivateIntent: CallbackAction<Intent>(
1149 1150
///         onInvoke: (Intent intent) => _toggleState(),
///       ),
1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
///     };
///   }
///
///   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(
1195
///               padding: const EdgeInsets.all(10.0),
1196 1197 1198 1199 1200 1201
///               color: color,
///               child: widget.child,
///             ),
///             Container(
///               width: 30,
///               height: 30,
1202
///               margin: const EdgeInsets.all(10.0),
1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213
///               color: _on ? Colors.red : Colors.transparent,
///             ),
///           ],
///         ),
///       ),
///     );
///   }
/// }
/// ```
///
/// ```dart
1214
/// @override
1215 1216 1217
/// Widget build(BuildContext context) {
///   return Scaffold(
///     appBar: AppBar(
1218
///       title: const Text('FocusableActionDetector Example'),
1219 1220 1221 1222 1223 1224 1225
///     ),
///     body: Center(
///       child: Row(
///         mainAxisAlignment: MainAxisAlignment.center,
///         children: <Widget>[
///           Padding(
///             padding: const EdgeInsets.all(8.0),
1226
///             child: TextButton(onPressed: () {}, child: const Text('Press Me')),
1227 1228 1229
///           ),
///           Padding(
///             padding: const EdgeInsets.all(8.0),
1230
///             child: FadButton(onPressed: () {}, child: const Text('And Me')),
1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246
///           ),
///         ],
///       ),
///     ),
///   );
/// }
/// ```
/// {@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].
  ///
1247
  /// The [enabled], [autofocus], [mouseCursor], and [child] arguments must not be null.
1248
  const FocusableActionDetector({
1249
    Key? key,
1250 1251 1252
    this.enabled = true,
    this.focusNode,
    this.autofocus = false,
1253
    this.descendantsAreFocusable = true,
1254 1255 1256 1257 1258
    this.shortcuts,
    this.actions,
    this.onShowFocusHighlight,
    this.onShowHoverHighlight,
    this.onFocusChange,
1259
    this.mouseCursor = MouseCursor.defer,
1260
    required this.child,
1261 1262
  })  : assert(enabled != null),
        assert(autofocus != null),
1263
        assert(mouseCursor != null),
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276
        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}
1277
  final FocusNode? focusNode;
1278 1279 1280 1281

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

1282 1283 1284
  /// {@macro flutter.widgets.Focus.descendantsAreFocusable}
  final bool descendantsAreFocusable;

1285
  /// {@macro flutter.widgets.actions.actions}
1286
  final Map<Type, Action<Intent>>? actions;
1287 1288

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

1291 1292 1293 1294
  /// 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.
1295
  final ValueChanged<bool>? onShowFocusHighlight;
1296 1297

  /// A function that will be called when the hover highlight should be shown or hidden.
1298 1299
  ///
  /// This method is not triggered at the unmount of the widget.
1300
  final ValueChanged<bool>? onShowHoverHighlight;
1301 1302 1303 1304

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

1307 1308 1309
  /// The cursor for a mouse pointer when it enters or is hovering over the
  /// widget.
  ///
1310
  /// The [mouseCursor] defaults to [MouseCursor.defer], deferring the choice of
1311
  /// cursor to the next region behind it in hit-test order.
1312 1313
  final MouseCursor mouseCursor;

1314 1315
  /// The child widget for this [FocusableActionDetector] widget.
  ///
1316
  /// {@macro flutter.widgets.ProxyWidget.child}
1317 1318 1319
  final Widget child;

  @override
1320
  State<FocusableActionDetector> createState() => _FocusableActionDetectorState();
1321 1322 1323 1324 1325 1326
}

class _FocusableActionDetectorState extends State<FocusableActionDetector> {
  @override
  void initState() {
    super.initState();
1327
    SchedulerBinding.instance!.addPostFrameCallback((Duration duration) {
1328 1329
      _updateHighlightMode(FocusManager.instance.highlightMode);
    });
1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340
    FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
  }

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

  bool _canShowHighlight = false;
  void _updateHighlightMode(FocusHighlightMode mode) {
1341 1342 1343 1344 1345 1346 1347 1348 1349 1350
    _mayTriggerCallback(task: () {
      switch (FocusManager.instance.highlightMode) {
        case FocusHighlightMode.touch:
          _canShowHighlight = false;
          break;
        case FocusHighlightMode.traditional:
          _canShowHighlight = true;
          break;
      }
    });
1351 1352
  }

1353 1354 1355 1356
  // 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.
1357 1358 1359 1360 1361 1362 1363 1364 1365 1366
  void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
    if (!mounted) {
      return;
    }
    _updateHighlightMode(mode);
  }

  bool _hovering = false;
  void _handleMouseEnter(PointerEnterEvent event) {
    if (!_hovering) {
1367 1368 1369
      _mayTriggerCallback(task: () {
        _hovering = true;
      });
1370 1371 1372 1373 1374
    }
  }

  void _handleMouseExit(PointerExitEvent event) {
    if (_hovering) {
1375 1376 1377
      _mayTriggerCallback(task: () {
        _hovering = false;
      });
1378 1379 1380 1381 1382 1383
    }
  }

  bool _focused = false;
  void _handleFocusChange(bool focused) {
    if (_focused != focused) {
1384
      _mayTriggerCallback(task: () {
1385 1386
        _focused = focused;
      });
1387
      widget.onFocusChange?.call(_focused);
1388 1389 1390
    }
  }

1391 1392 1393 1394 1395 1396
  // 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.
1397
  void _mayTriggerCallback({VoidCallback? task, FocusableActionDetector? oldWidget}) {
1398 1399 1400
    bool shouldShowHoverHighlight(FocusableActionDetector target) {
      return _hovering && target.enabled && _canShowHighlight;
    }
1401

1402
    bool canRequestFocus(FocusableActionDetector target) {
1403
      final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
1404 1405 1406 1407 1408 1409 1410 1411
      switch (mode) {
        case NavigationMode.traditional:
          return target.enabled;
        case NavigationMode.directional:
          return true;
      }
    }

1412
    bool shouldShowFocusHighlight(FocusableActionDetector target) {
1413
      return _focused && _canShowHighlight && canRequestFocus(target);
1414 1415
    }

1416
    assert(SchedulerBinding.instance!.schedulerPhase != SchedulerPhase.persistentCallbacks);
1417 1418 1419
    final FocusableActionDetector oldTarget = oldWidget ?? widget;
    final bool didShowHoverHighlight = shouldShowHoverHighlight(oldTarget);
    final bool didShowFocusHighlight = shouldShowFocusHighlight(oldTarget);
1420
    if (task != null) {
1421
      task();
1422
    }
1423 1424
    final bool doShowHoverHighlight = shouldShowHoverHighlight(widget);
    final bool doShowFocusHighlight = shouldShowFocusHighlight(widget);
1425
    if (didShowFocusHighlight != doShowFocusHighlight) {
1426
      widget.onShowFocusHighlight?.call(doShowFocusHighlight);
1427 1428
    }
    if (didShowHoverHighlight != doShowHoverHighlight) {
1429
      widget.onShowHoverHighlight?.call(doShowHoverHighlight);
1430
    }
1431 1432
  }

1433 1434 1435 1436
  @override
  void didUpdateWidget(FocusableActionDetector oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.enabled != oldWidget.enabled) {
1437
      SchedulerBinding.instance!.addPostFrameCallback((Duration duration) {
1438 1439 1440
        _mayTriggerCallback(oldWidget: oldWidget);
      });
    }
1441 1442
  }

1443
  bool get _canRequestFocus {
1444
    final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
1445 1446 1447 1448 1449 1450 1451 1452
    switch (mode) {
      case NavigationMode.traditional:
        return widget.enabled;
      case NavigationMode.directional:
        return true;
    }
  }

1453 1454 1455 1456 1457 1458 1459
  // 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();

1460 1461
  @override
  Widget build(BuildContext context) {
1462
    Widget child = MouseRegion(
1463
      key: _mouseRegionKey,
1464 1465 1466 1467 1468 1469
      onEnter: _handleMouseEnter,
      onExit: _handleMouseExit,
      cursor: widget.mouseCursor,
      child: Focus(
        focusNode: widget.focusNode,
        autofocus: widget.autofocus,
1470
        descendantsAreFocusable: widget.descendantsAreFocusable,
1471 1472 1473
        canRequestFocus: _canRequestFocus,
        onFocusChange: _handleFocusChange,
        child: widget.child,
1474 1475
      ),
    );
1476 1477
    if (widget.enabled && widget.actions != null && widget.actions!.isNotEmpty) {
      child = Actions(actions: widget.actions!, child: child);
1478
    }
1479 1480
    if (widget.enabled && widget.shortcuts != null && widget.shortcuts!.isNotEmpty) {
      child = Shortcuts(shortcuts: widget.shortcuts!, child: child);
1481 1482
    }
    return child;
1483 1484 1485
  }
}

1486
/// An [Intent], that is bound to a [DoNothingAction].
1487 1488
///
/// Attaching a [DoNothingIntent] to a [Shortcuts] mapping is one way to disable
1489 1490
/// a keyboard shortcut defined by a widget higher in the widget hierarchy and
/// consume any key event that triggers it via a shortcut.
1491
///
1492
/// This intent cannot be subclassed.
1493 1494 1495 1496 1497 1498
///
/// 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.
1499 1500 1501
class DoNothingIntent extends Intent {
  /// Creates a const [DoNothingIntent].
  factory DoNothingIntent() => const DoNothingIntent._();
1502

1503 1504 1505
  // Make DoNothingIntent constructor private so it can't be subclassed.
  const DoNothingIntent._();
}
1506

1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532
/// 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.
1533
///
1534 1535
/// Attaching a [DoNothingAction] to an [Actions.actions] mapping is a way to
/// disable an action defined by a widget higher in the widget hierarchy.
1536
///
1537 1538 1539 1540 1541 1542 1543 1544
/// 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].
1545 1546
///
/// See also:
1547
///  - [DoNothingIntent], which is an intent that can be bound to a [KeySet] in
1548
///    a [Shortcuts] widget to do nothing.
1549 1550 1551
///  - [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.
1552
class DoNothingAction extends Action<Intent> {
1553 1554 1555 1556 1557 1558 1559 1560 1561
  /// 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;

1562
  @override
1563 1564 1565
  void invoke(Intent intent) {}
}

1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578
/// 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]).
1579
class ActivateIntent extends Intent {
1580
  /// Creates an intent that activates the currently focused control.
1581
  const ActivateIntent();
1582
}
1583

1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596
/// 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 {
1597
  /// Creates an intent that the currently focused control, if it's a button.
1598 1599 1600
  const ButtonActivateIntent();
}

1601
/// An action that activates the currently focused control.
1602 1603
///
/// This is an abstract class that serves as a base class for actions that
1604 1605 1606 1607 1608 1609 1610
/// 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 {}
1611 1612 1613 1614

/// An action that selects the currently focused control.
///
/// This is an abstract class that serves as a base class for actions that
1615
/// select something. It is not bound to any key by default.
1616
abstract class SelectAction extends Action<SelectIntent> {}
1617 1618 1619 1620 1621 1622 1623 1624 1625 1626

/// 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 {
1627
  /// Creates an intent that dismisses the currently focused widget.
1628 1629 1630 1631 1632 1633 1634
  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> {}
1635 1636 1637

/// An [Intent] that evaluates a series of specified [orderedIntents] for
/// execution.
1638 1639
///
/// The first intent that matches an enabled action is used.
1640
class PrioritizedIntents extends Intent {
1641 1642
  /// Creates an intent that is used with [PrioritizedAction] to specify a list
  /// of intents, the first available of which will be used.
1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684
  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;
    if  (focus == null || focus.context == null)
      return false;
    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
  Object? invoke(PrioritizedIntents intent) {
    assert(_selectedAction != null);
    assert(_selectedIntent != null);
    _selectedAction.invoke(_selectedIntent);
  }
}