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 'dart:collection'; |
6 | |
7 | import 'package:flutter/foundation.dart'; |
8 | import 'package:flutter/scheduler.dart'; |
9 | import 'package:flutter/services.dart'; |
10 | |
11 | import 'actions.dart'; |
12 | import 'focus_manager.dart'; |
13 | import 'focus_scope.dart'; |
14 | import 'framework.dart'; |
15 | import 'platform_menu_bar.dart'; |
16 | |
17 | final Set<LogicalKeyboardKey> _controlSynonyms = LogicalKeyboardKey.expandSynonyms(<LogicalKeyboardKey>{LogicalKeyboardKey.control}); |
18 | final Set<LogicalKeyboardKey> _shiftSynonyms = LogicalKeyboardKey.expandSynonyms(<LogicalKeyboardKey>{LogicalKeyboardKey.shift}); |
19 | final Set<LogicalKeyboardKey> _altSynonyms = LogicalKeyboardKey.expandSynonyms(<LogicalKeyboardKey>{LogicalKeyboardKey.alt}); |
20 | final Set<LogicalKeyboardKey> _metaSynonyms = LogicalKeyboardKey.expandSynonyms(<LogicalKeyboardKey>{LogicalKeyboardKey.meta}); |
21 | |
22 | /// A set of [KeyboardKey]s that can be used as the keys in a [Map]. |
23 | /// |
24 | /// A key set contains the keys that are down simultaneously to represent a |
25 | /// shortcut. |
26 | /// |
27 | /// This is a thin wrapper around a [Set], but changes the equality comparison |
28 | /// from an identity comparison to a contents comparison so that non-identical |
29 | /// sets with the same keys in them will compare as equal. |
30 | /// |
31 | /// See also: |
32 | /// |
33 | /// * [ShortcutManager], which uses [LogicalKeySet] (a [KeySet] subclass) to |
34 | /// define its key map. |
35 | @immutable |
36 | class KeySet<T extends KeyboardKey> { |
37 | /// A constructor for making a [KeySet] of up to four keys. |
38 | /// |
39 | /// If you need a set of more than four keys, use [KeySet.fromSet]. |
40 | /// |
41 | /// The same [KeyboardKey] may not be appear more than once in the set. |
42 | KeySet( |
43 | T key1, [ |
44 | T? key2, |
45 | T? key3, |
46 | T? key4, |
47 | ]) : _keys = HashSet<T>()..add(key1) { |
48 | int count = 1; |
49 | if (key2 != null) { |
50 | _keys.add(key2); |
51 | assert(() { |
52 | count++; |
53 | return true; |
54 | }()); |
55 | } |
56 | if (key3 != null) { |
57 | _keys.add(key3); |
58 | assert(() { |
59 | count++; |
60 | return true; |
61 | }()); |
62 | } |
63 | if (key4 != null) { |
64 | _keys.add(key4); |
65 | assert(() { |
66 | count++; |
67 | return true; |
68 | }()); |
69 | } |
70 | assert(_keys.length == count, 'Two or more provided keys are identical. Each key must appear only once.' ); |
71 | } |
72 | |
73 | /// Create a [KeySet] from a set of [KeyboardKey]s. |
74 | /// |
75 | /// Do not mutate the `keys` set after passing it to this object. |
76 | /// |
77 | /// The `keys` set must not be empty. |
78 | KeySet.fromSet(Set<T> keys) |
79 | : assert(keys.isNotEmpty), |
80 | assert(!keys.contains(null)), |
81 | _keys = HashSet<T>.of(keys); |
82 | |
83 | /// Returns a copy of the [KeyboardKey]s in this [KeySet]. |
84 | Set<T> get keys => _keys.toSet(); |
85 | final HashSet<T> _keys; |
86 | |
87 | @override |
88 | bool operator ==(Object other) { |
89 | if (other.runtimeType != runtimeType) { |
90 | return false; |
91 | } |
92 | return other is KeySet<T> |
93 | && setEquals<T>(other._keys, _keys); |
94 | } |
95 | |
96 | // Cached hash code value. Improves [hashCode] performance by 27%-900%, |
97 | // depending on key set size and read/write ratio. |
98 | @override |
99 | late final int hashCode = _computeHashCode(_keys); |
100 | |
101 | // Arrays used to temporarily store hash codes for sorting. |
102 | static final List<int> _tempHashStore3 = <int>[0, 0, 0]; // used to sort exactly 3 keys |
103 | static final List<int> _tempHashStore4 = <int>[0, 0, 0, 0]; // used to sort exactly 4 keys |
104 | static int _computeHashCode<T>(Set<T> keys) { |
105 | // Compute order-independent hash and cache it. |
106 | final int length = keys.length; |
107 | final Iterator<T> iterator = keys.iterator; |
108 | |
109 | // There's always at least one key. Just extract it. |
110 | iterator.moveNext(); |
111 | final int h1 = iterator.current.hashCode; |
112 | |
113 | if (length == 1) { |
114 | // Don't do anything fancy if there's exactly one key. |
115 | return h1; |
116 | } |
117 | |
118 | iterator.moveNext(); |
119 | final int h2 = iterator.current.hashCode; |
120 | if (length == 2) { |
121 | // No need to sort if there's two keys, just compare them. |
122 | return h1 < h2 |
123 | ? Object.hash(h1, h2) |
124 | : Object.hash(h2, h1); |
125 | } |
126 | |
127 | // Sort key hash codes and feed to Object.hashAll to ensure the aggregate |
128 | // hash code does not depend on the key order. |
129 | final List<int> sortedHashes = length == 3 |
130 | ? _tempHashStore3 |
131 | : _tempHashStore4; |
132 | sortedHashes[0] = h1; |
133 | sortedHashes[1] = h2; |
134 | iterator.moveNext(); |
135 | sortedHashes[2] = iterator.current.hashCode; |
136 | if (length == 4) { |
137 | iterator.moveNext(); |
138 | sortedHashes[3] = iterator.current.hashCode; |
139 | } |
140 | sortedHashes.sort(); |
141 | return Object.hashAll(sortedHashes); |
142 | } |
143 | } |
144 | |
145 | /// An interface to define the keyboard key combination to trigger a shortcut. |
146 | /// |
147 | /// [ShortcutActivator]s are used by [Shortcuts] widgets, and are mapped to |
148 | /// [Intent]s, the intended behavior that the key combination should trigger. |
149 | /// When a [Shortcuts] widget receives a key event, its [ShortcutManager] looks |
150 | /// up the first matching [ShortcutActivator], and signals the corresponding |
151 | /// [Intent], which might trigger an action as defined by a hierarchy of |
152 | /// [Actions] widgets. For a detailed introduction on the mechanism and use of |
153 | /// the shortcut-action system, see [Actions]. |
154 | /// |
155 | /// The matching [ShortcutActivator] is looked up in the following way: |
156 | /// |
157 | /// * Find the registered [ShortcutActivator]s whose [triggers] contain the |
158 | /// incoming event. |
159 | /// * Of the previous list, finds the first activator whose [accepts] returns |
160 | /// true in the order of insertion. |
161 | /// |
162 | /// See also: |
163 | /// |
164 | /// * [SingleActivator], an implementation that represents a single key combined |
165 | /// with modifiers (control, shift, alt, meta). |
166 | /// * [CharacterActivator], an implementation that represents key combinations |
167 | /// that result in the specified character, such as question mark. |
168 | /// * [LogicalKeySet], an implementation that requires one or more |
169 | /// [LogicalKeyboardKey]s to be pressed at the same time. Prefer |
170 | /// [SingleActivator] when possible. |
171 | abstract class ShortcutActivator { |
172 | /// Abstract const constructor. This constructor enables subclasses to provide |
173 | /// const constructors so that they can be used in const expressions. |
174 | const ShortcutActivator(); |
175 | |
176 | /// An optional property to provide all the keys that might be the final event |
177 | /// to trigger this shortcut. |
178 | /// |
179 | /// For example, for `Ctrl-A`, [LogicalKeyboardKey.keyA] is the only trigger, |
180 | /// while [LogicalKeyboardKey.control] is not, because the shortcut should |
181 | /// only work by pressing KeyA *after* Ctrl, but not before. For `Ctrl-A-E`, |
182 | /// on the other hand, both KeyA and KeyE should be triggers, since either of |
183 | /// them is allowed to trigger. |
184 | /// |
185 | /// If provided, trigger keys can be used as a first-pass filter for incoming |
186 | /// events in order to optimize lookups, as [Intent]s are stored in a [Map] |
187 | /// and indexed by trigger keys. It is up to the individual implementors of |
188 | /// this interface to decide if they ignore triggers or not. |
189 | /// |
190 | /// Subclasses should make sure that the return value of this method does not |
191 | /// change throughout the lifespan of this object. |
192 | /// |
193 | /// This method might also return null, which means this activator declares |
194 | /// all keys as trigger keys. Activators whose [triggers] return null will be |
195 | /// tested with [accepts] on every event. Since this becomes a linear search, |
196 | /// and having too many might impact performance, it is preferred to return |
197 | /// non-null [triggers] whenever possible. |
198 | Iterable<LogicalKeyboardKey>? get triggers => null; |
199 | |
200 | /// Whether the triggering `event` and the keyboard `state` at the time of the |
201 | /// event meet required conditions, providing that the event is a triggering |
202 | /// event. |
203 | /// |
204 | /// For example, for `Ctrl-A`, it has to check if the event is a |
205 | /// [KeyDownEvent], if either side of the Ctrl key is pressed, and none of the |
206 | /// Shift keys, Alt keys, or Meta keys are pressed; it doesn't have to check |
207 | /// if KeyA is pressed, since it's already guaranteed. |
208 | /// |
209 | /// As a possible performance improvement, implementers of this function are |
210 | /// encouraged (but not required) to check the [triggers] member, if it is |
211 | /// non-null, to see if it contains the event's logical key before doing more |
212 | /// complicated work. |
213 | /// |
214 | /// This method must not cause any side effects for the `state`. Typically |
215 | /// this is only used to query whether [HardwareKeyboard.logicalKeysPressed] |
216 | /// contains a key. |
217 | /// |
218 | /// See also: |
219 | /// |
220 | /// * [LogicalKeyboardKey.collapseSynonyms], which helps deciding whether a |
221 | /// modifier key is pressed when the side variation is not important. |
222 | bool accepts(KeyEvent event, HardwareKeyboard state); |
223 | |
224 | /// Returns true if the event and current [HardwareKeyboard] state would cause |
225 | /// this [ShortcutActivator] to be activated. |
226 | @Deprecated( |
227 | 'Call accepts on the activator instead. ' |
228 | 'This feature was deprecated after v3.16.0-15.0.pre.' , |
229 | ) |
230 | static bool isActivatedBy(ShortcutActivator activator, KeyEvent event) { |
231 | return activator.accepts(event, HardwareKeyboard.instance); |
232 | } |
233 | |
234 | /// Returns a description of the key set that is short and readable. |
235 | /// |
236 | /// Intended to be used in debug mode for logging purposes. |
237 | String debugDescribeKeys(); |
238 | } |
239 | |
240 | /// A set of [LogicalKeyboardKey]s that can be used as the keys in a map. |
241 | /// |
242 | /// [LogicalKeySet] can be used as a [ShortcutActivator]. It is not recommended |
243 | /// to use [LogicalKeySet] for a common shortcut such as `Delete` or `Ctrl+C`, |
244 | /// prefer [SingleActivator] when possible, whose behavior more closely resembles |
245 | /// that of typical platforms. |
246 | /// |
247 | /// When used as a [ShortcutActivator], [LogicalKeySet] will activate the intent |
248 | /// when all [keys] are pressed, and no others, except that modifier keys are |
249 | /// considered without considering sides (e.g. control left and control right are |
250 | /// considered the same). |
251 | /// |
252 | /// {@tool dartpad} |
253 | /// In the following example, the counter is increased when the following key |
254 | /// sequences are pressed: |
255 | /// |
256 | /// * Control left, then C. |
257 | /// * Control right, then C. |
258 | /// * C, then Control left. |
259 | /// |
260 | /// But not when: |
261 | /// |
262 | /// * Control left, then A, then C. |
263 | /// |
264 | /// ** See code in examples/api/lib/widgets/shortcuts/logical_key_set.0.dart ** |
265 | /// {@end-tool} |
266 | /// |
267 | /// This is also a thin wrapper around a [Set], but changes the equality |
268 | /// comparison from an identity comparison to a contents comparison so that |
269 | /// non-identical sets with the same keys in them will compare as equal. |
270 | |
271 | class LogicalKeySet extends KeySet<LogicalKeyboardKey> with Diagnosticable |
272 | implements ShortcutActivator { |
273 | /// A constructor for making a [LogicalKeySet] of up to four keys. |
274 | /// |
275 | /// If you need a set of more than four keys, use [LogicalKeySet.fromSet]. |
276 | /// |
277 | /// The same [LogicalKeyboardKey] may not be appear more than once in the set. |
278 | LogicalKeySet( |
279 | super.key1, [ |
280 | super.key2, |
281 | super.key3, |
282 | super.key4, |
283 | ]); |
284 | |
285 | /// Create a [LogicalKeySet] from a set of [LogicalKeyboardKey]s. |
286 | /// |
287 | /// Do not mutate the `keys` set after passing it to this object. |
288 | LogicalKeySet.fromSet(super.keys) : super.fromSet(); |
289 | |
290 | @override |
291 | Iterable<LogicalKeyboardKey> get triggers => _triggers; |
292 | late final Set<LogicalKeyboardKey> _triggers = keys.expand( |
293 | (LogicalKeyboardKey key) => _unmapSynonyms[key] ?? <LogicalKeyboardKey>[key], |
294 | ).toSet(); |
295 | |
296 | bool _checkKeyRequirements(Set<LogicalKeyboardKey> pressed) { |
297 | final Set<LogicalKeyboardKey> collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys); |
298 | final Set<LogicalKeyboardKey> collapsedPressed = LogicalKeyboardKey.collapseSynonyms(pressed); |
299 | return collapsedRequired.length == collapsedPressed.length |
300 | && collapsedRequired.difference(collapsedPressed).isEmpty; |
301 | } |
302 | |
303 | @override |
304 | bool accepts(KeyEvent event, HardwareKeyboard state) { |
305 | if (event is! KeyDownEvent && event is! KeyRepeatEvent) { |
306 | return false; |
307 | } |
308 | return triggers.contains(event.logicalKey) |
309 | && _checkKeyRequirements(state.logicalKeysPressed); |
310 | } |
311 | |
312 | static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{ |
313 | LogicalKeyboardKey.alt, |
314 | LogicalKeyboardKey.control, |
315 | LogicalKeyboardKey.meta, |
316 | LogicalKeyboardKey.shift, |
317 | }; |
318 | static final Map<LogicalKeyboardKey, List<LogicalKeyboardKey>> _unmapSynonyms = <LogicalKeyboardKey, List<LogicalKeyboardKey>>{ |
319 | LogicalKeyboardKey.control: <LogicalKeyboardKey>[LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight], |
320 | LogicalKeyboardKey.shift: <LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight], |
321 | LogicalKeyboardKey.alt: <LogicalKeyboardKey>[LogicalKeyboardKey.altLeft, LogicalKeyboardKey.altRight], |
322 | LogicalKeyboardKey.meta: <LogicalKeyboardKey>[LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight], |
323 | }; |
324 | |
325 | @override |
326 | String debugDescribeKeys() { |
327 | final List<LogicalKeyboardKey> sortedKeys = keys.toList() |
328 | ..sort((LogicalKeyboardKey a, LogicalKeyboardKey b) { |
329 | // Put the modifiers first. If it has a synonym, then it's something |
330 | // like shiftLeft, altRight, etc. |
331 | final bool aIsModifier = a.synonyms.isNotEmpty || _modifiers.contains(a); |
332 | final bool bIsModifier = b.synonyms.isNotEmpty || _modifiers.contains(b); |
333 | if (aIsModifier && !bIsModifier) { |
334 | return -1; |
335 | } else if (bIsModifier && !aIsModifier) { |
336 | return 1; |
337 | } |
338 | return a.debugName!.compareTo(b.debugName!); |
339 | }); |
340 | return sortedKeys.map<String>((LogicalKeyboardKey key) => key.debugName.toString()).join(' + ' ); |
341 | } |
342 | |
343 | @override |
344 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
345 | super.debugFillProperties(properties); |
346 | properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('keys' , _keys, description: debugDescribeKeys())); |
347 | } |
348 | } |
349 | |
350 | /// A [DiagnosticsProperty] which handles formatting a `Map<LogicalKeySet, Intent>` |
351 | /// (the same type as the [Shortcuts.shortcuts] property) so that its |
352 | /// diagnostic output is human-readable. |
353 | class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Intent>> { |
354 | /// Create a diagnostics property for `Map<ShortcutActivator, Intent>` objects, |
355 | /// which are the same type as the [Shortcuts.shortcuts] property. |
356 | ShortcutMapProperty( |
357 | String super.name, |
358 | Map<ShortcutActivator, Intent> super.value, { |
359 | super.showName, |
360 | Object super.defaultValue, |
361 | super.level, |
362 | super.description, |
363 | }); |
364 | |
365 | @override |
366 | Map<ShortcutActivator, Intent> get value => super.value!; |
367 | |
368 | @override |
369 | String valueToString({TextTreeConfiguration? parentConfiguration}) { |
370 | return '{ ${value.keys.map<String>((ShortcutActivator keySet) => '{ ${keySet.debugDescribeKeys()}}: ${value[keySet]}' ).join(', ' )}}' ; |
371 | } |
372 | } |
373 | |
374 | /// A shortcut key combination of a single key and modifiers. |
375 | /// |
376 | /// The [SingleActivator] implements typical shortcuts such as: |
377 | /// |
378 | /// * ArrowLeft |
379 | /// * Shift + Delete |
380 | /// * Control + Alt + Meta + Shift + A |
381 | /// |
382 | /// More specifically, it creates shortcut key combinations that are composed of a |
383 | /// [trigger] key, and zero, some, or all of the four modifiers (control, shift, |
384 | /// alt, meta). The shortcut is activated when the following conditions are met: |
385 | /// |
386 | /// * The incoming event is a down event for a [trigger] key. |
387 | /// * If [control] is true, then at least one control key must be held. |
388 | /// Otherwise, no control keys must be held. |
389 | /// * Similar conditions apply for the [alt], [shift], and [meta] keys. |
390 | /// |
391 | /// This resembles the typical behavior of most operating systems, and handles |
392 | /// modifier keys differently from [LogicalKeySet] in the following way: |
393 | /// |
394 | /// * [SingleActivator]s allow additional non-modifier keys being pressed in |
395 | /// order to activate the shortcut. For example, pressing key X while holding |
396 | /// ControlLeft *and key A* will be accepted by |
397 | /// `SingleActivator(LogicalKeyboardKey.keyX, control: true)`. |
398 | /// * [SingleActivator]s do not consider modifiers to be a trigger key. For |
399 | /// example, pressing ControlLeft while holding key X *will not* activate a |
400 | /// `SingleActivator(LogicalKeyboardKey.keyX, control: true)`. |
401 | /// |
402 | /// See also: |
403 | /// |
404 | /// * [CharacterActivator], an activator that represents key combinations |
405 | /// that result in the specified character, such as question mark. |
406 | class SingleActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator { |
407 | /// Triggered when the [trigger] key is pressed while the modifiers are held. |
408 | /// |
409 | /// The [trigger] should be the non-modifier key that is pressed after all the |
410 | /// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not |
411 | /// be a modifier key (sided or unsided). |
412 | /// |
413 | /// The [control], [shift], [alt], and [meta] flags represent whether the |
414 | /// respective modifier keys should be held (true) or released (false). They |
415 | /// default to false. |
416 | /// |
417 | /// By default, the activator is checked on all [KeyDownEvent] events for the |
418 | /// [trigger] key. If [includeRepeats] is false, only [trigger] key events |
419 | /// which are not [KeyRepeatEvent]s will be considered. |
420 | /// |
421 | /// {@tool dartpad} |
422 | /// In the following example, the shortcut `Control + C` increases the |
423 | /// counter: |
424 | /// |
425 | /// ** See code in examples/api/lib/widgets/shortcuts/single_activator.single_activator.0.dart ** |
426 | /// {@end-tool} |
427 | const SingleActivator( |
428 | this.trigger, { |
429 | this.control = false, |
430 | this.shift = false, |
431 | this.alt = false, |
432 | this.meta = false, |
433 | this.includeRepeats = true, |
434 | }) : // The enumerated check with `identical` is cumbersome but the only way |
435 | // since const constructors can not call functions such as `==` or |
436 | // `Set.contains`. Checking with `identical` might not work when the |
437 | // key object is created from ID, but it covers common cases. |
438 | assert( |
439 | !identical(trigger, LogicalKeyboardKey.control) && |
440 | !identical(trigger, LogicalKeyboardKey.controlLeft) && |
441 | !identical(trigger, LogicalKeyboardKey.controlRight) && |
442 | !identical(trigger, LogicalKeyboardKey.shift) && |
443 | !identical(trigger, LogicalKeyboardKey.shiftLeft) && |
444 | !identical(trigger, LogicalKeyboardKey.shiftRight) && |
445 | !identical(trigger, LogicalKeyboardKey.alt) && |
446 | !identical(trigger, LogicalKeyboardKey.altLeft) && |
447 | !identical(trigger, LogicalKeyboardKey.altRight) && |
448 | !identical(trigger, LogicalKeyboardKey.meta) && |
449 | !identical(trigger, LogicalKeyboardKey.metaLeft) && |
450 | !identical(trigger, LogicalKeyboardKey.metaRight), |
451 | ); |
452 | |
453 | /// The non-modifier key of the shortcut that is pressed after all modifiers |
454 | /// to activate the shortcut. |
455 | /// |
456 | /// For example, for `Control + C`, [trigger] should be |
457 | /// [LogicalKeyboardKey.keyC]. |
458 | final LogicalKeyboardKey trigger; |
459 | |
460 | /// Whether either (or both) control keys should be held for [trigger] to |
461 | /// activate the shortcut. |
462 | /// |
463 | /// It defaults to false, meaning all Control keys must be released when the |
464 | /// event is received in order to activate the shortcut. If it's true, then |
465 | /// either or both Control keys must be pressed. |
466 | /// |
467 | /// See also: |
468 | /// |
469 | /// * [LogicalKeyboardKey.controlLeft], [LogicalKeyboardKey.controlRight]. |
470 | final bool control; |
471 | |
472 | /// Whether either (or both) shift keys should be held for [trigger] to |
473 | /// activate the shortcut. |
474 | /// |
475 | /// It defaults to false, meaning all Shift keys must be released when the |
476 | /// event is received in order to activate the shortcut. If it's true, then |
477 | /// either or both Shift keys must be pressed. |
478 | /// |
479 | /// See also: |
480 | /// |
481 | /// * [LogicalKeyboardKey.shiftLeft], [LogicalKeyboardKey.shiftRight]. |
482 | final bool shift; |
483 | |
484 | /// Whether either (or both) alt keys should be held for [trigger] to |
485 | /// activate the shortcut. |
486 | /// |
487 | /// It defaults to false, meaning all Alt keys must be released when the |
488 | /// event is received in order to activate the shortcut. If it's true, then |
489 | /// either or both Alt keys must be pressed. |
490 | /// |
491 | /// See also: |
492 | /// |
493 | /// * [LogicalKeyboardKey.altLeft], [LogicalKeyboardKey.altRight]. |
494 | final bool alt; |
495 | |
496 | /// Whether either (or both) meta keys should be held for [trigger] to |
497 | /// activate the shortcut. |
498 | /// |
499 | /// It defaults to false, meaning all Meta keys must be released when the |
500 | /// event is received in order to activate the shortcut. If it's true, then |
501 | /// either or both Meta keys must be pressed. |
502 | /// |
503 | /// See also: |
504 | /// |
505 | /// * [LogicalKeyboardKey.metaLeft], [LogicalKeyboardKey.metaRight]. |
506 | final bool meta; |
507 | |
508 | /// Whether this activator accepts repeat events of the [trigger] key. |
509 | /// |
510 | /// If [includeRepeats] is true, the activator is checked on all |
511 | /// [KeyDownEvent] or [KeyRepeatEvent]s for the [trigger] key. If |
512 | /// [includeRepeats] is false, only [trigger] key events which are |
513 | /// [KeyDownEvent]s will be considered. |
514 | final bool includeRepeats; |
515 | |
516 | @override |
517 | Iterable<LogicalKeyboardKey> get triggers { |
518 | return <LogicalKeyboardKey>[trigger]; |
519 | } |
520 | |
521 | bool _shouldAcceptModifiers(Set<LogicalKeyboardKey> pressed) { |
522 | return control == pressed.intersection(_controlSynonyms).isNotEmpty |
523 | && shift == pressed.intersection(_shiftSynonyms).isNotEmpty |
524 | && alt == pressed.intersection(_altSynonyms).isNotEmpty |
525 | && meta == pressed.intersection(_metaSynonyms).isNotEmpty; |
526 | } |
527 | |
528 | @override |
529 | bool accepts(KeyEvent event, HardwareKeyboard state) { |
530 | return (event is KeyDownEvent || (includeRepeats && event is KeyRepeatEvent)) |
531 | && triggers.contains(event.logicalKey) |
532 | && _shouldAcceptModifiers(state.logicalKeysPressed); |
533 | } |
534 | |
535 | @override |
536 | ShortcutSerialization serializeForMenu() { |
537 | return ShortcutSerialization.modifier( |
538 | trigger, |
539 | shift: shift, |
540 | alt: alt, |
541 | meta: meta, |
542 | control: control, |
543 | ); |
544 | } |
545 | |
546 | /// Returns a short and readable description of the key combination. |
547 | /// |
548 | /// Intended to be used in debug mode for logging purposes. In release mode, |
549 | /// [debugDescribeKeys] returns an empty string. |
550 | @override |
551 | String debugDescribeKeys() { |
552 | String result = '' ; |
553 | assert(() { |
554 | final List<String> keys = <String>[ |
555 | if (control) 'Control' , |
556 | if (alt) 'Alt' , |
557 | if (meta) 'Meta' , |
558 | if (shift) 'Shift' , |
559 | trigger.debugName ?? trigger.toStringShort(), |
560 | ]; |
561 | result = keys.join(' + ' ); |
562 | return true; |
563 | }()); |
564 | return result; |
565 | } |
566 | |
567 | @override |
568 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
569 | super.debugFillProperties(properties); |
570 | properties.add(MessageProperty('keys' , debugDescribeKeys())); |
571 | properties.add(FlagProperty('includeRepeats' , value: includeRepeats, ifFalse: 'excluding repeats' )); |
572 | } |
573 | } |
574 | |
575 | /// A shortcut combination that is triggered by a key event that produces a |
576 | /// specific character. |
577 | /// |
578 | /// Keys often produce different characters when combined with modifiers. For |
579 | /// example, it might be helpful for the user to bring up a help menu by |
580 | /// pressing the question mark ('?'). However, there is no logical key that |
581 | /// directly represents a question mark. Although 'Shift+Slash' produces a '?' |
582 | /// character on a US keyboard, its logical key is still considered a Slash key, |
583 | /// and hard-coding 'Shift+Slash' in this situation is unfriendly to other |
584 | /// keyboard layouts. |
585 | /// |
586 | /// For example, `CharacterActivator('?')` is triggered when a key combination |
587 | /// results in a question mark, which is 'Shift+Slash' on a US keyboard, but |
588 | /// 'Shift+Comma' on a French keyboard. |
589 | /// |
590 | /// {@tool dartpad} |
591 | /// In the following example, when a key combination results in a question mark, |
592 | /// the counter is increased: |
593 | /// |
594 | /// ** See code in examples/api/lib/widgets/shortcuts/character_activator.0.dart ** |
595 | /// {@end-tool} |
596 | /// |
597 | /// The [alt], [control], and [meta] flags represent whether the respective |
598 | /// modifier keys should be held (true) or released (false). They default to |
599 | /// false. [CharacterActivator] cannot check shifted keys, since the Shift key |
600 | /// affects the resulting character, and will accept whether either of the |
601 | /// Shift keys are pressed or not, as long as the key event produces the |
602 | /// correct character. |
603 | /// |
604 | /// By default, the activator is checked on all [KeyDownEvent] or |
605 | /// [KeyRepeatEvent]s for the [character] in combination with the requested |
606 | /// modifier keys. If `includeRepeats` is false, only the [character] events |
607 | /// with that are [KeyDownEvent]s will be considered. |
608 | /// |
609 | /// {@template flutter.widgets.shortcuts.CharacterActivator.alt} |
610 | /// On macOS and iOS, the [alt] flag indicates that the Option key (⌥) is |
611 | /// pressed. Because the Option key affects the character generated on these |
612 | /// platforms, it can be unintuitive to define [CharacterActivator]s for them. |
613 | /// |
614 | /// For instance, if you want the shortcut to trigger when Option+s (⌥-s) is |
615 | /// pressed, and what you intend is to trigger whenever the character 'ß' is |
616 | /// produced, you would use `CharacterActivator('ß')` or |
617 | /// `CharacterActivator('ß', alt: true)` instead of `CharacterActivator('s', |
618 | /// alt: true)`. This is because `CharacterActivator('s', alt: true)` will |
619 | /// never trigger, since the 's' character can't be produced when the Option |
620 | /// key is held down. |
621 | /// |
622 | /// If what is intended is that the shortcut is triggered when Option+s (⌥-s) |
623 | /// is pressed, regardless of which character is produced, it is better to use |
624 | /// [SingleActivator], as in `SingleActivator(LogicalKeyboardKey.keyS, alt: |
625 | /// true)`. |
626 | /// {@endtemplate} |
627 | /// |
628 | /// See also: |
629 | /// |
630 | /// * [SingleActivator], an activator that represents a single key combined |
631 | /// with modifiers, such as `Ctrl+C` or `Ctrl-Right Arrow`. |
632 | class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator { |
633 | /// Triggered when the key event yields the given character. |
634 | const CharacterActivator(this.character, { |
635 | this.alt = false, |
636 | this.control = false, |
637 | this.meta = false, |
638 | this.includeRepeats = true, |
639 | }); |
640 | |
641 | /// Whether either (or both) Alt keys should be held for the [character] to |
642 | /// activate the shortcut. |
643 | /// |
644 | /// It defaults to false, meaning all Alt keys must be released when the event |
645 | /// is received in order to activate the shortcut. If it's true, then either |
646 | /// one or both Alt keys must be pressed. |
647 | /// |
648 | /// {@macro flutter.widgets.shortcuts.CharacterActivator.alt} |
649 | /// |
650 | /// See also: |
651 | /// |
652 | /// * [LogicalKeyboardKey.altLeft], [LogicalKeyboardKey.altRight]. |
653 | final bool alt; |
654 | |
655 | /// Whether either (or both) Control keys should be held for the [character] |
656 | /// to activate the shortcut. |
657 | /// |
658 | /// It defaults to false, meaning all Control keys must be released when the |
659 | /// event is received in order to activate the shortcut. If it's true, then |
660 | /// either one or both Control keys must be pressed. |
661 | /// |
662 | /// See also: |
663 | /// |
664 | /// * [LogicalKeyboardKey.controlLeft], [LogicalKeyboardKey.controlRight]. |
665 | final bool control; |
666 | |
667 | /// Whether either (or both) Meta keys should be held for the [character] to |
668 | /// activate the shortcut. |
669 | /// |
670 | /// It defaults to false, meaning all Meta keys must be released when the |
671 | /// event is received in order to activate the shortcut. If it's true, then |
672 | /// either one or both Meta keys must be pressed. |
673 | /// |
674 | /// See also: |
675 | /// |
676 | /// * [LogicalKeyboardKey.metaLeft], [LogicalKeyboardKey.metaRight]. |
677 | final bool meta; |
678 | |
679 | /// Whether this activator accepts repeat events of the [character]. |
680 | /// |
681 | /// If [includeRepeats] is true, the activator is checked on all |
682 | /// [KeyDownEvent] and [KeyRepeatEvent]s for the [character]. If |
683 | /// [includeRepeats] is false, only the [character] events that are |
684 | /// [KeyDownEvent]s will be considered. |
685 | final bool includeRepeats; |
686 | |
687 | /// The character which triggers the shortcut. |
688 | /// |
689 | /// This is typically a single-character string, such as '?' or 'Å“', although |
690 | /// [CharacterActivator] doesn't check the length of [character] or whether it |
691 | /// can be matched by any key combination at all. It is case-sensitive, since |
692 | /// the [character] is directly compared by `==` to the character reported by |
693 | /// the platform. |
694 | /// |
695 | /// See also: |
696 | /// |
697 | /// * [KeyEvent.character], the character of a key event. |
698 | final String character; |
699 | |
700 | @override |
701 | Iterable<LogicalKeyboardKey>? get triggers => null; |
702 | |
703 | bool _shouldAcceptModifiers(Set<LogicalKeyboardKey> pressed) { |
704 | // Doesn't look for shift, since the character will encode that. |
705 | return control == pressed.intersection(_controlSynonyms).isNotEmpty |
706 | && alt == pressed.intersection(_altSynonyms).isNotEmpty |
707 | && meta == pressed.intersection(_metaSynonyms).isNotEmpty; |
708 | } |
709 | |
710 | @override |
711 | bool accepts(KeyEvent event, HardwareKeyboard state) { |
712 | // Ignore triggers, since we're only interested in the character. |
713 | return event.character == character |
714 | && (event is KeyDownEvent || (includeRepeats && event is KeyRepeatEvent)) |
715 | && _shouldAcceptModifiers(state.logicalKeysPressed); |
716 | } |
717 | |
718 | @override |
719 | String debugDescribeKeys() { |
720 | String result = '' ; |
721 | assert(() { |
722 | final List<String> keys = <String>[ |
723 | if (alt) 'Alt' , |
724 | if (control) 'Control' , |
725 | if (meta) 'Meta' , |
726 | "' $character'" , |
727 | ]; |
728 | result = keys.join(' + ' ); |
729 | return true; |
730 | }()); |
731 | return result; |
732 | } |
733 | |
734 | @override |
735 | ShortcutSerialization serializeForMenu() { |
736 | return ShortcutSerialization.character(character, alt: alt, control: control, meta: meta); |
737 | } |
738 | |
739 | @override |
740 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
741 | super.debugFillProperties(properties); |
742 | properties.add(MessageProperty('character' , debugDescribeKeys())); |
743 | properties.add(FlagProperty('includeRepeats' , value: includeRepeats, ifFalse: 'excluding repeats' )); |
744 | } |
745 | } |
746 | |
747 | class _ActivatorIntentPair with Diagnosticable { |
748 | const _ActivatorIntentPair(this.activator, this.intent); |
749 | final ShortcutActivator activator; |
750 | final Intent intent; |
751 | |
752 | @override |
753 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
754 | super.debugFillProperties(properties); |
755 | properties.add(DiagnosticsProperty<String>('activator' , activator.debugDescribeKeys())); |
756 | properties.add(DiagnosticsProperty<Intent>('intent' , intent)); |
757 | } |
758 | } |
759 | |
760 | /// A manager of keyboard shortcut bindings used by [Shortcuts] to handle key |
761 | /// events. |
762 | /// |
763 | /// The manager may be listened to (with [addListener]/[removeListener]) for |
764 | /// change notifications when the shortcuts change. |
765 | /// |
766 | /// Typically, a [Shortcuts] widget supplies its own manager, but in uncommon |
767 | /// cases where overriding the usual shortcut manager behavior is desired, a |
768 | /// subclassed [ShortcutManager] may be supplied. |
769 | class ShortcutManager with Diagnosticable, ChangeNotifier { |
770 | /// Constructs a [ShortcutManager]. |
771 | ShortcutManager({ |
772 | Map<ShortcutActivator, Intent> shortcuts = const <ShortcutActivator, Intent>{}, |
773 | this.modal = false, |
774 | }) : _shortcuts = shortcuts { |
775 | if (kFlutterMemoryAllocationsEnabled) { |
776 | ChangeNotifier.maybeDispatchObjectCreation(this); |
777 | } |
778 | } |
779 | |
780 | /// True if the [ShortcutManager] should not pass on keys that it doesn't |
781 | /// handle to any key-handling widgets that are ancestors to this one. |
782 | /// |
783 | /// Setting [modal] to true will prevent any key event given to this manager |
784 | /// from being given to any ancestor managers, even if that key doesn't appear |
785 | /// in the [shortcuts] map. |
786 | /// |
787 | /// The net effect of setting [modal] to true is to return |
788 | /// [KeyEventResult.skipRemainingHandlers] from [handleKeypress] if it does |
789 | /// not exist in the shortcut map, instead of returning |
790 | /// [KeyEventResult.ignored]. |
791 | final bool modal; |
792 | |
793 | /// Returns the shortcut map. |
794 | /// |
795 | /// When the map is changed, listeners to this manager will be notified. |
796 | /// |
797 | /// The returned map should not be modified. |
798 | Map<ShortcutActivator, Intent> get shortcuts => _shortcuts; |
799 | Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{}; |
800 | set shortcuts(Map<ShortcutActivator, Intent> value) { |
801 | if (!mapEquals<ShortcutActivator, Intent>(_shortcuts, value)) { |
802 | _shortcuts = value; |
803 | _indexedShortcutsCache = null; |
804 | notifyListeners(); |
805 | } |
806 | } |
807 | |
808 | static Map<LogicalKeyboardKey?, List<_ActivatorIntentPair>> _indexShortcuts(Map<ShortcutActivator, Intent> source) { |
809 | final Map<LogicalKeyboardKey?, List<_ActivatorIntentPair>> result = <LogicalKeyboardKey?, List<_ActivatorIntentPair>>{}; |
810 | source.forEach((ShortcutActivator activator, Intent intent) { |
811 | // This intermediate variable is necessary to comply with Dart analyzer. |
812 | final Iterable<LogicalKeyboardKey?>? nullableTriggers = activator.triggers; |
813 | for (final LogicalKeyboardKey? trigger in nullableTriggers ?? <LogicalKeyboardKey?>[null]) { |
814 | result.putIfAbsent(trigger, () => <_ActivatorIntentPair>[]) |
815 | .add(_ActivatorIntentPair(activator, intent)); |
816 | } |
817 | }); |
818 | return result; |
819 | } |
820 | |
821 | Map<LogicalKeyboardKey?, List<_ActivatorIntentPair>> get _indexedShortcuts { |
822 | return _indexedShortcutsCache ??= _indexShortcuts(shortcuts); |
823 | } |
824 | |
825 | Map<LogicalKeyboardKey?, List<_ActivatorIntentPair>>? _indexedShortcutsCache; |
826 | |
827 | Iterable<_ActivatorIntentPair> _getCandidates(LogicalKeyboardKey key) { |
828 | return <_ActivatorIntentPair>[ |
829 | ... _indexedShortcuts[key] ?? <_ActivatorIntentPair>[], |
830 | ... _indexedShortcuts[null] ?? <_ActivatorIntentPair>[], |
831 | ]; |
832 | } |
833 | |
834 | /// Returns the [Intent], if any, that matches the current set of pressed |
835 | /// keys. |
836 | /// |
837 | /// Returns null if no intent matches the current set of pressed keys. |
838 | Intent? _find(KeyEvent event, HardwareKeyboard state) { |
839 | for (final _ActivatorIntentPair activatorIntent in _getCandidates(event.logicalKey)) { |
840 | if (activatorIntent.activator.accepts(event, state)) { |
841 | return activatorIntent.intent; |
842 | } |
843 | } |
844 | return null; |
845 | } |
846 | |
847 | /// Handles a key press `event` in the given `context`. |
848 | /// |
849 | /// If a key mapping is found, then the associated action will be invoked |
850 | /// using the [Intent] activated by the [ShortcutActivator] in the [shortcuts] |
851 | /// map, and the currently focused widget's context (from |
852 | /// [FocusManager.primaryFocus]). |
853 | /// |
854 | /// Returns a [KeyEventResult.handled] if an action was invoked, otherwise a |
855 | /// [KeyEventResult.skipRemainingHandlers] if [modal] is true, or if it maps |
856 | /// to a [DoNothingAction] with [DoNothingAction.consumesKey] set to false, |
857 | /// and in all other cases returns [KeyEventResult.ignored]. |
858 | /// |
859 | /// In order for an action to be invoked (and [KeyEventResult.handled] |
860 | /// returned), a [ShortcutActivator] must accept the given [KeyEvent], be |
861 | /// mapped to an [Intent], the [Intent] must be mapped to an [Action], and the |
862 | /// [Action] must be enabled. |
863 | @protected |
864 | KeyEventResult handleKeypress(BuildContext context, KeyEvent event) { |
865 | final Intent? matchedIntent = _find(event, HardwareKeyboard.instance); |
866 | if (matchedIntent != null) { |
867 | final BuildContext? primaryContext = primaryFocus?.context; |
868 | if (primaryContext != null) { |
869 | final Action<Intent>? action = Actions.maybeFind<Intent>( |
870 | primaryContext, |
871 | intent: matchedIntent, |
872 | ); |
873 | if (action != null) { |
874 | final (bool enabled, Object? invokeResult) = Actions.of(primaryContext).invokeActionIfEnabled( |
875 | action, matchedIntent, primaryContext, |
876 | ); |
877 | if (enabled) { |
878 | return action.toKeyEventResult(matchedIntent, invokeResult); |
879 | } |
880 | } |
881 | } |
882 | } |
883 | return modal ? KeyEventResult.skipRemainingHandlers : KeyEventResult.ignored; |
884 | } |
885 | |
886 | @override |
887 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
888 | super.debugFillProperties(properties); |
889 | properties.add(DiagnosticsProperty<Map<ShortcutActivator, Intent>>('shortcuts' , shortcuts)); |
890 | properties.add(FlagProperty('modal' , value: modal, ifTrue: 'modal' , defaultValue: false)); |
891 | } |
892 | } |
893 | |
894 | /// A widget that creates key bindings to specific actions for its |
895 | /// descendants. |
896 | /// |
897 | /// {@youtube 560 315 https://www.youtube.com/watch?v=6ZcQmdoz9N8} |
898 | /// |
899 | /// This widget establishes a [ShortcutManager] to be used by its descendants |
900 | /// when invoking an [Action] via a keyboard key combination that maps to an |
901 | /// [Intent]. |
902 | /// |
903 | /// This is similar to but more powerful than the [CallbackShortcuts] widget. |
904 | /// Unlike [CallbackShortcuts], this widget separates key bindings and their |
905 | /// implementations. This separation allows [Shortcuts] to have key bindings |
906 | /// that adapt to the focused context. For example, the desired action for a |
907 | /// deletion intent may be to delete a character in a text input, or to delete |
908 | /// a file in a file menu. |
909 | /// |
910 | /// See the article on [Using Actions and |
911 | /// Shortcuts](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts) |
912 | /// for a detailed explanation. |
913 | /// |
914 | /// {@tool dartpad} |
915 | /// Here, we will use the [Shortcuts] and [Actions] widgets to add and subtract |
916 | /// from a counter. When the child widget has keyboard focus, and a user presses |
917 | /// the keys that have been defined in [Shortcuts], the action that is bound |
918 | /// to the appropriate [Intent] for the key is invoked. |
919 | /// |
920 | /// It also shows the use of a [CallbackAction] to avoid creating a new [Action] |
921 | /// subclass. |
922 | /// |
923 | /// ** See code in examples/api/lib/widgets/shortcuts/shortcuts.0.dart ** |
924 | /// {@end-tool} |
925 | /// |
926 | /// {@tool dartpad} |
927 | /// This slightly more complicated, but more flexible, example creates a custom |
928 | /// [Action] subclass to increment and decrement within a widget (a [Column]) |
929 | /// that has keyboard focus. When the user presses the up and down arrow keys, |
930 | /// the counter will increment and decrement a data model using the custom |
931 | /// actions. |
932 | /// |
933 | /// One thing that this demonstrates is passing arguments to the [Intent] to be |
934 | /// carried to the [Action]. This shows how actions can get data either from |
935 | /// their own construction (like the `model` in this example), or from the |
936 | /// intent passed to them when invoked (like the increment `amount` in this |
937 | /// example). |
938 | /// |
939 | /// ** See code in examples/api/lib/widgets/shortcuts/shortcuts.1.dart ** |
940 | /// {@end-tool} |
941 | /// |
942 | /// See also: |
943 | /// |
944 | /// * [CallbackShortcuts], a simpler but less flexible widget that defines key |
945 | /// bindings that invoke callbacks. |
946 | /// * [Intent], a class for containing a description of a user action to be |
947 | /// invoked. |
948 | /// * [Action], a class for defining an invocation of a user action. |
949 | /// * [CallbackAction], a class for creating an action from a callback. |
950 | class Shortcuts extends StatefulWidget { |
951 | /// Creates a const [Shortcuts] widget that owns the map of shortcuts and |
952 | /// creates its own manager. |
953 | /// |
954 | /// When using this constructor, [manager] will return null. |
955 | /// |
956 | /// The [child] and [shortcuts] arguments are required. |
957 | /// |
958 | /// See also: |
959 | /// |
960 | /// * [Shortcuts.manager], a constructor that uses a [ShortcutManager] to |
961 | /// manage the shortcuts list instead. |
962 | const Shortcuts({ |
963 | super.key, |
964 | required Map<ShortcutActivator, Intent> shortcuts, |
965 | required this.child, |
966 | this.debugLabel, |
967 | }) : _shortcuts = shortcuts, |
968 | manager = null; |
969 | |
970 | /// Creates a const [Shortcuts] widget that uses the [manager] to |
971 | /// manage the map of shortcuts. |
972 | /// |
973 | /// If this constructor is used, [shortcuts] will return the contents of |
974 | /// [ShortcutManager.shortcuts]. |
975 | /// |
976 | /// The [child] and [manager] arguments are required. |
977 | const Shortcuts.manager({ |
978 | super.key, |
979 | required ShortcutManager this.manager, |
980 | required this.child, |
981 | this.debugLabel, |
982 | }) : _shortcuts = const <ShortcutActivator, Intent>{}; |
983 | |
984 | /// The [ShortcutManager] that will manage the mapping between key |
985 | /// combinations and [Action]s. |
986 | /// |
987 | /// If this widget was created with [Shortcuts.manager], then |
988 | /// [ShortcutManager.shortcuts] will be used as the source for shortcuts. If |
989 | /// the unnamed constructor is used, this manager will be null, and a |
990 | /// default-constructed [ShortcutManager] will be used. |
991 | final ShortcutManager? manager; |
992 | |
993 | /// {@template flutter.widgets.shortcuts.shortcuts} |
994 | /// The map of shortcuts that describes the mapping between a key sequence |
995 | /// defined by a [ShortcutActivator] and the [Intent] that will be emitted |
996 | /// when that key sequence is pressed. |
997 | /// {@endtemplate} |
998 | Map<ShortcutActivator, Intent> get shortcuts { |
999 | return manager == null ? _shortcuts : manager!.shortcuts; |
1000 | } |
1001 | final Map<ShortcutActivator, Intent> _shortcuts; |
1002 | |
1003 | /// The child widget for this [Shortcuts] widget. |
1004 | /// |
1005 | /// {@macro flutter.widgets.ProxyWidget.child} |
1006 | final Widget child; |
1007 | |
1008 | /// The debug label that is printed for this node when logged. |
1009 | /// |
1010 | /// If this label is set, then it will be displayed instead of the shortcut |
1011 | /// map when logged. |
1012 | /// |
1013 | /// This allows simplifying the diagnostic output to avoid cluttering it |
1014 | /// unnecessarily with large default shortcut maps. |
1015 | final String? debugLabel; |
1016 | |
1017 | @override |
1018 | State<Shortcuts> createState() => _ShortcutsState(); |
1019 | |
1020 | @override |
1021 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1022 | super.debugFillProperties(properties); |
1023 | properties.add(DiagnosticsProperty<ShortcutManager>('manager' , manager, defaultValue: null)); |
1024 | properties.add(ShortcutMapProperty('shortcuts' , shortcuts, description: debugLabel?.isNotEmpty ?? false ? debugLabel : null)); |
1025 | } |
1026 | } |
1027 | |
1028 | class _ShortcutsState extends State<Shortcuts> { |
1029 | ShortcutManager? _internalManager; |
1030 | ShortcutManager get manager => widget.manager ?? _internalManager!; |
1031 | |
1032 | @override |
1033 | void dispose() { |
1034 | _internalManager?.dispose(); |
1035 | super.dispose(); |
1036 | } |
1037 | |
1038 | @override |
1039 | void initState() { |
1040 | super.initState(); |
1041 | if (widget.manager == null) { |
1042 | _internalManager = ShortcutManager(); |
1043 | _internalManager!.shortcuts = widget.shortcuts; |
1044 | } |
1045 | } |
1046 | |
1047 | @override |
1048 | void didUpdateWidget(Shortcuts oldWidget) { |
1049 | super.didUpdateWidget(oldWidget); |
1050 | if (widget.manager != oldWidget.manager) { |
1051 | if (widget.manager != null) { |
1052 | _internalManager?.dispose(); |
1053 | _internalManager = null; |
1054 | } else { |
1055 | _internalManager ??= ShortcutManager(); |
1056 | } |
1057 | } |
1058 | _internalManager?.shortcuts = widget.shortcuts; |
1059 | } |
1060 | |
1061 | KeyEventResult _handleOnKeyEvent(FocusNode node, KeyEvent event) { |
1062 | if (node.context == null) { |
1063 | return KeyEventResult.ignored; |
1064 | } |
1065 | return manager.handleKeypress(node.context!, event); |
1066 | } |
1067 | |
1068 | @override |
1069 | Widget build(BuildContext context) { |
1070 | return Focus( |
1071 | debugLabel: ' $Shortcuts' , |
1072 | canRequestFocus: false, |
1073 | onKeyEvent: _handleOnKeyEvent, |
1074 | child: widget.child, |
1075 | ); |
1076 | } |
1077 | } |
1078 | |
1079 | /// A widget that binds key combinations to specific callbacks. |
1080 | /// |
1081 | /// {@youtube 560 315 https://www.youtube.com/watch?v=VcQQ1ns_qNY} |
1082 | /// |
1083 | /// This is similar to but simpler than the [Shortcuts] widget as it doesn't |
1084 | /// require [Intent]s and [Actions] widgets. Instead, it accepts a map |
1085 | /// of [ShortcutActivator]s to [VoidCallback]s. |
1086 | /// |
1087 | /// Unlike [Shortcuts], this widget does not separate key bindings and their |
1088 | /// implementations. This separation allows [Shortcuts] to have key bindings |
1089 | /// that adapt to the focused context. For example, the desired action for a |
1090 | /// deletion intent may be to delete a character in a text input, or to delete |
1091 | /// a file in a file menu. |
1092 | /// |
1093 | /// {@tool dartpad} |
1094 | /// This example uses the [CallbackShortcuts] widget to add and subtract |
1095 | /// from a counter when the up or down arrow keys are pressed. |
1096 | /// |
1097 | /// ** See code in examples/api/lib/widgets/shortcuts/callback_shortcuts.0.dart ** |
1098 | /// {@end-tool} |
1099 | /// |
1100 | /// [Shortcuts] and [CallbackShortcuts] can both be used in the same app. As |
1101 | /// with any key handling widget, if this widget handles a key event then |
1102 | /// widgets above it in the focus chain will not receive the event. This means |
1103 | /// that if this widget handles a key, then an ancestor [Shortcuts] widget (or |
1104 | /// any other key handling widget) will not receive that key. Similarly, if |
1105 | /// a descendant of this widget handles the key, then the key event will not |
1106 | /// reach this widget for handling. |
1107 | /// |
1108 | /// See the article on [Using Actions and |
1109 | /// Shortcuts](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts) |
1110 | /// for a detailed explanation. |
1111 | /// |
1112 | /// See also: |
1113 | /// * [Shortcuts], a more powerful widget for defining key bindings. |
1114 | /// * [Focus], a widget that defines which widgets can receive keyboard focus. |
1115 | class CallbackShortcuts extends StatelessWidget { |
1116 | /// Creates a const [CallbackShortcuts] widget. |
1117 | const CallbackShortcuts({ |
1118 | super.key, |
1119 | required this.bindings, |
1120 | required this.child, |
1121 | }); |
1122 | |
1123 | /// A map of key combinations to callbacks used to define the shortcut |
1124 | /// bindings. |
1125 | /// |
1126 | /// If a descendant of this widget has focus, and a key is pressed, the |
1127 | /// activator keys of this map will be asked if they accept the key event. If |
1128 | /// they do, then the corresponding callback is invoked, and the key event |
1129 | /// propagation is halted. If none of the activators accept the key event, |
1130 | /// then the key event continues to be propagated up the focus chain. |
1131 | /// |
1132 | /// If more than one activator accepts the key event, then all of the |
1133 | /// callbacks associated with activators that accept the key event are |
1134 | /// invoked. |
1135 | /// |
1136 | /// Some examples of [ShortcutActivator] subclasses that can be used to define |
1137 | /// the key combinations here are [SingleActivator], [CharacterActivator], and |
1138 | /// [LogicalKeySet]. |
1139 | final Map<ShortcutActivator, VoidCallback> bindings; |
1140 | |
1141 | /// The widget below this widget in the tree. |
1142 | /// |
1143 | /// {@macro flutter.widgets.ProxyWidget.child} |
1144 | final Widget child; |
1145 | |
1146 | // A helper function to make the stack trace more useful if the callback |
1147 | // throws, by providing the activator and event as arguments that will appear |
1148 | // in the stack trace. |
1149 | bool _applyKeyEventBinding(ShortcutActivator activator, KeyEvent event) { |
1150 | if (activator.accepts(event, HardwareKeyboard.instance)) { |
1151 | bindings[activator]!.call(); |
1152 | return true; |
1153 | } |
1154 | return false; |
1155 | } |
1156 | |
1157 | @override |
1158 | Widget build(BuildContext context) { |
1159 | return Focus( |
1160 | canRequestFocus: false, |
1161 | skipTraversal: true, |
1162 | onKeyEvent: (FocusNode node, KeyEvent event) { |
1163 | KeyEventResult result = KeyEventResult.ignored; |
1164 | // Activates all key bindings that match, returns "handled" if any handle it. |
1165 | for (final ShortcutActivator activator in bindings.keys) { |
1166 | result = _applyKeyEventBinding(activator, event) ? KeyEventResult.handled : result; |
1167 | } |
1168 | return result; |
1169 | }, |
1170 | child: child, |
1171 | ); |
1172 | } |
1173 | } |
1174 | |
1175 | /// A entry returned by [ShortcutRegistry.addAll] that allows the caller to |
1176 | /// identify the shortcuts they registered with the [ShortcutRegistry] through |
1177 | /// the [ShortcutRegistrar]. |
1178 | /// |
1179 | /// When the entry is no longer needed, [dispose] should be called, and the |
1180 | /// entry should no longer be used. |
1181 | class ShortcutRegistryEntry { |
1182 | // Tokens can only be created by the ShortcutRegistry. |
1183 | const ShortcutRegistryEntry._(this.registry); |
1184 | |
1185 | /// The [ShortcutRegistry] that this entry was issued by. |
1186 | final ShortcutRegistry registry; |
1187 | |
1188 | /// Replaces the given shortcut bindings in the [ShortcutRegistry] that this |
1189 | /// entry was created from. |
1190 | /// |
1191 | /// This method will assert in debug mode if another [ShortcutRegistryEntry] |
1192 | /// exists (i.e. hasn't been disposed of) that has already added a given |
1193 | /// shortcut. |
1194 | /// |
1195 | /// It will also assert if this entry has already been disposed. |
1196 | /// |
1197 | /// If two equivalent, but different, [ShortcutActivator]s are added, all of |
1198 | /// them will be executed when triggered. For example, if both |
1199 | /// `SingleActivator(LogicalKeyboardKey.keyA)` and `CharacterActivator('a')` |
1200 | /// are added, then both will be executed when an "a" key is pressed. |
1201 | void replaceAll(Map<ShortcutActivator, Intent> value) { |
1202 | registry._replaceAll(this, value); |
1203 | } |
1204 | |
1205 | /// Called when the entry is no longer needed. |
1206 | /// |
1207 | /// Call this will remove all shortcuts associated with this |
1208 | /// [ShortcutRegistryEntry] from the [registry]. |
1209 | @mustCallSuper |
1210 | void dispose() { |
1211 | registry._disposeEntry(this); |
1212 | } |
1213 | } |
1214 | |
1215 | /// A class used by [ShortcutRegistrar] that allows adding or removing shortcut |
1216 | /// bindings by descendants of the [ShortcutRegistrar]. |
1217 | /// |
1218 | /// You can reach the nearest [ShortcutRegistry] using [of] and [maybeOf]. |
1219 | /// |
1220 | /// The registry may be listened to (with [addListener]/[removeListener]) for |
1221 | /// change notifications when the registered shortcuts change. Change |
1222 | /// notifications take place after the current frame is drawn, so that |
1223 | /// widgets that are not descendants of the registry can listen to it (e.g. in |
1224 | /// overlays). |
1225 | class ShortcutRegistry with ChangeNotifier { |
1226 | /// Creates an instance of [ShortcutRegistry]. |
1227 | ShortcutRegistry() { |
1228 | if (kFlutterMemoryAllocationsEnabled) { |
1229 | ChangeNotifier.maybeDispatchObjectCreation(this); |
1230 | } |
1231 | } |
1232 | |
1233 | bool _notificationScheduled = false; |
1234 | bool _disposed = false; |
1235 | |
1236 | @override |
1237 | void dispose() { |
1238 | super.dispose(); |
1239 | _disposed = true; |
1240 | } |
1241 | |
1242 | /// Gets the combined shortcut bindings from all contexts that are registered |
1243 | /// with this [ShortcutRegistry]. |
1244 | /// |
1245 | /// Listeners will be notified when the value returned by this getter changes. |
1246 | /// |
1247 | /// Returns a copy: modifying the returned map will have no effect. |
1248 | Map<ShortcutActivator, Intent> get shortcuts { |
1249 | assert(ChangeNotifier.debugAssertNotDisposed(this)); |
1250 | return <ShortcutActivator, Intent>{ |
1251 | for (final MapEntry<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> entry in _registeredShortcuts.entries) |
1252 | ...entry.value, |
1253 | }; |
1254 | } |
1255 | |
1256 | final Map<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> _registeredShortcuts = |
1257 | <ShortcutRegistryEntry, Map<ShortcutActivator, Intent>>{}; |
1258 | |
1259 | /// Adds all the given shortcut bindings to this [ShortcutRegistry], and |
1260 | /// returns a entry for managing those bindings. |
1261 | /// |
1262 | /// The entry should have [ShortcutRegistryEntry.dispose] called on it when |
1263 | /// these shortcuts are no longer needed. This will remove them from the |
1264 | /// registry, and invalidate the entry. |
1265 | /// |
1266 | /// This method will assert in debug mode if another entry exists (i.e. hasn't |
1267 | /// been disposed of) that has already added a given shortcut. |
1268 | /// |
1269 | /// If two equivalent, but different, [ShortcutActivator]s are added, all of |
1270 | /// them will be executed when triggered. For example, if both |
1271 | /// `SingleActivator(LogicalKeyboardKey.keyA)` and `CharacterActivator('a')` |
1272 | /// are added, then both will be executed when an "a" key is pressed. |
1273 | /// |
1274 | /// See also: |
1275 | /// |
1276 | /// * [ShortcutRegistryEntry.replaceAll], a function used to replace the set of |
1277 | /// shortcuts associated with a particular entry. |
1278 | /// * [ShortcutRegistryEntry.dispose], a function used to remove the set of |
1279 | /// shortcuts associated with a particular entry. |
1280 | ShortcutRegistryEntry addAll(Map<ShortcutActivator, Intent> value) { |
1281 | assert(ChangeNotifier.debugAssertNotDisposed(this)); |
1282 | assert(value.isNotEmpty, 'Cannot register an empty map of shortcuts' ); |
1283 | final ShortcutRegistryEntry entry = ShortcutRegistryEntry._(this); |
1284 | _registeredShortcuts[entry] = value; |
1285 | assert(_debugCheckForDuplicates()); |
1286 | _notifyListenersNextFrame(); |
1287 | return entry; |
1288 | } |
1289 | |
1290 | // Subscriber notification has to happen in the next frame because shortcuts |
1291 | // are often registered that affect things in the overlay or different parts |
1292 | // of the tree, and so can cause build ordering issues if notifications happen |
1293 | // during the build. The _notificationScheduled check makes sure we only |
1294 | // notify once per frame. |
1295 | void _notifyListenersNextFrame() { |
1296 | if (!_notificationScheduled) { |
1297 | SchedulerBinding.instance.addPostFrameCallback((Duration _) { |
1298 | _notificationScheduled = false; |
1299 | if (!_disposed) { |
1300 | notifyListeners(); |
1301 | } |
1302 | }, debugLabel: 'ShortcutRegistry.notifyListeners' ); |
1303 | _notificationScheduled = true; |
1304 | } |
1305 | } |
1306 | |
1307 | /// Returns the [ShortcutRegistry] that belongs to the [ShortcutRegistrar] |
1308 | /// which most tightly encloses the given [BuildContext]. |
1309 | /// |
1310 | /// If no [ShortcutRegistrar] widget encloses the context given, [of] will |
1311 | /// throw an exception in debug mode. |
1312 | /// |
1313 | /// There is a default [ShortcutRegistrar] instance in [WidgetsApp], so if |
1314 | /// [WidgetsApp], [MaterialApp] or [CupertinoApp] are used, an additional |
1315 | /// [ShortcutRegistrar] isn't needed. |
1316 | /// |
1317 | /// See also: |
1318 | /// |
1319 | /// * [maybeOf], which is similar to this function, but will return null if |
1320 | /// it doesn't find a [ShortcutRegistrar] ancestor. |
1321 | static ShortcutRegistry of(BuildContext context) { |
1322 | final _ShortcutRegistrarScope? inherited = |
1323 | context.dependOnInheritedWidgetOfExactType<_ShortcutRegistrarScope>(); |
1324 | assert(() { |
1325 | if (inherited == null) { |
1326 | throw FlutterError( |
1327 | 'Unable to find a $ShortcutRegistrar widget in the context.\n' |
1328 | ' $ShortcutRegistrar.of() was called with a context that does not contain a ' |
1329 | ' $ShortcutRegistrar widget.\n' |
1330 | 'No $ShortcutRegistrar ancestor could be found starting from the context that was ' |
1331 | 'passed to $ShortcutRegistrar.of().\n' |
1332 | 'The context used was:\n' |
1333 | ' $context' , |
1334 | ); |
1335 | } |
1336 | return true; |
1337 | }()); |
1338 | return inherited!.registry; |
1339 | } |
1340 | |
1341 | /// Returns [ShortcutRegistry] of the [ShortcutRegistrar] that most tightly |
1342 | /// encloses the given [BuildContext]. |
1343 | /// |
1344 | /// If no [ShortcutRegistrar] widget encloses the given context, [maybeOf] |
1345 | /// will return null. |
1346 | /// |
1347 | /// There is a default [ShortcutRegistrar] instance in [WidgetsApp], so if |
1348 | /// [WidgetsApp], [MaterialApp] or [CupertinoApp] are used, an additional |
1349 | /// [ShortcutRegistrar] isn't needed. |
1350 | /// |
1351 | /// See also: |
1352 | /// |
1353 | /// * [of], which is similar to this function, but returns a non-nullable |
1354 | /// result, and will throw an exception if it doesn't find a |
1355 | /// [ShortcutRegistrar] ancestor. |
1356 | static ShortcutRegistry? maybeOf(BuildContext context) { |
1357 | final _ShortcutRegistrarScope? inherited = |
1358 | context.dependOnInheritedWidgetOfExactType<_ShortcutRegistrarScope>(); |
1359 | return inherited?.registry; |
1360 | } |
1361 | |
1362 | // Replaces all the shortcuts associated with the given entry from this |
1363 | // registry. |
1364 | void _replaceAll(ShortcutRegistryEntry entry, Map<ShortcutActivator, Intent> value) { |
1365 | assert(ChangeNotifier.debugAssertNotDisposed(this)); |
1366 | assert(_debugCheckEntryIsValid(entry)); |
1367 | _registeredShortcuts[entry] = value; |
1368 | assert(_debugCheckForDuplicates()); |
1369 | _notifyListenersNextFrame(); |
1370 | } |
1371 | |
1372 | // Removes all the shortcuts associated with the given entry from this |
1373 | // registry. |
1374 | void _disposeEntry(ShortcutRegistryEntry entry) { |
1375 | assert(_debugCheckEntryIsValid(entry)); |
1376 | final Map<ShortcutActivator, Intent>? removedShortcut = _registeredShortcuts.remove(entry); |
1377 | if (removedShortcut != null) { |
1378 | _notifyListenersNextFrame(); |
1379 | } |
1380 | } |
1381 | |
1382 | bool _debugCheckEntryIsValid(ShortcutRegistryEntry entry) { |
1383 | if (!_registeredShortcuts.containsKey(entry)) { |
1384 | if (entry.registry == this) { |
1385 | throw FlutterError('entry ${describeIdentity(entry)} is invalid.\n' |
1386 | 'The entry has already been disposed of. Tokens are not valid after ' |
1387 | 'dispose is called on them, and should no longer be used.' ); |
1388 | } else { |
1389 | throw FlutterError('Foreign entry ${describeIdentity(entry)} used.\n' |
1390 | 'This entry was not created by this registry, it was created by ' |
1391 | ' ${describeIdentity(entry.registry)}, and should be used with that ' |
1392 | 'registry instead.' ); |
1393 | } |
1394 | } |
1395 | return true; |
1396 | } |
1397 | |
1398 | bool _debugCheckForDuplicates() { |
1399 | final Map<ShortcutActivator, ShortcutRegistryEntry?> previous = <ShortcutActivator, ShortcutRegistryEntry?>{}; |
1400 | for (final MapEntry<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> tokenEntry in _registeredShortcuts.entries) { |
1401 | for (final ShortcutActivator shortcut in tokenEntry.value.keys) { |
1402 | if (previous.containsKey(shortcut)) { |
1403 | throw FlutterError( |
1404 | ' $ShortcutRegistry: Received a duplicate registration for the ' |
1405 | 'shortcut $shortcut in ${describeIdentity(tokenEntry.key)} and ${previous[shortcut]}.' ); |
1406 | } |
1407 | previous[shortcut] = tokenEntry.key; |
1408 | } |
1409 | } |
1410 | return true; |
1411 | } |
1412 | } |
1413 | |
1414 | /// A widget that holds a [ShortcutRegistry] which allows descendants to add, |
1415 | /// remove, or replace shortcuts. |
1416 | /// |
1417 | /// This widget holds a [ShortcutRegistry] so that its descendants can find it |
1418 | /// with [ShortcutRegistry.of] or [ShortcutRegistry.maybeOf]. |
1419 | /// |
1420 | /// The registered shortcuts are valid whenever a widget below this one in the |
1421 | /// hierarchy has focus. |
1422 | /// |
1423 | /// To add shortcuts to the registry, call [ShortcutRegistry.of] or |
1424 | /// [ShortcutRegistry.maybeOf] to get the [ShortcutRegistry], and then add them |
1425 | /// using [ShortcutRegistry.addAll], which will return a [ShortcutRegistryEntry] |
1426 | /// which must be disposed by calling [ShortcutRegistryEntry.dispose] when the |
1427 | /// shortcuts are no longer needed. |
1428 | /// |
1429 | /// To replace or update the shortcuts in the registry, call |
1430 | /// [ShortcutRegistryEntry.replaceAll]. |
1431 | /// |
1432 | /// To remove previously added shortcuts from the registry, call |
1433 | /// [ShortcutRegistryEntry.dispose] on the entry returned by |
1434 | /// [ShortcutRegistry.addAll]. |
1435 | class ShortcutRegistrar extends StatefulWidget { |
1436 | /// Creates a const [ShortcutRegistrar]. |
1437 | /// |
1438 | /// The [child] parameter is required. |
1439 | const ShortcutRegistrar({super.key, required this.child}); |
1440 | |
1441 | /// The widget below this widget in the tree. |
1442 | /// |
1443 | /// {@macro flutter.widgets.ProxyWidget.child} |
1444 | final Widget child; |
1445 | |
1446 | @override |
1447 | State<ShortcutRegistrar> createState() => _ShortcutRegistrarState(); |
1448 | } |
1449 | |
1450 | class _ShortcutRegistrarState extends State<ShortcutRegistrar> { |
1451 | final ShortcutRegistry registry = ShortcutRegistry(); |
1452 | final ShortcutManager manager = ShortcutManager(); |
1453 | |
1454 | @override |
1455 | void initState() { |
1456 | super.initState(); |
1457 | registry.addListener(_shortcutsChanged); |
1458 | } |
1459 | |
1460 | void _shortcutsChanged() { |
1461 | // This shouldn't need to update the widget, and avoids calling setState |
1462 | // during build phase. |
1463 | manager.shortcuts = registry.shortcuts; |
1464 | } |
1465 | |
1466 | @override |
1467 | void dispose() { |
1468 | registry.removeListener(_shortcutsChanged); |
1469 | registry.dispose(); |
1470 | manager.dispose(); |
1471 | super.dispose(); |
1472 | } |
1473 | |
1474 | @override |
1475 | Widget build(BuildContext context) { |
1476 | return _ShortcutRegistrarScope( |
1477 | registry: registry, |
1478 | child: Shortcuts.manager( |
1479 | manager: manager, |
1480 | child: widget.child, |
1481 | ), |
1482 | ); |
1483 | } |
1484 | } |
1485 | |
1486 | class _ShortcutRegistrarScope extends InheritedWidget { |
1487 | const _ShortcutRegistrarScope({ |
1488 | required this.registry, |
1489 | required super.child, |
1490 | }); |
1491 | |
1492 | final ShortcutRegistry registry; |
1493 | |
1494 | @override |
1495 | bool updateShouldNotify(covariant _ShortcutRegistrarScope oldWidget) { |
1496 | return registry != oldWidget.registry; |
1497 | } |
1498 | } |
1499 | |