|
| 1 | +// Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:flutter/material.dart'; |
| 6 | +import 'package:flutter/services.dart'; |
| 7 | + |
| 8 | +// This example app demonstrates a use case of patching |
| 9 | +// `KeyEventManager.keyMessageHandler`: be notified of key events that are not |
| 10 | +// handled by any focus handlers (such as shortcuts). |
| 11 | + |
| 12 | +void main() => runApp( |
| 13 | + const MaterialApp( |
| 14 | + home: Scaffold( |
| 15 | + body: Center( |
| 16 | + child: FallbackDemo(), |
| 17 | + ) |
| 18 | + ), |
| 19 | + ), |
| 20 | +); |
| 21 | + |
| 22 | +class FallbackDemo extends StatefulWidget { |
| 23 | + const FallbackDemo({super.key}); |
| 24 | + |
| 25 | + @override |
| 26 | + State<StatefulWidget> createState() => FallbackDemoState(); |
| 27 | +} |
| 28 | + |
| 29 | +class FallbackDemoState extends State<FallbackDemo> { |
| 30 | + String? _capture; |
| 31 | + late final FallbackFocusNode _node = FallbackFocusNode( |
| 32 | + onKeyEvent: (KeyEvent event) { |
| 33 | + if (event is! KeyDownEvent) { |
| 34 | + return false; |
| 35 | + } |
| 36 | + setState(() { |
| 37 | + _capture = event.logicalKey.keyLabel; |
| 38 | + }); |
| 39 | + // TRY THIS: Change the return value to true. You will no longer be able |
| 40 | + // to type text, because these key events will no longer be sent to the |
| 41 | + // text input system. |
| 42 | + return false; |
| 43 | + } |
| 44 | + ); |
| 45 | + |
| 46 | + @override |
| 47 | + Widget build(BuildContext context) { |
| 48 | + return FallbackFocus( |
| 49 | + node: _node, |
| 50 | + child: Container( |
| 51 | + decoration: BoxDecoration(border: Border.all(color: Colors.red)), |
| 52 | + padding: const EdgeInsets.all(10), |
| 53 | + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 400), |
| 54 | + child: Column( |
| 55 | + children: <Widget>[ |
| 56 | + const Text('This area handles key pressses that are unhandled by any shortcuts by displaying them below. ' |
| 57 | + 'Try text shortcuts such as Ctrl-A!'), |
| 58 | + Text(_capture == null ? '' : '$_capture is not handled by shortcuts.'), |
| 59 | + const TextField(decoration: InputDecoration(label: Text('Text field 1'))), |
| 60 | + Shortcuts( |
| 61 | + shortcuts: <ShortcutActivator, Intent>{ |
| 62 | + const SingleActivator(LogicalKeyboardKey.keyQ): VoidCallbackIntent(() {}), |
| 63 | + }, |
| 64 | + child: const TextField( |
| 65 | + decoration: InputDecoration(label: Text('This field also considers key Q as a shortcut (that does nothing).')), |
| 66 | + ), |
| 67 | + ), |
| 68 | + ], |
| 69 | + ), |
| 70 | + ) |
| 71 | + ); |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +/// A node used by [FallbackKeyEventRegistrar] to register fallback key handlers. |
| 76 | +/// |
| 77 | +/// This class must not be replaced by bare [KeyEventCallback] because Dart |
| 78 | +/// does not allow comparing with `==` on annonymous functions (always returns |
| 79 | +/// false.) |
| 80 | +class FallbackFocusNode { |
| 81 | + FallbackFocusNode({required this.onKeyEvent}); |
| 82 | + |
| 83 | + final KeyEventCallback onKeyEvent; |
| 84 | +} |
| 85 | + |
| 86 | +/// A singleton class that allows [FallbackFocus] to register fallback key |
| 87 | +/// event handlers. |
| 88 | +/// |
| 89 | +/// This class is initialized when [instance] is first called, at which time it |
| 90 | +/// patches [KeyEventManager.keyMessageHandler] with its own handler. |
| 91 | +/// |
| 92 | +/// A global registrar like [FallbackKeyEventRegistrar] is almost always needed |
| 93 | +/// when patching [KeyEventManager.keyMessageHandler]. This is because |
| 94 | +/// [FallbackFocus] will add and and remove callbacks constantly, but |
| 95 | +/// [KeyEventManager.keyMessageHandler] can only be patched once, and can not |
| 96 | +/// be unpatched. Therefore [FallbackFocus] must not directly interact with |
| 97 | +/// [KeyEventManager.keyMessageHandler], but through a separate registrar that |
| 98 | +/// handles listening reversibly. |
| 99 | +class FallbackKeyEventRegistrar { |
| 100 | + FallbackKeyEventRegistrar._(); |
| 101 | + static FallbackKeyEventRegistrar get instance { |
| 102 | + if (!_initialized) { |
| 103 | + // Get the global handler. |
| 104 | + final KeyMessageHandler? existing = ServicesBinding.instance.keyEventManager.keyMessageHandler; |
| 105 | + // The handler is guaranteed non-null since |
| 106 | + // `FallbackKeyEventRegistrar.instance` is only called during |
| 107 | + // `Focus.onFocusChange`, at which time `ServicesBinding.instance` must |
| 108 | + // have been called somewhere. |
| 109 | + assert(existing != null); |
| 110 | + // Assign the global handler with a patched handler. |
| 111 | + ServicesBinding.instance.keyEventManager.keyMessageHandler = _instance._buildHandler(existing!); |
| 112 | + _initialized = true; |
| 113 | + } |
| 114 | + return _instance; |
| 115 | + } |
| 116 | + static bool _initialized = false; |
| 117 | + static final FallbackKeyEventRegistrar _instance = FallbackKeyEventRegistrar._(); |
| 118 | + |
| 119 | + final List<FallbackFocusNode> _fallbackNodes = <FallbackFocusNode>[]; |
| 120 | + |
| 121 | + // Returns a handler that patches the existing `KeyEventManager.keyMessageHandler`. |
| 122 | + // |
| 123 | + // The existing `KeyEventManager.keyMessageHandler` is typically the one |
| 124 | + // assigned by the shortcut system, but it can be anything. The returned |
| 125 | + // handler calls that handler first, and if the event is not handled at all |
| 126 | + // by the framework, invokes the innermost `FallbackNode`'s handler. |
| 127 | + KeyMessageHandler _buildHandler(KeyMessageHandler existing) { |
| 128 | + return (KeyMessage message) { |
| 129 | + if (existing(message)) { |
| 130 | + return true; |
| 131 | + } |
| 132 | + if (_fallbackNodes.isNotEmpty) { |
| 133 | + for (final KeyEvent event in message.events) { |
| 134 | + if (_fallbackNodes.last.onKeyEvent(event)) { |
| 135 | + return true; |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | + return false; |
| 140 | + }; |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +/// A widget that, when focused, handles key events only if no other handlers |
| 145 | +/// do. |
| 146 | +/// |
| 147 | +/// If a [FallbackFocus] is being focused on, then key events that are not |
| 148 | +/// handled by other handlers will be dispatched to the `onKeyEvent` of [node]. |
| 149 | +/// If `onKeyEvent` returns true, this event is considered "handled" and will |
| 150 | +/// not move forward with the text input system. |
| 151 | +/// |
| 152 | +/// If multiple [FallbackFocus] nest, then only the innermost takes effect. |
| 153 | +/// |
| 154 | +/// Internally, this class registers its node to the singleton |
| 155 | +/// [FallbackKeyEventRegistrar]. The inner this widget is, the later its node |
| 156 | +/// will be added to the registrar's list when focused on. |
| 157 | +class FallbackFocus extends StatelessWidget { |
| 158 | + const FallbackFocus({ |
| 159 | + super.key, |
| 160 | + required this.node, |
| 161 | + required this.child, |
| 162 | + }); |
| 163 | + |
| 164 | + final Widget child; |
| 165 | + final FallbackFocusNode node; |
| 166 | + |
| 167 | + void _onFocusChange(bool focused) { |
| 168 | + if (focused) { |
| 169 | + FallbackKeyEventRegistrar.instance._fallbackNodes.add(node); |
| 170 | + } else { |
| 171 | + assert(FallbackKeyEventRegistrar.instance._fallbackNodes.last == node); |
| 172 | + FallbackKeyEventRegistrar.instance._fallbackNodes.removeLast(); |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + @override |
| 177 | + Widget build(BuildContext context) { |
| 178 | + return Focus( |
| 179 | + onFocusChange: _onFocusChange, |
| 180 | + child: child, |
| 181 | + ); |
| 182 | + } |
| 183 | +} |
0 commit comments