# Using Actions and Shortcuts

> How to use Actions and Shortcuts in your Flutter app.



This page describes how to bind physical keyboard events to actions in the user
interface. For instance, to define keyboard shortcuts in your application, this
page is for you.

## Overview

For a GUI application to do anything, it has to have actions: users want to tell
the application to _do_ something. Actions are often simple functions that
directly perform the action (such as set a value or save a file). In a larger
application, however, things are more complex: the code for invoking the action,
and the code for the action itself might need to be in different places.
Shortcuts (key bindings) might need definition at a level that knows nothing
about the actions they invoke.

That's where Flutter's actions and shortcuts system comes in. It allows
developers to define actions that fulfill intents bound to them. In this
context, an intent is a generic action that the user wishes to perform, and an
[`Intent`][] class instance represents these user intents in Flutter. An
`Intent` can be general purpose, fulfilled by different actions in different
contexts. An [`Action`][] can be a simple callback (as in the case of
the [`CallbackAction`][]) or something more complex that integrates with entire
undo/redo architectures (for example) or other logic.

![Using Shortcuts Diagram][]{:width="100%" .diagram-wrap}

[`Shortcuts`][] are key bindings that activate by pressing a key or combination
of keys. The key combinations reside in a table with their bound intent. When
the `Shortcuts` widget invokes them, it sends their matching intent to the
actions subsystem for fulfillment.

To illustrate the concepts in actions and shortcuts, this article creates a
simple app that allows a user to select and copy text in a text field using both
buttons and shortcuts.

### Why separate Actions from Intents?

You might wonder: why not just map a key combination directly to an action?  Why
have intents at all? This is because it is useful to have a separation of
concerns between where the key mapping definitions are (often at a high level),
and where the action definitions are (often at a low level), and because it is
important to be able to have a single key combination map to an intended
operation in an app, and have it adapt automatically to whichever action
fulfills that intended operation for the focused context.

For instance, Flutter has an `ActivateIntent` widget that maps each type of
control to its corresponding version of an `ActivateAction` (and that executes
the code that activates the control). This code often needs fairly private
access to do its work. If the extra layer of indirection that `Intent`s provide
didn't exist, it would be necessary to elevate the definition of the actions to
where the defining instance of the `Shortcuts` widget could see them, causing
the shortcuts to have more knowledge than necessary about which action to
invoke, and to have access to or provide state that it wouldn't necessarily have
or need otherwise. This allows your code to separate the two concerns to be more
independent.

Intents configure an action so that the same action can serve multiple uses. An
example of this is `DirectionalFocusIntent`, which takes a direction to move
the focus in, allowing the `DirectionalFocusAction` to know which direction to
move the focus. Just be careful: don't pass state in the `Intent` that applies
to all  invocations of an `Action`: that kind of state should be passed to the
constructor of the `Action` itself, to keep the `Intent` from needing to know
too much.

### Why not use callbacks?

You also might wonder: why not just use a callback instead of an `Action`
object? The main reason is that it's useful for actions to decide whether they
are enabled by implementing `isEnabled`. Also, it is often helpful if the key
bindings, and the implementation of those bindings, are in different places.

If all you need are callbacks without the flexibility of `Actions` and
`Shortcuts`, you can use the [`CallbackShortcuts`][] widget:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (callback-shortcuts)"?>
```dart
@override
Widget build(BuildContext context) {
  return CallbackShortcuts(
    bindings: <ShortcutActivator, VoidCallback>{
      const SingleActivator(LogicalKeyboardKey.arrowUp): () {
        setState(() => count = count + 1);
      },
      const SingleActivator(LogicalKeyboardKey.arrowDown): () {
        setState(() => count = count - 1);
      },
    },
    child: Focus(
      autofocus: true,
      child: Column(
        children: <Widget>[
          const Text('Press the up arrow key to add to the counter'),
          const Text('Press the down arrow key to subtract from the counter'),
          Text('count: $count'),
        ],
      ),
    ),
  );
}
```

