Unverified Commit a7899c19 authored by Tong Mu's avatar Tong Mu Committed by GitHub

[gen_keycodes] Remove nonexistent Web keys and improve their emulation (#87098)

parent c451b6b0
......@@ -11,11 +11,7 @@ import 'package:path/path.dart' as path;
import 'constants.dart';
import 'physical_key_data.dart';
bool _isControlCharacter(String label) {
if (label.length != 1) {
return false;
}
final int codeUnit = label.codeUnitAt(0);
bool _isControlCharacter(int codeUnit) {
return (codeUnit <= 0x1f && codeUnit >= 0x00) || (codeUnit >= 0x7f && codeUnit <= 0x9f);
}
......@@ -27,8 +23,11 @@ class _ModifierPair {
final String right;
}
List<T> _toNonEmptyArray<T>(dynamic source) {
final List<dynamic>? dynamicNullableList = source as List<dynamic>?;
// Return map[key1][key2] as a non-nullable List<T>, where both map[key1] or
// map[key1][key2] might be null.
List<T> _getGrandchildList<T>(Map<String, dynamic> map, String key1, String key2) {
final dynamic value = (map[key1] as Map<String, dynamic>?)?[key2];
final List<dynamic>? dynamicNullableList = value as List<dynamic>?;
final List<dynamic> dynamicList = dynamicNullableList ?? <dynamic>[];
return dynamicList.cast<T>();
}
......@@ -155,20 +154,23 @@ class LogicalKeyData {
final int value = match.namedGroup('unicode') != null ?
getHex(match.namedGroup('unicode')!) :
match.namedGroup('char')!.codeUnitAt(0);
final String? keyLabel = match.namedGroup('kind')! == 'UNI' ? String.fromCharCode(value) : null;
final String? keyLabel = (match.namedGroup('kind')! == 'UNI' && !_isControlCharacter(value)) ?
String.fromCharCode(value) : null;
// Skip modifier keys from DOM. They will be added with supplemental data.
if (_chromeModifiers.containsKey(name) && source == 'DOM') {
continue;
}
final bool isPrintable = (keyLabel != null && !_isControlCharacter(keyLabel))
|| printable.containsKey(name);
final bool isPrintable = keyLabel != null;
data.putIfAbsent(name, () {
return LogicalKeyEntry.fromName(
final LogicalKeyEntry entry = LogicalKeyEntry.fromName(
value: toPlane(value, _sourceToPlane(source, isPrintable)),
name: name,
keyLabel: keyLabel,
)..webNames.add(webName);
);
if (source == 'DOM' && !isPrintable)
entry.webNames.add(webName);
return entry;
});
}
}
......@@ -418,9 +420,11 @@ class LogicalKeyData {
})();
static int _sourceToPlane(String source, bool isPrintable) {
if (isPrintable)
return kUnicodePlane.value;
switch (source) {
case 'DOM':
return isPrintable ? kUnicodePlane.value : kUnprintablePlane.value;
return kUnprintablePlane.value;
case 'FLUTTER':
return kFlutterPlane.value;
default:
......@@ -470,20 +474,20 @@ class LogicalKeyEntry {
LogicalKeyEntry.fromJsonMapEntry(Map<String, dynamic> map)
: value = map['value'] as int,
name = map['name'] as String,
webNames = _toNonEmptyArray<String>((map['names'] as Map<String, dynamic>)['web']),
macOSKeyCodeNames = _toNonEmptyArray<String>((map['names'] as Map<String, dynamic>)['macos']),
macOSKeyCodeValues = _toNonEmptyArray<int>((map['values'] as Map<String, dynamic>?)?['macos']),
iOSKeyCodeNames = _toNonEmptyArray<String>((map['names'] as Map<String, dynamic>)['ios']),
iOSKeyCodeValues = _toNonEmptyArray<int>((map['values'] as Map<String, dynamic>?)?['ios']),
gtkNames = _toNonEmptyArray<String>((map['names'] as Map<String, dynamic>)['gtk']),
gtkValues = _toNonEmptyArray<int>((map['values'] as Map<String, dynamic>?)?['gtk']),
windowsNames = _toNonEmptyArray<String>((map['names'] as Map<String, dynamic>)['windows']),
windowsValues = _toNonEmptyArray<int>((map['values'] as Map<String, dynamic>?)?['windows']),
androidNames = _toNonEmptyArray<String>((map['names'] as Map<String, dynamic>)['android']),
androidValues = _toNonEmptyArray<int>((map['values'] as Map<String, dynamic>?)?['android']),
fuchsiaValues = _toNonEmptyArray<int>((map['values'] as Map<String, dynamic>?)?['fuchsia']),
glfwNames = _toNonEmptyArray<String>((map['names'] as Map<String, dynamic>)['glfw']),
glfwValues = _toNonEmptyArray<int>((map['values'] as Map<String, dynamic>?)?['glfw']),
webNames = _getGrandchildList<String>(map, 'names', 'web'),
macOSKeyCodeNames = _getGrandchildList<String>(map, 'names', 'macos'),
macOSKeyCodeValues = _getGrandchildList<int>(map, 'values', 'macos'),
iOSKeyCodeNames = _getGrandchildList<String>(map, 'names', 'ios'),
iOSKeyCodeValues = _getGrandchildList<int>(map, 'values', 'ios'),
gtkNames = _getGrandchildList<String>(map, 'names', 'gtk'),
gtkValues = _getGrandchildList<int>(map, 'values', 'gtk'),
windowsNames = _getGrandchildList<String>(map, 'names', 'windows'),
windowsValues = _getGrandchildList<int>(map, 'values', 'windows'),
androidNames = _getGrandchildList<String>(map, 'names', 'android'),
androidValues = _getGrandchildList<int>(map, 'values', 'android'),
fuchsiaValues = _getGrandchildList<int>(map, 'values', 'fuchsia'),
glfwNames = _getGrandchildList<String>(map, 'names', 'glfw'),
glfwValues = _getGrandchildList<int>(map, 'values', 'glfw'),
keyLabel = map['keyLabel'] as String?;
final int value;
......
......@@ -22,8 +22,10 @@ String readDataFile(String fileName) {
return File(path.join(dataRoot, fileName)).readAsStringSync();
}
final String testPhysicalData = path.join(dataRoot, 'physical_key_data.json');
final String testLogicalData = path.join(dataRoot,'logical_key_data.json');
final PhysicalKeyData physicalData = PhysicalKeyData.fromJson(
json.decode(readDataFile('physical_key_data.json')) as Map<String, dynamic>);
final LogicalKeyData logicalData = LogicalKeyData.fromJson(
json.decode(readDataFile('logical_key_data.json')) as Map<String, dynamic>);
void main() {
setUp(() {
......@@ -45,14 +47,6 @@ void main() {
}
test('Generate Keycodes for Android', () {
PhysicalKeyData physicalData;
LogicalKeyData logicalData;
physicalData = PhysicalKeyData.fromJson(
json.decode(File(testPhysicalData).readAsStringSync()) as Map<String, dynamic>);
logicalData = LogicalKeyData.fromJson(
json.decode(File(testLogicalData).readAsStringSync()) as Map<String, dynamic>);
const String platform = 'android';
final PlatformCodeGenerator codeGenerator = AndroidCodeGenerator(
physicalData,
......@@ -67,14 +61,6 @@ void main() {
checkCommonOutput(output);
});
test('Generate Keycodes for macOS', () {
PhysicalKeyData physicalData;
LogicalKeyData logicalData;
physicalData = PhysicalKeyData.fromJson(
json.decode(File(testPhysicalData).readAsStringSync()) as Map<String, dynamic>);
logicalData = LogicalKeyData.fromJson(
json.decode(File(testLogicalData).readAsStringSync()) as Map<String, dynamic>);
const String platform = 'macos';
final PlatformCodeGenerator codeGenerator = MacOSCodeGenerator(
physicalData,
......@@ -93,14 +79,6 @@ void main() {
checkCommonOutput(output);
});
test('Generate Keycodes for iOS', () {
PhysicalKeyData physicalData;
LogicalKeyData logicalData;
physicalData = PhysicalKeyData.fromJson(
json.decode(File(testPhysicalData).readAsStringSync()) as Map<String, dynamic>);
logicalData = LogicalKeyData.fromJson(
json.decode(File(testLogicalData).readAsStringSync()) as Map<String, dynamic>);
const String platform = 'ios';
final PlatformCodeGenerator codeGenerator = IOSCodeGenerator(
physicalData,
......@@ -120,14 +98,6 @@ void main() {
checkCommonOutput(output);
});
test('Generate Keycodes for Windows', () {
PhysicalKeyData physicalData;
LogicalKeyData logicalData;
physicalData = PhysicalKeyData.fromJson(
json.decode(File(testPhysicalData).readAsStringSync()) as Map<String, dynamic>);
logicalData = LogicalKeyData.fromJson(
json.decode(File(testLogicalData).readAsStringSync()) as Map<String, dynamic>);
const String platform = 'windows';
final PlatformCodeGenerator codeGenerator = WindowsCodeGenerator(
physicalData,
......@@ -143,14 +113,6 @@ void main() {
checkCommonOutput(output);
});
test('Generate Keycodes for Linux', () {
PhysicalKeyData physicalData;
LogicalKeyData logicalData;
physicalData = PhysicalKeyData.fromJson(
json.decode(File(testPhysicalData).readAsStringSync()) as Map<String, dynamic>);
logicalData = LogicalKeyData.fromJson(
json.decode(File(testLogicalData).readAsStringSync()) as Map<String, dynamic>);
const String platform = 'gtk';
final PlatformCodeGenerator codeGenerator = GtkCodeGenerator(
physicalData,
......@@ -166,14 +128,6 @@ void main() {
checkCommonOutput(output);
});
test('Generate Keycodes for Web', () {
PhysicalKeyData physicalData;
LogicalKeyData logicalData;
physicalData = PhysicalKeyData.fromJson(
json.decode(File(testPhysicalData).readAsStringSync()) as Map<String, dynamic>);
logicalData = LogicalKeyData.fromJson(
json.decode(File(testLogicalData).readAsStringSync()) as Map<String, dynamic>);
const String platform = 'web';
final PlatformCodeGenerator codeGenerator = WebCodeGenerator(
physicalData,
......@@ -188,4 +142,25 @@ void main() {
expect(output, contains('kWebLogicalLocationMap'));
checkCommonOutput(output);
});
test('LogicalKeyData', () async {
final List<LogicalKeyEntry> entries = logicalData.entries.toList();
// Regression tests for https://github.com/flutter/flutter/pull/87098
expect(
entries.indexWhere((LogicalKeyEntry entry) => entry.name == 'ShiftLeft'),
isNot(-1));
expect(
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('ShiftLeft')),
-1);
// 'Shift' maps to both 'ShiftLeft' and 'ShiftRight', and should be resolved
// by other ways.
expect(
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Shift')),
-1);
// Printable keys must not be added with Web key of their names.
expect(
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Slash')),
-1);
});
}
......@@ -285,12 +285,10 @@ abstract class RawKeyEvent with Diagnosticable {
/// Creates a concrete [RawKeyEvent] class from a message in the form received
/// on the [SystemChannels.keyEvent] channel.
factory RawKeyEvent.fromMessage(Map<String, dynamic> message) {
final RawKeyEventData data;
String? character;
RawKeyEventData _dataFromWeb() {
final String? key = message['key'] as String?;
if (key != null && key.isNotEmpty) {
if (key != null && key.isNotEmpty && key.length == 1) {
character = key;
}
return RawKeyEventDataWeb(
......@@ -300,6 +298,8 @@ abstract class RawKeyEvent with Diagnosticable {
metaState: message['metaState'] as int? ?? 0,
);
}
final RawKeyEventData data;
if (kIsWeb) {
data = _dataFromWeb();
} else {
......
......@@ -10,6 +10,13 @@ import 'keyboard_key.dart';
import 'keyboard_maps.dart';
import 'raw_keyboard.dart';
String? _unicodeChar(String key) {
if (key.length == 1) {
return key.substring(0, 1);
}
return null;
}
/// Platform-specific key event data for Web.
///
/// See also:
......@@ -74,7 +81,7 @@ class RawKeyEventDataWeb extends RawKeyEventData {
final int metaState;
@override
String get keyLabel => key == 'Unidentified' ? '' : key;
String get keyLabel => key == 'Unidentified' ? '' : _unicodeChar(key) ?? '';
@override
PhysicalKeyboardKey get physicalKey {
......@@ -95,9 +102,14 @@ class RawKeyEventDataWeb extends RawKeyEventData {
return newKey;
}
final bool isPrintable = key.length == 1;
if (isPrintable)
return LogicalKeyboardKey(key.codeUnitAt(0));
// This is a non-printable key that we don't know about, so we mint a new
// code.
return LogicalKeyboardKey(code.hashCode | LogicalKeyboardKey.webPlane);
// key from `code`. Don't mint with `key`, because the `key` will always be
// "Unidentified" .
return LogicalKeyboardKey(code.hashCode + LogicalKeyboardKey.webPlane);
}
@override
......
......@@ -2400,6 +2400,7 @@ void main() {
'keymap': 'web',
'code': 'KeyA',
'key': 'a',
'location': 0,
'metaState': 0x0,
});
final RawKeyEventDataWeb data = keyAEvent.data as RawKeyEventDataWeb;
......@@ -2413,6 +2414,8 @@ void main() {
'type': 'keydown',
'keymap': 'web',
'code': 'Escape',
'key': 'Escape',
'location': 0,
'metaState': 0x0,
});
final RawKeyEventDataWeb data = escapeKeyEvent.data as RawKeyEventDataWeb;
......@@ -2426,6 +2429,8 @@ void main() {
'type': 'keydown',
'keymap': 'web',
'code': 'ShiftLeft',
'key': 'Shift',
'location': 1,
'metaState': RawKeyEventDataWeb.modifierShift,
});
final RawKeyEventDataWeb data = shiftKeyEvent.data as RawKeyEventDataWeb;
......@@ -2439,6 +2444,8 @@ void main() {
'type': 'keydown',
'keymap': 'web',
'code': 'ArrowDown',
'key': 'ArrowDown',
'location': 0,
'metaState': 0x0,
});
final RawKeyEventDataWeb data = arrowKeyDown.data as RawKeyEventDataWeb;
......@@ -2447,6 +2454,25 @@ void main() {
expect(data.keyLabel, isEmpty);
});
test('Unrecognized keys are mapped to Web plane', () {
final RawKeyEvent arrowKeyDown = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'web',
'code': 'Unrecog1',
'key': 'Unrecog2',
'location': 0,
'metaState': 0x0,
});
final RawKeyEventDataWeb data = arrowKeyDown.data as RawKeyEventDataWeb;
// This might be easily broken on Web if the code fails to acknowledge
// that JavaScript doesn't handle 64-bit bit-wise operation.
expect(data.physicalKey.usbHidUsage, greaterThan(0x01700000000));
expect(data.physicalKey.usbHidUsage, lessThan(0x01800000000));
expect(data.logicalKey.keyId, greaterThan(0x01700000000));
expect(data.logicalKey.keyId, lessThan(0x01800000000));
expect(data.keyLabel, isEmpty);
});
test('data.toString', () {
expect(RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
......
......@@ -14,6 +14,16 @@ import 'package:flutter_test/flutter_test.dart';
import 'binding.dart';
import 'test_async_utils.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
......@@ -145,8 +155,32 @@ class KeyEventSimulator {
}
}
static String _getWebKeyCode(LogicalKeyboardKey key) {
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;
......@@ -154,6 +188,18 @@ class KeyEventSimulator {
}
}
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!;
}
......@@ -215,8 +261,6 @@ class KeyEventSimulator {
physicalKey ??= _findPhysicalKeyByPlatform(key, platform);
assert(key.debugName != null);
final int keyCode = _getKeyCode(key, platform);
final int scanCode = _getScanCode(physicalKey, platform);
final Map<String, dynamic> result = <String, dynamic>{
'type': isDown ? 'keydown' : 'keyup',
......@@ -225,14 +269,19 @@ class KeyEventSimulator {
final String resultCharacter = character ?? _keyLabel(key) ?? '';
void assignWeb() {
result['code'] = _getWebKeyCode(key);
result['key'] = resultCharacter;
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':
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment