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