blob: e54a7e5671d79904e1146890f5e3678034967eee [file] [log] [blame]
// 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.
/// Flutter code sample for [KeyEventManager.keyMessageHandler].
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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 presses 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 anonymous 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,
);
}
}