How to process a paste command but still let TextField handle it?

It took me hours to figure out. I was not able to find sample code, and ChatGPT and Gemini kept hallucinating answers...

Basic implementation. TextField stops responding to paste events

Here's the usual way of handling a shortkey:

Shortcuts(
  shortcuts: const <ShortcutActivator, Intent>{
    SingleActivator(
      LogicalKeyboardKey.keyV,
      // Command key on Mac.
      meta: true,
    ): PasteIntent(),
  },
  child: Actions(
      actions: {
        PasteIntent:
            CallbackAction<PasteIntent>(onInvoke: (intent) {
          // Do something
        })
      },
      child: TextField(...)
      ))


// Later define our custom Intent
class PasteIntent {}

And it will indeed execute your code when you press Command+V. However, this will prevent the TextField from ever processing the command.

Attempting to pass through the event to TextField

DoNothingAndStopPropagationIntent

I found this  DoNothingAndStopPropagationIntent that supposedly "disable[s] a keyboard shortcut defined by a widget higher in the widget hierarchy", so I thought I could create a Shortcuts, handle the event, then add another Shortcuts using that StopPropagationIntent that wraps the TextField. It did not work.

Reusing PasteTextIntent, Actions.maybeFind

Then I tried to reuse what I assumed was the original PasteIntent used by Flutter widgets themselves. Instead of mapping Command+V to my custom PasteIntent, I used PasteTextIntent. Then in my callback, I tried to find other actions in the Context based on https://docs.flutter.dev/ui/interactivity/actions-and-shortcuts#invoking-actions. Something like this:

Action<PasteTextIntent>? paste = Actions.maybeFind<PasteTextIntent>(
  context,
);
paste?.invoke(intent);

But it did not find TextField's Action for PasteTextIntent.

consumesKey: false?

Another thing I tried was to create a real Action instead of a CallbackAction, and implement Action's consumesKey.

consumesKey method - Action class - widgets library - Dart API
API docs for the consumesKey method from the Action class, for the Dart programming language.

By returning false, I hoped that Shortcut would think my Action didn't consume the event, and propagate it down to the TextField. This did not work either.

The solution: Action.overridable

Eventually, I stumbled upon Action.overridable and that's how I fixed my issue. Action.overridable makes someone allow someone else to override their Action. The beauty of it is that if you override that Action, you also get a reference to the default action.

Action.overridable constructor - Action - widgets library - Dart API
API docs for the Action.overridable constructor from Class Action from the widgets library, for the Dart programming language.

The issue is, it only works if the creator of the original Action allows their Action to be overridable. Does TextField allow it?

DefaultTextEditingShortcuts class - widgets library - Dart API
API docs for the DefaultTextEditingShortcuts class from the widgets library, for the Dart programming language.
flutter/packages/flutter/lib/src/widgets/editable_text.dart at 7ade3f344755eb86b20aae39610a5758b7e510d8 · flutter/flutter
Flutter makes it easy and fast to build beautiful apps for mobile and beyond - flutter/flutter
flutter/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart at 7ade3f344755eb86b20aae39610a5758b7e510d8 · flutter/flutter
Flutter makes it easy and fast to build beautiful apps for mobile and beyond - flutter/flutter

And the answer is YES! So here's my final solution. This will execute my code to handle Command+V but also let TextField handle it:

Shortcuts(
  shortcuts: const <ShortcutActivator, Intent>{
    SingleActivator(
      LogicalKeyboardKey.keyV,
      // command on Mac.
      meta: true,
    ): PasteTextIntent(SelectionChangedCause.keyboard),
  },
  child: Actions(
      actions: <Type, Action<Intent>>{
        // Override TextField's handler to also support images.
        PasteTextIntent:
            PasteScreenshotAction(onFilePasted: (xfile) {
          filesPicked.add([xfile]);
        })
      },
      child: TextField(...)
  ))
...

class PasteScreenshotAction extends Action<PasteTextIntent> {
  PasteScreenshotAction({required this.onFilePasted});

  final void Function(XFile xfile) onFilePasted;

  @override
  Object? invoke(covariant PasteTextIntent intent) async {
    // Let TextField process the paste.
    callingAction?.invoke(intent);

    // And also handle images from the clipboard.
    ..
  }
}