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