key method
One keystroke — escape, enter, meta+k, shift+tab.
chord is +-separated: the last name is the key that fires, everything
before it is held down for it and released after, in reverse. Names are
LogicalKeyboardKey debug names spelled any way that reads (arrowDown,
Arrow Down), a single character (k), or one of the shorthands people
actually type — cmd, ctrl, alt, opt, shift, esc. A shorthand
modifier resolves to its left key, which is what every SingleActivator
checks for. A Mac shortcut and its Windows/Linux twin are different chords:
meta+k and control+k.
This is for shortcuts and navigation, not for typing. A character
does not reach a text field through a key event on any platform — the
platform's text input sends the edit, and the key event is a separate
thing that happens to accompany it. So key('a') into a focused
TextField leaves it empty, here and in a real app; enterText is the
verb that types. What this is for is escape, tab, the arrows,
enter, and every Shortcuts binding the app declares.
flutter_test's simulateKeyDownEvent cannot be used here, which is why
this reimplements it. It always also sends the raw key message, and it
sends it through TestDefaultBinaryMessengerBinding.instance — which, in a
process whose binding is the real WidgetsFlutterBinding, throws
'_debugInitializedType == null': is not true. Measured, first attempt.
See KeyChord for what a name means and what a keystroke is — the half
a scenario presses too.
Implementation
Future<DriveStep> key(String chord, {Duration? settle}) async {
var watch = Stopwatch()..start();
var keys = KeyChord.parse(chord);
// **Refused rather than injected on top.** A down for a key the human is
// physically holding leaves the framework's idea of the keyboard wrong
// the moment this releases it — and this is a surface two people drive
// at once.
if (keys.held case var key?) {
throw TargetError(
TargetFailure.covered,
'${key.debugName} is already held down: someone is pressing it, or a '
'previous chord was interrupted. Pressing it again would leave the '
'keyboard in the wrong state. Release it and retry.',
);
}
var handled = await keys.press();
// **The one way this verb can silently do nothing, caught.** Key events
// dispatch from whatever holds primary focus and bubble to its *ancestors*.
// With nothing focused that is the root scope, which sits above the app's
// `Shortcuts` — so every binding in the app is missed and the keystroke
// lands nowhere. On a window that was launched hidden, or that the human
// has never clicked, that is the *normal* state rather than an edge case:
// measured on a real app, ⌘K did nothing three times running until one
// `enterText` put focus in a field, and then opened the palette.
//
// Both halves are needed. Plenty of keystrokes are legitimately unhandled —
// a letter typed at nothing, an Escape with no binding — so `handled` alone
// would refuse constantly; and an app can handle a key through a
// `HardwareKeyboard` handler with nothing focused at all, so the focus
// alone would refuse wrongly. Together they mean the dispatch never reached
// the app's tree and nothing else took it either.
if (!handled && nothingFocused) {
throw TargetError(
TargetFailure.notFound,
'the keystroke went nowhere: nothing in the app holds focus, so none '
"of the app's `Shortcuts` received it. Give the app focus and retry: "
'`tap` a control, or `enterText` into a field. A window that was '
'launched hidden, or that nobody has clicked, starts out like this. '
'The keys were pressed and released, so nothing is stuck.',
);
}
var result = await lane.settle(settle ?? settleBudget);
return DriveStep(
verb: 'key',
target: chord,
settle: result,
elapsed: watch.elapsed,
);
}