## Shortcuts

As you'll see below, actions are useful on their own, but the most common use
case involves binding them to a keyboard shortcut. This is what the `Shortcuts`
widget is for.

It is inserted into the widget hierarchy to define key combinations that
represent the user's intent when that key combination is pressed. To convert
that intended purpose for the key combination into a concrete action, the
`Actions` widget used to map the `Intent` to an `Action`. For instance, you can
define a `SelectAllIntent`, and bind it to your own `SelectAllAction` or to your
`CanvasSelectAllAction`, and from that one key binding, the system invokes
either one, depending on which part of your application has focus. Let's see how
the key binding part works:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (shortcuts)"?>
```dart
@override
Widget build(BuildContext context) {
  return Shortcuts(
    shortcuts: <LogicalKeySet, Intent>{
      LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
          const SelectAllIntent(),
    },
    child: Actions(
      dispatcher: LoggingActionDispatcher(),
      actions: <Type, Action<Intent>>{
        SelectAllIntent: SelectAllAction(model),
      },
      child: Builder(
        builder: (context) => TextButton(
          onPressed: Actions.handler<SelectAllIntent>(
            context,
            const SelectAllIntent(),
          ),
          child: const Text('SELECT ALL'),
        ),
      ),
    ),
  );
}
```

The map given to a `Shortcuts` widget maps a `LogicalKeySet` (or a
`ShortcutActivator`, see note below) to an `Intent` instance. The logical key
set defines a set of one or more keys, and the intent indicates the intended
purpose of the keypress. The `Shortcuts` widget looks up key presses in the map,
to find an `Intent` instance, which it gives to the action's `invoke()` method.

:::note
`ShortcutActivator` is a replacement for `LogicalKeySet`.
It allows for more flexible and correct activation of shortcuts.
`LogicalKeySet` is a `ShortcutActivator`, of course, but
there is also `SingleActivator`, which takes a single key and the
optional modifiers to be pressed before the key.
Then there is `CharacterActivator`, which activates a shortcut based on the
character produced by a key sequence, instead of the logical keys themselves.
`ShortcutActivator` is also meant to be subclassed to allow for
custom ways of activating shortcuts from key events.
:::

### The ShortcutManager

The shortcut manager, a longer-lived object than the `Shortcuts` widget, passes
on key events when it receives them. It contains the logic for deciding how to
handle the keys, the logic for walking up the tree to find other shortcut
mappings, and maintains a map of key combinations to intents.

While the default behavior of the `ShortcutManager` is usually desirable, the
`Shortcuts` widget takes a `ShortcutManager` that you can subclass to customize
its functionality.

For example, if you wanted to log each key that a `Shortcuts` widget handled,
you could make a `LoggingShortcutManager`:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (logging-shortcut-manager)"?>
```dart
class LoggingShortcutManager extends ShortcutManager {
  @override
  KeyEventResult handleKeypress(BuildContext context, KeyEvent event) {
    final KeyEventResult result = super.handleKeypress(context, event);
    if (result == KeyEventResult.handled) {
      print('Handled shortcut $event in $context');
    }
    return result;
  }
}
```

Now, every time the `Shortcuts` widget handles a shortcut, it prints out the key
event and relevant context.

## Actions

`Actions` allow for the definition of operations that the application can
perform by invoking them with an `Intent`. Actions can be enabled or disabled,
and receive the intent instance that invoked them as an argument to allow
configuration by the intent.

### Defining actions

Actions, in their simplest form, are just subclasses of `Action<Intent>` with an
`invoke()` method. Here's a simple action that simply invokes a function on the
provided model:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (select-all-action)"?>
```dart
class SelectAllAction extends Action<SelectAllIntent> {
  SelectAllAction(this.model);

  final Model model;

  @override
  void invoke(covariant SelectAllIntent intent) => model.selectAll();
}
```

Or, if it's too much of a bother to create a new class, use a `CallbackAction`:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (callback-action)"?>
```dart
CallbackAction(onInvoke: (intent) => model.selectAll());
```

