// 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'; // TODO(gspencergoog): Delete this example when deprecated RawKeyEvent API is // removed. // ignore_for_file: deprecated_member_use /// Flutter code sample for [KeyEventManager.keyMessageHandler]. 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 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, ); } }