// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'basic.dart'; import 'focus_manager.dart'; import 'focus_scope.dart'; import 'framework.dart'; import 'media_query.dart'; import 'shortcuts.dart'; // 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) { late final BuildContext parent; context.visitAncestorElements((Element ancestor) { parent = ancestor; return false; }); return parent; } /// An abstract class representing a particular configuration of an [Action]. /// /// This class is what the [Shortcuts.shortcuts] map has as values, and is used /// by an [ActionDispatcher] to look up an action and invoke it, giving it this /// object to extract configuration information from. /// /// See also: /// /// * [Actions.invoke], which invokes the action associated with a specified /// [Intent] using the [Actions] widget that most tightly encloses the given /// [BuildContext]. @immutable abstract class Intent with Diagnosticable { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const Intent(); /// An intent that is mapped to a [DoNothingAction], which, as the name /// implies, does nothing. /// /// 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. static const DoNothingIntent doNothing = DoNothingIntent._(); } /// 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); /// 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. /// /// ### 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]. /// /// See the article on [Using Actions and /// Shortcuts](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts) /// for a detailed explanation. /// /// See also: /// /// * [Shortcuts], which is a widget that contains a key map, in which it looks /// up key combinations in order to invoke actions. /// * [Actions], which is a widget that defines a map of [Intent] to [Action] /// and allows redefining of actions for its descendants. /// * [ActionDispatcher], a class that takes an [Action] and invokes it, passing /// a given [Intent]. /// * [Action.overridable] for an example on how to make an [Action] /// overridable. abstract class Action<T extends Intent> with Diagnosticable { /// 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]. /// /// {@tool dartpad} /// 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. /// /// ** See code in examples/api/lib/widgets/actions/action.action_overridable.0.dart ** /// {@end-tool} factory Action.overridable({ required Action<T> defaultAction, required BuildContext context, }) { return defaultAction._makeOverridableAction(context); } final ObserverList<ActionListenerCallback> _listeners = ObserverList<ActionListenerCallback>(); Action<T>? _currentCallingAction; // ignore: use_setters_to_change_properties, (code predates enabling of this lint) void _updateCallingAction(Action<T>? value) { _currentCallingAction = value; } /// 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; /// Gets the type of intent this action responds to. Type get intentType => T; /// Returns true if the action is enabled and is ready to be invoked. /// /// This will be called by the [ActionDispatcher] before attempting to invoke /// the action. bool isEnabled(T intent) => isActionEnabled; /// Whether this [Action] is inherently enabled. /// /// If [isActionEnabled] is false, then this [Action] is disabled for any /// given [Intent]. // /// If the enabled state changes, overriding subclasses must call /// [notifyActionListeners] to notify any listeners of the change. /// /// In the case of an overridable `Action`, accessing this property creates /// an dependency on the overridable `Action`s `lookupContext`. bool get isActionEnabled => true; /// 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(T intent) => true; /// Converts the result of [invoke] of this action to a [KeyEventResult]. /// /// This is typically used when the action is invoked in response to a keyboard /// shortcut. /// /// The [invokeResult] argument is the value returned by the [invoke] method. /// /// By default, calls [consumesKey] and converts the returned boolean to /// [KeyEventResult.handled] if it's true, and [KeyEventResult.skipRemainingHandlers] /// if it's false. /// /// Concrete implementations may refine the type of [invokeResult], since /// they know the type returned by [invoke]. KeyEventResult toKeyEventResult(T intent, covariant Object? invokeResult) { return consumesKey(intent) ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers; } /// 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 /// its subclasses, and only when [isEnabled] is true. /// /// 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 an override of [invoke] returned an `int`, then it might /// be defined like so: /// /// ```dart /// class IncrementIntent extends Intent { /// const IncrementIntent({required this.index}); /// /// final int index; /// } /// /// class MyIncrementAction extends Action<IncrementIntent> { /// @override /// int invoke(IncrementIntent intent) { /// return intent.index + 1; /// } /// } /// ``` /// /// To receive the result of invoking an action, it must be invoked using /// [Actions.invoke], or by invoking it using an [ActionDispatcher]. An action /// invoked via a [Shortcuts] widget will have its return value ignored. @protected Object? invoke(T intent); /// 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. /// /// {@template flutter.widgets.Action.addActionListener} /// 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. /// /// {@macro flutter.widgets.Action.addActionListener} @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 @pragma('vm:notify-debugger-on-exception') 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>.of(_listeners); for (final ActionListenerCallback listener in localListeners) { InformationCollector? collector; assert(() { collector = () => <DiagnosticsNode>[ 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, )); } } } Action<T> _makeOverridableAction(BuildContext context) { return _OverridableAction<T>(defaultAction: this, lookupContext: context); } } /// 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. /// /// {@tool dartpad} /// This example shows how ActionListener handles adding and removing of /// the [listener] in the widget lifecycle. /// /// ** See code in examples/api/lib/widgets/actions/action_listener.0.dart ** /// {@end-tool} /// @immutable class ActionListener extends StatefulWidget { /// Create a const [ActionListener]. /// /// The [listener], [action], and [child] arguments must not be null. const ActionListener({ super.key, required this.listener, required this.action, required this.child, }); /// 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; /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; @override State<ActionListener> createState() => _ActionListenerState(); } class _ActionListenerState extends State<ActionListener> { @override void initState() { super.initState(); widget.action.addActionListener(widget.listener); } @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 /// its subclasses, and only when [isEnabled] is true. /// /// The optional `context` parameter is the context of the invocation of the /// action, and in the case of an action invoked by a [ShortcutManager], via /// 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 an override of [invoke] returned an `int`, then it might /// be defined like so: /// /// ```dart /// class IncrementIntent extends Intent { /// const IncrementIntent({required this.index}); /// /// final int index; /// } /// /// class MyIncrementAction extends ContextAction<IncrementIntent> { /// @override /// int invoke(IncrementIntent intent, [BuildContext? context]) { /// return intent.index + 1; /// } /// } /// ``` @protected @override Object? invoke(T intent, [BuildContext? context]); @override ContextAction<T> _makeOverridableAction(BuildContext context) { return _OverridableContextAction<T>(defaultAction: this, lookupContext: context); } } /// The signature of a callback accepted by [CallbackAction]. typedef OnInvokeCallback<T extends Intent> = Object? Function(T intent); /// An [Action] that takes a callback in order to configure it without having to /// create an explicit [Action] subclass just to call a callback. /// /// See also: /// /// * [Shortcuts], which is a widget that contains a key map, in which it looks /// up key combinations in order to invoke actions. /// * [Actions], which is a widget that defines a map of [Intent] to [Action] /// and allows redefining of actions for its descendants. /// * [ActionDispatcher], a class that takes an [Action] and invokes it using a /// [FocusNode] for context. class CallbackAction<T extends Intent> extends Action<T> { /// A constructor for a [CallbackAction]. /// /// The `intentKey` and [onInvoke] parameters must not be null. /// The [onInvoke] parameter is required. CallbackAction({required this.onInvoke}); /// The callback to be called when invoked. /// /// Must not be null. @protected final OnInvokeCallback<T> onInvoke; @override Object? invoke(T intent) => onInvoke(intent); } /// 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. /// /// 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]. class ActionDispatcher with Diagnosticable { /// Creates an action dispatcher that invokes actions directly. const ActionDispatcher(); /// Invokes the given `action`, passing it the given `intent`. /// /// 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. /// /// 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, ]) { 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); } } } /// A widget that establishes an [ActionDispatcher] and a map of [Intent] to /// [Action] to be used by its descendants when invoking an [Action]. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=XawP1i314WM} /// /// Actions are typically invoked using [Actions.invoke] with the context /// containing the ambient [Actions] widget. /// /// {@tool dartpad} /// 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. /// /// ** See code in examples/api/lib/widgets/actions/actions.0.dart ** /// {@end-tool} /// /// See also: /// /// * [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. class Actions extends StatefulWidget { /// Creates an [Actions] widget. /// /// The [child], [actions], and [dispatcher] arguments must not be null. const Actions({ super.key, this.dispatcher, required this.actions, required this.child, }); /// The [ActionDispatcher] object that invokes actions. /// /// This is what is returned from [Actions.of], and used by [Actions.invoke]. /// /// 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]. final ActionDispatcher? dispatcher; /// {@template flutter.widgets.actions.actions} /// A map of [Intent] keys to [Action<Intent>] objects that defines which /// actions this widget knows about. /// /// 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. /// {@endtemplate} final Map<Type, Action<Intent>> actions; /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; // Visits the Actions widget ancestors of the given element using // getElementForInheritedWidgetOfExactType. Returns true if the visitor found // what it was looking for. static bool _visitActionsAncestors(BuildContext context, bool Function(InheritedElement element) visitor) { InheritedElement? actionsElement = context.getElementForInheritedWidgetOfExactType<_ActionsScope>(); 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); actionsElement = parent.getElementForInheritedWidgetOfExactType<_ActionsScope>(); } return actionsElement != null; } // Finds the nearest valid ActionDispatcher, or creates a new one if it // doesn't find one. static ActionDispatcher _findDispatcher(BuildContext context) { ActionDispatcher? dispatcher; _visitActionsAncestors(context, (InheritedElement element) { final ActionDispatcher? found = (element.widget as _ActionsScope).dispatcher; if (found != null) { dispatcher = found; return true; } return false; }); return dispatcher ?? const ActionDispatcher(); } /// 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 /// not enabled, or no matching action is found. /// /// 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. static VoidCallback? handler<T extends Intent>(BuildContext context, T intent) { final Action<T>? action = Actions.maybeFind<T>(context); if (action != null && action.isEnabled(intent)) { return () { // 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); } }; } 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. /// /// 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 /// assert in debug mode, and throw an exception in release mode. /// /// See also: /// /// * [maybeFind], which is similar to this function, but will return null if /// no [Actions] ancestor is found. static Action<T> find<T extends Intent>(BuildContext context, { T? intent }) { final Action<T>? action = maybeFind(context, intent: intent); assert(() { if (action == null) { final Type type = intent?.runtimeType ?? T; throw FlutterError( 'Unable to find an action for a $type in an $Actions widget ' 'in the given context.\n' "$Actions.find() was called on a context that doesn't contain an " '$Actions widget with a mapping for the given intent type.\n' 'The context used was:\n' ' $context\n' 'The intent type requested was:\n' ' $type', ); } return true; }()); return action!; } /// Finds the [Action] bound to the given intent type `T` in the given `context`. /// /// Creates a dependency on the [Actions] widget that maps the bound action so /// that if the actions change, the context will be rebuilt and find the /// updated action. /// /// The optional `intent` argument supplies the type of the intent to look for /// if the concrete type of the intent sought isn't available. If not /// supplied, then `T` is used. /// /// If no [Actions] widget surrounds the given context, this function will /// return null. /// /// See also: /// /// * [find], which is similar to this function, but will throw if /// no [Actions] ancestor is found. static Action<T>? maybeFind<T extends Intent>(BuildContext context, { T? intent }) { 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 _ActionsScope actions = element.widget as _ActionsScope; final Action<T>? result = _castAction(actions, intent: intent); if (result != null) { context.dependOnInheritedElement(element); action = result; return true; } return false; }); return action; } 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 _ActionsScope actions = element.widget as _ActionsScope; 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 // `_ActionsScope`, and verify it has the right type parameter. static Action<T>? _castAction<T extends Intent>(_ActionsScope 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; } } /// Returns the [ActionDispatcher] associated with the [Actions] widget that /// most tightly encloses the given [BuildContext]. /// /// Will return a newly created [ActionDispatcher] if no ambient [Actions] /// widget is found. static ActionDispatcher of(BuildContext context) { final _ActionsScope? marker = context.dependOnInheritedWidgetOfExactType<_ActionsScope>(); return marker?.dispatcher ?? _findDispatcher(context); } /// 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. /// /// The `context` and `intent` arguments must not be null. /// /// 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. /// /// This method will throw an exception if no ambient [Actions] widget is /// found, or when a suitable [Action] is found but it returns false for /// [Action.isEnabled]. static Object? invoke<T extends Intent>( BuildContext context, T intent, ) { Object? returnValue; final bool actionFound = _visitActionsAncestors(context, (InheritedElement element) { final _ActionsScope actions = element.widget as _ActionsScope; 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); } return result != null; }); assert(() { if (!actionFound) { 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}', ); } return true; }()); return returnValue; } /// 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 /// first action found was 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, 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. static Object? maybeInvoke<T extends Intent>( BuildContext context, T intent, ) { Object? returnValue; _visitActionsAncestors(context, (InheritedElement element) { final _ActionsScope actions = element.widget as _ActionsScope; 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); } return result != null; }); return returnValue; } @override State<Actions> createState() => _ActionsState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher)); properties.add(DiagnosticsProperty<Map<Type, Action<Intent>>>('actions', actions)); } } class _ActionsState extends State<Actions> { // The set of actions that this Actions widget is current listening to. Set<Action<Intent>>? listenedActions = <Action<Intent>>{}; // Used to tell the marker to rebuild its dependencies when the state of an // action in the map changes. Object rebuildKey = Object(); @override void initState() { super.initState(); _updateActionListeners(); } void _handleActionChanged(Action<Intent> action) { // Generate a new key so that the marker notifies dependents. setState(() { rebuildKey = Object(); }); } void _updateActionListeners() { final Set<Action<Intent>> widgetActions = widget.actions.values.toSet(); final Set<Action<Intent>> removedActions = listenedActions!.difference(widgetActions); final Set<Action<Intent>> addedActions = widgetActions.difference(listenedActions!); for (final Action<Intent> action in removedActions) { action.removeActionListener(_handleActionChanged); } for (final Action<Intent> action in addedActions) { action.addActionListener(_handleActionChanged); } listenedActions = widgetActions; } @override void didUpdateWidget(Actions oldWidget) { super.didUpdateWidget(oldWidget); _updateActionListeners(); } @override void dispose() { super.dispose(); for (final Action<Intent> action in listenedActions!) { action.removeActionListener(_handleActionChanged); } listenedActions = null; } @override Widget build(BuildContext context) { return _ActionsScope( 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 _ActionsScope extends InheritedWidget { const _ActionsScope({ required this.dispatcher, required this.actions, required this.rebuildKey, required super.child, }); final ActionDispatcher? dispatcher; final Map<Type, Action<Intent>> actions; final Object rebuildKey; @override bool updateShouldNotify(_ActionsScope oldWidget) { return rebuildKey != oldWidget.rebuildKey || oldWidget.dispatcher != dispatcher || !mapEquals<Type, Action<Intent>>(oldWidget.actions, actions); } } /// 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. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=R84AGg0lKs8} /// /// 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. /// /// {@tool dartpad} /// 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 /// 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). /// /// 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 /// [CupertinoApp]), so the `ENTER` key will also activate the buttons. /// /// ** See code in examples/api/lib/widgets/actions/focusable_action_detector.0.dart ** /// {@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]. /// /// The [enabled], [autofocus], [mouseCursor], and [child] arguments must not be null. const FocusableActionDetector({ super.key, this.enabled = true, this.focusNode, this.autofocus = false, this.descendantsAreFocusable = true, this.descendantsAreTraversable = true, this.shortcuts, this.actions, this.onShowFocusHighlight, this.onShowHoverHighlight, this.onFocusChange, this.mouseCursor = MouseCursor.defer, this.includeFocusSemantics = true, required this.child, }); /// 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} final FocusNode? focusNode; /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// {@macro flutter.widgets.Focus.descendantsAreFocusable} final bool descendantsAreFocusable; /// {@macro flutter.widgets.Focus.descendantsAreTraversable} final bool descendantsAreTraversable; /// {@macro flutter.widgets.actions.actions} final Map<Type, Action<Intent>>? actions; /// {@macro flutter.widgets.shortcuts.shortcuts} final Map<ShortcutActivator, Intent>? shortcuts; /// 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. final ValueChanged<bool>? onShowFocusHighlight; /// A function that will be called when the hover highlight should be shown or hidden. /// /// This method is not triggered at the unmount of the widget. final ValueChanged<bool>? onShowHoverHighlight; /// A function that will be called when the focus changes. /// /// Called with true if the [focusNode] has primary focus. final ValueChanged<bool>? onFocusChange; /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. /// /// The [mouseCursor] defaults to [MouseCursor.defer], deferring the choice of /// cursor to the next region behind it in hit-test order. final MouseCursor mouseCursor; /// Whether to include semantics from [Focus]. /// /// Defaults to true. final bool includeFocusSemantics; /// The child widget for this [FocusableActionDetector] widget. /// /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; @override State<FocusableActionDetector> createState() => _FocusableActionDetectorState(); } class _FocusableActionDetectorState extends State<FocusableActionDetector> { @override void initState() { super.initState(); SchedulerBinding.instance.addPostFrameCallback((Duration duration) { _updateHighlightMode(FocusManager.instance.highlightMode); }); FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange); } @override void dispose() { FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange); super.dispose(); } bool _canShowHighlight = false; void _updateHighlightMode(FocusHighlightMode mode) { _mayTriggerCallback(task: () { switch (FocusManager.instance.highlightMode) { case FocusHighlightMode.touch: _canShowHighlight = false; break; case FocusHighlightMode.traditional: _canShowHighlight = true; break; } }); } // 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. void _handleFocusHighlightModeChange(FocusHighlightMode mode) { if (!mounted) { return; } _updateHighlightMode(mode); } bool _hovering = false; void _handleMouseEnter(PointerEnterEvent event) { if (!_hovering) { _mayTriggerCallback(task: () { _hovering = true; }); } } void _handleMouseExit(PointerExitEvent event) { if (_hovering) { _mayTriggerCallback(task: () { _hovering = false; }); } } bool _focused = false; void _handleFocusChange(bool focused) { if (_focused != focused) { _mayTriggerCallback(task: () { _focused = focused; }); widget.onFocusChange?.call(_focused); } } // 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. void _mayTriggerCallback({VoidCallback? task, FocusableActionDetector? oldWidget}) { bool shouldShowHoverHighlight(FocusableActionDetector target) { return _hovering && target.enabled && _canShowHighlight; } bool canRequestFocus(FocusableActionDetector target) { final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional; switch (mode) { case NavigationMode.traditional: return target.enabled; case NavigationMode.directional: return true; } } bool shouldShowFocusHighlight(FocusableActionDetector target) { return _focused && _canShowHighlight && canRequestFocus(target); } assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks); final FocusableActionDetector oldTarget = oldWidget ?? widget; final bool didShowHoverHighlight = shouldShowHoverHighlight(oldTarget); final bool didShowFocusHighlight = shouldShowFocusHighlight(oldTarget); if (task != null) { task(); } final bool doShowHoverHighlight = shouldShowHoverHighlight(widget); final bool doShowFocusHighlight = shouldShowFocusHighlight(widget); if (didShowFocusHighlight != doShowFocusHighlight) { widget.onShowFocusHighlight?.call(doShowFocusHighlight); } if (didShowHoverHighlight != doShowHoverHighlight) { widget.onShowHoverHighlight?.call(doShowHoverHighlight); } } @override void didUpdateWidget(FocusableActionDetector oldWidget) { super.didUpdateWidget(oldWidget); if (widget.enabled != oldWidget.enabled) { SchedulerBinding.instance.addPostFrameCallback((Duration duration) { _mayTriggerCallback(oldWidget: oldWidget); }); } } bool get _canRequestFocus { final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional; switch (mode) { case NavigationMode.traditional: return widget.enabled; case NavigationMode.directional: return true; } } // 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(); @override Widget build(BuildContext context) { Widget child = MouseRegion( key: _mouseRegionKey, onEnter: _handleMouseEnter, onExit: _handleMouseExit, cursor: widget.mouseCursor, child: Focus( focusNode: widget.focusNode, autofocus: widget.autofocus, descendantsAreFocusable: widget.descendantsAreFocusable, descendantsAreTraversable: widget.descendantsAreTraversable, canRequestFocus: _canRequestFocus, onFocusChange: _handleFocusChange, includeSemantics: widget.includeFocusSemantics, child: widget.child, ), ); if (widget.enabled && widget.actions != null && widget.actions!.isNotEmpty) { child = Actions(actions: widget.actions!, child: child); } if (widget.enabled && widget.shortcuts != null && widget.shortcuts!.isNotEmpty) { child = Shortcuts(shortcuts: widget.shortcuts!, child: child); } return child; } } /// An [Intent] that keeps a [VoidCallback] to be invoked by a /// [VoidCallbackAction] when it receives this intent. class VoidCallbackIntent extends Intent { /// Creates a [VoidCallbackIntent]. const VoidCallbackIntent(this.callback); /// The callback that is to be called by the [VoidCallbackAction] that /// receives this intent. final VoidCallback callback; } /// An [Action] that invokes the [VoidCallback] given to it in the /// [VoidCallbackIntent] passed to it when invoked. /// /// See also: /// /// * [CallbackAction], which is an action that will invoke a callback with the /// intent passed to the action's invoke method. The callback is configured /// on the action, not the intent, like this class. class VoidCallbackAction extends Action<VoidCallbackIntent> { @override Object? invoke(VoidCallbackIntent intent) { intent.callback(); return null; } } /// An [Intent] that is bound to a [DoNothingAction]. /// /// Attaching a [DoNothingIntent] to a [Shortcuts] mapping is one way to disable /// a keyboard shortcut defined by a widget higher in the widget hierarchy and /// consume any key event that triggers it via a shortcut. /// /// This intent cannot be subclassed. /// /// 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. class DoNothingIntent extends Intent { /// Creates a const [DoNothingIntent]. const factory DoNothingIntent() = DoNothingIntent._; // Make DoNothingIntent constructor private so it can't be subclassed. const DoNothingIntent._(); } /// 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]. const factory DoNothingAndStopPropagationIntent() = DoNothingAndStopPropagationIntent._; // Make DoNothingAndStopPropagationIntent constructor private so it can't be subclassed. const DoNothingAndStopPropagationIntent._(); } /// An [Action] that doesn't perform any action when invoked. /// /// Attaching a [DoNothingAction] to an [Actions.actions] mapping is a way to /// disable an action defined by a widget higher in the widget hierarchy. /// /// 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]. /// /// See also: /// - [DoNothingIntent], which is an intent that can be bound to a [KeySet] in /// a [Shortcuts] widget to do nothing. /// - [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. class DoNothingAction extends Action<Intent> { /// 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; @override void invoke(Intent intent) {} } /// 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]). class ActivateIntent extends Intent { /// Creates an intent that activates the currently focused control. const ActivateIntent(); } /// 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 { /// Creates an intent that activates the currently focused control, /// if it's a button. const ButtonActivateIntent(); } /// An [Action] that activates the currently focused control. /// /// This is an abstract class that serves as a base class for actions that /// 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 { /// Creates an intent that selects the currently focused control. const SelectIntent(); } /// An action that selects the currently focused control. /// /// This is an abstract class that serves as a base class for actions that /// select something. It is not bound to any key by default. abstract class SelectAction extends Action<SelectIntent> { } /// An [Intent] that dismisses the currently focused widget. /// /// The [WidgetsApp.defaultShortcuts] binds this intent to the /// [LogicalKeyboardKey.escape] and [LogicalKeyboardKey.gameButtonB] keys. /// /// See also: /// - [ModalRoute] which listens for this intent to dismiss modal routes /// (dialogs, pop-up menus, drawers, etc). class DismissIntent extends Intent { /// Creates an intent that dismisses the currently focused widget. 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> { } /// An [Intent] that evaluates a series of specified [orderedIntents] for /// execution. /// /// The first intent that matches an enabled action is used. class PrioritizedIntents extends Intent { /// Creates an intent that is used with [PrioritizedAction] to specify a list /// of intents, the first available of which will be used. const PrioritizedIntents({ required this.orderedIntents, }); /// 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 void invoke(PrioritizedIntents intent) { _selectedAction.invoke(_selectedIntent); } } 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 void _updateCallingAction(Action<T>? value) { super._updateCallingAction(value); defaultAction._updateCallingAction(value); } Object? _invokeOverride(Action<T> overrideAction, T intent, BuildContext? context) { assert(!debugAssertMutuallyRecursive); assert(() { debugAssertMutuallyRecursive = true; return true; }()); overrideAction._updateCallingAction(defaultAction); final Object? returnValue = overrideAction is ContextAction<T> ? overrideAction.invoke(intent, context) : overrideAction.invoke(intent); overrideAction._updateCallingAction(null); 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; }()); overrideAction._updateCallingAction(defaultAction); final bool isOverrideEnabled = overrideAction.isActionEnabled; overrideAction._updateCallingAction(null); 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(); overrideAction?._updateCallingAction(defaultAction); final bool returnValue = (overrideAction ?? defaultAction).isEnabled(intent); overrideAction?._updateCallingAction(null); assert(() { debugAssertIsEnabledMutuallyRecursive = false; return true; }()); return returnValue; } @override bool consumesKey(T intent) { assert(!debugAssertConsumeKeyMutuallyRecursive); assert(() { debugAssertConsumeKeyMutuallyRecursive = true; return true; }()); final Action<T>? overrideAction = getOverrideAction(); overrideAction?._updateCallingAction(defaultAction); final bool isEnabled = (overrideAction ?? defaultAction).consumesKey(intent); overrideAction?._updateCallingAction(null); 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); overrideAction._updateCallingAction(wrappedDefault); final Object? returnValue = overrideAction is ContextAction<T> ? overrideAction.invoke(intent, context) : overrideAction.invoke(intent); overrideAction._updateCallingAction(null); 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 void _updateCallingAction(Action<T>? value) { action._updateCallingAction(value); } @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); }