Once you have an action, you add it to your application using the [`Actions`][]
widget, which takes a map of `Intent` types to `Action`s:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (select-all-usage)"?>
```dart
@override
Widget build(BuildContext context) {
  return Actions(
    actions: <Type, Action<Intent>>{SelectAllIntent: SelectAllAction(model)},
    child: child,
  );
}
```

The `Shortcuts` widget uses the `Focus` widget's context and `Actions.invoke` to
find which action to invoke. If the `Shortcuts` widget doesn't find a matching
intent type in the first `Actions` widget encountered, it considers the next
ancestor `Actions` widget, and so on, until it reaches the root of the widget
tree, or finds a matching intent type and invokes the corresponding action.

### Invoking Actions

The actions system has several ways to invoke actions.  By far the most common
way is through the use of a `Shortcuts` widget covered in the previous section,
but there are other ways to interrogate the actions subsystem and invoke an
action. It's possible to invoke actions that are not bound to keys.

For instance, to find an action associated with an intent, you can use:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (maybe-find)"?>
```dart
Action<SelectAllIntent>? selectAll = Actions.maybeFind<SelectAllIntent>(
  context,
);
```

This returns an `Action` associated with the `SelectAllIntent` type if one is
available in the given `context`.  If one isn't available, it returns null. If
an associated `Action` should always be available, then use `find` instead of
`maybeFind`, which throws an exception when it doesn't find a matching `Intent`
type.

To invoke the action (if it exists), call:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (invoke-action)"?>
```dart
Object? result;
if (selectAll != null) {
  result = Actions.of(
    context,
  ).invokeAction(selectAll, const SelectAllIntent());
}
```

Combine that into one call with the following:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (maybe-invoke)"?>
```dart
Object? result = Actions.maybeInvoke<SelectAllIntent>(
  context,
  const SelectAllIntent(),
);
```

Sometimes you want to invoke an action as a
result of pressing a button or another control.
You can do this with the `Actions.handler` function.
If the intent has a mapping to an enabled action,
the `Actions.handler` function creates a handler closure.
However, if it doesn't have a mapping, it returns `null`.
This allows the button to be disabled if
there is no enabled action that matches in the context.

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (handler)"?>
```dart
@override
Widget build(BuildContext context) {
  return Actions(
    actions: <Type, Action<Intent>>{SelectAllIntent: SelectAllAction(model)},
    child: Builder(
      builder: (context) => TextButton(
        onPressed: Actions.handler<SelectAllIntent>(
          context,
          SelectAllIntent(controller: controller),
        ),
        child: const Text('SELECT ALL'),
      ),
    ),
  );
}
```

The `Actions` widget only invokes actions when `isEnabled(Intent intent)`
returns true, allowing the action to decide if the dispatcher should consider it
for invocation. If the action isn't enabled, the action is not invoked and the
framework stops searching for matching actions at that point.

The previous example uses a `Builder` because `Actions.handler` and
`Actions.invoke` (for example) only finds actions in the provided `context`, and
if the example passes the `context` given to the `build` function, the framework
starts looking _above_ the current widget.  Using a `Builder` allows the
framework to find the actions defined in the same `build` function.

You can invoke an action without needing a `BuildContext`, but since the
`Actions` widget requires a context to find an enabled action to invoke, you
need to provide one, either by creating your own `Action` instance, or by
finding one in an appropriate context with `Actions.find`.

To invoke the action, pass the action to the `invoke` method on an
`ActionDispatcher`, either one you created yourself, or one retrieved from an
existing `Actions` widget using the `Actions.of(context)` method. Check whether
the action is enabled before calling `invoke`. Of course, you can also just call
`invoke` on the action itself, passing an `Intent`, but then you opt out of any
services that an action dispatcher might provide (like logging, undo/redo, and
so on).

### Action dispatchers

