Explain the "patching" protocol in KeyMessageManager.keyMessageHandler
and add an example (#105280)
This commit is contained in:
parent
d155bc1bad
commit
4056d3ffde
@ -0,0 +1,183 @@
|
||||
// Copyright 2014 The Flutter 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/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// This example app demonstrates a use case of patching
|
||||
// `KeyEventManager.keyMessageHandler`: be notified of key events that are not
|
||||
// handled by any focus handlers (such as shortcuts).
|
||||
|
||||
void main() => runApp(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: FallbackDemo(),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
class FallbackDemo extends StatefulWidget {
|
||||
const FallbackDemo({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => FallbackDemoState();
|
||||
}
|
||||
|
||||
class FallbackDemoState extends State<FallbackDemo> {
|
||||
String? _capture;
|
||||
late final FallbackFocusNode _node = FallbackFocusNode(
|
||||
onKeyEvent: (KeyEvent event) {
|
||||
if (event is! KeyDownEvent) {
|
||||
return false;
|
||||
}
|
||||
setState(() {
|
||||
_capture = event.logicalKey.keyLabel;
|
||||
});
|
||||
// TRY THIS: Change the return value to true. You will no longer be able
|
||||
// to type text, because these key events will no longer be sent to the
|
||||
// text input system.
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FallbackFocus(
|
||||
node: _node,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.red)),
|
||||
padding: const EdgeInsets.all(10),
|
||||
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 400),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Text('This area handles key pressses that are unhandled by any shortcuts by displaying them below. '
|
||||
'Try text shortcuts such as Ctrl-A!'),
|
||||
Text(_capture == null ? '' : '$_capture is not handled by shortcuts.'),
|
||||
const TextField(decoration: InputDecoration(label: Text('Text field 1'))),
|
||||
Shortcuts(
|
||||
shortcuts: <ShortcutActivator, Intent>{
|
||||
const SingleActivator(LogicalKeyboardKey.keyQ): VoidCallbackIntent(() {}),
|
||||
},
|
||||
child: const TextField(
|
||||
decoration: InputDecoration(label: Text('This field also considers key Q as a shortcut (that does nothing).')),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A node used by [FallbackKeyEventRegistrar] to register fallback key handlers.
|
||||
///
|
||||
/// This class must not be replaced by bare [KeyEventCallback] because Dart
|
||||
/// does not allow comparing with `==` on annonymous functions (always returns
|
||||
/// false.)
|
||||
class FallbackFocusNode {
|
||||
FallbackFocusNode({required this.onKeyEvent});
|
||||
|
||||
final KeyEventCallback onKeyEvent;
|
||||
}
|
||||
|
||||
/// A singleton class that allows [FallbackFocus] to register fallback key
|
||||
/// event handlers.
|
||||
///
|
||||
/// This class is initialized when [instance] is first called, at which time it
|
||||
/// patches [KeyEventManager.keyMessageHandler] with its own handler.
|
||||
///
|
||||
/// A global registrar like [FallbackKeyEventRegistrar] is almost always needed
|
||||
/// when patching [KeyEventManager.keyMessageHandler]. This is because
|
||||
/// [FallbackFocus] will add and and remove callbacks constantly, but
|
||||
/// [KeyEventManager.keyMessageHandler] can only be patched once, and can not
|
||||
/// be unpatched. Therefore [FallbackFocus] must not directly interact with
|
||||
/// [KeyEventManager.keyMessageHandler], but through a separate registrar that
|
||||
/// handles listening reversibly.
|
||||
class FallbackKeyEventRegistrar {
|
||||
FallbackKeyEventRegistrar._();
|
||||
static FallbackKeyEventRegistrar get instance {
|
||||
if (!_initialized) {
|
||||
// Get the global handler.
|
||||
final KeyMessageHandler? existing = ServicesBinding.instance.keyEventManager.keyMessageHandler;
|
||||
// The handler is guaranteed non-null since
|
||||
// `FallbackKeyEventRegistrar.instance` is only called during
|
||||
// `Focus.onFocusChange`, at which time `ServicesBinding.instance` must
|
||||
// have been called somewhere.
|
||||
assert(existing != null);
|
||||
// Assign the global handler with a patched handler.
|
||||
ServicesBinding.instance.keyEventManager.keyMessageHandler = _instance._buildHandler(existing!);
|
||||
_initialized = true;
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
static bool _initialized = false;
|
||||
static final FallbackKeyEventRegistrar _instance = FallbackKeyEventRegistrar._();
|
||||
|
||||
final List<FallbackFocusNode> _fallbackNodes = <FallbackFocusNode>[];
|
||||
|
||||
// Returns a handler that patches the existing `KeyEventManager.keyMessageHandler`.
|
||||
//
|
||||
// The existing `KeyEventManager.keyMessageHandler` is typically the one
|
||||
// assigned by the shortcut system, but it can be anything. The returned
|
||||
// handler calls that handler first, and if the event is not handled at all
|
||||
// by the framework, invokes the innermost `FallbackNode`'s handler.
|
||||
KeyMessageHandler _buildHandler(KeyMessageHandler existing) {
|
||||
return (KeyMessage message) {
|
||||
if (existing(message)) {
|
||||
return true;
|
||||
}
|
||||
if (_fallbackNodes.isNotEmpty) {
|
||||
for (final KeyEvent event in message.events) {
|
||||
if (_fallbackNodes.last.onKeyEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that, when focused, handles key events only if no other handlers
|
||||
/// do.
|
||||
///
|
||||
/// If a [FallbackFocus] is being focused on, then key events that are not
|
||||
/// handled by other handlers will be dispatched to the `onKeyEvent` of [node].
|
||||
/// If `onKeyEvent` returns true, this event is considered "handled" and will
|
||||
/// not move forward with the text input system.
|
||||
///
|
||||
/// If multiple [FallbackFocus] nest, then only the innermost takes effect.
|
||||
///
|
||||
/// Internally, this class registers its node to the singleton
|
||||
/// [FallbackKeyEventRegistrar]. The inner this widget is, the later its node
|
||||
/// will be added to the registrar's list when focused on.
|
||||
class FallbackFocus extends StatelessWidget {
|
||||
const FallbackFocus({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final FallbackFocusNode node;
|
||||
|
||||
void _onFocusChange(bool focused) {
|
||||
if (focused) {
|
||||
FallbackKeyEventRegistrar.instance._fallbackNodes.add(node);
|
||||
} else {
|
||||
assert(FallbackKeyEventRegistrar.instance._fallbackNodes.last == node);
|
||||
FallbackKeyEventRegistrar.instance._fallbackNodes.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
onFocusChange: _onFocusChange,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
// Copyright 2014 The Flutter 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/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_api_samples/widgets/hardware_keyboard/key_event_manager.0.dart'
|
||||
as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('App tracks lifecycle states', (WidgetTester tester) async {
|
||||
Future<String> getCapturedKey() async {
|
||||
final Widget textWidget = tester.firstWidget(
|
||||
find.textContaining('is not handled by shortcuts.'));
|
||||
expect(textWidget, isA<Text>());
|
||||
return (textWidget as Text).data!.split(' ').first;
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: example.FallbackDemo(),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Focus on the first text field.
|
||||
await tester.tap(find.byType(TextField).first);
|
||||
|
||||
// Press Q, which is taken as a text input, unhandled by the keyboard system.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyQ);
|
||||
await tester.pump();
|
||||
expect(await getCapturedKey(), 'Q');
|
||||
|
||||
// Press Ctrl-A, which is taken as a text short cut, handled by the keyboard system.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
expect(await getCapturedKey(), 'Q');
|
||||
|
||||
// Press A, which is taken as a text input, handled by the keyboard system.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.pump();
|
||||
expect(await getCapturedKey(), 'A');
|
||||
|
||||
// Focus on the second text field.
|
||||
await tester.tap(find.byType(TextField).last);
|
||||
|
||||
// Press Q, which is taken as a stub shortcut, handled by the keyboard system.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyQ);
|
||||
await tester.pump();
|
||||
expect(await getCapturedKey(), 'A');
|
||||
|
||||
// Press B, which is taken as a text input, unhandled by the keyboard system.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
|
||||
await tester.pump();
|
||||
expect(await getCapturedKey(), 'B');
|
||||
|
||||
// Press Ctrl-A, which is taken as a text short cut, handled by the keyboard system.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
expect(await getCapturedKey(), 'B');
|
||||
});
|
||||
}
|
@ -618,25 +618,50 @@ enum KeyDataTransitMode {
|
||||
keyDataThenRawKeyData,
|
||||
}
|
||||
|
||||
/// The assumbled information corresponding to a native key message.
|
||||
/// The assembled information converted from a native key message.
|
||||
///
|
||||
/// While Flutter's [KeyEvent]s are created from key messages from the native
|
||||
/// platform, every native message might result in multiple [KeyEvent]s. For
|
||||
/// example, this might happen in order to synthesize missed modifier key
|
||||
/// presses or releases.
|
||||
/// Native key messages, produced by physically pressing or releasing
|
||||
/// keyboard keys, are translated into two different event streams in Flutter:
|
||||
///
|
||||
/// A [KeyMessage] bundles all information related to a native key message
|
||||
/// together for the convenience of propagation on the [FocusNode] tree.
|
||||
/// * The [KeyEvent] stream, represented by [KeyMessage.events] (recommended).
|
||||
/// * The [RawKeyEvent] stream, represented by [KeyMessage.rawEvent] (legacy,
|
||||
/// to be deprecated).
|
||||
///
|
||||
/// When dispatched to handlers or listeners, or propagated through the
|
||||
/// [FocusNode] tree, all handlers or listeners belonging to a node are
|
||||
/// executed regardless of their [KeyEventResult], and all results are combined
|
||||
/// into the result of the node using [combineKeyEventResults]. Empty [events]
|
||||
/// or [rawEvent] should be considered as a result of [KeyEventResult.ignored].
|
||||
/// Either the [KeyEvent] stream or the [RawKeyEvent] stream alone provides a
|
||||
/// complete description of the keyboard messages, but in different event
|
||||
/// models. Flutter is still transitioning from the legacy model to the new
|
||||
/// model, therefore it dispatches both streams simultaneously until the
|
||||
/// transition is completed. [KeyMessage] is used to bundle the
|
||||
/// stream segments of both models from a native key message together for the
|
||||
/// convenience of propagation.
|
||||
///
|
||||
/// In very rare cases, a native key message might not result in a [KeyMessage].
|
||||
/// For example, key messages for Fn key are ignored on macOS for the
|
||||
/// convenience of cross-platform code.
|
||||
/// Typically, an application either processes [KeyMessage.events]
|
||||
/// or [KeyMessage.rawEvent], not both. For example, handling a
|
||||
/// [KeyMessage], means handling each event in [KeyMessage.events].
|
||||
///
|
||||
/// In advanced cases, a widget needs to process both streams at the same time.
|
||||
/// For example, [FocusNode] has an `onKey` that dispatches [RawKeyEvent]s and
|
||||
/// an `onKeyEvent` that dispatches [KeyEvent]s. To processes a [KeyMessage],
|
||||
/// it first calls `onKeyEvent` with each [KeyEvent] of [events], and then
|
||||
/// `onKey` with [rawEvent]. All callbacks are invoked regardless of their
|
||||
/// [KeyEventResult]. Their results are combined into the result of the node
|
||||
/// using [combineKeyEventResults].
|
||||
///
|
||||
/// ```dart
|
||||
/// void handleMessage(FocusNode node, KeyMessage message) {
|
||||
/// final List<KeyEventResult> results = <KeyEventResult>[];
|
||||
/// if (node.onKeyEvent != null) {
|
||||
/// for (final KeyEvent event in message.events) {
|
||||
/// results.add(node.onKeyEvent!(node, event));
|
||||
/// }
|
||||
/// }
|
||||
/// if (node.onKey != null && message.rawEvent != null) {
|
||||
/// results.add(node.onKey!(node, message.rawEvent!));
|
||||
/// }
|
||||
/// final KeyEventResult result = combineKeyEventResults(results);
|
||||
/// // Progress based on `result`...
|
||||
/// }
|
||||
/// ```
|
||||
@immutable
|
||||
class KeyMessage {
|
||||
/// Create a [KeyMessage] by providing all information.
|
||||
@ -710,39 +735,87 @@ class KeyEventManager {
|
||||
/// This is typically only called by [ServicesBinding].
|
||||
KeyEventManager(this._hardwareKeyboard, this._rawKeyboard);
|
||||
|
||||
/// The global handler for all hardware key messages of Flutter.
|
||||
/// The global entrance which handles all key events sent to Flutter.
|
||||
///
|
||||
/// Key messages received from the platform are first sent to [RawKeyboard]'s
|
||||
/// listeners and [HardwareKeyboard]'s handlers, then sent to
|
||||
/// [keyMessageHandler], regardless of the results of [HardwareKeyboard]'s
|
||||
/// handlers. The event results from the handlers and [keyMessageHandler] are
|
||||
/// combined and returned to the platform. The event result is explained
|
||||
/// below.
|
||||
/// Typical applications use [WidgetsBinding], where this field is
|
||||
/// set by the focus system (see `FocusManger`) on startup to a function that
|
||||
/// dispatches incoming events to the focus system, including
|
||||
/// `FocusNode.onKey`, `FocusNode.onKeyEvent`, and `Shortcuts`. In this case,
|
||||
/// the application does not need to read, assign, or invoke this value.
|
||||
///
|
||||
/// For most common applications, which use [WidgetsBinding], this field
|
||||
/// is set by the focus system (see `FocusManger`) on startup and should not
|
||||
/// be change explicitly.
|
||||
/// For advanced uses, the application can "patch" this callback. See below
|
||||
/// for details.
|
||||
///
|
||||
/// ## Handlers and event results
|
||||
///
|
||||
/// Roughly speaking, Flutter processes each native key event with the
|
||||
/// following phases:
|
||||
///
|
||||
/// 1. Platform-side pre-filtering, sometimes used for IME.
|
||||
/// 2. The key event system.
|
||||
/// 3. The text input system.
|
||||
/// 4. Other native components (possibly non-Flutter).
|
||||
///
|
||||
/// Each phase will conclude with a boolean called an "event result". If the
|
||||
/// result is true, this phase _handles_ the event and prevents the event
|
||||
/// from being propagated to the next phase. This mechanism allows shortcuts
|
||||
/// such as "Ctrl-C" to not generate a text "C" in the text field, or
|
||||
/// shortcuts that are not handled by any components to trigger special alerts
|
||||
/// (such as the "bonk" noise on macOS).
|
||||
///
|
||||
/// In the second phase, known as "the key event system", the event is dispatched
|
||||
/// to several destinations: [RawKeyboard]'s listeners,
|
||||
/// [HardwareKeyboard]'s handlers, and [keyMessageHandler].
|
||||
/// All destinations will always receive the event regardless of the handlers'
|
||||
/// results. If any handler's result is true, then the overall result of the
|
||||
/// second phase is true, and event propagation is stopped.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RawKeyboard.addListener], which adds a raw keyboard listener.
|
||||
/// * [RawKeyboardListener], which is also implemented by adding a raw
|
||||
/// keyboard listener.
|
||||
/// * [HardwareKeyboard.addHandler], which adds a hardware keyboard handler.
|
||||
///
|
||||
/// ## Advanced usages: Manual assignment or patching
|
||||
///
|
||||
/// If you are not using the focus system to manage focus, set this
|
||||
/// attribute to a [KeyMessageHandler] that returns true if the propagation
|
||||
/// on the platform should not be continued. If this field is null, key events
|
||||
/// will be assumed to not have been handled by Flutter.
|
||||
/// will be assumed to not have been handled by Flutter, a result of "false".
|
||||
///
|
||||
/// ## Event result
|
||||
/// Even if you are using the focus system, you might also want to do more
|
||||
/// than the focus system allows. In these cases, you can _patch_
|
||||
/// [keyMessageHandler] by setting it to a callback that performs your tasks
|
||||
/// and calls the original callback in between (or not at all.)
|
||||
///
|
||||
/// Key messages on the platform are given to Flutter to be handled by the
|
||||
/// engine. If they are not handled, then the platform will continue to
|
||||
/// distribute the keys (i.e. propagate them) to other (possibly non-Flutter)
|
||||
/// components in the application. The return value from this handler tells
|
||||
/// the platform to either stop propagation (by returning true: "event
|
||||
/// handled"), or pass the event on to other controls (false: "event not
|
||||
/// handled"). Some platforms might trigger special alerts if the event
|
||||
/// is not handled by other controls either (such as the "bonk" noise on
|
||||
/// macOS).
|
||||
/// Patching [keyMessageHandler] can not be reverted. You should always assume
|
||||
/// that another component might haved patched it before you and after you.
|
||||
/// This means that you might want to write your own global notification
|
||||
/// manager, to which callbacks can be added and removed.
|
||||
///
|
||||
/// The result from [keyMessageHandler] and [HardwareKeyboard]'s handlers
|
||||
/// are combined. If any of the handlers claim to handle the event,
|
||||
/// the overall result will be "event handled".
|
||||
/// You should not patch [keyMessageHandler] until the `FocusManager` has assigned
|
||||
/// its callback. This is assured during any time within the widget lifecycle
|
||||
/// (such as `initState`), or after calling `WidgetManager.instance`.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to process key events that are not handled by any
|
||||
/// focus handler (such as `Shortcuts`) by patching [keyMessageHandler].
|
||||
///
|
||||
/// The app prints out any key events that are not handled by the app body.
|
||||
/// Try typing something in the first text field. These key presses are not
|
||||
/// handled by `Shorcuts` and will be sent to the fallback handler and printed
|
||||
/// out. Now try some text shortcuts, such as Ctrl+A. The KeyA press is
|
||||
/// handled as a shortcut, and is not sent to the fallback handler and so is
|
||||
/// not printed out.
|
||||
///
|
||||
/// The key widget is `FallbackKeyEventRegistrar`, a necessity class to allow
|
||||
/// reversible patching. `FallbackFocus` and `FallbackFocusNode` are also
|
||||
/// useful to recognize the widget tree's structure. `FallbackDemo` is an
|
||||
/// example of using them in an app.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/hardware_keyboard/key_event_manager.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
@ -1466,7 +1466,6 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
|
||||
@override
|
||||
void dispose() {
|
||||
if (ServicesBinding.instance.keyEventManager.keyMessageHandler == _handleKeyMessage) {
|
||||
ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
|
||||
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent);
|
||||
}
|
||||
super.dispose();
|
||||
|
@ -951,7 +951,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
||||
_pendingExceptionDetails = null;
|
||||
_parentZone = null;
|
||||
buildOwner!.focusManager.dispose();
|
||||
|
||||
ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
|
||||
buildOwner!.focusManager = FocusManager()..registerGlobalHandlers();
|
||||
|
||||
// Disabling the warning because @visibleForTesting doesn't take the testing
|
||||
// framework itself into account, but we don't want it visible outside of
|
||||
// tests.
|
||||
|
Loading…
x
Reference in New Issue
Block a user