1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/cupertino.dart';
6/// @docImport 'package:flutter/material.dart';
7///
8/// @docImport 'app.dart';
9/// @docImport 'routes.dart';
10/// @docImport 'text_editing_intents.dart';
11library;
12
13import 'package:flutter/foundation.dart';
14import 'package:flutter/gestures.dart';
15import 'package:flutter/rendering.dart';
16import 'package:flutter/scheduler.dart';
17import 'package:flutter/services.dart';
18
19import 'basic.dart';
20import 'focus_manager.dart';
21import 'focus_scope.dart';
22import 'framework.dart';
23import 'media_query.dart';
24import 'shortcuts.dart';
25
26/// Returns the parent [BuildContext] of a given `context`.
27///
28/// [BuildContext] (or rather, [Element]) doesn't have a `parent` accessor, but
29/// the parent can be obtained using [BuildContext.visitAncestorElements].
30///
31/// [BuildContext.getElementForInheritedWidgetOfExactType] returns the same
32/// [BuildContext] if it happens to be of the correct type. To obtain the
33/// previous inherited widget, the search must therefore start from the parent;
34/// this is what [_getParent] is used for.
35///
36/// [_getParent] is O(1), because it always stops at the first ancestor.
37BuildContext _getParent(BuildContext context) {
38 late final BuildContext parent;
39 context.visitAncestorElements((Element ancestor) {
40 parent = ancestor;
41 return false;
42 });
43 return parent;
44}
45
46/// An abstract class representing a particular configuration of an [Action].
47///
48/// This class is what the [Shortcuts.shortcuts] map has as values, and is used
49/// by an [ActionDispatcher] to look up an action and invoke it, giving it this
50/// object to extract configuration information from.
51///
52/// See also:
53///
54/// * [Shortcuts], a widget used to bind key combinations to [Intent]s.
55/// * [Actions], a widget used to map [Intent]s to [Action]s.
56/// * [Actions.invoke], which invokes the action associated with a specified
57/// [Intent] using the [Actions] widget that most tightly encloses the given
58/// [BuildContext].
59@immutable
60abstract class Intent with Diagnosticable {
61 /// Abstract const constructor. This constructor enables subclasses to provide
62 /// const constructors so that they can be used in const expressions.
63 const Intent();
64
65 /// An intent that is mapped to a [DoNothingAction], which, as the name
66 /// implies, does nothing.
67 ///
68 /// This Intent is mapped to an action in the [WidgetsApp] that does nothing,
69 /// so that it can be bound to a key in a [Shortcuts] widget in order to
70 /// disable a key binding made above it in the hierarchy.
71 static const DoNothingIntent doNothing = DoNothingIntent._();
72}
73
74/// The kind of callback that an [Action] uses to notify of changes to the
75/// action's state.
76///
77/// To register an action listener, call [Action.addActionListener].
78typedef ActionListenerCallback = void Function(Action<Intent> action);
79
80/// Base class for an action or command to be performed.
81///
82/// {@youtube 560 315 https://www.youtube.com/watch?v=XawP1i314WM}
83///
84/// [Action]s are typically invoked as a result of a user action. For example,
85/// the [Shortcuts] widget will map a keyboard shortcut into an [Intent], which
86/// is given to an [ActionDispatcher] to map the [Intent] to an [Action] and
87/// invoke it.
88///
89/// The [ActionDispatcher] can invoke an [Action] on the primary focus, or
90/// without regard for focus.
91///
92/// ### Action Overriding
93///
94/// When using a leaf widget to build a more specialized widget, it's sometimes
95/// desirable to change the default handling of an [Intent] defined in the leaf
96/// widget. For instance, [TextField]'s [SelectAllTextIntent] by default selects
97/// the text it currently contains, but in a US phone number widget that
98/// consists of 3 different [TextField]s (area code, prefix and line number),
99/// [SelectAllTextIntent] should instead select the text within all 3
100/// [TextField]s.
101///
102/// An overridable [Action] is a special kind of [Action] created using the
103/// [Action.overridable] constructor. It has access to a default [Action], and a
104/// nullable override [Action]. It has the same behavior as its override if that
105/// exists, and mirrors the behavior of its `defaultAction` otherwise.
106///
107/// The [Action.overridable] constructor creates overridable [Action]s that use
108/// a [BuildContext] to find a suitable override in its ancestor [Actions]
109/// widget. This can be used to provide a default implementation when creating a
110/// general purpose leaf widget, and later override it when building a more
111/// specialized widget using that leaf widget. Using the [TextField] example
112/// above, the [TextField] widget uses an overridable [Action] to provide a
113/// sensible default for [SelectAllTextIntent], while still allowing app
114/// developers to change that if they add an ancestor [Actions] widget that maps
115/// [SelectAllTextIntent] to a different [Action].
116///
117/// See the article on
118/// [Using Actions and Shortcuts](https://flutter.dev/to/actions-shortcuts)
119/// for a detailed explanation.
120///
121/// See also:
122///
123/// * [Shortcuts], which is a widget that contains a key map, in which it looks
124/// up key combinations in order to invoke actions.
125/// * [Actions], which is a widget that defines a map of [Intent] to [Action]
126/// and allows redefining of actions for its descendants.
127/// * [ActionDispatcher], a class that takes an [Action] and invokes it, passing
128/// a given [Intent].
129/// * [Action.overridable] for an example on how to make an [Action]
130/// overridable.
131abstract class Action<T extends Intent> with Diagnosticable {
132 /// Creates an [Action].
133 Action();
134
135 /// Creates an [Action] that allows itself to be overridden by the closest
136 /// ancestor [Action] in the given [context] that handles the same [Intent],
137 /// if one exists.
138 ///
139 /// When invoked, the resulting [Action] tries to find the closest [Action] in
140 /// the given `context` that handles the same type of [Intent] as the
141 /// `defaultAction`, then calls its [Action.invoke] method. When no override
142 /// [Action]s can be found, it invokes the `defaultAction`.
143 ///
144 /// An overridable action delegates everything to its override if one exists,
145 /// and has the same behavior as its `defaultAction` otherwise. For this
146 /// reason, the override has full control over whether and how an [Intent]
147 /// should be handled, or a key event should be consumed. An override
148 /// [Action]'s [callingAction] property will be set to the [Action] it
149 /// currently overrides, giving it access to the default behavior. See the
150 /// [callingAction] property for an example.
151 ///
152 /// The `context` argument is the [BuildContext] to find the override with. It
153 /// is typically a [BuildContext] above the [Actions] widget that contains
154 /// this overridable [Action].
155 ///
156 /// The `defaultAction` argument is the [Action] to be invoked where there's
157 /// no ancestor [Action]s can't be found in `context` that handle the same
158 /// type of [Intent].
159 ///
160 /// This is useful for providing a set of default [Action]s in a leaf widget
161 /// to allow further overriding, or to allow the [Intent] to propagate to
162 /// parent widgets that also support this [Intent].
163 ///
164 /// {@tool dartpad}
165 /// This sample shows how to implement a rudimentary `CopyableText` widget
166 /// that responds to Ctrl-C by copying its own content to the clipboard.
167 ///
168 /// if `CopyableText` is to be provided in a package, developers using the
169 /// widget may want to change how copying is handled. As the author of the
170 /// package, you can enable that by making the corresponding [Action]
171 /// overridable. In the second part of the code sample, three `CopyableText`
172 /// widgets are used to build a verification code widget which overrides the
173 /// "copy" action by copying the combined numbers from all three `CopyableText`
174 /// widgets.
175 ///
176 /// ** See code in examples/api/lib/widgets/actions/action.action_overridable.0.dart **
177 /// {@end-tool}
178 factory Action.overridable({required Action<T> defaultAction, required BuildContext context}) {
179 return defaultAction._makeOverridableAction(context);
180 }
181
182 final ObserverList<ActionListenerCallback> _listeners = ObserverList<ActionListenerCallback>();
183
184 Action<T>? _currentCallingAction;
185 // ignore: use_setters_to_change_properties, (code predates enabling of this lint)
186 void _updateCallingAction(Action<T>? value) {
187 _currentCallingAction = value;
188 }
189
190 /// The [Action] overridden by this [Action].
191 ///
192 /// The [Action.overridable] constructor creates an overridable [Action] that
193 /// allows itself to be overridden by the closest ancestor [Action], and falls
194 /// back to its own `defaultAction` when no overrides can be found. When an
195 /// override is present, an overridable [Action] forwards all incoming
196 /// method calls to the override, and allows the override to access the
197 /// `defaultAction` via its [callingAction] property.
198 ///
199 /// Before forwarding the call to the override, the overridable [Action] is
200 /// responsible for setting [callingAction] to its `defaultAction`, which is
201 /// already taken care of by the overridable [Action] created using
202 /// [Action.overridable].
203 ///
204 /// This property is only non-null when this [Action] is an override of the
205 /// [callingAction], and is currently being invoked from [callingAction].
206 ///
207 /// Invoking [callingAction]'s methods, or accessing its properties, is
208 /// allowed and does not introduce infinite loops or infinite recursions.
209 ///
210 /// {@tool snippet}
211 /// An example `Action` that handles [PasteTextIntent] but has mostly the same
212 /// behavior as the overridable action. It's OK to call
213 /// `callingAction?.isActionEnabled` in the implementation of this `Action`.
214 ///
215 /// ```dart
216 /// class MyPasteAction extends Action<PasteTextIntent> {
217 /// @override
218 /// Object? invoke(PasteTextIntent intent) {
219 /// print(intent);
220 /// return callingAction?.invoke(intent);
221 /// }
222 ///
223 /// @override
224 /// bool get isActionEnabled => callingAction?.isActionEnabled ?? false;
225 ///
226 /// @override
227 /// bool consumesKey(PasteTextIntent intent) => callingAction?.consumesKey(intent) ?? false;
228 /// }
229 /// ```
230 /// {@end-tool}
231 @protected
232 Action<T>? get callingAction => _currentCallingAction;
233
234 /// Gets the type of intent this action responds to.
235 Type get intentType => T;
236
237 /// Returns true if the action is enabled and is ready to be invoked.
238 ///
239 /// This will be called by the [ActionDispatcher] before attempting to invoke
240 /// the action.
241 ///
242 /// If the action's enable state depends on a [BuildContext], subclass
243 /// [ContextAction] instead of [Action].
244 bool isEnabled(T intent) => isActionEnabled;
245
246 bool _isEnabled(T intent, BuildContext? context) => switch (this) {
247 final ContextAction<T> action => action.isEnabled(intent, context),
248 _ => isEnabled(intent),
249 };
250
251 /// Whether this [Action] is inherently enabled.
252 ///
253 /// If [isActionEnabled] is false, then this [Action] is disabled for any
254 /// given [Intent].
255 //
256 /// If the enabled state changes, overriding subclasses must call
257 /// [notifyActionListeners] to notify any listeners of the change.
258 ///
259 /// In the case of an overridable `Action`, accessing this property creates
260 /// an dependency on the overridable `Action`s `lookupContext`.
261 bool get isActionEnabled => true;
262
263 /// Indicates whether this action should treat key events mapped to this
264 /// action as being "handled" when it is invoked via the key event.
265 ///
266 /// If the key is handled, then no other key event handlers in the focus chain
267 /// will receive the event.
268 ///
269 /// If the key event is not handled, it will be passed back to the engine, and
270 /// continue to be processed there, allowing text fields and non-Flutter
271 /// widgets to receive the key event.
272 ///
273 /// The default implementation returns true.
274 bool consumesKey(T intent) => true;
275
276 /// Converts the result of [invoke] of this action to a [KeyEventResult].
277 ///
278 /// This is typically used when the action is invoked in response to a keyboard
279 /// shortcut.
280 ///
281 /// The [invokeResult] argument is the value returned by the [invoke] method.
282 ///
283 /// By default, calls [consumesKey] and converts the returned boolean to
284 /// [KeyEventResult.handled] if it's true, and [KeyEventResult.skipRemainingHandlers]
285 /// if it's false.
286 ///
287 /// Concrete implementations may refine the type of [invokeResult], since
288 /// they know the type returned by [invoke].
289 KeyEventResult toKeyEventResult(T intent, covariant Object? invokeResult) {
290 return consumesKey(intent) ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers;
291 }
292
293 /// Called when the action is to be performed.
294 ///
295 /// This is called by the [ActionDispatcher] when an action is invoked via
296 /// [Actions.invoke], or when an action is invoked using
297 /// [ActionDispatcher.invokeAction] directly.
298 ///
299 /// This method is only meant to be invoked by an [ActionDispatcher], or by
300 /// its subclasses, and only when [isEnabled] is true.
301 ///
302 /// When overriding this method, the returned value can be any [Object], but
303 /// changing the return type of the override to match the type of the returned
304 /// value provides more type safety.
305 ///
306 /// For instance, if an override of [invoke] returned an `int`, then it might
307 /// be defined like so:
308 ///
309 /// ```dart
310 /// class IncrementIntent extends Intent {
311 /// const IncrementIntent({required this.index});
312 ///
313 /// final int index;
314 /// }
315 ///
316 /// class MyIncrementAction extends Action<IncrementIntent> {
317 /// @override
318 /// int invoke(IncrementIntent intent) {
319 /// return intent.index + 1;
320 /// }
321 /// }
322 /// ```
323 ///
324 /// To receive the result of invoking an action, it must be invoked using
325 /// [Actions.invoke], or by invoking it using an [ActionDispatcher]. An action
326 /// invoked via a [Shortcuts] widget will have its return value ignored.
327 ///
328 /// If the action's behavior depends on a [BuildContext], subclass
329 /// [ContextAction] instead of [Action].
330 @protected
331 Object? invoke(T intent);
332
333 Object? _invoke(T intent, BuildContext? context) => switch (this) {
334 final ContextAction<T> action => action.invoke(intent, context),
335 _ => invoke(intent),
336 };
337
338 /// Register a callback to listen for changes to the state of this action.
339 ///
340 /// If you call this, you must call [removeActionListener] a matching number
341 /// of times, or memory leaks will occur. To help manage this and avoid memory
342 /// leaks, use of the [ActionListener] widget to register and unregister your
343 /// listener appropriately is highly recommended.
344 ///
345 /// {@template flutter.widgets.Action.addActionListener}
346 /// If a listener had been added twice, and is removed once during an
347 /// iteration (i.e. in response to a notification), it will still be called
348 /// again. If, on the other hand, it is removed as many times as it was
349 /// registered, then it will no longer be called. This odd behavior is the
350 /// result of the [Action] not being able to determine which listener
351 /// is being removed, since they are identical, and therefore conservatively
352 /// still calling all the listeners when it knows that any are still
353 /// registered.
354 ///
355 /// This surprising behavior can be unexpectedly observed when registering a
356 /// listener on two separate objects which are both forwarding all
357 /// registrations to a common upstream object.
358 /// {@endtemplate}
359 @mustCallSuper
360 void addActionListener(ActionListenerCallback listener) => _listeners.add(listener);
361
362 /// Remove a previously registered closure from the list of closures that are
363 /// notified when the object changes.
364 ///
365 /// If the given listener is not registered, the call is ignored.
366 ///
367 /// If you call [addActionListener], you must call this method a matching
368 /// number of times, or memory leaks will occur. To help manage this and avoid
369 /// memory leaks, use of the [ActionListener] widget to register and
370 /// unregister your listener appropriately is highly recommended.
371 ///
372 /// {@macro flutter.widgets.Action.addActionListener}
373 @mustCallSuper
374 void removeActionListener(ActionListenerCallback listener) => _listeners.remove(listener);
375
376 /// Call all the registered listeners.
377 ///
378 /// Subclasses should call this method whenever the object changes, to notify
379 /// any clients the object may have changed. Listeners that are added during this
380 /// iteration will not be visited. Listeners that are removed during this
381 /// iteration will not be visited after they are removed.
382 ///
383 /// Exceptions thrown by listeners will be caught and reported using
384 /// [FlutterError.reportError].
385 ///
386 /// Surprising behavior can result when reentrantly removing a listener (i.e.
387 /// in response to a notification) that has been registered multiple times.
388 /// See the discussion at [removeActionListener].
389 @protected
390 @visibleForTesting
391 @pragma('vm:notify-debugger-on-exception')
392 void notifyActionListeners() {
393 if (_listeners.isEmpty) {
394 return;
395 }
396
397 // Make a local copy so that a listener can unregister while the list is
398 // being iterated over.
399 final List<ActionListenerCallback> localListeners = List<ActionListenerCallback>.of(_listeners);
400 for (final ActionListenerCallback listener in localListeners) {
401 InformationCollector? collector;
402 assert(() {
403 collector = () => <DiagnosticsNode>[
404 DiagnosticsProperty<Action<T>>(
405 'The $runtimeType sending notification was',
406 this,
407 style: DiagnosticsTreeStyle.errorProperty,
408 ),
409 ];
410 return true;
411 }());
412 try {
413 if (_listeners.contains(listener)) {
414 listener(this);
415 }
416 } catch (exception, stack) {
417 FlutterError.reportError(
418 FlutterErrorDetails(
419 exception: exception,
420 stack: stack,
421 library: 'widgets library',
422 context: ErrorDescription('while dispatching notifications for $runtimeType'),
423 informationCollector: collector,
424 ),
425 );
426 }
427 }
428 }
429
430 Action<T> _makeOverridableAction(BuildContext context) {
431 return _OverridableAction<T>(defaultAction: this, lookupContext: context);
432 }
433}
434
435/// A helper widget for making sure that listeners on an action are removed properly.
436///
437/// Listeners on the [Action] class must have their listener callbacks removed
438/// with [Action.removeActionListener] when the listener is disposed of. This widget
439/// helps with that, by providing a lifetime for the connection between the
440/// [listener] and the [Action], and by handling the adding and removing of
441/// the [listener] at the right points in the widget lifecycle.
442///
443/// If you listen to an [Action] widget in a widget hierarchy, you should use
444/// this widget. If you are using an [Action] outside of a widget context, then
445/// you must call removeListener yourself.
446///
447/// {@tool dartpad}
448/// This example shows how ActionListener handles adding and removing of
449/// the [listener] in the widget lifecycle.
450///
451/// ** See code in examples/api/lib/widgets/actions/action_listener.0.dart **
452/// {@end-tool}
453///
454@immutable
455class ActionListener extends StatefulWidget {
456 /// Create a const [ActionListener].
457 const ActionListener({
458 super.key,
459 required this.listener,
460 required this.action,
461 required this.child,
462 });
463
464 /// The [ActionListenerCallback] callback to register with the [action].
465 final ActionListenerCallback listener;
466
467 /// The [Action] that the callback will be registered with.
468 final Action<Intent> action;
469
470 /// {@macro flutter.widgets.ProxyWidget.child}
471 final Widget child;
472
473 @override
474 State<ActionListener> createState() => _ActionListenerState();
475}
476
477class _ActionListenerState extends State<ActionListener> {
478 @override
479 void initState() {
480 super.initState();
481 widget.action.addActionListener(widget.listener);
482 }
483
484 @override
485 void didUpdateWidget(ActionListener oldWidget) {
486 super.didUpdateWidget(oldWidget);
487 if (oldWidget.action == widget.action && oldWidget.listener == widget.listener) {
488 return;
489 }
490 oldWidget.action.removeActionListener(oldWidget.listener);
491 widget.action.addActionListener(widget.listener);
492 }
493
494 @override
495 void dispose() {
496 widget.action.removeActionListener(widget.listener);
497 super.dispose();
498 }
499
500 @override
501 Widget build(BuildContext context) => widget.child;
502}
503
504/// An abstract [Action] subclass that adds an optional [BuildContext] to the
505/// [isEnabled] and [invoke] methods to be able to provide context to actions.
506///
507/// [ActionDispatcher.invokeAction] checks to see if the action it is invoking
508/// is a [ContextAction], and if it is, supplies it with a context.
509abstract class ContextAction<T extends Intent> extends Action<T> {
510 /// Returns true if the action is enabled and is ready to be invoked.
511 ///
512 /// This will be called by the [ActionDispatcher] before attempting to invoke
513 /// the action.
514 ///
515 /// The optional `context` parameter is the context of the invocation of the
516 /// action, and in the case of an action invoked by a [ShortcutManager], via
517 /// a [Shortcuts] widget, will be the context of the [Shortcuts] widget.
518 @override
519 bool isEnabled(T intent, [BuildContext? context]) => super.isEnabled(intent);
520
521 /// Called when the action is to be performed.
522 ///
523 /// This is called by the [ActionDispatcher] when an action is invoked via
524 /// [Actions.invoke], or when an action is invoked using
525 /// [ActionDispatcher.invokeAction] directly.
526 ///
527 /// This method is only meant to be invoked by an [ActionDispatcher], or by
528 /// its subclasses, and only when [isEnabled] is true.
529 ///
530 /// The optional `context` parameter is the context of the invocation of the
531 /// action, and in the case of an action invoked by a [ShortcutManager], via
532 /// a [Shortcuts] widget, will be the context of the [Shortcuts] widget.
533 ///
534 /// When overriding this method, the returned value can be any Object, but
535 /// changing the return type of the override to match the type of the returned
536 /// value provides more type safety.
537 ///
538 /// For instance, if an override of [invoke] returned an `int`, then it might
539 /// be defined like so:
540 ///
541 /// ```dart
542 /// class IncrementIntent extends Intent {
543 /// const IncrementIntent({required this.index});
544 ///
545 /// final int index;
546 /// }
547 ///
548 /// class MyIncrementAction extends ContextAction<IncrementIntent> {
549 /// @override
550 /// int invoke(IncrementIntent intent, [BuildContext? context]) {
551 /// return intent.index + 1;
552 /// }
553 /// }
554 /// ```
555 @protected
556 @override
557 Object? invoke(T intent, [BuildContext? context]);
558
559 @override
560 ContextAction<T> _makeOverridableAction(BuildContext context) {
561 return _OverridableContextAction<T>(defaultAction: this, lookupContext: context);
562 }
563}
564
565/// The signature of a callback accepted by [CallbackAction.onInvoke].
566///
567/// Such callbacks are implementations of [Action.invoke]. The returned value
568/// is the return value of [Action.invoke], the argument is the intent passed
569/// to [Action.invoke], and so forth.
570typedef OnInvokeCallback<T extends Intent> = Object? Function(T intent);
571
572/// An [Action] that takes a callback in order to configure it without having to
573/// create an explicit [Action] subclass just to call a callback.
574///
575/// See also:
576///
577/// * [Shortcuts], which is a widget that contains a key map, in which it looks
578/// up key combinations in order to invoke actions.
579/// * [Actions], which is a widget that defines a map of [Intent] to [Action]
580/// and allows redefining of actions for its descendants.
581/// * [ActionDispatcher], a class that takes an [Action] and invokes it using a
582/// [FocusNode] for context.
583class CallbackAction<T extends Intent> extends Action<T> {
584 /// A constructor for a [CallbackAction].
585 ///
586 /// The given callback is used as the implementation of [invoke].
587 CallbackAction({required this.onInvoke});
588
589 /// The callback to be called when invoked.
590 ///
591 /// This is effectively the implementation of [invoke].
592 @protected
593 final OnInvokeCallback<T> onInvoke;
594
595 @override
596 Object? invoke(T intent) => onInvoke(intent);
597}
598
599/// An action dispatcher that invokes the actions given to it.
600///
601/// The [invokeAction] method on this class directly calls the [Action.invoke]
602/// method on the [Action] object.
603///
604/// For [ContextAction] actions, if no `context` is provided, the
605/// [BuildContext] of the [primaryFocus] is used instead.
606///
607/// See also:
608///
609/// - [ShortcutManager], that uses this class to invoke actions.
610/// - [Shortcuts] widget, which defines key mappings to [Intent]s.
611/// - [Actions] widget, which defines a mapping between a in [Intent] type and
612/// an [Action].
613class ActionDispatcher with Diagnosticable {
614 /// Creates an action dispatcher that invokes actions directly.
615 const ActionDispatcher();
616
617 /// Invokes the given `action`, passing it the given `intent`.
618 ///
619 /// The action will be invoked with the given `context`, if given, but only if
620 /// the action is a [ContextAction] subclass. If no `context` is given, and
621 /// the action is a [ContextAction], then the context from the [primaryFocus]
622 /// is used.
623 ///
624 /// Returns the object returned from [Action.invoke].
625 ///
626 /// The caller must receive a `true` result from [Action.isEnabled] before
627 /// calling this function (or [ContextAction.isEnabled] with the same
628 /// `context`, if the `action` is a [ContextAction]). This function will
629 /// assert if the action is not enabled when called.
630 ///
631 /// Consider using [invokeActionIfEnabled] to invoke the action conditionally
632 /// based on whether it is enabled or not, without having to check first.
633 Object? invokeAction(
634 covariant Action<Intent> action,
635 covariant Intent intent, [
636 BuildContext? context,
637 ]) {
638 final BuildContext? target = context ?? primaryFocus?.context;
639 assert(action._isEnabled(intent, target), 'Action must be enabled when calling invokeAction');
640 return action._invoke(intent, target);
641 }
642
643 /// Invokes the given `action`, passing it the given `intent`, but only if the
644 /// action is enabled.
645 ///
646 /// The action will be invoked with the given `context`, if given, but only if
647 /// the action is a [ContextAction] subclass. If no `context` is given, and
648 /// the action is a [ContextAction], then the context from the [primaryFocus]
649 /// is used.
650 ///
651 /// The return value has two components. The first is a boolean indicating if
652 /// the action was enabled (as per [Action.isEnabled]). If this is false, the
653 /// second return value is null. Otherwise, the second return value is the
654 /// object returned from [Action.invoke].
655 ///
656 /// Consider using [invokeAction] if the enabled state of the action is not in
657 /// question; this avoids calling [Action.isEnabled] redundantly.
658 (bool, Object?) invokeActionIfEnabled(
659 covariant Action<Intent> action,
660 covariant Intent intent, [
661 BuildContext? context,
662 ]) {
663 final BuildContext? target = context ?? primaryFocus?.context;
664 if (action._isEnabled(intent, target)) {
665 return (true, action._invoke(intent, target));
666 }
667 return (false, null);
668 }
669}
670
671/// A widget that maps [Intent]s to [Action]s to be used by its descendants
672/// when invoking an [Action].
673///
674/// {@youtube 560 315 https://www.youtube.com/watch?v=XawP1i314WM}
675///
676/// Actions are typically invoked using [Shortcuts]. They can also be invoked
677/// using [Actions.invoke] on a context containing an ambient [Actions] widget.
678///
679/// {@tool dartpad}
680/// This example creates a custom [Action] subclass `ModifyAction` for modifying
681/// a model, and another, `SaveAction` for saving it.
682///
683/// This example demonstrates passing arguments to the [Intent] to be carried to
684/// the [Action]. Actions can get data either from their own construction (like
685/// the `model` in this example), or from the intent passed to them when invoked
686/// (like the increment `amount` in this example).
687///
688/// This example also demonstrates how to use Intents to limit a widget's
689/// dependencies on its surroundings. The `SaveButton` widget defined in this
690/// example can invoke actions defined in its ancestor widgets, which can be
691/// customized to match the part of the widget tree that it is in. It doesn't
692/// need to know about the `SaveAction` class, only the `SaveIntent`, and it
693/// only needs to know about a value notifier, not the entire model.
694///
695/// ** See code in examples/api/lib/widgets/actions/actions.0.dart **
696/// {@end-tool}
697///
698/// See also:
699///
700/// * [Shortcuts], a widget used to bind key combinations to [Intent]s.
701/// * [Intent], a class that contains configuration information for running an
702/// [Action].
703/// * [Action], a class for containing and defining an invocation of a user
704/// action.
705/// * [ActionDispatcher], the object that this widget uses to manage actions.
706class Actions extends StatefulWidget {
707 /// Creates an [Actions] widget.
708 const Actions({super.key, this.dispatcher, required this.actions, required this.child});
709
710 /// The [ActionDispatcher] object that invokes actions.
711 ///
712 /// This is what is returned from [Actions.of], and used by [Actions.invoke].
713 ///
714 /// If this [dispatcher] is null, then [Actions.of] and [Actions.invoke] will
715 /// look up the tree until they find an Actions widget that has a dispatcher
716 /// set. If no such widget is found, then they will return/use a
717 /// default-constructed [ActionDispatcher].
718 final ActionDispatcher? dispatcher;
719
720 /// {@template flutter.widgets.actions.actions}
721 /// A map of [Intent] keys to [Action<Intent>] objects that defines which
722 /// actions this widget knows about.
723 ///
724 /// For performance reasons, it is recommended that a pre-built map is
725 /// passed in here (e.g. a final variable from your widget class) instead of
726 /// defining it inline in the build function.
727 /// {@endtemplate}
728 final Map<Type, Action<Intent>> actions;
729
730 /// {@macro flutter.widgets.ProxyWidget.child}
731 final Widget child;
732
733 // Visits the Actions widget ancestors of the given element using
734 // getElementForInheritedWidgetOfExactType. Returns true if the visitor found
735 // what it was looking for.
736 static bool _visitActionsAncestors(
737 BuildContext context,
738 bool Function(InheritedElement element) visitor,
739 ) {
740 if (!context.mounted) {
741 return false;
742 }
743 InheritedElement? actionsElement = context
744 .getElementForInheritedWidgetOfExactType<_ActionsScope>();
745 while (actionsElement != null) {
746 if (visitor(actionsElement)) {
747 break;
748 }
749 // _getParent is needed here because
750 // context.getElementForInheritedWidgetOfExactType will return itself if it
751 // happens to be of the correct type.
752 final BuildContext parent = _getParent(actionsElement);
753 actionsElement = parent.getElementForInheritedWidgetOfExactType<_ActionsScope>();
754 }
755 return actionsElement != null;
756 }
757
758 // Finds the nearest valid ActionDispatcher, or creates a new one if it
759 // doesn't find one.
760 static ActionDispatcher _findDispatcher(BuildContext context) {
761 ActionDispatcher? dispatcher;
762 _visitActionsAncestors(context, (InheritedElement element) {
763 final ActionDispatcher? found = (element.widget as _ActionsScope).dispatcher;
764 if (found != null) {
765 dispatcher = found;
766 return true;
767 }
768 return false;
769 });
770 return dispatcher ?? const ActionDispatcher();
771 }
772
773 /// Returns a [VoidCallback] handler that invokes the bound action for the
774 /// given `intent` if the action is enabled, and returns null if the action is
775 /// not enabled, or no matching action is found.
776 ///
777 /// This is intended to be used in widgets which have something similar to an
778 /// `onTap` handler, which takes a `VoidCallback`, and can be set to the
779 /// result of calling this function.
780 ///
781 /// Creates a dependency on the [Actions] widget that maps the bound action so
782 /// that if the actions change, the context will be rebuilt and find the
783 /// updated action.
784 ///
785 /// The value returned from the [Action.invoke] method is discarded when the
786 /// returned callback is called. If the return value is needed, consider using
787 /// [Actions.invoke] instead.
788 static VoidCallback? handler<T extends Intent>(BuildContext context, T intent) {
789 final Action<T>? action = Actions.maybeFind<T>(context);
790 if (action != null && action._isEnabled(intent, context)) {
791 return () {
792 // Could be that the action was enabled when the closure was created,
793 // but is now no longer enabled, so check again.
794 if (action._isEnabled(intent, context)) {
795 Actions.of(context).invokeAction(action, intent, context);
796 }
797 };
798 }
799 return null;
800 }
801
802 /// Finds the [Action] bound to the given intent type `T` in the given `context`.
803 ///
804 /// Creates a dependency on the [Actions] widget that maps the bound action so
805 /// that if the actions change, the context will be rebuilt and find the
806 /// updated action.
807 ///
808 /// The optional `intent` argument supplies the type of the intent to look for
809 /// if the concrete type of the intent sought isn't available. If not
810 /// supplied, then `T` is used.
811 ///
812 /// If no [Actions] widget surrounds the given context, this function will
813 /// assert in debug mode, and throw an exception in release mode.
814 ///
815 /// See also:
816 ///
817 /// * [maybeFind], which is similar to this function, but will return null if
818 /// no [Actions] ancestor is found.
819 static Action<T> find<T extends Intent>(BuildContext context, {T? intent}) {
820 final Action<T>? action = maybeFind(context, intent: intent);
821
822 assert(() {
823 if (action == null) {
824 final Type type = intent?.runtimeType ?? T;
825 throw FlutterError(
826 'Unable to find an action for a $type in an $Actions widget '
827 'in the given context.\n'
828 "$Actions.find() was called on a context that doesn't contain an "
829 '$Actions widget with a mapping for the given intent type.\n'
830 'The context used was:\n'
831 ' $context\n'
832 'The intent type requested was:\n'
833 ' $type',
834 );
835 }
836 return true;
837 }());
838 return action!;
839 }
840
841 /// Finds the [Action] bound to the given intent type `T` in the given `context`.
842 ///
843 /// Creates a dependency on the [Actions] widget that maps the bound action so
844 /// that if the actions change, the context will be rebuilt and find the
845 /// updated action.
846 ///
847 /// The optional `intent` argument supplies the type of the intent to look for
848 /// if the concrete type of the intent sought isn't available. If not
849 /// supplied, then `T` is used.
850 ///
851 /// If no [Actions] widget surrounds the given context, this function will
852 /// return null.
853 ///
854 /// See also:
855 ///
856 /// * [find], which is similar to this function, but will throw if
857 /// no [Actions] ancestor is found.
858 static Action<T>? maybeFind<T extends Intent>(BuildContext context, {T? intent}) {
859 Action<T>? action;
860
861 // Specialize the type if a runtime example instance of the intent is given.
862 // This allows this function to be called by code that doesn't know the
863 // concrete type of the intent at compile time.
864 final Type type = intent?.runtimeType ?? T;
865 assert(
866 type != Intent,
867 'The type passed to "find" resolved to "Intent": either a non-Intent '
868 'generic type argument or an example intent derived from Intent must be '
869 'specified. Intent may be used as the generic type as long as the optional '
870 '"intent" argument is passed.',
871 );
872
873 _visitActionsAncestors(context, (InheritedElement element) {
874 final _ActionsScope actions = element.widget as _ActionsScope;
875 final Action<T>? result = _castAction(actions, intent: intent);
876 if (result != null) {
877 context.dependOnInheritedElement(element);
878 action = result;
879 return true;
880 }
881 return false;
882 });
883
884 return action;
885 }
886
887 static Action<T>? _maybeFindWithoutDependingOn<T extends Intent>(
888 BuildContext context, {
889 T? intent,
890 }) {
891 Action<T>? action;
892
893 // Specialize the type if a runtime example instance of the intent is given.
894 // This allows this function to be called by code that doesn't know the
895 // concrete type of the intent at compile time.
896 final Type type = intent?.runtimeType ?? T;
897 assert(
898 type != Intent,
899 'The type passed to "find" resolved to "Intent": either a non-Intent '
900 'generic type argument or an example intent derived from Intent must be '
901 'specified. Intent may be used as the generic type as long as the optional '
902 '"intent" argument is passed.',
903 );
904
905 _visitActionsAncestors(context, (InheritedElement element) {
906 final _ActionsScope actions = element.widget as _ActionsScope;
907 final Action<T>? result = _castAction(actions, intent: intent);
908 if (result != null) {
909 action = result;
910 return true;
911 }
912 return false;
913 });
914
915 return action;
916 }
917
918 // Find the [Action] that handles the given `intent` in the given
919 // `_ActionsScope`, and verify it has the right type parameter.
920 static Action<T>? _castAction<T extends Intent>(_ActionsScope actionsMarker, {T? intent}) {
921 final Action<Intent>? mappedAction = actionsMarker.actions[intent?.runtimeType ?? T];
922 if (mappedAction is Action<T>?) {
923 return mappedAction;
924 } else {
925 assert(
926 false,
927 '$T cannot be handled by an Action of runtime type ${mappedAction.runtimeType}.',
928 );
929 return null;
930 }
931 }
932
933 /// Returns the [ActionDispatcher] associated with the [Actions] widget that
934 /// most tightly encloses the given [BuildContext].
935 ///
936 /// Will return a newly created [ActionDispatcher] if no ambient [Actions]
937 /// widget is found.
938 static ActionDispatcher of(BuildContext context) {
939 final _ActionsScope? marker = context.dependOnInheritedWidgetOfExactType<_ActionsScope>();
940 return marker?.dispatcher ?? _findDispatcher(context);
941 }
942
943 /// Invokes the action associated with the given [Intent] using the
944 /// [Actions] widget that most tightly encloses the given [BuildContext].
945 ///
946 /// This method returns the result of invoking the action's [Action.invoke]
947 /// method.
948 ///
949 /// If the given `intent` doesn't map to an action, then it will look to the
950 /// next ancestor [Actions] widget in the hierarchy until it reaches the root.
951 ///
952 /// This method will throw an exception if no ambient [Actions] widget is
953 /// found, or when a suitable [Action] is found but it returns false for
954 /// [Action.isEnabled].
955 static Object? invoke<T extends Intent>(BuildContext context, T intent) {
956 Object? returnValue;
957
958 final bool actionFound = _visitActionsAncestors(context, (InheritedElement element) {
959 final _ActionsScope actions = element.widget as _ActionsScope;
960 final Action<T>? result = _castAction(actions, intent: intent);
961 if (result != null && result._isEnabled(intent, context)) {
962 // Invoke the action we found using the relevant dispatcher from the Actions
963 // Element we found.
964 returnValue = _findDispatcher(element).invokeAction(result, intent, context);
965 }
966 return result != null;
967 });
968
969 assert(() {
970 if (!actionFound) {
971 throw FlutterError(
972 'Unable to find an action for an Intent with type '
973 '${intent.runtimeType} in an $Actions widget in the given context.\n'
974 '$Actions.invoke() was unable to find an $Actions widget that '
975 "contained a mapping for the given intent, or the intent type isn't the "
976 'same as the type argument to invoke (which is $T - try supplying a '
977 'type argument to invoke if one was not given)\n'
978 'The context used was:\n'
979 ' $context\n'
980 'The intent type requested was:\n'
981 ' ${intent.runtimeType}',
982 );
983 }
984 return true;
985 }());
986 return returnValue;
987 }
988
989 /// Invokes the action associated with the given [Intent] using the
990 /// [Actions] widget that most tightly encloses the given [BuildContext].
991 ///
992 /// This method returns the result of invoking the action's [Action.invoke]
993 /// method. If no action mapping was found for the specified intent, or if the
994 /// first action found was disabled, or the action itself returns null
995 /// from [Action.invoke], then this method returns null.
996 ///
997 /// If the given `intent` doesn't map to an action, then it will look to the
998 /// next ancestor [Actions] widget in the hierarchy until it reaches the root.
999 /// If a suitable [Action] is found but its [Action.isEnabled] returns false,
1000 /// the search will stop and this method will return null.
1001 static Object? maybeInvoke<T extends Intent>(BuildContext context, T intent) {
1002 Object? returnValue;
1003 _visitActionsAncestors(context, (InheritedElement element) {
1004 final _ActionsScope actions = element.widget as _ActionsScope;
1005 final Action<T>? result = _castAction(actions, intent: intent);
1006 if (result != null && result._isEnabled(intent, context)) {
1007 // Invoke the action we found using the relevant dispatcher from the Actions
1008 // element we found.
1009 returnValue = _findDispatcher(element).invokeAction(result, intent, context);
1010 }
1011 return result != null;
1012 });
1013 return returnValue;
1014 }
1015
1016 @override
1017 State<Actions> createState() => _ActionsState();
1018
1019 @override
1020 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1021 super.debugFillProperties(properties);
1022 properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher));
1023 properties.add(DiagnosticsProperty<Map<Type, Action<Intent>>>('actions', actions));
1024 }
1025}
1026
1027class _ActionsState extends State<Actions> {
1028 // The set of actions that this Actions widget is current listening to.
1029 Set<Action<Intent>>? listenedActions = <Action<Intent>>{};
1030 // Used to tell the marker to rebuild its dependencies when the state of an
1031 // action in the map changes.
1032 Object rebuildKey = Object();
1033
1034 @override
1035 void initState() {
1036 super.initState();
1037 _updateActionListeners();
1038 }
1039
1040 void _handleActionChanged(Action<Intent> action) {
1041 // Generate a new key so that the marker notifies dependents.
1042 setState(() {
1043 rebuildKey = Object();
1044 });
1045 }
1046
1047 void _updateActionListeners() {
1048 final Set<Action<Intent>> widgetActions = widget.actions.values.toSet();
1049 final Set<Action<Intent>> removedActions = listenedActions!.difference(widgetActions);
1050 final Set<Action<Intent>> addedActions = widgetActions.difference(listenedActions!);
1051
1052 for (final Action<Intent> action in removedActions) {
1053 action.removeActionListener(_handleActionChanged);
1054 }
1055 for (final Action<Intent> action in addedActions) {
1056 action.addActionListener(_handleActionChanged);
1057 }
1058 listenedActions = widgetActions;
1059 }
1060
1061 @override
1062 void didUpdateWidget(Actions oldWidget) {
1063 super.didUpdateWidget(oldWidget);
1064 _updateActionListeners();
1065 }
1066
1067 @override
1068 void dispose() {
1069 super.dispose();
1070 for (final Action<Intent> action in listenedActions!) {
1071 action.removeActionListener(_handleActionChanged);
1072 }
1073 listenedActions = null;
1074 }
1075
1076 @override
1077 Widget build(BuildContext context) {
1078 return _ActionsScope(
1079 actions: widget.actions,
1080 dispatcher: widget.dispatcher,
1081 rebuildKey: rebuildKey,
1082 child: widget.child,
1083 );
1084 }
1085}
1086
1087// An inherited widget used by Actions widget for fast lookup of the Actions
1088// widget information.
1089class _ActionsScope extends InheritedWidget {
1090 const _ActionsScope({
1091 required this.dispatcher,
1092 required this.actions,
1093 required this.rebuildKey,
1094 required super.child,
1095 });
1096
1097 final ActionDispatcher? dispatcher;
1098 final Map<Type, Action<Intent>> actions;
1099 final Object rebuildKey;
1100
1101 @override
1102 bool updateShouldNotify(_ActionsScope oldWidget) {
1103 return rebuildKey != oldWidget.rebuildKey ||
1104 oldWidget.dispatcher != dispatcher ||
1105 !mapEquals<Type, Action<Intent>>(oldWidget.actions, actions);
1106 }
1107}
1108
1109/// A widget that combines the functionality of [Actions], [Shortcuts],
1110/// [MouseRegion] and a [Focus] widget to create a detector that defines actions
1111/// and key bindings, and provides callbacks for handling focus and hover
1112/// highlights.
1113///
1114/// {@youtube 560 315 https://www.youtube.com/watch?v=R84AGg0lKs8}
1115///
1116/// This widget can be used to give a control the required detection modes for
1117/// focus and hover handling. It is most often used when authoring a new control
1118/// widget, and the new control should be enabled for keyboard traversal and
1119/// activation.
1120///
1121/// {@tool dartpad}
1122/// This example shows how keyboard interaction can be added to a custom control
1123/// that changes color when hovered and focused, and can toggle a light when
1124/// activated, either by touch or by hitting the `X` key on the keyboard when
1125/// the "And Me" button has the keyboard focus (be sure to use TAB to move the
1126/// focus to the "And Me" button before trying it out).
1127///
1128/// This example defines its own key binding for the `X` key, but in this case,
1129/// there is also a default key binding for [ActivateAction] in the default key
1130/// bindings created by [WidgetsApp] (the parent for [MaterialApp], and
1131/// [CupertinoApp]), so the `ENTER` key will also activate the buttons.
1132///
1133/// ** See code in examples/api/lib/widgets/actions/focusable_action_detector.0.dart **
1134/// {@end-tool}
1135///
1136/// This widget doesn't have any visual representation, it is just a detector that
1137/// provides focus and hover capabilities.
1138///
1139/// It hosts its own [FocusNode] or uses [focusNode], if given.
1140class FocusableActionDetector extends StatefulWidget {
1141 /// Create a const [FocusableActionDetector].
1142 const FocusableActionDetector({
1143 super.key,
1144 this.enabled = true,
1145 this.focusNode,
1146 this.autofocus = false,
1147 this.descendantsAreFocusable = true,
1148 this.descendantsAreTraversable = true,
1149 this.shortcuts,
1150 this.actions,
1151 this.onShowFocusHighlight,
1152 this.onShowHoverHighlight,
1153 this.onFocusChange,
1154 this.mouseCursor = MouseCursor.defer,
1155 this.includeFocusSemantics = true,
1156 required this.child,
1157 });
1158
1159 /// Is this widget enabled or not.
1160 ///
1161 /// If disabled, will not send any notifications needed to update highlight or
1162 /// focus state, and will not define or respond to any actions or shortcuts.
1163 ///
1164 /// When disabled, adds [Focus] to the widget tree, but sets
1165 /// [Focus.canRequestFocus] to false.
1166 final bool enabled;
1167
1168 /// {@macro flutter.widgets.Focus.focusNode}
1169 final FocusNode? focusNode;
1170
1171 /// {@macro flutter.widgets.Focus.autofocus}
1172 final bool autofocus;
1173
1174 /// {@macro flutter.widgets.Focus.descendantsAreFocusable}
1175 final bool descendantsAreFocusable;
1176
1177 /// {@macro flutter.widgets.Focus.descendantsAreTraversable}
1178 final bool descendantsAreTraversable;
1179
1180 /// {@macro flutter.widgets.actions.actions}
1181 final Map<Type, Action<Intent>>? actions;
1182
1183 /// {@macro flutter.widgets.shortcuts.shortcuts}
1184 final Map<ShortcutActivator, Intent>? shortcuts;
1185
1186 /// A function that will be called when the focus highlight should be shown or
1187 /// hidden.
1188 ///
1189 /// This method is not triggered at the unmount of the widget.
1190 final ValueChanged<bool>? onShowFocusHighlight;
1191
1192 /// A function that will be called when the hover highlight should be shown or hidden.
1193 ///
1194 /// This method is not triggered at the unmount of the widget.
1195 final ValueChanged<bool>? onShowHoverHighlight;
1196
1197 /// A function that will be called when the focus changes.
1198 ///
1199 /// Called with true if the [focusNode] has primary focus.
1200 final ValueChanged<bool>? onFocusChange;
1201
1202 /// The cursor for a mouse pointer when it enters or is hovering over the
1203 /// widget.
1204 ///
1205 /// The [mouseCursor] defaults to [MouseCursor.defer], deferring the choice of
1206 /// cursor to the next region behind it in hit-test order.
1207 final MouseCursor mouseCursor;
1208
1209 /// Whether to include semantics from [Focus].
1210 ///
1211 /// Defaults to true.
1212 final bool includeFocusSemantics;
1213
1214 /// The child widget for this [FocusableActionDetector] widget.
1215 ///
1216 /// {@macro flutter.widgets.ProxyWidget.child}
1217 final Widget child;
1218
1219 @override
1220 State<FocusableActionDetector> createState() => _FocusableActionDetectorState();
1221}
1222
1223class _FocusableActionDetectorState extends State<FocusableActionDetector> {
1224 @override
1225 void initState() {
1226 super.initState();
1227 SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
1228 _updateHighlightMode(FocusManager.instance.highlightMode);
1229 }, debugLabel: 'FocusableActionDetector.updateHighlightMode');
1230 FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
1231 }
1232
1233 @override
1234 void dispose() {
1235 FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange);
1236 super.dispose();
1237 }
1238
1239 bool _canShowHighlight = false;
1240 void _updateHighlightMode(FocusHighlightMode mode) {
1241 _mayTriggerCallback(
1242 task: () {
1243 _canShowHighlight = switch (FocusManager.instance.highlightMode) {
1244 FocusHighlightMode.touch => false,
1245 FocusHighlightMode.traditional => true,
1246 };
1247 },
1248 );
1249 }
1250
1251 // Have to have this separate from the _updateHighlightMode because it gets
1252 // called in initState, where things aren't mounted yet.
1253 // Since this method is a highlight mode listener, it is only called
1254 // immediately following pointer events.
1255 void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
1256 if (!mounted) {
1257 return;
1258 }
1259 _updateHighlightMode(mode);
1260 }
1261
1262 bool _hovering = false;
1263 void _handleMouseEnter(PointerEnterEvent event) {
1264 if (!_hovering) {
1265 _mayTriggerCallback(
1266 task: () {
1267 _hovering = true;
1268 },
1269 );
1270 }
1271 }
1272
1273 void _handleMouseExit(PointerExitEvent event) {
1274 if (_hovering) {
1275 _mayTriggerCallback(
1276 task: () {
1277 _hovering = false;
1278 },
1279 );
1280 }
1281 }
1282
1283 bool _focused = false;
1284 void _handleFocusChange(bool focused) {
1285 if (_focused != focused) {
1286 _mayTriggerCallback(
1287 task: () {
1288 _focused = focused;
1289 },
1290 );
1291 widget.onFocusChange?.call(_focused);
1292 }
1293 }
1294
1295 // Record old states, do `task` if not null, then compare old states with the
1296 // new states, and trigger callbacks if necessary.
1297 //
1298 // The old states are collected from `oldWidget` if it is provided, or the
1299 // current widget (before doing `task`) otherwise. The new states are always
1300 // collected from the current widget.
1301 void _mayTriggerCallback({VoidCallback? task, FocusableActionDetector? oldWidget}) {
1302 bool shouldShowHoverHighlight(FocusableActionDetector target) {
1303 return _hovering && target.enabled && _canShowHighlight;
1304 }
1305
1306 bool canRequestFocus(FocusableActionDetector target) {
1307 return switch (MediaQuery.maybeNavigationModeOf(context)) {
1308 NavigationMode.traditional || null => target.enabled,
1309 NavigationMode.directional => true,
1310 };
1311 }
1312
1313 bool shouldShowFocusHighlight(FocusableActionDetector target) {
1314 return _focused && _canShowHighlight && canRequestFocus(target);
1315 }
1316
1317 assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks);
1318 final FocusableActionDetector oldTarget = oldWidget ?? widget;
1319 final bool didShowHoverHighlight = shouldShowHoverHighlight(oldTarget);
1320 final bool didShowFocusHighlight = shouldShowFocusHighlight(oldTarget);
1321 task?.call();
1322 final bool doShowHoverHighlight = shouldShowHoverHighlight(widget);
1323 final bool doShowFocusHighlight = shouldShowFocusHighlight(widget);
1324 if (didShowFocusHighlight != doShowFocusHighlight) {
1325 widget.onShowFocusHighlight?.call(doShowFocusHighlight);
1326 }
1327 if (didShowHoverHighlight != doShowHoverHighlight) {
1328 widget.onShowHoverHighlight?.call(doShowHoverHighlight);
1329 }
1330 }
1331
1332 @override
1333 void didUpdateWidget(FocusableActionDetector oldWidget) {
1334 super.didUpdateWidget(oldWidget);
1335 if (widget.enabled != oldWidget.enabled) {
1336 SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
1337 _mayTriggerCallback(oldWidget: oldWidget);
1338 }, debugLabel: 'FocusableActionDetector.mayTriggerCallback');
1339 }
1340 }
1341
1342 bool get _canRequestFocus => switch (MediaQuery.maybeNavigationModeOf(context)) {
1343 NavigationMode.traditional || null => widget.enabled,
1344 NavigationMode.directional => true,
1345 };
1346
1347 // This global key is needed to keep only the necessary widgets in the tree
1348 // while maintaining the subtree's state.
1349 //
1350 // See https://github.com/flutter/flutter/issues/64058 for an explanation of
1351 // why using a global key over keeping the shape of the tree.
1352 final GlobalKey _mouseRegionKey = GlobalKey();
1353
1354 @override
1355 Widget build(BuildContext context) {
1356 Widget child = MouseRegion(
1357 key: _mouseRegionKey,
1358 onEnter: _handleMouseEnter,
1359 onExit: _handleMouseExit,
1360 cursor: widget.mouseCursor,
1361 child: Focus(
1362 focusNode: widget.focusNode,
1363 autofocus: widget.autofocus,
1364 descendantsAreFocusable: widget.descendantsAreFocusable,
1365 descendantsAreTraversable: widget.descendantsAreTraversable,
1366 canRequestFocus: _canRequestFocus,
1367 onFocusChange: _handleFocusChange,
1368 includeSemantics: widget.includeFocusSemantics,
1369 child: widget.child,
1370 ),
1371 );
1372 if (widget.enabled && widget.actions != null && widget.actions!.isNotEmpty) {
1373 child = Actions(actions: widget.actions!, child: child);
1374 }
1375 if (widget.enabled && widget.shortcuts != null && widget.shortcuts!.isNotEmpty) {
1376 child = Shortcuts(shortcuts: widget.shortcuts!, child: child);
1377 }
1378 return child;
1379 }
1380}
1381
1382/// An [Intent] that keeps a [VoidCallback] to be invoked by a
1383/// [VoidCallbackAction] when it receives this intent.
1384class VoidCallbackIntent extends Intent {
1385 /// Creates a [VoidCallbackIntent].
1386 const VoidCallbackIntent(this.callback);
1387
1388 /// The callback that is to be called by the [VoidCallbackAction] that
1389 /// receives this intent.
1390 final VoidCallback callback;
1391}
1392
1393/// An [Action] that invokes the [VoidCallback] given to it in the
1394/// [VoidCallbackIntent] passed to it when invoked.
1395///
1396/// See also:
1397///
1398/// * [CallbackAction], which is an action that will invoke a callback with the
1399/// intent passed to the action's invoke method. The callback is configured
1400/// on the action, not the intent, like this class.
1401class VoidCallbackAction extends Action<VoidCallbackIntent> {
1402 @override
1403 Object? invoke(VoidCallbackIntent intent) {
1404 intent.callback();
1405 return null;
1406 }
1407}
1408
1409/// An [Intent] that is bound to a [DoNothingAction].
1410///
1411/// Attaching a [DoNothingIntent] to a [Shortcuts] mapping is one way to disable
1412/// a keyboard shortcut defined by a widget higher in the widget hierarchy and
1413/// consume any key event that triggers it via a shortcut.
1414///
1415/// This intent cannot be subclassed.
1416///
1417/// See also:
1418///
1419/// * [DoNothingAndStopPropagationIntent], a similar intent that will not
1420/// handle the key event, but will still keep it from being passed to other key
1421/// handlers in the focus chain.
1422class DoNothingIntent extends Intent {
1423 /// Creates a const [DoNothingIntent].
1424 const factory DoNothingIntent() = DoNothingIntent._;
1425
1426 // Make DoNothingIntent constructor private so it can't be subclassed.
1427 const DoNothingIntent._();
1428}
1429
1430/// An [Intent] that is bound to a [DoNothingAction], but, in addition to not
1431/// performing an action, also stops the propagation of the key event bound to
1432/// this intent to other key event handlers in the focus chain.
1433///
1434/// Attaching a [DoNothingAndStopPropagationIntent] to a [Shortcuts.shortcuts]
1435/// mapping is one way to disable a keyboard shortcut defined by a widget higher
1436/// in the widget hierarchy. In addition, the bound [DoNothingAction] will
1437/// return false from [DoNothingAction.consumesKey], causing the key bound to
1438/// this intent to be passed on to the platform embedding as "not handled" with
1439/// out passing it to other key handlers in the focus chain (e.g. parent
1440/// `Shortcuts` widgets higher up in the chain).
1441///
1442/// This intent cannot be subclassed.
1443///
1444/// See also:
1445///
1446/// * [DoNothingIntent], a similar intent that will handle the key event.
1447class DoNothingAndStopPropagationIntent extends Intent {
1448 /// Creates a const [DoNothingAndStopPropagationIntent].
1449 const factory DoNothingAndStopPropagationIntent() = DoNothingAndStopPropagationIntent._;
1450
1451 // Make DoNothingAndStopPropagationIntent constructor private so it can't be subclassed.
1452 const DoNothingAndStopPropagationIntent._();
1453}
1454
1455/// An [Action] that doesn't perform any action when invoked.
1456///
1457/// Attaching a [DoNothingAction] to an [Actions.actions] mapping is a way to
1458/// disable an action defined by a widget higher in the widget hierarchy.
1459///
1460/// If [consumesKey] returns false, then not only will this action do nothing,
1461/// but it will stop the propagation of the key event used to trigger it to
1462/// other widgets in the focus chain and tell the embedding that the key wasn't
1463/// handled, allowing text input fields or other non-Flutter elements to receive
1464/// that key event. The return value of [consumesKey] can be set via the
1465/// `consumesKey` argument to the constructor.
1466///
1467/// This action can be bound to any [Intent].
1468///
1469/// See also:
1470/// - [DoNothingIntent], which is an intent that can be bound to a [KeySet] in
1471/// a [Shortcuts] widget to do nothing.
1472/// - [DoNothingAndStopPropagationIntent], which is an intent that can be bound
1473/// to a [KeySet] in a [Shortcuts] widget to do nothing and also stop key event
1474/// propagation to other key handlers in the focus chain.
1475class DoNothingAction extends Action<Intent> {
1476 /// Creates a [DoNothingAction].
1477 ///
1478 /// The optional [consumesKey] argument defaults to true.
1479 DoNothingAction({bool consumesKey = true}) : _consumesKey = consumesKey;
1480
1481 @override
1482 bool consumesKey(Intent intent) => _consumesKey;
1483 final bool _consumesKey;
1484
1485 @override
1486 void invoke(Intent intent) {}
1487}
1488
1489/// An [Intent] that activates the currently focused control.
1490///
1491/// This intent is bound by default to the [LogicalKeyboardKey.space] key on all
1492/// platforms, and also to the [LogicalKeyboardKey.enter] key on all platforms
1493/// except the web, where ENTER doesn't toggle selection. On the web, ENTER is
1494/// bound to [ButtonActivateIntent] instead.
1495///
1496/// See also:
1497///
1498/// * [WidgetsApp.defaultShortcuts], which contains the default shortcuts used
1499/// in apps.
1500/// * [WidgetsApp.shortcuts], which defines the shortcuts to use in an
1501/// application (and defaults to [WidgetsApp.defaultShortcuts]).
1502class ActivateIntent extends Intent {
1503 /// Creates an intent that activates the currently focused control.
1504 const ActivateIntent();
1505}
1506
1507/// An [Intent] that activates the currently focused button.
1508///
1509/// This intent is bound by default to the [LogicalKeyboardKey.enter] key on the
1510/// web, where ENTER can be used to activate buttons, but not toggle selection.
1511/// All other platforms bind [LogicalKeyboardKey.enter] to [ActivateIntent].
1512///
1513/// See also:
1514///
1515/// * [WidgetsApp.defaultShortcuts], which contains the default shortcuts used
1516/// in apps.
1517/// * [WidgetsApp.shortcuts], which defines the shortcuts to use in an
1518/// application (and defaults to [WidgetsApp.defaultShortcuts]).
1519class ButtonActivateIntent extends Intent {
1520 /// Creates an intent that activates the currently focused control,
1521 /// if it's a button.
1522 const ButtonActivateIntent();
1523}
1524
1525/// An [Action] that activates the currently focused control.
1526///
1527/// This is an abstract class that serves as a base class for actions that
1528/// activate a control. By default, is bound to [LogicalKeyboardKey.enter],
1529/// [LogicalKeyboardKey.gameButtonA], and [LogicalKeyboardKey.space] in the
1530/// default keyboard map in [WidgetsApp].
1531abstract class ActivateAction extends Action<ActivateIntent> {}
1532
1533/// An [Intent] that selects the currently focused control.
1534class SelectIntent extends Intent {
1535 /// Creates an intent that selects the currently focused control.
1536 const SelectIntent();
1537}
1538
1539/// An action that selects the currently focused control.
1540///
1541/// This is an abstract class that serves as a base class for actions that
1542/// select something. It is not bound to any key by default.
1543abstract class SelectAction extends Action<SelectIntent> {}
1544
1545/// An [Intent] that dismisses the currently focused widget.
1546///
1547/// The [WidgetsApp.defaultShortcuts] binds this intent to the
1548/// [LogicalKeyboardKey.escape] and [LogicalKeyboardKey.gameButtonB] keys.
1549///
1550/// See also:
1551/// - [ModalRoute] which listens for this intent to dismiss modal routes
1552/// (dialogs, pop-up menus, drawers, etc).
1553class DismissIntent extends Intent {
1554 /// Creates an intent that dismisses the currently focused widget.
1555 const DismissIntent();
1556}
1557
1558/// An [Action] that dismisses the focused widget.
1559///
1560/// This is an abstract class that serves as a base class for dismiss actions.
1561abstract class DismissAction extends Action<DismissIntent> {}
1562
1563/// An [Intent] that evaluates a series of specified [orderedIntents] for
1564/// execution.
1565///
1566/// The first intent that matches an enabled action is used.
1567class PrioritizedIntents extends Intent {
1568 /// Creates an intent that is used with [PrioritizedAction] to specify a list
1569 /// of intents, the first available of which will be used.
1570 const PrioritizedIntents({required this.orderedIntents});
1571
1572 /// List of intents to be evaluated in order for execution. When an
1573 /// [Action.isEnabled] returns true, that action will be invoked and
1574 /// progression through the ordered intents stops.
1575 final List<Intent> orderedIntents;
1576}
1577
1578/// An [Action] that iterates through a list of [Intent]s, invoking the first
1579/// that is enabled.
1580///
1581/// The [isEnabled] method must be called before [invoke]. Calling [isEnabled]
1582/// configures the object by seeking the first intent with an enabled action.
1583/// If the actions have an opportunity to change enabled state, [isEnabled]
1584/// must be called again before calling [invoke].
1585class PrioritizedAction extends ContextAction<PrioritizedIntents> {
1586 late Action<dynamic> _selectedAction;
1587 late Intent _selectedIntent;
1588
1589 @override
1590 bool isEnabled(PrioritizedIntents intent, [BuildContext? context]) {
1591 final FocusNode? focus = primaryFocus;
1592 if (focus == null || focus.context == null) {
1593 return false;
1594 }
1595 for (final Intent candidateIntent in intent.orderedIntents) {
1596 final Action<Intent>? candidateAction = Actions.maybeFind<Intent>(
1597 focus.context!,
1598 intent: candidateIntent,
1599 );
1600 if (candidateAction != null && candidateAction._isEnabled(candidateIntent, context)) {
1601 _selectedAction = candidateAction;
1602 _selectedIntent = candidateIntent;
1603 return true;
1604 }
1605 }
1606 return false;
1607 }
1608
1609 @override
1610 void invoke(PrioritizedIntents intent, [BuildContext? context]) {
1611 _selectedAction._invoke(_selectedIntent, context);
1612 }
1613}
1614
1615mixin _OverridableActionMixin<T extends Intent> on Action<T> {
1616 // When debugAssertMutuallyRecursive is true, this action will throw an
1617 // assertion error when the override calls this action's "invoke" method and
1618 // the override is already being invoked from within the "invoke" method.
1619 bool debugAssertMutuallyRecursive = false;
1620 bool debugAssertIsActionEnabledMutuallyRecursive = false;
1621 bool debugAssertIsEnabledMutuallyRecursive = false;
1622 bool debugAssertConsumeKeyMutuallyRecursive = false;
1623
1624 // The default action to invoke if an enabled override Action can't be found
1625 // using [lookupContext].
1626 Action<T> get defaultAction;
1627
1628 // The [BuildContext] used to find the override of this [Action].
1629 BuildContext get lookupContext;
1630
1631 // How to invoke [defaultAction], given the caller [fromAction].
1632 Object? invokeDefaultAction(T intent, Action<T>? fromAction, BuildContext? context);
1633
1634 Action<T>? getOverrideAction({bool declareDependency = false}) {
1635 final Action<T>? override = declareDependency
1636 ? Actions.maybeFind(lookupContext)
1637 : Actions._maybeFindWithoutDependingOn(lookupContext);
1638 assert(!identical(override, this));
1639 return override;
1640 }
1641
1642 @override
1643 void _updateCallingAction(Action<T>? value) {
1644 super._updateCallingAction(value);
1645 defaultAction._updateCallingAction(value);
1646 }
1647
1648 Object? _invokeOverride(Action<T> overrideAction, T intent, BuildContext? context) {
1649 assert(!debugAssertMutuallyRecursive);
1650 assert(() {
1651 debugAssertMutuallyRecursive = true;
1652 return true;
1653 }());
1654 overrideAction._updateCallingAction(defaultAction);
1655 final Object? returnValue = overrideAction._invoke(intent, context);
1656 overrideAction._updateCallingAction(null);
1657 assert(() {
1658 debugAssertMutuallyRecursive = false;
1659 return true;
1660 }());
1661 return returnValue;
1662 }
1663
1664 @override
1665 Object? invoke(T intent, [BuildContext? context]) {
1666 final Action<T>? overrideAction = getOverrideAction();
1667 final Object? returnValue = overrideAction == null
1668 ? invokeDefaultAction(intent, callingAction, context)
1669 : _invokeOverride(overrideAction, intent, context);
1670 return returnValue;
1671 }
1672
1673 bool isOverrideActionEnabled(Action<T> overrideAction) {
1674 assert(!debugAssertIsActionEnabledMutuallyRecursive);
1675 assert(() {
1676 debugAssertIsActionEnabledMutuallyRecursive = true;
1677 return true;
1678 }());
1679 overrideAction._updateCallingAction(defaultAction);
1680 final bool isOverrideEnabled = overrideAction.isActionEnabled;
1681 overrideAction._updateCallingAction(null);
1682 assert(() {
1683 debugAssertIsActionEnabledMutuallyRecursive = false;
1684 return true;
1685 }());
1686 return isOverrideEnabled;
1687 }
1688
1689 @override
1690 bool get isActionEnabled {
1691 final Action<T>? overrideAction = getOverrideAction(declareDependency: true);
1692 final bool returnValue = overrideAction != null
1693 ? isOverrideActionEnabled(overrideAction)
1694 : defaultAction.isActionEnabled;
1695 return returnValue;
1696 }
1697
1698 @override
1699 bool isEnabled(T intent, [BuildContext? context]) {
1700 assert(!debugAssertIsEnabledMutuallyRecursive);
1701 assert(() {
1702 debugAssertIsEnabledMutuallyRecursive = true;
1703 return true;
1704 }());
1705
1706 final Action<T>? overrideAction = getOverrideAction();
1707 overrideAction?._updateCallingAction(defaultAction);
1708 final bool returnValue = (overrideAction ?? defaultAction)._isEnabled(intent, context);
1709 overrideAction?._updateCallingAction(null);
1710 assert(() {
1711 debugAssertIsEnabledMutuallyRecursive = false;
1712 return true;
1713 }());
1714 return returnValue;
1715 }
1716
1717 @override
1718 bool consumesKey(T intent) {
1719 assert(!debugAssertConsumeKeyMutuallyRecursive);
1720 assert(() {
1721 debugAssertConsumeKeyMutuallyRecursive = true;
1722 return true;
1723 }());
1724 final Action<T>? overrideAction = getOverrideAction();
1725 overrideAction?._updateCallingAction(defaultAction);
1726 final bool isEnabled = (overrideAction ?? defaultAction).consumesKey(intent);
1727 overrideAction?._updateCallingAction(null);
1728 assert(() {
1729 debugAssertConsumeKeyMutuallyRecursive = false;
1730 return true;
1731 }());
1732 return isEnabled;
1733 }
1734
1735 @override
1736 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1737 super.debugFillProperties(properties);
1738 properties.add(DiagnosticsProperty<Action<T>>('defaultAction', defaultAction));
1739 }
1740}
1741
1742class _OverridableAction<T extends Intent> extends ContextAction<T>
1743 with _OverridableActionMixin<T> {
1744 _OverridableAction({required this.defaultAction, required this.lookupContext});
1745
1746 @override
1747 final Action<T> defaultAction;
1748
1749 @override
1750 final BuildContext lookupContext;
1751
1752 @override
1753 Object? invokeDefaultAction(T intent, Action<T>? fromAction, BuildContext? context) {
1754 if (fromAction == null) {
1755 return defaultAction.invoke(intent);
1756 } else {
1757 final Object? returnValue = defaultAction.invoke(intent);
1758 return returnValue;
1759 }
1760 }
1761
1762 @override
1763 ContextAction<T> _makeOverridableAction(BuildContext context) {
1764 return _OverridableAction<T>(defaultAction: defaultAction, lookupContext: context);
1765 }
1766}
1767
1768class _OverridableContextAction<T extends Intent> extends ContextAction<T>
1769 with _OverridableActionMixin<T> {
1770 _OverridableContextAction({required this.defaultAction, required this.lookupContext});
1771
1772 @override
1773 final ContextAction<T> defaultAction;
1774
1775 @override
1776 final BuildContext lookupContext;
1777
1778 @override
1779 Object? _invokeOverride(Action<T> overrideAction, T intent, BuildContext? context) {
1780 assert(context != null);
1781 assert(!debugAssertMutuallyRecursive);
1782 assert(() {
1783 debugAssertMutuallyRecursive = true;
1784 return true;
1785 }());
1786
1787 // Wrap the default Action together with the calling context in case
1788 // overrideAction is not a ContextAction and thus have no access to the
1789 // calling BuildContext.
1790 final Action<T> wrappedDefault = _ContextActionToActionAdapter<T>(
1791 invokeContext: context!,
1792 action: defaultAction,
1793 );
1794 overrideAction._updateCallingAction(wrappedDefault);
1795 final Object? returnValue = overrideAction._invoke(intent, context);
1796 overrideAction._updateCallingAction(null);
1797
1798 assert(() {
1799 debugAssertMutuallyRecursive = false;
1800 return true;
1801 }());
1802 return returnValue;
1803 }
1804
1805 @override
1806 Object? invokeDefaultAction(T intent, Action<T>? fromAction, BuildContext? context) {
1807 if (fromAction == null) {
1808 return defaultAction.invoke(intent, context);
1809 } else {
1810 final Object? returnValue = defaultAction.invoke(intent, context);
1811 return returnValue;
1812 }
1813 }
1814
1815 @override
1816 ContextAction<T> _makeOverridableAction(BuildContext context) {
1817 return _OverridableContextAction<T>(defaultAction: defaultAction, lookupContext: context);
1818 }
1819}
1820
1821class _ContextActionToActionAdapter<T extends Intent> extends Action<T> {
1822 _ContextActionToActionAdapter({required this.invokeContext, required this.action});
1823
1824 final BuildContext invokeContext;
1825 final ContextAction<T> action;
1826
1827 @override
1828 void _updateCallingAction(Action<T>? value) {
1829 action._updateCallingAction(value);
1830 }
1831
1832 @override
1833 Action<T>? get callingAction => action.callingAction;
1834
1835 @override
1836 bool isEnabled(T intent) => action.isEnabled(intent, invokeContext);
1837
1838 @override
1839 bool get isActionEnabled => action.isActionEnabled;
1840
1841 @override
1842 bool consumesKey(T intent) => action.consumesKey(intent);
1843
1844 @override
1845 void addActionListener(ActionListenerCallback listener) {
1846 super.addActionListener(listener);
1847 action.addActionListener(listener);
1848 }
1849
1850 @override
1851 void removeActionListener(ActionListenerCallback listener) {
1852 super.removeActionListener(listener);
1853 action.removeActionListener(listener);
1854 }
1855
1856 @override
1857 @protected
1858 void notifyActionListeners() => action.notifyActionListeners();
1859
1860 @override
1861 Object? invoke(T intent) => action.invoke(intent, invokeContext);
1862}
1863