Most of the time, you just want to invoke an action, have it do its thing, and
forget about it. Sometimes, however, you might want to log the executed actions.

This is where replacing the default `ActionDispatcher` with a custom dispatcher
comes in.  You pass your `ActionDispatcher` to the `Actions` widget, and it
invokes actions from any `Actions` widgets below that one that doesn't set a
dispatcher of its own.

The first thing `Actions` does when invoking an action is look up the
`ActionDispatcher` and pass the action to it for invocation. If there is none,
it creates a default `ActionDispatcher` that simply invokes the action.

If you want a log of all the actions invoked, however, you can create your own
`LoggingActionDispatcher` to do the job:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (logging-action-dispatcher)"?>
```dart
class LoggingActionDispatcher extends ActionDispatcher {
  @override
  Object? invokeAction(
    covariant Action<Intent> action,
    covariant Intent intent, [
    BuildContext? context,
  ]) {
    print('Action invoked: $action($intent) from $context');
    super.invokeAction(action, intent, context);

    return null;
  }

  @override
  (bool, Object?) invokeActionIfEnabled(
    covariant Action<Intent> action,
    covariant Intent intent, [
    BuildContext? context,
  ]) {
    print('Action invoked: $action($intent) from $context');
    return super.invokeActionIfEnabled(action, intent, context);
  }
}
```

Then you pass that to your top-level `Actions` widget:

<?code-excerpt "ui/actions_and_shortcuts/lib/samples.dart (logging-action-dispatcher-usage)"?>
```dart
@override
Widget build(BuildContext context) {
  return Actions(
    dispatcher: LoggingActionDispatcher(),
    actions: <Type, Action<Intent>>{SelectAllIntent: SelectAllAction(model)},
    child: Builder(
      builder: (context) => TextButton(
        onPressed: Actions.handler<SelectAllIntent>(
          context,
          const SelectAllIntent(),
        ),
        child: const Text('SELECT ALL'),
      ),
    ),
  );
}
```

This logs every action as it executes, like so:

```console
flutter: Action invoked: SelectAllAction#906fc(SelectAllIntent#a98e3) from Builder(dependencies: _[ActionsMarker])
```

## Putting it together

The combination of `Actions` and `Shortcuts` is powerful: you can define generic
intents that map to specific actions at the widget level. Here's a simple app
that illustrates the concepts described above. The app creates a text field that
also has "select all" and "copy to clipboard" buttons next to it. The buttons
invoke actions to accomplish their work. All the invoked actions and
shortcuts are logged.

