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