mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
1006 lines
36 KiB
Dart
1006 lines
36 KiB
Dart
// 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.indexOf(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;
|
|
}
|
|
}
|