<?code-excerpt "ui/actions_and_shortcuts/lib/copyable_text.dart"?>
```dartpad title="Copyable text DartPad hands-on example" run="true"
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/// A text field that also has buttons to select all the text and copy the
/// selected text to the clipboard.
class CopyableTextField extends StatefulWidget {
  const CopyableTextField({super.key, required this.title});

  final String title;

  @override
  State<CopyableTextField> createState() => _CopyableTextFieldState();
}

class _CopyableTextFieldState extends State<CopyableTextField> {
  late final TextEditingController controller = TextEditingController();
  late final FocusNode focusNode = FocusNode();

  @override
  void dispose() {
    controller.dispose();
    focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Actions(
      dispatcher: LoggingActionDispatcher(),
      actions: <Type, Action<Intent>>{
        ClearIntent: ClearAction(controller),
        CopyIntent: CopyAction(controller),
        SelectAllIntent: SelectAllAction(controller, focusNode),
      },
      child: Builder(
        builder: (context) {
          return Scaffold(
            body: Center(
              child: Row(
                children: <Widget>[
                  const Spacer(),
                  Expanded(
                    child: TextField(
                      controller: controller,
                      focusNode: focusNode,
                    ),
                  ),
                  IconButton(
                    icon: const Icon(Icons.copy),
                    onPressed: Actions.handler<CopyIntent>(
                      context,
                      const CopyIntent(),
                    ),
                  ),
                  IconButton(
                    icon: const Icon(Icons.select_all),
                    onPressed: Actions.handler<SelectAllIntent>(
                      context,
                      const SelectAllIntent(),
                    ),
                  ),
                  const Spacer(),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

/// A ShortcutManager that logs all keys that it handles.
class LoggingShortcutManager extends ShortcutManager {
  @override
  KeyEventResult handleKeypress(BuildContext context, KeyEvent event) {
    final KeyEventResult result = super.handleKeypress(context, event);
    if (result == KeyEventResult.handled) {
      print('Handled shortcut $event in $context');
    }
    return result;
  }
}

/// An ActionDispatcher that logs all the actions that it invokes.
class LoggingActionDispatcher extends ActionDispatcher {
  @override
  Object? invokeAction(
    covariant Action<Intent> action,
    covariant Intent intent, [
    BuildContext? context,
  ]) {
    print('Action invoked: $action($intent) from $context');
    super.invokeAction(action, intent, context);

    return null;
  }
}

/// An intent that is bound to ClearAction in order to clear its
/// TextEditingController.
class ClearIntent extends Intent {
  const ClearIntent();
}

/// An action that is bound to ClearIntent that clears its
/// TextEditingController.
class ClearAction extends Action<ClearIntent> {
  ClearAction(this.controller);

  final TextEditingController controller;

  @override
  Object? invoke(covariant ClearIntent intent) {
    controller.clear();

    return null;
  }
}

/// An intent that is bound to CopyAction to copy from its
/// TextEditingController.
class CopyIntent extends Intent {
  const CopyIntent();
}

/// An action that is bound to CopyIntent that copies the text in its
/// TextEditingController to the clipboard.
class CopyAction extends Action<CopyIntent> {
  CopyAction(this.controller);

  final TextEditingController controller;

  @override
  Object? invoke(covariant CopyIntent intent) {
    final String selectedString = controller.text.substring(
      controller.selection.baseOffset,
      controller.selection.extentOffset,
    );
    Clipboard.setData(ClipboardData(text: selectedString));

    return null;
  }
}

/// An intent that is bound to SelectAllAction to select all the text in its
/// controller.
class SelectAllIntent extends Intent {
  const SelectAllIntent();
}

/// An action that is bound to SelectAllAction that selects all text in its
/// TextEditingController.
class SelectAllAction extends Action<SelectAllIntent> {
  SelectAllAction(this.controller, this.focusNode);

  final TextEditingController controller;
  final FocusNode focusNode;

  @override
  Object? invoke(covariant SelectAllIntent intent) {
    controller.selection = controller.selection.copyWith(
      baseOffset: 0,
      extentOffset: controller.text.length,
      affinity: controller.selection.affinity,
    );

    focusNode.requestFocus();

    return null;
  }
}

/// The top level application class.
///
/// Shortcuts defined here are in effect for the whole app,
/// although different widgets may fulfill them differently.
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  static const String title = 'Shortcuts and Actions Demo';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: title,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Shortcuts(
        shortcuts: <LogicalKeySet, Intent>{
          LogicalKeySet(LogicalKeyboardKey.escape): const ClearIntent(),
          LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
              const CopyIntent(),
          LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
              const SelectAllIntent(),
        },
        child: const CopyableTextField(title: title),
      ),
    );
  }
}

void main() => runApp(const MyApp());
```


[`Action`]: https://api.flutter.dev/flutter/widgets/Action-class.html
[`Actions`]: https://api.flutter.dev/flutter/widgets/Actions-class.html
[`CallbackAction`]: https://api.flutter.dev/flutter/widgets/CallbackAction-class.html
[`CallbackShortcuts`]: https://api.flutter.dev/flutter/widgets/CallbackShortcuts-class.html
[`Intent`]: https://api.flutter.dev/flutter/widgets/Intent-class.html
[`Shortcuts`]: https://api.flutter.dev/flutter/widgets/Shortcuts-class.html
[Using Shortcuts Diagram]: /assets/images/docs/using_shortcuts.png

