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