blob: 8008ee04ca90632361cffcf3a5fec2762d2f3f90 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'binding.dart';
import 'focus_manager.dart';
import 'framework.dart';
/// Creates actions for use in defining shortcuts.
///
/// Used by clients of [ShortcutMap] to define shortcut maps.
typedef ActionFactory = Action Function();
/// A class representing a particular configuration of an action.
///
/// This class is what a key map in a [ShortcutMap] has as values, and is used
/// by an [ActionDispatcher] to look up an action and invoke it, giving it this
/// object to extract configuration information from.
///
/// If this intent returns false from [isEnabled], then its associated action will
/// not be invoked if requested.
class Intent extends Diagnosticable {
/// A const constructor for an [Intent].
///
/// The [key] argument must not be null.
const Intent(this.key) : assert(key != null);
/// An intent that can't be mapped to an action.
///
/// This Intent is mapped to an action in the [WidgetsApp] that does nothing,
/// so that it can be bound to a key in a [Shortcuts] widget in order to
/// disable a key binding made above it in the hierarchy.
static const Intent doNothing = Intent(DoNothingAction.key);
/// The key for the action this intent is associated with.
final LocalKey key;
/// Returns true if the associated action is able to be executed in the
/// given `context`.
///
/// Returns true by default.
bool isEnabled(BuildContext context) => true;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LocalKey>('key', key));
}
}
/// Base class for actions.
///
/// As the name implies, an [Action] is an action or command to be performed.
/// They are typically invoked as a result of a user action, such as a keyboard
/// shortcut in a [Shortcuts] widget, which is used to look up an [Intent],
/// which is given to an [ActionDispatcher] to map the [Intent] to an [Action]
/// and invoke it.
///
/// The [ActionDispatcher] can invoke an [Action] on the primary focus, or
/// without regard for focus.
///
/// See also:
///
/// - [Shortcuts], which is a widget that contains a key map, in which it looks
/// up key combinations in order to invoke actions.
/// - [Actions], which is a widget that defines a map of [Intent] to [Action]
/// and allows redefining of actions for its descendants.
/// - [ActionDispatcher], a class that takes an [Action] and invokes it using a
/// [FocusNode] for context.
abstract class Action extends Diagnosticable {
/// A const constructor for an [Action].
///
/// The [intentKey] parameter must not be null.
const Action(this.intentKey) : assert(intentKey != null);
/// The unique key for this action.
///
/// This key will be used to map to this action in an [ActionDispatcher].
final LocalKey intentKey;
/// Called when the action is to be performed.
///
/// This is called by the [ActionDispatcher] when an action is accepted by a
/// [FocusNode] by returning true from its `onAction` callback, or when an
/// action is invoked using [ActionDispatcher.invokeAction].
///
/// This method is only meant to be invoked by an [ActionDispatcher], or by
/// subclasses.
///
/// Actions invoked directly with [ActionDispatcher.invokeAction] may receive a
/// null `node`. If the information available from a focus node is
/// needed in the action, use [ActionDispatcher.invokeFocusedAction] instead.
@protected
@mustCallSuper
void invoke(FocusNode node, covariant Intent intent);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LocalKey>('intentKey', intentKey));
}
}
/// The signature of a callback accepted by [CallbackAction].
typedef OnInvokeCallback = void Function(FocusNode node, Intent tag);
/// An [Action] that takes a callback in order to configure it without having to
/// subclass it.
///
/// See also:
///
/// - [Shortcuts], which is a widget that contains a key map, in which it looks
/// up key combinations in order to invoke actions.
/// - [Actions], which is a widget that defines a map of [Intent] to [Action]
/// and allows redefining of actions for its descendants.
/// - [ActionDispatcher], a class that takes an [Action] and invokes it using a
/// [FocusNode] for context.
class CallbackAction extends Action {
/// A const constructor for an [Action].
///
/// The `intentKey` and [onInvoke] parameters must not be null.
/// The [onInvoke] parameter is required.
const CallbackAction(LocalKey intentKey, {@required this.onInvoke})
: assert(onInvoke != null),
super(intentKey);
/// The callback to be called when invoked.
///
/// Must not be null.
@protected
final OnInvokeCallback onInvoke;
@override
void invoke(FocusNode node, Intent intent) => onInvoke.call(node, intent);
}
/// An action manager that simply invokes the actions given to it.
class ActionDispatcher extends Diagnosticable {
/// Const constructor so that subclasses can be const.
const ActionDispatcher();
/// Invokes the given action, optionally without regard for the currently
/// focused node in the focus tree.
///
/// Actions invoked will receive the given `focusNode`, or the
/// [FocusManager.primaryFocus] if the given `focusNode` is null.
///
/// The `action` and `intent` arguments must not be null.
///
/// Returns true if the action was successfully invoked.
bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) {
assert(action != null);
assert(intent != null);
focusNode ??= WidgetsBinding.instance.focusManager.primaryFocus;
if (action != null && intent.isEnabled(focusNode.context)) {
action.invoke(focusNode, intent);
return true;
}
return false;
}
}
/// A widget that establishes an [ActionDispatcher] and a map of [Intent] to
/// [Action] to be used by its descendants when invoking an [Action].
///
/// Actions are typically invoked using [Actions.invoke] with the context
/// containing the ambient [Actions] widget.
///
/// See also:
///
/// * [ActionDispatcher], the object that this widget uses to manage actions.
/// * [Action], a class for containing and defining an invocation of a user
/// action.
/// * [Intent], a class that holds a unique [LocalKey] identifying an action,
/// as well as configuration information for running the [Action].
/// * [Shortcuts], a widget used to bind key combinations to [Intent]s.
class Actions extends InheritedWidget {
/// Creates an [Actions] widget.
///
/// The [child], [actions], and [dispatcher] arguments must not be null.
const Actions({
Key key,
this.dispatcher,
@required this.actions,
@required Widget child,
}) : assert(actions != null),
super(key: key, child: child);
/// The [ActionDispatcher] object that invokes actions.
///
/// This is what is returned from [Actions.of], and used by [Actions.invoke].
///
/// If this [dispatcher] is null, then [Actions.of] and [Actions.invoke] will
/// look up the tree until they find an Actions widget that has a dispatcher
/// set. If not such widget is found, then they will return/use a
/// default-constructed [ActionDispatcher].
final ActionDispatcher dispatcher;
/// A map of [Intent] keys to [ActionFactory] factory methods that defines
/// which actions this widget knows about.
final Map<LocalKey, ActionFactory> actions;
// Finds the nearest valid ActionDispatcher, or creates a new one if it
// doesn't find one.
static ActionDispatcher _findDispatcher(Element element) {
assert(element.widget is Actions);
final Actions actions = element.widget;
ActionDispatcher dispatcher = actions.dispatcher;
if (dispatcher == null) {
bool visitAncestorElement(Element visitedElement) {
if (visitedElement.widget is! Actions) {
// Continue visiting.
return true;
}
final Actions actions = visitedElement.widget;
if (actions.dispatcher == null) {
// Continue visiting.
return true;
}
dispatcher = actions.dispatcher;
// Stop visiting.
return false;
}
element.visitAncestorElements(visitAncestorElement);
}
return dispatcher ?? const ActionDispatcher();
}
/// Returns the [ActionDispatcher] associated with the [Actions] widget that
/// most tightly encloses the given [BuildContext].
///
/// Will throw if no ambient [Actions] widget is found.
///
/// If `nullOk` is set to true, then if no ambient [Actions] widget is found,
/// this will return null.
///
/// The `context` argument must not be null.
static ActionDispatcher of(BuildContext context, {bool nullOk = false}) {
assert(context != null);
final InheritedElement inheritedElement = context.ancestorInheritedElementForWidgetOfExactType(Actions);
final Actions inherited = context.inheritFromElement(inheritedElement);
assert(() {
if (nullOk) {
return true;
}
if (inherited == null) {
throw FlutterError('Unable to find an $Actions widget in the context.\n'
'$Actions.of() was called with a context that does not contain an '
'$Actions widget.\n'
'No $Actions ancestor could be found starting from the context that '
'was passed to $Actions.of(). This can happen if the context comes '
'from a widget above those widgets.\n'
'The context used was:\n'
' $context');
}
return true;
}());
return inherited?.dispatcher ?? _findDispatcher(inheritedElement);
}
/// Invokes the action associated with the given [Intent] using the
/// [Actions] widget that most tightly encloses the given [BuildContext].
///
/// The `context`, `intent` and `nullOk` arguments must not be null.
///
/// If the given `intent` isn't found in the first [Actions.actions] map, then
/// it will move up to the next [Actions] widget in the hierarchy until it
/// reaches the root.
///
/// Will throw if no ambient [Actions] widget is found, or if the given
/// `intent` doesn't map to an action in any of the [Actions.actions] maps
/// that are found.
///
/// Returns true if an action was successfully invoked.
///
/// Setting `nullOk` to true means that if no ambient [Actions] widget is
/// found, then this method will return false instead of throwing.
static bool invoke(
BuildContext context,
Intent intent, {
FocusNode focusNode,
bool nullOk = false,
}) {
assert(context != null);
assert(intent != null);
Element actionsElement;
Action action;
bool visitAncestorElement(Element element) {
if (element.widget is! Actions) {
// Continue visiting.
return true;
}
// Below when we invoke the action, we need to use the dispatcher from the
// Actions widget where we found the action, in case they need to match.
actionsElement = element;
final Actions actions = element.widget;
action = actions.actions[intent.key]?.call();
// Keep looking if we failed to find and create an action.
return action == null;
}
context.visitAncestorElements(visitAncestorElement);
assert(() {
if (nullOk) {
return true;
}
if (actionsElement == null) {
throw FlutterError('Unable to find a $Actions widget in the context.\n'
'$Actions.invoke() was called with a context that does not contain an '
'$Actions widget.\n'
'No $Actions ancestor could be found starting from the context that '
'was passed to $Actions.invoke(). This can happen if the context comes '
'from a widget above those widgets.\n'
'The context used was:\n'
' $context');
}
if (action == null) {
throw FlutterError('Unable to find an action for an intent in the $Actions widget in the context.\n'
'$Actions.invoke() was called on an $Actions widget that doesn\'t '
'contain a mapping for the given intent.\n'
'The context used was:\n'
' $context\n'
'The intent requested was:\n'
' $intent');
}
return true;
}());
if (action == null) {
// Will only get here if nullOk is true.
return false;
}
// Invoke the action we found using the dispatcher from the Actions Element
// we found, using the given focus node.
return _findDispatcher(actionsElement).invokeAction(action, intent, focusNode: focusNode);
}
@override
bool updateShouldNotify(Actions oldWidget) {
return oldWidget.dispatcher != dispatcher || oldWidget.actions != actions;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher));
properties.add(DiagnosticsProperty<Map<LocalKey, ActionFactory>>('actions', actions));
}
}
/// An [Action], that, as the name implies, does nothing.
///
/// This action is bound to the [Intent.doNothing] intent inside of
/// [WidgetsApp.build] so that a [Shortcuts] widget can bind a key to it to
/// override another shortcut binding defined above it in the hierarchy.
class DoNothingAction extends Action {
/// Const constructor for [DoNothingAction].
const DoNothingAction() : super(key);
/// The Key used for the [DoNothingIntent] intent, and registered at the top
/// level actions in [WidgetsApp.build].
static const LocalKey key = ValueKey<Type>(DoNothingAction);
@override
void invoke(FocusNode node, Intent intent) { }
}