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