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