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