1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'actions.dart';
import 'binding.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'inherited_notifier.dart';
/// A set of [KeyboardKey]s that can be used as the keys in a [Map].
///
/// A key set contains the keys that are down simultaneously to represent a
/// shortcut.
///
/// This is a thin wrapper around a [Set], but changes the equality comparison
/// from an identity comparison to a contents comparison so that non-identical
/// sets with the same keys in them will compare as equal.
///
/// See also:
///
/// - [ShortcutManager], which uses [LogicalKeySet] (a [KeySet] subclass) to
/// define its key map.
class KeySet<T extends KeyboardKey> extends Diagnosticable {
/// A constructor for making a [KeySet] of up to four keys.
///
/// If you need a set of more than four keys, use [KeySet.fromSet].
///
/// The `key1` parameter must not be null. The same [KeyboardKey] may
/// not be appear more than once in the set.
KeySet(
T key1, [
T key2,
T key3,
T key4,
]) : assert(key1 != null),
_keys = HashSet<T>()..add(key1) {
int count = 1;
if (key2 != null) {
_keys.add(key2);
assert(() {
count++;
return true;
}());
}
if (key3 != null) {
_keys.add(key3);
assert(() {
count++;
return true;
}());
}
if (key4 != null) {
_keys.add(key4);
assert(() {
count++;
return true;
}());
}
assert(_keys.length == count, 'Two or more provided keys are identical. Each key must appear only once.');
}
/// Create a [KeySet] from a set of [KeyboardKey]s.
///
/// Do not mutate the `keys` set after passing it to this object.
///
/// The `keys` set must not be null, contain nulls, or be empty.
KeySet.fromSet(Set<T> keys)
: assert(keys != null),
assert(keys.isNotEmpty),
assert(!keys.contains(null)),
_keys = HashSet<T>.from(keys);
/// Returns an unmodifiable view of the [KeyboardKey]s in this [KeySet].
Set<T> get keys => UnmodifiableSetView<T>(_keys);
// This needs to be a hash set to be sure that the hashCode accessor returns
// consistent results. LinkedHashSet (the default Set implementation) depends
// upon insertion order, and HashSet does not.
final HashSet<T> _keys;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
final KeySet<T> typedOther = other;
return setEquals<T>(_keys, typedOther._keys);
}
@override
int get hashCode {
return hashList(_keys);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Set<T>>('keys', _keys));
}
}
/// A set of [LogicalKeyboardKey]s that can be used as the keys in a map.
///
/// A key set contains the keys that are down simultaneously to represent a
/// shortcut.
///
/// This is mainly used by [ShortcutManager] to allow the definition of shortcut
/// mappings.
///
/// This is a thin wrapper around a [Set], but changes the equality comparison
/// from an identity comparison to a contents comparison so that non-identical
/// sets with the same keys in them will compare as equal.
class LogicalKeySet extends KeySet<LogicalKeyboardKey> {
/// A constructor for making a [LogicalKeySet] of up to four keys.
///
/// If you need a set of more than four keys, use [LogicalKeySet.fromSet].
///
/// The `key1` parameter must not be null. The same [LogicalKeyboardKey] may
/// not be appear more than once in the set.
LogicalKeySet(
LogicalKeyboardKey key1, [
LogicalKeyboardKey key2,
LogicalKeyboardKey key3,
LogicalKeyboardKey key4,
]) : super(key1, key2, key3, key4);
/// Create a [LogicalKeySet] from a set of [LogicalKeyboardKey]s.
///
/// Do not mutate the `keys` set after passing it to this object.
///
/// The `keys` must not be null.
LogicalKeySet.fromSet(Set<LogicalKeyboardKey> keys) : super.fromSet(keys);
}
/// A manager of keyboard shortcut bindings.
///
/// A [ShortcutManager] is obtained by calling [Shortcuts.of] on the context of
/// the widget that you want to find a manager for.
class ShortcutManager extends ChangeNotifier with DiagnosticableMixin {
/// Constructs a [ShortcutManager].
///
/// The [shortcuts] argument must not be null.
ShortcutManager({
Map<LogicalKeySet, Intent> shortcuts = const <LogicalKeySet, Intent>{},
this.modal = false,
}) : assert(shortcuts != null),
_shortcuts = shortcuts;
/// True if the [ShortcutManager] should not pass on keys that it doesn't
/// handle to any key-handling widgets that are ancestors to this one.
///
/// Setting [modal] to true is the equivalent of always handling any key given
/// to it, even if that key doesn't appear in the [shortcuts] map. Keys that
/// don't appear in the map will be dropped.
final bool modal;
/// Returns the shortcut map.
///
/// When the map is changed, listeners to this manager will be notified.
///
/// The returned [LogicalKeyMap] should not be modified.
Map<LogicalKeySet, Intent> get shortcuts => _shortcuts;
Map<LogicalKeySet, Intent> _shortcuts;
set shortcuts(Map<LogicalKeySet, Intent> value) {
if (!mapEquals<LogicalKeySet, Intent>(_shortcuts, value)) {
_shortcuts = value;
notifyListeners();
}
}
/// Handles a key pressed `event` in the given `context`.
///
/// The optional `keysPressed` argument provides an override to keys that the
/// [RawKeyboard] reports. If not specified, uses [RawKeyboard.keysPressed]
/// instead.
@protected
bool handleKeypress(
BuildContext context,
RawKeyEvent event, {
LogicalKeySet keysPressed,
}) {
if (event is! RawKeyDownEvent) {
return false;
}
assert(context != null);
final LogicalKeySet keySet = keysPressed ?? LogicalKeySet.fromSet(RawKeyboard.instance.keysPressed);
Intent matchedIntent = _shortcuts[keySet];
if (matchedIntent == null) {
// If there's not a more specific match, We also look for any keys that
// have synonyms in the map. This is for things like left and right shift
// keys mapping to just the "shift" pseudo-key.
final Set<LogicalKeyboardKey> pseudoKeys = <LogicalKeyboardKey>{};
for (LogicalKeyboardKey setKey in keySet.keys) {
final Set<LogicalKeyboardKey> synonyms = setKey.synonyms;
if (synonyms.isNotEmpty) {
// There currently aren't any synonyms that match more than one key.
pseudoKeys.add(synonyms.first);
} else {
pseudoKeys.add(setKey);
}
}
matchedIntent = _shortcuts[LogicalKeySet.fromSet(pseudoKeys)];
}
if (matchedIntent != null) {
final BuildContext primaryContext = WidgetsBinding.instance.focusManager.primaryFocus?.context;
if (primaryContext == null) {
return false;
}
return Actions.invoke(primaryContext, matchedIntent, nullOk: true);
}
return false;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Map<LogicalKeySet, Intent>>('shortcuts', _shortcuts));
properties.add(FlagProperty('modal', value: modal, ifTrue: 'modal', defaultValue: false));
}
}
/// A widget that establishes an [ShortcutManager] to be used by its descendants
/// when invoking an [Action] via a keyboard key combination that maps to an
/// [Intent].
///
/// See also:
///
/// * [Intent], a class for containing a description of a user
/// action to be invoked.
/// * [Action], a class for defining an invocation of a user action.
class Shortcuts extends StatefulWidget {
/// Creates a ActionManager object.
///
/// The [child] argument must not be null.
const Shortcuts({
Key key,
this.manager,
this.shortcuts,
this.child,
}) : super(key: key);
/// The [ShortcutManager] that will manage the mapping between key
/// combinations and [Action]s.
///
/// If not specified, uses a default-constructed [ShortcutManager].
///
/// This manager will be given new [shortcuts] to manage whenever the
/// [shortcuts] change materially.
final ShortcutManager manager;
/// The map of shortcuts that the [manager] will be given to manage.
///
/// For performance reasons, it is recommended that a pre-built map is passed
/// in here (e.g. a final variable from your widget class) instead of defining
/// it inline in the build function.
final Map<LogicalKeySet, Intent> shortcuts;
/// The child widget for this [Shortcuts] widget.
///
/// {@macro flutter.widgets.child}
final Widget child;
/// Returns the [ActionDispatcher] that most tightly encloses the given
/// [BuildContext].
///
/// The [context] argument must not be null.
static ShortcutManager of(BuildContext context, {bool nullOk = false}) {
assert(context != null);
final _ShortcutsMarker inherited = context.inheritFromWidgetOfExactType(_ShortcutsMarker);
assert(() {
if (nullOk) {
return true;
}
if (inherited == null) {
throw FlutterError('Unable to find a $Shortcuts widget in the context.\n'
'$Shortcuts.of() was called with a context that does not contain a '
'$Shortcuts widget.\n'
'No $Shortcuts ancestor could be found starting from the context that was '
'passed to $Shortcuts.of().\n'
'The context used was:\n'
' $context');
}
return true;
}());
return inherited?.notifier;
}
@override
_ShortcutsState createState() => _ShortcutsState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ShortcutManager>('manager', manager));
properties.add(DiagnosticsProperty<Map<LogicalKeySet, Intent>>('shortcuts', shortcuts));
}
}
class _ShortcutsState extends State<Shortcuts> {
ShortcutManager _internalManager;
ShortcutManager get manager => widget.manager ?? _internalManager;
@override
void dispose() {
_internalManager?.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
if (widget.manager == null) {
_internalManager = ShortcutManager();
}
manager.shortcuts = widget.shortcuts;
}
@override
void didUpdateWidget(Shortcuts oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.manager != oldWidget.manager) {
if (widget.manager != null) {
_internalManager?.dispose();
_internalManager = null;
} else {
_internalManager ??= ShortcutManager();
}
}
manager.shortcuts = widget.shortcuts;
}
bool _handleOnKey(FocusNode node, RawKeyEvent event) {
if (node.context == null) {
return false;
}
return manager.handleKeypress(node.context, event) || manager.modal;
}
@override
Widget build(BuildContext context) {
return Focus(
debugLabel: '$Shortcuts',
canRequestFocus: false,
onKey: _handleOnKey,
child: _ShortcutsMarker(
manager: manager,
child: widget.child,
),
);
}
}
class _ShortcutsMarker extends InheritedNotifier<ShortcutManager> {
const _ShortcutsMarker({
@required ShortcutManager manager,
@required Widget child,
}) : assert(manager != null),
assert(child != null),
super(notifier: manager, child: child);
}