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