| // Copyright 2014 The Flutter 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:async'; |
| import 'dart:io'; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/services.dart'; |
| |
| import 'binding.dart'; |
| import 'test_async_utils.dart'; |
| import 'widget_tester.dart'; |
| |
| // A tuple of `key` and `location` from Web's `KeyboardEvent` class. |
| // |
| // See [RawKeyEventDataWeb]'s `key` and `location` fields for details. |
| @immutable |
| class _WebKeyLocationPair { |
| const _WebKeyLocationPair(this.key, this.location); |
| final String key; |
| final int location; |
| } |
| |
| // TODO(gspencergoog): Replace this with more robust key simulation code once |
| // the new key event code is in. |
| // https://github.com/flutter/flutter/issues/33521 |
| // This code can only simulate keys which appear in the key maps. |
| |
| String? _keyLabel(LogicalKeyboardKey key) { |
| final String keyLabel = key.keyLabel; |
| if (keyLabel.length == 1) { |
| return keyLabel.toLowerCase(); |
| } |
| return null; |
| } |
| |
| // ignore: avoid_classes_with_only_static_members |
| /// A class that serves as a namespace for a bunch of keyboard-key generation |
| /// utilities. |
| class KeyEventSimulator { |
| // This class is not meant to be instantiated or extended; this constructor |
| // prevents instantiation and extension. |
| KeyEventSimulator._(); |
| |
| // Look up a synonym key, and just return the left version of it. |
| static LogicalKeyboardKey _getKeySynonym(LogicalKeyboardKey origKey) { |
| if (origKey == LogicalKeyboardKey.shift) { |
| return LogicalKeyboardKey.shiftLeft; |
| } |
| if (origKey == LogicalKeyboardKey.alt) { |
| return LogicalKeyboardKey.altLeft; |
| } |
| if (origKey == LogicalKeyboardKey.meta) { |
| return LogicalKeyboardKey.metaLeft; |
| } |
| if (origKey == LogicalKeyboardKey.control) { |
| return LogicalKeyboardKey.controlLeft; |
| } |
| return origKey; |
| } |
| |
| static bool _osIsSupported(String platform) { |
| switch (platform) { |
| case 'android': |
| case 'fuchsia': |
| case 'macos': |
| case 'linux': |
| case 'web': |
| case 'ios': |
| case 'windows': |
| return true; |
| } |
| return false; |
| } |
| |
| static int _getScanCode(PhysicalKeyboardKey key, String platform) { |
| assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation'); |
| late Map<int, PhysicalKeyboardKey> map; |
| switch (platform) { |
| case 'android': |
| map = kAndroidToPhysicalKey; |
| break; |
| case 'fuchsia': |
| map = kFuchsiaToPhysicalKey; |
| break; |
| case 'macos': |
| map = kMacOsToPhysicalKey; |
| break; |
| case 'ios': |
| map = kIosToPhysicalKey; |
| break; |
| case 'linux': |
| map = kLinuxToPhysicalKey; |
| break; |
| case 'windows': |
| map = kWindowsToPhysicalKey; |
| break; |
| case 'web': |
| // web doesn't have int type code |
| return -1; |
| } |
| int? scanCode; |
| for (final int code in map.keys) { |
| if (key.usbHidUsage == map[code]!.usbHidUsage) { |
| scanCode = code; |
| break; |
| } |
| } |
| assert(scanCode != null, 'Physical key for $key not found in $platform scanCode map'); |
| return scanCode!; |
| } |
| |
| static int _getKeyCode(LogicalKeyboardKey key, String platform) { |
| assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation'); |
| if (kIsWeb) { |
| // web doesn't have int type code. This check is used to treeshake |
| // keyboard map code. |
| return -1; |
| } else { |
| late Map<int, LogicalKeyboardKey> map; |
| switch (platform) { |
| case 'android': |
| map = kAndroidToLogicalKey; |
| break; |
| case 'fuchsia': |
| map = kFuchsiaToLogicalKey; |
| break; |
| case 'macos': |
| // macOS doesn't do key codes, just scan codes. |
| return -1; |
| case 'ios': |
| // iOS doesn't do key codes, just scan codes. |
| return -1; |
| case 'web': |
| // web doesn't have int type code. |
| return -1; |
| case 'linux': |
| map = kGlfwToLogicalKey; |
| break; |
| case 'windows': |
| map = kWindowsToLogicalKey; |
| break; |
| } |
| int? keyCode; |
| for (final int code in map.keys) { |
| if (key.keyId == map[code]!.keyId) { |
| keyCode = code; |
| break; |
| } |
| } |
| assert(keyCode != null, 'Key $key not found in $platform keyCode map'); |
| return keyCode!; |
| } |
| } |
| |
| static PhysicalKeyboardKey _inferPhysicalKey(LogicalKeyboardKey key) { |
| PhysicalKeyboardKey? result; |
| for (final PhysicalKeyboardKey physicalKey in PhysicalKeyboardKey.knownPhysicalKeys) { |
| if (physicalKey.debugName == key.debugName) { |
| result = physicalKey; |
| break; |
| } |
| } |
| assert(result != null, 'Unable to infer physical key for $key'); |
| return result!; |
| } |
| |
| static _WebKeyLocationPair _getWebKeyLocation(LogicalKeyboardKey key, String keyLabel) { |
| String? result; |
| for (final MapEntry<String, List<LogicalKeyboardKey?>> entry in kWebLocationMap.entries) { |
| final int foundIndex = entry.value.lastIndexOf(key); |
| // If foundIndex is -1, then the key is not defined in kWebLocationMap. |
| // If foundIndex is 0, then the key is in the standard part of the keyboard, |
| // but we have to check `keyLabel` to see if it's remapped or modified. |
| if (foundIndex != -1 && foundIndex != 0) { |
| return _WebKeyLocationPair(entry.key, foundIndex); |
| } |
| } |
| if (keyLabel.isNotEmpty) { |
| return _WebKeyLocationPair(keyLabel, 0); |
| } |
| for (final String code in kWebToLogicalKey.keys) { |
| if (key.keyId == kWebToLogicalKey[code]!.keyId) { |
| result = code; |
| break; |
| } |
| } |
| assert(result != null, 'Key $key not found in web keyCode map'); |
| return _WebKeyLocationPair(result!, 0); |
| } |
| |
| static String _getWebCode(PhysicalKeyboardKey key) { |
| String? result; |
| for (final MapEntry<String, PhysicalKeyboardKey> entry in kWebToPhysicalKey.entries) { |
| if (entry.value.usbHidUsage == key.usbHidUsage) { |
| result = entry.key; |
| break; |
| } |
| } |
| assert(result != null, 'Key $key not found in web code map'); |
| return result!; |
| } |
| |
| static PhysicalKeyboardKey _findPhysicalKeyByPlatform(LogicalKeyboardKey key, String platform) { |
| assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation'); |
| late Map<dynamic, PhysicalKeyboardKey> map; |
| if (kIsWeb) { |
| // This check is used to treeshake keymap code. |
| map = kWebToPhysicalKey; |
| } else { |
| switch (platform) { |
| case 'android': |
| map = kAndroidToPhysicalKey; |
| break; |
| case 'fuchsia': |
| map = kFuchsiaToPhysicalKey; |
| break; |
| case 'macos': |
| map = kMacOsToPhysicalKey; |
| break; |
| case 'ios': |
| map = kIosToPhysicalKey; |
| break; |
| case 'linux': |
| map = kLinuxToPhysicalKey; |
| break; |
| case 'web': |
| map = kWebToPhysicalKey; |
| break; |
| case 'windows': |
| map = kWindowsToPhysicalKey; |
| break; |
| } |
| } |
| PhysicalKeyboardKey? result; |
| for (final PhysicalKeyboardKey physicalKey in map.values) { |
| if (key.debugName == physicalKey.debugName) { |
| result = physicalKey; |
| break; |
| } |
| } |
| assert(result != null, 'Physical key for $key not found in $platform physical key map'); |
| return result!; |
| } |
| |
| /// Get a raw key data map given a [LogicalKeyboardKey] and a platform. |
| static Map<String, dynamic> getKeyData( |
| LogicalKeyboardKey key, { |
| required String platform, |
| bool isDown = true, |
| PhysicalKeyboardKey? physicalKey, |
| String? character, |
| }) { |
| assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation'); |
| |
| key = _getKeySynonym(key); |
| |
| // Find a suitable physical key if none was supplied. |
| physicalKey ??= _findPhysicalKeyByPlatform(key, platform); |
| |
| assert(key.debugName != null); |
| |
| final Map<String, dynamic> result = <String, dynamic>{ |
| 'type': isDown ? 'keydown' : 'keyup', |
| 'keymap': platform, |
| }; |
| |
| final String resultCharacter = character ?? _keyLabel(key) ?? ''; |
| void assignWeb() { |
| final _WebKeyLocationPair keyLocation = _getWebKeyLocation(key, resultCharacter); |
| final PhysicalKeyboardKey actualPhysicalKey = physicalKey ?? _inferPhysicalKey(key); |
| result['code'] = _getWebCode(actualPhysicalKey); |
| result['key'] = keyLocation.key; |
| result['location'] = keyLocation.location; |
| result['metaState'] = _getWebModifierFlags(key, isDown); |
| } |
| if (kIsWeb) { |
| assignWeb(); |
| return result; |
| } |
| final int keyCode = _getKeyCode(key, platform); |
| final int scanCode = _getScanCode(physicalKey, platform); |
| |
| switch (platform) { |
| case 'android': |
| result['keyCode'] = keyCode; |
| if (resultCharacter.isNotEmpty) { |
| result['codePoint'] = resultCharacter.codeUnitAt(0); |
| result['character'] = resultCharacter; |
| } |
| result['scanCode'] = scanCode; |
| result['metaState'] = _getAndroidModifierFlags(key, isDown); |
| break; |
| case 'fuchsia': |
| result['hidUsage'] = physicalKey.usbHidUsage; |
| if (resultCharacter.isNotEmpty) { |
| result['codePoint'] = resultCharacter.codeUnitAt(0); |
| } |
| result['modifiers'] = _getFuchsiaModifierFlags(key, isDown); |
| break; |
| case 'linux': |
| result['toolkit'] = 'glfw'; |
| result['keyCode'] = keyCode; |
| result['scanCode'] = scanCode; |
| result['modifiers'] = _getGlfwModifierFlags(key, isDown); |
| result['unicodeScalarValues'] = resultCharacter.isNotEmpty ? resultCharacter.codeUnitAt(0) : 0; |
| break; |
| case 'macos': |
| result['keyCode'] = scanCode; |
| if (resultCharacter.isNotEmpty) { |
| result['characters'] = resultCharacter; |
| result['charactersIgnoringModifiers'] = resultCharacter; |
| } |
| result['modifiers'] = _getMacOsModifierFlags(key, isDown); |
| break; |
| case 'ios': |
| result['keyCode'] = scanCode; |
| result['characters'] = resultCharacter; |
| result['charactersIgnoringModifiers'] = resultCharacter; |
| result['modifiers'] = _getIOSModifierFlags(key, isDown); |
| break; |
| case 'windows': |
| result['keyCode'] = keyCode; |
| result['scanCode'] = scanCode; |
| if (resultCharacter.isNotEmpty) { |
| result['characterCodePoint'] = resultCharacter.codeUnitAt(0); |
| } |
| result['modifiers'] = _getWindowsModifierFlags(key, isDown); |
| break; |
| case 'web': |
| assignWeb(); |
| break; |
| } |
| return result; |
| } |
| |
| static int _getAndroidModifierFlags(LogicalKeyboardKey newKey, bool isDown) { |
| int result = 0; |
| final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed; |
| if (isDown) { |
| pressed.add(newKey); |
| } else { |
| pressed.remove(newKey); |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftLeft)) { |
| result |= RawKeyEventDataAndroid.modifierLeftShift | RawKeyEventDataAndroid.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftRight)) { |
| result |= RawKeyEventDataAndroid.modifierRightShift | RawKeyEventDataAndroid.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaLeft)) { |
| result |= RawKeyEventDataAndroid.modifierLeftMeta | RawKeyEventDataAndroid.modifierMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaRight)) { |
| result |= RawKeyEventDataAndroid.modifierRightMeta | RawKeyEventDataAndroid.modifierMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlLeft)) { |
| result |= RawKeyEventDataAndroid.modifierLeftControl | RawKeyEventDataAndroid.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlRight)) { |
| result |= RawKeyEventDataAndroid.modifierRightControl | RawKeyEventDataAndroid.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altLeft)) { |
| result |= RawKeyEventDataAndroid.modifierLeftAlt | RawKeyEventDataAndroid.modifierAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altRight)) { |
| result |= RawKeyEventDataAndroid.modifierRightAlt | RawKeyEventDataAndroid.modifierAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.fn)) { |
| result |= RawKeyEventDataAndroid.modifierFunction; |
| } |
| if (pressed.contains(LogicalKeyboardKey.scrollLock)) { |
| result |= RawKeyEventDataAndroid.modifierScrollLock; |
| } |
| if (pressed.contains(LogicalKeyboardKey.numLock)) { |
| result |= RawKeyEventDataAndroid.modifierNumLock; |
| } |
| if (pressed.contains(LogicalKeyboardKey.capsLock)) { |
| result |= RawKeyEventDataAndroid.modifierCapsLock; |
| } |
| return result; |
| } |
| |
| static int _getGlfwModifierFlags(LogicalKeyboardKey newKey, bool isDown) { |
| int result = 0; |
| final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed; |
| if (isDown) { |
| pressed.add(newKey); |
| } else { |
| pressed.remove(newKey); |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftLeft) || pressed.contains(LogicalKeyboardKey.shiftRight)) { |
| result |= GLFWKeyHelper.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaLeft) || pressed.contains(LogicalKeyboardKey.metaRight)) { |
| result |= GLFWKeyHelper.modifierMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlLeft) || pressed.contains(LogicalKeyboardKey.controlRight)) { |
| result |= GLFWKeyHelper.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altLeft) || pressed.contains(LogicalKeyboardKey.altRight)) { |
| result |= GLFWKeyHelper.modifierAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.capsLock)) { |
| result |= GLFWKeyHelper.modifierCapsLock; |
| } |
| return result; |
| } |
| |
| static int _getWindowsModifierFlags(LogicalKeyboardKey newKey, bool isDown) { |
| int result = 0; |
| final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed; |
| if (isDown) { |
| pressed.add(newKey); |
| } else { |
| pressed.remove(newKey); |
| } |
| if (pressed.contains(LogicalKeyboardKey.shift)) { |
| result |= RawKeyEventDataWindows.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftLeft)) { |
| result |= RawKeyEventDataWindows.modifierLeftShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftRight)) { |
| result |= RawKeyEventDataWindows.modifierRightShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaLeft)) { |
| result |= RawKeyEventDataWindows.modifierLeftMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaRight)) { |
| result |= RawKeyEventDataWindows.modifierRightMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.control)) { |
| result |= RawKeyEventDataWindows.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlLeft)) { |
| result |= RawKeyEventDataWindows.modifierLeftControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlRight)) { |
| result |= RawKeyEventDataWindows.modifierRightControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.alt)) { |
| result |= RawKeyEventDataWindows.modifierAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altLeft)) { |
| result |= RawKeyEventDataWindows.modifierLeftAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altRight)) { |
| result |= RawKeyEventDataWindows.modifierRightAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.capsLock)) { |
| result |= RawKeyEventDataWindows.modifierCaps; |
| } |
| if (pressed.contains(LogicalKeyboardKey.numLock)) { |
| result |= RawKeyEventDataWindows.modifierNumLock; |
| } |
| if (pressed.contains(LogicalKeyboardKey.scrollLock)) { |
| result |= RawKeyEventDataWindows.modifierScrollLock; |
| } |
| return result; |
| } |
| |
| static int _getFuchsiaModifierFlags(LogicalKeyboardKey newKey, bool isDown) { |
| int result = 0; |
| final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed; |
| if (isDown) { |
| pressed.add(newKey); |
| } else { |
| pressed.remove(newKey); |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftLeft)) { |
| result |= RawKeyEventDataFuchsia.modifierLeftShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftRight)) { |
| result |= RawKeyEventDataFuchsia.modifierRightShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaLeft)) { |
| result |= RawKeyEventDataFuchsia.modifierLeftMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaRight)) { |
| result |= RawKeyEventDataFuchsia.modifierRightMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlLeft)) { |
| result |= RawKeyEventDataFuchsia.modifierLeftControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlRight)) { |
| result |= RawKeyEventDataFuchsia.modifierRightControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altLeft)) { |
| result |= RawKeyEventDataFuchsia.modifierLeftAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altRight)) { |
| result |= RawKeyEventDataFuchsia.modifierRightAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.capsLock)) { |
| result |= RawKeyEventDataFuchsia.modifierCapsLock; |
| } |
| return result; |
| } |
| |
| static int _getWebModifierFlags(LogicalKeyboardKey newKey, bool isDown) { |
| int result = 0; |
| final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed; |
| if (isDown) { |
| pressed.add(newKey); |
| } else { |
| pressed.remove(newKey); |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftLeft)) { |
| result |= RawKeyEventDataWeb.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftRight)) { |
| result |= RawKeyEventDataWeb.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaLeft)) { |
| result |= RawKeyEventDataWeb.modifierMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaRight)) { |
| result |= RawKeyEventDataWeb.modifierMeta; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlLeft)) { |
| result |= RawKeyEventDataWeb.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlRight)) { |
| result |= RawKeyEventDataWeb.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altLeft)) { |
| result |= RawKeyEventDataWeb.modifierAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altRight)) { |
| result |= RawKeyEventDataWeb.modifierAlt; |
| } |
| if (pressed.contains(LogicalKeyboardKey.capsLock)) { |
| result |= RawKeyEventDataWeb.modifierCapsLock; |
| } |
| if (pressed.contains(LogicalKeyboardKey.numLock)) { |
| result |= RawKeyEventDataWeb.modifierNumLock; |
| } |
| if (pressed.contains(LogicalKeyboardKey.scrollLock)) { |
| result |= RawKeyEventDataWeb.modifierScrollLock; |
| } |
| return result; |
| } |
| |
| static int _getMacOsModifierFlags(LogicalKeyboardKey newKey, bool isDown) { |
| int result = 0; |
| final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed; |
| if (isDown) { |
| pressed.add(newKey); |
| } else { |
| pressed.remove(newKey); |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftLeft)) { |
| result |= RawKeyEventDataMacOs.modifierLeftShift | RawKeyEventDataMacOs.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftRight)) { |
| result |= RawKeyEventDataMacOs.modifierRightShift | RawKeyEventDataMacOs.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaLeft)) { |
| result |= RawKeyEventDataMacOs.modifierLeftCommand | RawKeyEventDataMacOs.modifierCommand; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaRight)) { |
| result |= RawKeyEventDataMacOs.modifierRightCommand | RawKeyEventDataMacOs.modifierCommand; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlLeft)) { |
| result |= RawKeyEventDataMacOs.modifierLeftControl | RawKeyEventDataMacOs.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlRight)) { |
| result |= RawKeyEventDataMacOs.modifierRightControl | RawKeyEventDataMacOs.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altLeft)) { |
| result |= RawKeyEventDataMacOs.modifierLeftOption | RawKeyEventDataMacOs.modifierOption; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altRight)) { |
| result |= RawKeyEventDataMacOs.modifierRightOption | RawKeyEventDataMacOs.modifierOption; |
| } |
| final Set<LogicalKeyboardKey> functionKeys = <LogicalKeyboardKey>{ |
| LogicalKeyboardKey.f1, |
| LogicalKeyboardKey.f2, |
| LogicalKeyboardKey.f3, |
| LogicalKeyboardKey.f4, |
| LogicalKeyboardKey.f5, |
| LogicalKeyboardKey.f6, |
| LogicalKeyboardKey.f7, |
| LogicalKeyboardKey.f8, |
| LogicalKeyboardKey.f9, |
| LogicalKeyboardKey.f10, |
| LogicalKeyboardKey.f11, |
| LogicalKeyboardKey.f12, |
| LogicalKeyboardKey.f13, |
| LogicalKeyboardKey.f14, |
| LogicalKeyboardKey.f15, |
| LogicalKeyboardKey.f16, |
| LogicalKeyboardKey.f17, |
| LogicalKeyboardKey.f18, |
| LogicalKeyboardKey.f19, |
| LogicalKeyboardKey.f20, |
| LogicalKeyboardKey.f21, |
| }; |
| if (pressed.intersection(functionKeys).isNotEmpty) { |
| result |= RawKeyEventDataMacOs.modifierFunction; |
| } |
| if (pressed.intersection(kMacOsNumPadMap.values.toSet()).isNotEmpty) { |
| result |= RawKeyEventDataMacOs.modifierNumericPad; |
| } |
| if (pressed.contains(LogicalKeyboardKey.capsLock)) { |
| result |= RawKeyEventDataMacOs.modifierCapsLock; |
| } |
| return result; |
| } |
| |
| static int _getIOSModifierFlags(LogicalKeyboardKey newKey, bool isDown) { |
| int result = 0; |
| final Set<LogicalKeyboardKey> pressed = RawKeyboard.instance.keysPressed; |
| if (isDown) { |
| pressed.add(newKey); |
| } else { |
| pressed.remove(newKey); |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftLeft)) { |
| result |= RawKeyEventDataIos.modifierLeftShift | RawKeyEventDataIos.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.shiftRight)) { |
| result |= RawKeyEventDataIos.modifierRightShift | RawKeyEventDataIos.modifierShift; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaLeft)) { |
| result |= RawKeyEventDataIos.modifierLeftCommand | RawKeyEventDataIos.modifierCommand; |
| } |
| if (pressed.contains(LogicalKeyboardKey.metaRight)) { |
| result |= RawKeyEventDataIos.modifierRightCommand | RawKeyEventDataIos.modifierCommand; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlLeft)) { |
| result |= RawKeyEventDataIos.modifierLeftControl | RawKeyEventDataIos.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.controlRight)) { |
| result |= RawKeyEventDataIos.modifierRightControl | RawKeyEventDataIos.modifierControl; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altLeft)) { |
| result |= RawKeyEventDataIos.modifierLeftOption | RawKeyEventDataIos.modifierOption; |
| } |
| if (pressed.contains(LogicalKeyboardKey.altRight)) { |
| result |= RawKeyEventDataIos.modifierRightOption | RawKeyEventDataIos.modifierOption; |
| } |
| final Set<LogicalKeyboardKey> functionKeys = <LogicalKeyboardKey>{ |
| LogicalKeyboardKey.f1, |
| LogicalKeyboardKey.f2, |
| LogicalKeyboardKey.f3, |
| LogicalKeyboardKey.f4, |
| LogicalKeyboardKey.f5, |
| LogicalKeyboardKey.f6, |
| LogicalKeyboardKey.f7, |
| LogicalKeyboardKey.f8, |
| LogicalKeyboardKey.f9, |
| LogicalKeyboardKey.f10, |
| LogicalKeyboardKey.f11, |
| LogicalKeyboardKey.f12, |
| LogicalKeyboardKey.f13, |
| LogicalKeyboardKey.f14, |
| LogicalKeyboardKey.f15, |
| LogicalKeyboardKey.f16, |
| LogicalKeyboardKey.f17, |
| LogicalKeyboardKey.f18, |
| LogicalKeyboardKey.f19, |
| LogicalKeyboardKey.f20, |
| LogicalKeyboardKey.f21, |
| }; |
| if (pressed.intersection(functionKeys).isNotEmpty) { |
| result |= RawKeyEventDataIos.modifierFunction; |
| } |
| if (pressed.intersection(kMacOsNumPadMap.values.toSet()).isNotEmpty) { |
| result |= RawKeyEventDataIos.modifierNumericPad; |
| } |
| if (pressed.contains(LogicalKeyboardKey.capsLock)) { |
| result |= RawKeyEventDataIos.modifierCapsLock; |
| } |
| return result; |
| } |
| |
| static Future<bool> _simulateKeyEventByRawEvent(ValueGetter<Map<String, dynamic>> buildKeyData) async { |
| return TestAsyncUtils.guard<bool>(() async { |
| final Completer<bool> result = Completer<bool>(); |
| await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( |
| SystemChannels.keyEvent.name, |
| SystemChannels.keyEvent.codec.encodeMessage(buildKeyData()), |
| (ByteData? data) { |
| if (data == null) { |
| result.complete(false); |
| return; |
| } |
| final Map<String, Object?> decoded = SystemChannels.keyEvent.codec.decodeMessage(data)! as Map<String, dynamic>; |
| result.complete(decoded['handled']! as bool); |
| } |
| ); |
| return result.future; |
| }); |
| } |
| |
| static final Map<String, PhysicalKeyboardKey> _debugNameToPhysicalKey = (() { |
| final Map<String, PhysicalKeyboardKey> result = <String, PhysicalKeyboardKey>{}; |
| for (final PhysicalKeyboardKey key in PhysicalKeyboardKey.knownPhysicalKeys) { |
| final String? debugName = key.debugName; |
| if (debugName != null) { |
| result[debugName] = key; |
| } |
| } |
| return result; |
| })(); |
| static PhysicalKeyboardKey _findPhysicalKey(LogicalKeyboardKey key) { |
| final PhysicalKeyboardKey? result = _debugNameToPhysicalKey[key.debugName]; |
| assert(result != null, 'Physical key for $key not found in known physical keys'); |
| return result!; |
| } |
| |
| static const KeyDataTransitMode _defaultTransitMode = KeyDataTransitMode.rawKeyData; |
| |
| // The simulation transit mode for [simulateKeyDownEvent], [simulateKeyUpEvent], |
| // and [simulateKeyRepeatEvent]. |
| // |
| // Simulation transit mode is the mode that simulated key events are constructed |
| // and delivered. For detailed introduction, see [KeyDataTransitMode] and |
| // its values. |
| // |
| // The `_transitMode` defaults to [KeyDataTransitMode.rawKeyEvent], and can be |
| // overridden with [debugKeyEventSimulatorTransitModeOverride]. In widget tests, it |
| // is often set with [KeySimulationModeVariant]. |
| static KeyDataTransitMode get _transitMode { |
| KeyDataTransitMode? result; |
| assert(() { |
| result = debugKeyEventSimulatorTransitModeOverride; |
| return true; |
| }()); |
| return result ?? _defaultTransitMode; |
| } |
| |
| static String get _defaultPlatform => kIsWeb ? 'web' : Platform.operatingSystem; |
| |
| /// Simulates sending a hardware key down event. |
| /// |
| /// This only simulates key presses coming from a physical keyboard, not from a |
| /// soft keyboard. |
| /// |
| /// Specify `platform` as one of the platforms allowed in |
| /// [Platform.operatingSystem] to make the event appear to be from that type of |
| /// system. Defaults to the operating system that the test is running on. Some |
| /// platforms (e.g. Windows, iOS) are not yet supported. |
| /// |
| /// Keys that are down when the test completes are cleared after each test. |
| /// |
| /// Returns true if the key event was handled by the framework. |
| /// |
| /// See also: |
| /// |
| /// * [simulateKeyUpEvent] to simulate the corresponding key up event. |
| static Future<bool> simulateKeyDownEvent( |
| LogicalKeyboardKey key, { |
| String? platform, |
| PhysicalKeyboardKey? physicalKey, |
| String? character, |
| }) async { |
| Future<bool> simulateByRawEvent() { |
| return _simulateKeyEventByRawEvent(() { |
| platform ??= _defaultPlatform; |
| return getKeyData(key, platform: platform!, physicalKey: physicalKey, character: character); |
| }); |
| } |
| switch (_transitMode) { |
| case KeyDataTransitMode.rawKeyData: |
| return simulateByRawEvent(); |
| case KeyDataTransitMode.keyDataThenRawKeyData: |
| final LogicalKeyboardKey logicalKey = _getKeySynonym(key); |
| final bool resultByKeyEvent = ServicesBinding.instance.keyEventManager.handleKeyData( |
| ui.KeyData( |
| type: ui.KeyEventType.down, |
| physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage, |
| logical: logicalKey.keyId, |
| timeStamp: Duration.zero, |
| character: character ?? _keyLabel(key), |
| synthesized: false, |
| ), |
| ); |
| return (await simulateByRawEvent()) || resultByKeyEvent; |
| } |
| } |
| |
| /// Simulates sending a hardware key up event through the system channel. |
| /// |
| /// This only simulates key presses coming from a physical keyboard, not from a |
| /// soft keyboard. |
| /// |
| /// Specify `platform` as one of the platforms allowed in |
| /// [Platform.operatingSystem] to make the event appear to be from that type of |
| /// system. Defaults to the operating system that the test is running on. Some |
| /// platforms (e.g. Windows, iOS) are not yet supported. |
| /// |
| /// Returns true if the key event was handled by the framework. |
| /// |
| /// See also: |
| /// |
| /// * [simulateKeyDownEvent] to simulate the corresponding key down event. |
| static Future<bool> simulateKeyUpEvent( |
| LogicalKeyboardKey key, { |
| String? platform, |
| PhysicalKeyboardKey? physicalKey, |
| }) async { |
| Future<bool> simulateByRawEvent() { |
| return _simulateKeyEventByRawEvent(() { |
| platform ??= _defaultPlatform; |
| return getKeyData(key, platform: platform!, isDown: false, physicalKey: physicalKey); |
| }); |
| } |
| switch (_transitMode) { |
| case KeyDataTransitMode.rawKeyData: |
| return simulateByRawEvent(); |
| case KeyDataTransitMode.keyDataThenRawKeyData: |
| final LogicalKeyboardKey logicalKey = _getKeySynonym(key); |
| final bool resultByKeyEvent = ServicesBinding.instance.keyEventManager.handleKeyData( |
| ui.KeyData( |
| type: ui.KeyEventType.up, |
| physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage, |
| logical: logicalKey.keyId, |
| timeStamp: Duration.zero, |
| character: null, |
| synthesized: false, |
| ), |
| ); |
| return (await simulateByRawEvent()) || resultByKeyEvent; |
| } |
| } |
| |
| /// Simulates sending a hardware key repeat event through the system channel. |
| /// |
| /// This only simulates key presses coming from a physical keyboard, not from a |
| /// soft keyboard. |
| /// |
| /// Specify `platform` as one of the platforms allowed in |
| /// [Platform.operatingSystem] to make the event appear to be from that type of |
| /// system. Defaults to the operating system that the test is running on. Some |
| /// platforms (e.g. Windows, iOS) are not yet supported. |
| /// |
| /// Returns true if the key event was handled by the framework. |
| /// |
| /// See also: |
| /// |
| /// * [simulateKeyDownEvent] to simulate the corresponding key down event. |
| static Future<bool> simulateKeyRepeatEvent( |
| LogicalKeyboardKey key, { |
| String? platform, |
| PhysicalKeyboardKey? physicalKey, |
| String? character, |
| }) async { |
| Future<bool> simulateByRawEvent() { |
| return _simulateKeyEventByRawEvent(() { |
| platform ??= _defaultPlatform; |
| return getKeyData(key, platform: platform!, physicalKey: physicalKey, character: character); |
| }); |
| } |
| switch (_transitMode) { |
| case KeyDataTransitMode.rawKeyData: |
| return simulateByRawEvent(); |
| case KeyDataTransitMode.keyDataThenRawKeyData: |
| final LogicalKeyboardKey logicalKey = _getKeySynonym(key); |
| final bool resultByKeyEvent = ServicesBinding.instance.keyEventManager.handleKeyData( |
| ui.KeyData( |
| type: ui.KeyEventType.repeat, |
| physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage, |
| logical: logicalKey.keyId, |
| timeStamp: Duration.zero, |
| character: character ?? _keyLabel(key), |
| synthesized: false, |
| ), |
| ); |
| return (await simulateByRawEvent()) || resultByKeyEvent; |
| } |
| } |
| } |
| |
| /// Simulates sending a hardware key down event through the system channel. |
| /// |
| /// It is intended for use in writing tests. |
| /// |
| /// This only simulates key presses coming from a physical keyboard, not from a |
| /// soft keyboard, and it can only simulate keys that appear in the key maps |
| /// such as [kAndroidToLogicalKey], [kMacOsToPhysicalKey], etc. |
| /// |
| /// Specify `platform` as one of the platforms allowed in |
| /// [Platform.operatingSystem] to make the event appear to be from that type of |
| /// system. Defaults to the operating system that the test is running on. Some |
| /// platforms (e.g. Windows, iOS) are not yet supported. |
| /// |
| /// Keys that are down when the test completes are cleared after each test. |
| /// |
| /// Returns true if the key event was handled by the framework. |
| /// |
| /// See also: |
| /// |
| /// * [simulateKeyUpEvent] and [simulateKeyRepeatEvent] to simulate the |
| /// corresponding key up and repeat event. |
| Future<bool> simulateKeyDownEvent( |
| LogicalKeyboardKey key, { |
| String? platform, |
| PhysicalKeyboardKey? physicalKey, |
| String? character, |
| }) { |
| return KeyEventSimulator.simulateKeyDownEvent(key, platform: platform, physicalKey: physicalKey, character: character); |
| } |
| |
| /// Simulates sending a hardware key up event through the system channel. |
| /// |
| /// It is intended for use in writing tests. |
| /// |
| /// This only simulates key presses coming from a physical keyboard, not from a |
| /// soft keyboard, and it can only simulate keys that appear in the key maps |
| /// such as [kAndroidToLogicalKey], [kMacOsToPhysicalKey], etc. |
| /// |
| /// Specify `platform` as one of the platforms allowed in |
| /// [Platform.operatingSystem] to make the event appear to be from that type of |
| /// system. Defaults to the operating system that the test is running on. Some |
| /// platforms (e.g. Windows, iOS) are not yet supported. |
| /// |
| /// Returns true if the key event was handled by the framework. |
| /// |
| /// See also: |
| /// |
| /// * [simulateKeyDownEvent] and [simulateKeyRepeatEvent] to simulate the |
| /// corresponding key down and repeat event. |
| Future<bool> simulateKeyUpEvent( |
| LogicalKeyboardKey key, { |
| String? platform, |
| PhysicalKeyboardKey? physicalKey, |
| }) { |
| return KeyEventSimulator.simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey); |
| } |
| |
| /// Simulates sending a hardware key repeat event through the system channel. |
| /// |
| /// This only simulates key presses coming from a physical keyboard, not from a |
| /// soft keyboard. |
| /// |
| /// Specify `platform` as one of the platforms allowed in |
| /// [Platform.operatingSystem] to make the event appear to be from that type of |
| /// system. Defaults to the operating system that the test is running on. Some |
| /// platforms (e.g. Windows, iOS) are not yet supported. |
| /// |
| /// Returns true if the key event was handled by the framework. |
| /// |
| /// See also: |
| /// |
| /// - [simulateKeyDownEvent] and [simulateKeyUpEvent] to simulate the |
| /// corresponding key down and up event. |
| Future<bool> simulateKeyRepeatEvent( |
| LogicalKeyboardKey key, { |
| String? platform, |
| PhysicalKeyboardKey? physicalKey, |
| String? character, |
| }) { |
| return KeyEventSimulator.simulateKeyRepeatEvent(key, platform: platform, physicalKey: physicalKey, character: character); |
| } |
| |
| /// A [TestVariant] that runs tests with transit modes set to different values |
| /// of [KeyDataTransitMode]. |
| class KeySimulatorTransitModeVariant extends TestVariant<KeyDataTransitMode> { |
| /// Creates a [KeySimulatorTransitModeVariant] that tests the given [values]. |
| const KeySimulatorTransitModeVariant(this.values); |
| |
| /// Creates a [KeySimulatorTransitModeVariant] for each value option of |
| /// [KeyDataTransitMode]. |
| KeySimulatorTransitModeVariant.all() |
| : this(KeyDataTransitMode.values.toSet()); |
| |
| /// Creates a [KeySimulatorTransitModeVariant] that only contains |
| /// [KeyDataTransitMode.keyDataThenRawKeyData]. |
| KeySimulatorTransitModeVariant.keyDataThenRawKeyData() |
| : this(<KeyDataTransitMode>{KeyDataTransitMode.keyDataThenRawKeyData}); |
| |
| @override |
| final Set<KeyDataTransitMode> values; |
| |
| @override |
| String describeValue(KeyDataTransitMode value) { |
| switch (value) { |
| case KeyDataTransitMode.rawKeyData: |
| return 'RawKeyEvent'; |
| case KeyDataTransitMode.keyDataThenRawKeyData: |
| return 'ui.KeyData then RawKeyEvent'; |
| } |
| } |
| |
| @override |
| Future<KeyDataTransitMode?> setUp(KeyDataTransitMode value) async { |
| final KeyDataTransitMode? previousSetting = debugKeyEventSimulatorTransitModeOverride; |
| debugKeyEventSimulatorTransitModeOverride = value; |
| return previousSetting; |
| } |
| |
| @override |
| Future<void> tearDown(KeyDataTransitMode value, KeyDataTransitMode? memento) async { |
| // ignore: invalid_use_of_visible_for_testing_member |
| RawKeyboard.instance.clearKeysPressed(); |
| // ignore: invalid_use_of_visible_for_testing_member |
| HardwareKeyboard.instance.clearState(); |
| // ignore: invalid_use_of_visible_for_testing_member |
| ServicesBinding.instance.keyEventManager.clearState(); |
| debugKeyEventSimulatorTransitModeOverride = memento; |
| } |
| } |