1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:io';
7import 'dart:ui' as ui;
8
9import 'package:flutter/foundation.dart';
10import 'package:flutter/services.dart';
11
12import 'binding.dart';
13import 'test_async_utils.dart';
14import 'widget_tester.dart';
15
16// A tuple of `key` and `location` from Web's `KeyboardEvent` class.
17//
18// See [RawKeyEventDataWeb]'s `key` and `location` fields for details.
19@immutable
20class _WebKeyLocationPair {
21 const _WebKeyLocationPair(this.key, this.location);
22 final String key;
23 final int location;
24}
25
26// TODO(gspencergoog): Replace this with more robust key simulation code once
27// the new key event code is in.
28// https://github.com/flutter/flutter/issues/33521
29// This code can only simulate keys which appear in the key maps.
30
31String? _keyLabel(LogicalKeyboardKey key) {
32 final String keyLabel = key.keyLabel;
33 if (keyLabel.length == 1) {
34 return keyLabel.toLowerCase();
35 }
36 return null;
37}
38
39/// A class that serves as a namespace for a bunch of keyboard-key generation
40/// utilities.
41abstract final class KeyEventSimulator {
42 // Look up a synonym key, and just return the left version of it.
43 static LogicalKeyboardKey _getKeySynonym(LogicalKeyboardKey origKey) {
44 if (origKey == LogicalKeyboardKey.shift) {
45 return LogicalKeyboardKey.shiftLeft;
46 }
47 if (origKey == LogicalKeyboardKey.alt) {
48 return LogicalKeyboardKey.altLeft;
49 }
50 if (origKey == LogicalKeyboardKey.meta) {
51 return LogicalKeyboardKey.metaLeft;
52 }
53 if (origKey == LogicalKeyboardKey.control) {
54 return LogicalKeyboardKey.controlLeft;
55 }
56 return origKey;
57 }
58
59 static bool _osIsSupported(String platform) {
60 switch (platform) {
61 case 'android':
62 case 'fuchsia':
63 case 'macos':
64 case 'linux':
65 case 'web':
66 case 'ios':
67 case 'windows':
68 return true;
69 }
70 return false;
71 }
72
73 static int _getScanCode(PhysicalKeyboardKey key, String platform) {
74 assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation');
75 late Map<int, PhysicalKeyboardKey> map;
76 switch (platform) {
77 case 'android':
78 map = kAndroidToPhysicalKey;
79 case 'fuchsia':
80 map = kFuchsiaToPhysicalKey;
81 case 'macos':
82 map = kMacOsToPhysicalKey;
83 case 'ios':
84 map = kIosToPhysicalKey;
85 case 'linux':
86 map = kLinuxToPhysicalKey;
87 case 'windows':
88 map = kWindowsToPhysicalKey;
89 case 'web':
90 // web doesn't have int type code
91 return -1;
92 }
93 int? scanCode;
94 for (final int code in map.keys) {
95 if (key.usbHidUsage == map[code]!.usbHidUsage) {
96 scanCode = code;
97 break;
98 }
99 }
100 assert(scanCode != null, 'Physical key for $key not found in $platform scanCode map');
101 return scanCode!;
102 }
103
104 static int _getKeyCode(LogicalKeyboardKey key, String platform) {
105 assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation');
106 if (kIsWeb) {
107 // web doesn't have int type code. This check is used to treeshake
108 // keyboard map code.
109 return -1;
110 } else {
111 late Map<int, LogicalKeyboardKey> map;
112 switch (platform) {
113 case 'android':
114 map = kAndroidToLogicalKey;
115 case 'fuchsia':
116 map = kFuchsiaToLogicalKey;
117 case 'macos':
118 // macOS doesn't do key codes, just scan codes.
119 return -1;
120 case 'ios':
121 // iOS doesn't do key codes, just scan codes.
122 return -1;
123 case 'web':
124 // web doesn't have int type code.
125 return -1;
126 case 'linux':
127 map = kGlfwToLogicalKey;
128 case 'windows':
129 map = kWindowsToLogicalKey;
130 }
131 int? keyCode;
132 for (final int code in map.keys) {
133 if (key.keyId == map[code]!.keyId) {
134 keyCode = code;
135 break;
136 }
137 }
138 assert(keyCode != null, 'Key $key not found in $platform keyCode map');
139 return keyCode!;
140 }
141 }
142
143 static PhysicalKeyboardKey _inferPhysicalKey(LogicalKeyboardKey key) {
144 PhysicalKeyboardKey? result;
145 for (final PhysicalKeyboardKey physicalKey in PhysicalKeyboardKey.knownPhysicalKeys) {
146 if (physicalKey.debugName == key.debugName) {
147 result = physicalKey;
148 break;
149 }
150 }
151 assert(result != null, 'Unable to infer physical key for $key');
152 return result!;
153 }
154
155 static _WebKeyLocationPair _getWebKeyLocation(LogicalKeyboardKey key, String keyLabel) {
156 String? result;
157 for (final MapEntry<String, List<LogicalKeyboardKey?>> entry in kWebLocationMap.entries) {
158 final int foundIndex = entry.value.lastIndexOf(key);
159 // If foundIndex is -1, then the key is not defined in kWebLocationMap.
160 // If foundIndex is 0, then the key is in the standard part of the keyboard,
161 // but we have to check `keyLabel` to see if it's remapped or modified.
162 if (foundIndex != -1 && foundIndex != 0) {
163 return _WebKeyLocationPair(entry.key, foundIndex);
164 }
165 }
166 if (keyLabel.isNotEmpty) {
167 return _WebKeyLocationPair(keyLabel, 0);
168 }
169 for (final String code in kWebToLogicalKey.keys) {
170 if (key.keyId == kWebToLogicalKey[code]!.keyId) {
171 result = code;
172 break;
173 }
174 }
175 assert(result != null, 'Key $key not found in web keyCode map');
176 return _WebKeyLocationPair(result!, 0);
177 }
178
179 static String _getWebCode(PhysicalKeyboardKey key) {
180 String? result;
181 for (final MapEntry<String, PhysicalKeyboardKey> entry in kWebToPhysicalKey.entries) {
182 if (entry.value.usbHidUsage == key.usbHidUsage) {
183 result = entry.key;
184 break;
185 }
186 }
187 assert(result != null, 'Key $key not found in web code map');
188 return result!;
189 }
190
191 static PhysicalKeyboardKey _findPhysicalKeyByPlatform(LogicalKeyboardKey key, String platform) {
192 assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation');
193 late Map<dynamic, PhysicalKeyboardKey> map;
194 if (kIsWeb) {
195 // This check is used to treeshake keymap code.
196 map = kWebToPhysicalKey;
197 } else {
198 switch (platform) {
199 case 'android':
200 map = kAndroidToPhysicalKey;
201 case 'fuchsia':
202 map = kFuchsiaToPhysicalKey;
203 case 'macos':
204 map = kMacOsToPhysicalKey;
205 case 'ios':
206 map = kIosToPhysicalKey;
207 case 'linux':
208 map = kLinuxToPhysicalKey;
209 case 'web':
210 map = kWebToPhysicalKey;
211 case 'windows':
212 map = kWindowsToPhysicalKey;
213 }
214 }
215 PhysicalKeyboardKey? result;
216 for (final PhysicalKeyboardKey physicalKey in map.values) {
217 if (key.debugName == physicalKey.debugName) {
218 result = physicalKey;
219 break;
220 }
221 }
222 assert(result != null, 'Physical key for $key not found in $platform physical key map');
223 return result!;
224 }
225
226 /// Get a raw key data map given a [LogicalKeyboardKey] and a platform.
227 static Map<String, dynamic> getKeyData(
228 LogicalKeyboardKey key, {
229 required String platform,
230 bool isDown = true,
231 PhysicalKeyboardKey? physicalKey,
232 String? character,
233 }) {
234 assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation');
235
236 key = _getKeySynonym(key);
237
238 // Find a suitable physical key if none was supplied.
239 physicalKey ??= _findPhysicalKeyByPlatform(key, platform);
240
241 assert(key.debugName != null);
242
243 final Map<String, dynamic> result = <String, dynamic>{
244 'type': isDown ? 'keydown' : 'keyup',
245 'keymap': platform,
246 };
247
248 final String resultCharacter = character ?? _keyLabel(key) ?? '';
249 void assignWeb() {
250 final _WebKeyLocationPair keyLocation = _getWebKeyLocation(key, resultCharacter);
251 final PhysicalKeyboardKey actualPhysicalKey = physicalKey ?? _inferPhysicalKey(key);
252 result['code'] = _getWebCode(actualPhysicalKey);
253 result['key'] = keyLocation.key;
254 result['location'] = keyLocation.location;
255 result['metaState'] = _getWebModifierFlags(key, isDown);
256 }
257
258 if (kIsWeb) {
259 assignWeb();
260 return result;
261 }
262 final int keyCode = _getKeyCode(key, platform);
263 final int scanCode = _getScanCode(physicalKey, platform);
264
265 switch (platform) {
266 case 'android':
267 result['keyCode'] = keyCode;
268 if (resultCharacter.isNotEmpty) {
269 result['codePoint'] = resultCharacter.codeUnitAt(0);
270 result['character'] = resultCharacter;
271 }
272 result['scanCode'] = scanCode;
273 result['metaState'] = _getAndroidModifierFlags(key, isDown);
274 case 'fuchsia':
275 result['hidUsage'] = physicalKey.usbHidUsage;
276 if (resultCharacter.isNotEmpty) {
277 result['codePoint'] = resultCharacter.codeUnitAt(0);
278 }
279 result['modifiers'] = _getFuchsiaModifierFlags(key, isDown);
280 case 'linux':
281 result['toolkit'] = 'glfw';
282 result['keyCode'] = keyCode;
283 result['scanCode'] = scanCode;
284 result['modifiers'] = _getGlfwModifierFlags(key, isDown);
285 result['unicodeScalarValues'] = resultCharacter.isNotEmpty
286 ? resultCharacter.codeUnitAt(0)
287 : 0;
288 case 'macos':
289 result['keyCode'] = scanCode;
290 if (resultCharacter.isNotEmpty) {
291 result['characters'] = resultCharacter;
292 result['charactersIgnoringModifiers'] = resultCharacter;
293 }
294 result['modifiers'] = _getMacOsModifierFlags(key, isDown);
295 case 'ios':
296 result['keyCode'] = scanCode;
297 result['characters'] = resultCharacter;
298 result['charactersIgnoringModifiers'] = resultCharacter;
299 result['modifiers'] = _getIOSModifierFlags(key, isDown);
300 case 'windows':
301 result['keyCode'] = keyCode;
302 result['scanCode'] = scanCode;
303 if (resultCharacter.isNotEmpty) {
304 result['characterCodePoint'] = resultCharacter.codeUnitAt(0);
305 }
306 result['modifiers'] = _getWindowsModifierFlags(key, isDown);
307 case 'web':
308 assignWeb();
309 }
310 return result;
311 }
312
313 static int _getAndroidModifierFlags(LogicalKeyboardKey newKey, bool isDown) {
314 int result = 0;
315 final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed;
316 if (isDown) {
317 pressed.add(newKey);
318 } else {
319 pressed.remove(newKey);
320 }
321 if (pressed.contains(LogicalKeyboardKey.shiftLeft)) {
322 result |= RawKeyEventDataAndroid.modifierLeftShift | RawKeyEventDataAndroid.modifierShift;
323 }
324 if (pressed.contains(LogicalKeyboardKey.shiftRight)) {
325 result |= RawKeyEventDataAndroid.modifierRightShift | RawKeyEventDataAndroid.modifierShift;
326 }
327 if (pressed.contains(LogicalKeyboardKey.metaLeft)) {
328 result |= RawKeyEventDataAndroid.modifierLeftMeta | RawKeyEventDataAndroid.modifierMeta;
329 }
330 if (pressed.contains(LogicalKeyboardKey.metaRight)) {
331 result |= RawKeyEventDataAndroid.modifierRightMeta | RawKeyEventDataAndroid.modifierMeta;
332 }
333 if (pressed.contains(LogicalKeyboardKey.controlLeft)) {
334 result |= RawKeyEventDataAndroid.modifierLeftControl | RawKeyEventDataAndroid.modifierControl;
335 }
336 if (pressed.contains(LogicalKeyboardKey.controlRight)) {
337 result |=
338 RawKeyEventDataAndroid.modifierRightControl | RawKeyEventDataAndroid.modifierControl;
339 }
340 if (pressed.contains(LogicalKeyboardKey.altLeft)) {
341 result |= RawKeyEventDataAndroid.modifierLeftAlt | RawKeyEventDataAndroid.modifierAlt;
342 }
343 if (pressed.contains(LogicalKeyboardKey.altRight)) {
344 result |= RawKeyEventDataAndroid.modifierRightAlt | RawKeyEventDataAndroid.modifierAlt;
345 }
346 if (pressed.contains(LogicalKeyboardKey.fn)) {
347 result |= RawKeyEventDataAndroid.modifierFunction;
348 }
349 if (pressed.contains(LogicalKeyboardKey.scrollLock)) {
350 result |= RawKeyEventDataAndroid.modifierScrollLock;
351 }
352 if (pressed.contains(LogicalKeyboardKey.numLock)) {
353 result |= RawKeyEventDataAndroid.modifierNumLock;
354 }
355 if (pressed.contains(LogicalKeyboardKey.capsLock)) {
356 result |= RawKeyEventDataAndroid.modifierCapsLock;
357 }
358 return result;
359 }
360
361 static int _getGlfwModifierFlags(LogicalKeyboardKey newKey, bool isDown) {
362 int result = 0;
363 final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed;
364 if (isDown) {
365 pressed.add(newKey);
366 } else {
367 pressed.remove(newKey);
368 }
369 if (pressed.contains(LogicalKeyboardKey.shiftLeft) ||
370 pressed.contains(LogicalKeyboardKey.shiftRight)) {
371 result |= GLFWKeyHelper.modifierShift;
372 }
373 if (pressed.contains(LogicalKeyboardKey.metaLeft) ||
374 pressed.contains(LogicalKeyboardKey.metaRight)) {
375 result |= GLFWKeyHelper.modifierMeta;
376 }
377 if (pressed.contains(LogicalKeyboardKey.controlLeft) ||
378 pressed.contains(LogicalKeyboardKey.controlRight)) {
379 result |= GLFWKeyHelper.modifierControl;
380 }
381 if (pressed.contains(LogicalKeyboardKey.altLeft) ||
382 pressed.contains(LogicalKeyboardKey.altRight)) {
383 result |= GLFWKeyHelper.modifierAlt;
384 }
385 if (pressed.contains(LogicalKeyboardKey.capsLock)) {
386 result |= GLFWKeyHelper.modifierCapsLock;
387 }
388 return result;
389 }
390
391 static int _getWindowsModifierFlags(LogicalKeyboardKey newKey, bool isDown) {
392 int result = 0;
393 final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed;
394 if (isDown) {
395 pressed.add(newKey);
396 } else {
397 pressed.remove(newKey);
398 }
399 if (pressed.contains(LogicalKeyboardKey.shift)) {
400 result |= RawKeyEventDataWindows.modifierShift;
401 }
402 if (pressed.contains(LogicalKeyboardKey.shiftLeft)) {
403 result |= RawKeyEventDataWindows.modifierLeftShift;
404 }
405 if (pressed.contains(LogicalKeyboardKey.shiftRight)) {
406 result |= RawKeyEventDataWindows.modifierRightShift;
407 }
408 if (pressed.contains(LogicalKeyboardKey.metaLeft)) {
409 result |= RawKeyEventDataWindows.modifierLeftMeta;
410 }
411 if (pressed.contains(LogicalKeyboardKey.metaRight)) {
412 result |= RawKeyEventDataWindows.modifierRightMeta;
413 }
414 if (pressed.contains(LogicalKeyboardKey.control)) {
415 result |= RawKeyEventDataWindows.modifierControl;
416 }
417 if (pressed.contains(LogicalKeyboardKey.controlLeft)) {
418 result |= RawKeyEventDataWindows.modifierLeftControl;
419 }
420 if (pressed.contains(LogicalKeyboardKey.controlRight)) {
421 result |= RawKeyEventDataWindows.modifierRightControl;
422 }
423 if (pressed.contains(LogicalKeyboardKey.alt)) {
424 result |= RawKeyEventDataWindows.modifierAlt;
425 }
426 if (pressed.contains(LogicalKeyboardKey.altLeft)) {
427 result |= RawKeyEventDataWindows.modifierLeftAlt;
428 }
429 if (pressed.contains(LogicalKeyboardKey.altRight)) {
430 result |= RawKeyEventDataWindows.modifierRightAlt;
431 }
432 if (pressed.contains(LogicalKeyboardKey.capsLock)) {
433 result |= RawKeyEventDataWindows.modifierCaps;
434 }
435 if (pressed.contains(LogicalKeyboardKey.numLock)) {
436 result |= RawKeyEventDataWindows.modifierNumLock;
437 }
438 if (pressed.contains(LogicalKeyboardKey.scrollLock)) {
439 result |= RawKeyEventDataWindows.modifierScrollLock;
440 }
441 return result;
442 }
443
444 static int _getFuchsiaModifierFlags(LogicalKeyboardKey newKey, bool isDown) {
445 int result = 0;
446 final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed;
447 if (isDown) {
448 pressed.add(newKey);
449 } else {
450 pressed.remove(newKey);
451 }
452 if (pressed.contains(LogicalKeyboardKey.shiftLeft)) {
453 result |= RawKeyEventDataFuchsia.modifierLeftShift;
454 }
455 if (pressed.contains(LogicalKeyboardKey.shiftRight)) {
456 result |= RawKeyEventDataFuchsia.modifierRightShift;
457 }
458 if (pressed.contains(LogicalKeyboardKey.metaLeft)) {
459 result |= RawKeyEventDataFuchsia.modifierLeftMeta;
460 }
461 if (pressed.contains(LogicalKeyboardKey.metaRight)) {
462 result |= RawKeyEventDataFuchsia.modifierRightMeta;
463 }
464 if (pressed.contains(LogicalKeyboardKey.controlLeft)) {
465 result |= RawKeyEventDataFuchsia.modifierLeftControl;
466 }
467 if (pressed.contains(LogicalKeyboardKey.controlRight)) {
468 result |= RawKeyEventDataFuchsia.modifierRightControl;
469 }
470 if (pressed.contains(LogicalKeyboardKey.altLeft)) {
471 result |= RawKeyEventDataFuchsia.modifierLeftAlt;
472 }
473 if (pressed.contains(LogicalKeyboardKey.altRight)) {
474 result |= RawKeyEventDataFuchsia.modifierRightAlt;
475 }
476 if (pressed.contains(LogicalKeyboardKey.capsLock)) {
477 result |= RawKeyEventDataFuchsia.modifierCapsLock;
478 }
479 return result;
480 }
481
482 static int _getWebModifierFlags(LogicalKeyboardKey newKey, bool isDown) {
483 int result = 0;
484 final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed;
485 if (isDown) {
486 pressed.add(newKey);
487 } else {
488 pressed.remove(newKey);
489 }
490 if (pressed.contains(LogicalKeyboardKey.shiftLeft)) {
491 result |= RawKeyEventDataWeb.modifierShift;
492 }
493 if (pressed.contains(LogicalKeyboardKey.shiftRight)) {
494 result |= RawKeyEventDataWeb.modifierShift;
495 }
496 if (pressed.contains(LogicalKeyboardKey.metaLeft)) {
497 result |= RawKeyEventDataWeb.modifierMeta;
498 }
499 if (pressed.contains(LogicalKeyboardKey.metaRight)) {
500 result |= RawKeyEventDataWeb.modifierMeta;
501 }
502 if (pressed.contains(LogicalKeyboardKey.controlLeft)) {
503 result |= RawKeyEventDataWeb.modifierControl;
504 }
505 if (pressed.contains(LogicalKeyboardKey.controlRight)) {
506 result |= RawKeyEventDataWeb.modifierControl;
507 }
508 if (pressed.contains(LogicalKeyboardKey.altLeft)) {
509 result |= RawKeyEventDataWeb.modifierAlt;
510 }
511 if (pressed.contains(LogicalKeyboardKey.altRight)) {
512 result |= RawKeyEventDataWeb.modifierAlt;
513 }
514 if (pressed.contains(LogicalKeyboardKey.capsLock)) {
515 result |= RawKeyEventDataWeb.modifierCapsLock;
516 }
517 if (pressed.contains(LogicalKeyboardKey.numLock)) {
518 result |= RawKeyEventDataWeb.modifierNumLock;
519 }
520 if (pressed.contains(LogicalKeyboardKey.scrollLock)) {
521 result |= RawKeyEventDataWeb.modifierScrollLock;
522 }
523 return result;
524 }
525
526 static int _getMacOsModifierFlags(LogicalKeyboardKey newKey, bool isDown) {
527 int result = 0;
528 final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed;
529 if (isDown) {
530 pressed.add(newKey);
531 } else {
532 pressed.remove(newKey);
533 }
534 if (pressed.contains(LogicalKeyboardKey.shiftLeft)) {
535 result |= RawKeyEventDataMacOs.modifierLeftShift | RawKeyEventDataMacOs.modifierShift;
536 }
537 if (pressed.contains(LogicalKeyboardKey.shiftRight)) {
538 result |= RawKeyEventDataMacOs.modifierRightShift | RawKeyEventDataMacOs.modifierShift;
539 }
540 if (pressed.contains(LogicalKeyboardKey.metaLeft)) {
541 result |= RawKeyEventDataMacOs.modifierLeftCommand | RawKeyEventDataMacOs.modifierCommand;
542 }
543 if (pressed.contains(LogicalKeyboardKey.metaRight)) {
544 result |= RawKeyEventDataMacOs.modifierRightCommand | RawKeyEventDataMacOs.modifierCommand;
545 }
546 if (pressed.contains(LogicalKeyboardKey.controlLeft)) {
547 result |= RawKeyEventDataMacOs.modifierLeftControl | RawKeyEventDataMacOs.modifierControl;
548 }
549 if (pressed.contains(LogicalKeyboardKey.controlRight)) {
550 result |= RawKeyEventDataMacOs.modifierRightControl | RawKeyEventDataMacOs.modifierControl;
551 }
552 if (pressed.contains(LogicalKeyboardKey.altLeft)) {
553 result |= RawKeyEventDataMacOs.modifierLeftOption | RawKeyEventDataMacOs.modifierOption;
554 }
555 if (pressed.contains(LogicalKeyboardKey.altRight)) {
556 result |= RawKeyEventDataMacOs.modifierRightOption | RawKeyEventDataMacOs.modifierOption;
557 }
558 final Set<LogicalKeyboardKey> functionKeys = <LogicalKeyboardKey>{
559 LogicalKeyboardKey.f1,
560 LogicalKeyboardKey.f2,
561 LogicalKeyboardKey.f3,
562 LogicalKeyboardKey.f4,
563 LogicalKeyboardKey.f5,
564 LogicalKeyboardKey.f6,
565 LogicalKeyboardKey.f7,
566 LogicalKeyboardKey.f8,
567 LogicalKeyboardKey.f9,
568 LogicalKeyboardKey.f10,
569 LogicalKeyboardKey.f11,
570 LogicalKeyboardKey.f12,
571 LogicalKeyboardKey.f13,
572 LogicalKeyboardKey.f14,
573 LogicalKeyboardKey.f15,
574 LogicalKeyboardKey.f16,
575 LogicalKeyboardKey.f17,
576 LogicalKeyboardKey.f18,
577 LogicalKeyboardKey.f19,
578 LogicalKeyboardKey.f20,
579 LogicalKeyboardKey.f21,
580 };
581 if (pressed.intersection(functionKeys).isNotEmpty) {
582 result |= RawKeyEventDataMacOs.modifierFunction;
583 }
584 if (pressed.intersection(kMacOsNumPadMap.values.toSet()).isNotEmpty) {
585 result |= RawKeyEventDataMacOs.modifierNumericPad;
586 }
587 if (pressed.contains(LogicalKeyboardKey.capsLock)) {
588 result |= RawKeyEventDataMacOs.modifierCapsLock;
589 }
590 return result;
591 }
592
593 static int _getIOSModifierFlags(LogicalKeyboardKey newKey, bool isDown) {
594 int result = 0;
595 final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed;
596 if (isDown) {
597 pressed.add(newKey);
598 } else {
599 pressed.remove(newKey);
600 }
601 if (pressed.contains(LogicalKeyboardKey.shiftLeft)) {
602 result |= RawKeyEventDataIos.modifierLeftShift | RawKeyEventDataIos.modifierShift;
603 }
604 if (pressed.contains(LogicalKeyboardKey.shiftRight)) {
605 result |= RawKeyEventDataIos.modifierRightShift | RawKeyEventDataIos.modifierShift;
606 }
607 if (pressed.contains(LogicalKeyboardKey.metaLeft)) {
608 result |= RawKeyEventDataIos.modifierLeftCommand | RawKeyEventDataIos.modifierCommand;
609 }
610 if (pressed.contains(LogicalKeyboardKey.metaRight)) {
611 result |= RawKeyEventDataIos.modifierRightCommand | RawKeyEventDataIos.modifierCommand;
612 }
613 if (pressed.contains(LogicalKeyboardKey.controlLeft)) {
614 result |= RawKeyEventDataIos.modifierLeftControl | RawKeyEventDataIos.modifierControl;
615 }
616 if (pressed.contains(LogicalKeyboardKey.controlRight)) {
617 result |= RawKeyEventDataIos.modifierRightControl | RawKeyEventDataIos.modifierControl;
618 }
619 if (pressed.contains(LogicalKeyboardKey.altLeft)) {
620 result |= RawKeyEventDataIos.modifierLeftOption | RawKeyEventDataIos.modifierOption;
621 }
622 if (pressed.contains(LogicalKeyboardKey.altRight)) {
623 result |= RawKeyEventDataIos.modifierRightOption | RawKeyEventDataIos.modifierOption;
624 }
625 final Set<LogicalKeyboardKey> functionKeys = <LogicalKeyboardKey>{
626 LogicalKeyboardKey.f1,
627 LogicalKeyboardKey.f2,
628 LogicalKeyboardKey.f3,
629 LogicalKeyboardKey.f4,
630 LogicalKeyboardKey.f5,
631 LogicalKeyboardKey.f6,
632 LogicalKeyboardKey.f7,
633 LogicalKeyboardKey.f8,
634 LogicalKeyboardKey.f9,
635 LogicalKeyboardKey.f10,
636 LogicalKeyboardKey.f11,
637 LogicalKeyboardKey.f12,
638 LogicalKeyboardKey.f13,
639 LogicalKeyboardKey.f14,
640 LogicalKeyboardKey.f15,
641 LogicalKeyboardKey.f16,
642 LogicalKeyboardKey.f17,
643 LogicalKeyboardKey.f18,
644 LogicalKeyboardKey.f19,
645 LogicalKeyboardKey.f20,
646 LogicalKeyboardKey.f21,
647 };
648 if (pressed.intersection(functionKeys).isNotEmpty) {
649 result |= RawKeyEventDataIos.modifierFunction;
650 }
651 if (pressed.intersection(kMacOsNumPadMap.values.toSet()).isNotEmpty) {
652 result |= RawKeyEventDataIos.modifierNumericPad;
653 }
654 if (pressed.contains(LogicalKeyboardKey.capsLock)) {
655 result |= RawKeyEventDataIos.modifierCapsLock;
656 }
657 return result;
658 }
659
660 static Future<bool> _simulateKeyEventByRawEvent(
661 ValueGetter<Map<String, dynamic>> buildKeyData,
662 ) async {
663 return TestAsyncUtils.guard<bool>(() async {
664 final Completer<bool> result = Completer<bool>();
665 await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
666 SystemChannels.keyEvent.name,
667 SystemChannels.keyEvent.codec.encodeMessage(buildKeyData()),
668 (ByteData? data) {
669 if (data == null) {
670 result.complete(false);
671 return;
672 }
673 final Map<String, Object?> decoded =
674 SystemChannels.keyEvent.codec.decodeMessage(data)! as Map<String, dynamic>;
675 result.complete(decoded['handled']! as bool);
676 },
677 );
678 return result.future;
679 });
680 }
681
682 static final Map<String, PhysicalKeyboardKey> _debugNameToPhysicalKey = (() {
683 final Map<String, PhysicalKeyboardKey> result = <String, PhysicalKeyboardKey>{};
684 for (final PhysicalKeyboardKey key in PhysicalKeyboardKey.knownPhysicalKeys) {
685 final String? debugName = key.debugName;
686 if (debugName != null) {
687 result[debugName] = key;
688 }
689 }
690 return result;
691 })();
692 static PhysicalKeyboardKey _findPhysicalKey(LogicalKeyboardKey key) {
693 final PhysicalKeyboardKey? result = _debugNameToPhysicalKey[key.debugName];
694 assert(result != null, 'Physical key for $key not found in known physical keys');
695 return result!;
696 }
697
698 static const KeyDataTransitMode _defaultTransitMode = KeyDataTransitMode.keyDataThenRawKeyData;
699
700 // The simulation transit mode for [simulateKeyDownEvent], [simulateKeyUpEvent],
701 // and [simulateKeyRepeatEvent].
702 //
703 // Simulation transit mode is the mode that simulated key events are constructed
704 // and delivered. For detailed introduction, see [KeyDataTransitMode] and
705 // its values.
706 //
707 // The `_transitMode` defaults to [KeyDataTransitMode.keyDataThenRawKeyData], and can
708 // be overridden with [debugKeyEventSimulatorTransitModeOverride]. In widget tests, it
709 // is often set with [KeySimulationModeVariant].
710 static KeyDataTransitMode get _transitMode {
711 KeyDataTransitMode? result;
712 assert(() {
713 result = debugKeyEventSimulatorTransitModeOverride;
714 return true;
715 }());
716 return result ?? _defaultTransitMode;
717 }
718
719 /// Returns the transit mode that simulated key events are constructed
720 /// and delivered. For detailed introduction, see [KeyDataTransitMode]
721 /// and its values.
722 @visibleForTesting
723 static KeyDataTransitMode get transitMode => _transitMode;
724
725 static String get _defaultPlatform => kIsWeb ? 'web' : defaultTargetPlatform.name.toLowerCase();
726
727 /// Simulates sending a hardware key down event.
728 ///
729 /// This only simulates key presses coming from a physical keyboard, not from a
730 /// soft keyboard.
731 ///
732 /// Specify `platform` as one of the platforms allowed in
733 /// [Platform.operatingSystem] to make the event appear to be from that type of
734 /// system. Defaults to "web" on web, and the operating system name based on
735 /// [defaultTargetPlatform] everywhere else.
736 ///
737 /// Keys that are down when the test completes are cleared after each test.
738 ///
739 /// Returns true if the key event was handled by the framework.
740 ///
741 /// See also:
742 ///
743 /// * [simulateKeyUpEvent] to simulate the corresponding key up event.
744 static Future<bool> simulateKeyDownEvent(
745 LogicalKeyboardKey key, {
746 String? platform,
747 PhysicalKeyboardKey? physicalKey,
748 String? character,
749 }) async {
750 Future<bool> simulateByRawEvent() {
751 return _simulateKeyEventByRawEvent(() {
752 platform ??= _defaultPlatform;
753 return getKeyData(key, platform: platform!, physicalKey: physicalKey, character: character);
754 });
755 }
756
757 switch (_transitMode) {
758 case KeyDataTransitMode.rawKeyData:
759 return simulateByRawEvent();
760 case KeyDataTransitMode.keyDataThenRawKeyData:
761 final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
762 final bool resultByKeyEvent = ServicesBinding.instance.keyEventManager.handleKeyData(
763 ui.KeyData(
764 type: ui.KeyEventType.down,
765 physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
766 logical: logicalKey.keyId,
767 timeStamp: Duration.zero,
768 character: character ?? _keyLabel(key),
769 synthesized: false,
770 ),
771 );
772 return (await simulateByRawEvent()) || resultByKeyEvent;
773 }
774 }
775
776 /// Simulates sending a hardware key up event through the system channel.
777 ///
778 /// This only simulates key presses coming from a physical keyboard, not from a
779 /// soft keyboard.
780 ///
781 /// Specify `platform` as one of the platforms allowed in
782 /// [Platform.operatingSystem] to make the event appear to be from that type of
783 /// system. Defaults to "web" on web, and the operating system name based on
784 /// [defaultTargetPlatform] everywhere else.
785 ///
786 /// Returns true if the key event was handled by the framework.
787 ///
788 /// See also:
789 ///
790 /// * [simulateKeyDownEvent] to simulate the corresponding key down event.
791 static Future<bool> simulateKeyUpEvent(
792 LogicalKeyboardKey key, {
793 String? platform,
794 PhysicalKeyboardKey? physicalKey,
795 }) async {
796 Future<bool> simulateByRawEvent() {
797 return _simulateKeyEventByRawEvent(() {
798 platform ??= _defaultPlatform;
799 return getKeyData(key, platform: platform!, isDown: false, physicalKey: physicalKey);
800 });
801 }
802
803 switch (_transitMode) {
804 case KeyDataTransitMode.rawKeyData:
805 return simulateByRawEvent();
806 case KeyDataTransitMode.keyDataThenRawKeyData:
807 final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
808 final bool resultByKeyEvent = ServicesBinding.instance.keyEventManager.handleKeyData(
809 ui.KeyData(
810 type: ui.KeyEventType.up,
811 physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
812 logical: logicalKey.keyId,
813 timeStamp: Duration.zero,
814 character: null,
815 synthesized: false,
816 ),
817 );
818 return (await simulateByRawEvent()) || resultByKeyEvent;
819 }
820 }
821
822 /// Simulates sending a hardware key repeat event through the system channel.
823 ///
824 /// This only simulates key presses coming from a physical keyboard, not from a
825 /// soft keyboard.
826 ///
827 /// Specify `platform` as one of the platforms allowed in
828 /// [Platform.operatingSystem] to make the event appear to be from that type of
829 /// system. Defaults to "web" on web, and the operating system name based on
830 /// [defaultTargetPlatform] everywhere else.
831 ///
832 /// Returns true if the key event was handled by the framework.
833 ///
834 /// See also:
835 ///
836 /// * [simulateKeyDownEvent] to simulate the corresponding key down event.
837 static Future<bool> simulateKeyRepeatEvent(
838 LogicalKeyboardKey key, {
839 String? platform,
840 PhysicalKeyboardKey? physicalKey,
841 String? character,
842 }) async {
843 Future<bool> simulateByRawEvent() {
844 return _simulateKeyEventByRawEvent(() {
845 platform ??= _defaultPlatform;
846 return getKeyData(key, platform: platform!, physicalKey: physicalKey, character: character);
847 });
848 }
849
850 switch (_transitMode) {
851 case KeyDataTransitMode.rawKeyData:
852 return simulateByRawEvent();
853 case KeyDataTransitMode.keyDataThenRawKeyData:
854 final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
855 final bool resultByKeyEvent = ServicesBinding.instance.keyEventManager.handleKeyData(
856 ui.KeyData(
857 type: ui.KeyEventType.repeat,
858 physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
859 logical: logicalKey.keyId,
860 timeStamp: Duration.zero,
861 character: character ?? _keyLabel(key),
862 synthesized: false,
863 ),
864 );
865 return (await simulateByRawEvent()) || resultByKeyEvent;
866 }
867 }
868}
869
870/// Simulates sending a hardware key down event through the system channel.
871///
872/// It is intended for use in writing tests.
873///
874/// This only simulates key presses coming from a physical keyboard, not from a
875/// soft keyboard, and it can only simulate keys that appear in the key maps
876/// such as [kAndroidToLogicalKey], [kMacOsToPhysicalKey], etc.
877///
878/// Specify `platform` as one of the platforms allowed in
879/// [Platform.operatingSystem] to make the event appear to be from that type of
880/// system. Defaults to "web" on web, and the operating system name based on
881/// [defaultTargetPlatform] everywhere else.
882///
883/// Keys that are down when the test completes are cleared after each test.
884///
885/// Returns true if the key event was handled by the framework.
886///
887/// See also:
888///
889/// * [simulateKeyUpEvent] and [simulateKeyRepeatEvent] to simulate the
890/// corresponding key up and repeat event.
891Future<bool> simulateKeyDownEvent(
892 LogicalKeyboardKey key, {
893 String? platform,
894 PhysicalKeyboardKey? physicalKey,
895 String? character,
896}) async {
897 final bool handled = await KeyEventSimulator.simulateKeyDownEvent(
898 key,
899 platform: platform,
900 physicalKey: physicalKey,
901 character: character,
902 );
903 final ServicesBinding binding = ServicesBinding.instance;
904 if (!handled && binding is TestWidgetsFlutterBinding) {
905 await binding.testTextInput.handleKeyDownEvent(key);
906 }
907 return handled;
908}
909
910/// Simulates sending a hardware key up event through the system channel.
911///
912/// It is intended for use in writing tests.
913///
914/// This only simulates key presses coming from a physical keyboard, not from a
915/// soft keyboard, and it can only simulate keys that appear in the key maps
916/// such as [kAndroidToLogicalKey], [kMacOsToPhysicalKey], etc.
917///
918/// Specify `platform` as one of the platforms allowed in
919/// [Platform.operatingSystem] to make the event appear to be from that type of
920/// system. Defaults to "web" on web, and the operating system name based on
921/// [defaultTargetPlatform] everywhere else.
922///
923/// Returns true if the key event was handled by the framework.
924///
925/// See also:
926///
927/// * [simulateKeyDownEvent] and [simulateKeyRepeatEvent] to simulate the
928/// corresponding key down and repeat event.
929Future<bool> simulateKeyUpEvent(
930 LogicalKeyboardKey key, {
931 String? platform,
932 PhysicalKeyboardKey? physicalKey,
933}) async {
934 final bool handled = await KeyEventSimulator.simulateKeyUpEvent(
935 key,
936 platform: platform,
937 physicalKey: physicalKey,
938 );
939 final ServicesBinding binding = ServicesBinding.instance;
940 if (!handled && binding is TestWidgetsFlutterBinding) {
941 await binding.testTextInput.handleKeyUpEvent(key);
942 }
943 return handled;
944}
945
946/// Simulates sending a hardware key repeat event through the system channel.
947///
948/// This only simulates key presses coming from a physical keyboard, not from a
949/// soft keyboard.
950///
951/// Specify `platform` as one of the platforms allowed in
952/// [Platform.operatingSystem] to make the event appear to be from that type of
953/// system. Defaults to "web" on web, and the operating system name based on
954/// [defaultTargetPlatform] everywhere else.
955///
956/// Returns true if the key event was handled by the framework.
957///
958/// See also:
959///
960/// - [simulateKeyDownEvent] and [simulateKeyUpEvent] to simulate the
961/// corresponding key down and up event.
962Future<bool> simulateKeyRepeatEvent(
963 LogicalKeyboardKey key, {
964 String? platform,
965 PhysicalKeyboardKey? physicalKey,
966 String? character,
967}) {
968 return KeyEventSimulator.simulateKeyRepeatEvent(
969 key,
970 platform: platform,
971 physicalKey: physicalKey,
972 character: character,
973 );
974}
975
976/// A [TestVariant] that runs tests with transit modes set to different values
977/// of [KeyDataTransitMode].
978@Deprecated(
979 'No longer supported. Transit mode is always key data only. '
980 'This feature was deprecated after v3.18.0-2.0.pre.',
981)
982class KeySimulatorTransitModeVariant extends TestVariant<KeyDataTransitMode> {
983 /// Creates a [KeySimulatorTransitModeVariant] that tests the given [values].
984 @Deprecated(
985 'No longer supported. Transit mode is always key data only. '
986 'This feature was deprecated after v3.18.0-2.0.pre.',
987 )
988 const KeySimulatorTransitModeVariant(this.values);
989
990 /// Creates a [KeySimulatorTransitModeVariant] for each value option of
991 /// [KeyDataTransitMode].
992 @Deprecated(
993 'No longer supported. Transit mode is always key data only. '
994 'This feature was deprecated after v3.18.0-2.0.pre.',
995 )
996 KeySimulatorTransitModeVariant.all() : this(KeyDataTransitMode.values.toSet());
997
998 /// Creates a [KeySimulatorTransitModeVariant] that only contains
999 /// [KeyDataTransitMode.keyDataThenRawKeyData].
1000 @Deprecated(
1001 'No longer supported. Transit mode is always key data only. '
1002 'This feature was deprecated after v3.18.0-2.0.pre.',
1003 )
1004 KeySimulatorTransitModeVariant.keyDataThenRawKeyData()
1005 : this(<KeyDataTransitMode>{KeyDataTransitMode.keyDataThenRawKeyData});
1006
1007 /// Creates a [KeySimulatorTransitModeVariant] that only contains
1008 /// [KeyDataTransitMode.rawKeyData].
1009 @Deprecated(
1010 'No longer supported. Transit mode is always key data only. '
1011 'This feature was deprecated after v3.18.0-2.0.pre.',
1012 )
1013 KeySimulatorTransitModeVariant.rawKeyData()
1014 : this(<KeyDataTransitMode>{KeyDataTransitMode.rawKeyData});
1015
1016 @override
1017 final Set<KeyDataTransitMode> values;
1018
1019 @override
1020 String describeValue(KeyDataTransitMode value) {
1021 switch (value) {
1022 case KeyDataTransitMode.rawKeyData:
1023 return 'RawKeyEvent';
1024 case KeyDataTransitMode.keyDataThenRawKeyData:
1025 return 'ui.KeyData then RawKeyEvent';
1026 }
1027 }
1028
1029 @override
1030 Future<KeyDataTransitMode?> setUp(KeyDataTransitMode value) async {
1031 final KeyDataTransitMode? previousSetting = debugKeyEventSimulatorTransitModeOverride;
1032 debugKeyEventSimulatorTransitModeOverride = value;
1033 return previousSetting;
1034 }
1035
1036 @override
1037 Future<void> tearDown(KeyDataTransitMode value, KeyDataTransitMode? memento) async {
1038 // ignore: invalid_use_of_visible_for_testing_member
1039 RawKeyboard.instance.clearKeysPressed();
1040 // ignore: invalid_use_of_visible_for_testing_member
1041 HardwareKeyboard.instance.clearState();
1042 // ignore: invalid_use_of_visible_for_testing_member
1043 ServicesBinding.instance.keyEventManager.clearState();
1044 debugKeyEventSimulatorTransitModeOverride = memento;
1045 }
1046}
1047