mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This PR updates the ID used by logical keyboard keys. The logical key ID is still composed of 2 parts: 32 bits of value, and 8 bits of plane. But the assignment of planes has been drastically changed. HID plane is removed, and unprintable plane and Flutter plane are added. This is to reflect the new generation method for logical key IDs. Now keys that are defined by Flutter but not by dom_key_data are placed into the Flutter plane, including numpad keys, sided modifier keys, and gamepad keys. The values for platform planes have also been adjusted. The generation script and README have been updated accordingly as well. A new file, test_utils/key_codes.h is now generated to assist engine unit testing. All lists generated by the script are now sorted by the key.
394 lines
15 KiB
Dart
394 lines
15 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:convert';
|
|
|
|
import 'package:gen_keycodes/utils.dart';
|
|
|
|
/// The data structure used to manage keyboard key entries.
|
|
///
|
|
/// The main constructor parses the given input data into the data structure.
|
|
///
|
|
/// The data structure can be also loaded and saved to JSON, with the
|
|
/// [PhysicalKeyData.fromJson] constructor and [toJson] method, respectively.
|
|
class PhysicalKeyData {
|
|
factory PhysicalKeyData(
|
|
String chromiumHidCodes,
|
|
String androidKeyboardLayout,
|
|
String androidNameMap,
|
|
String glfwHeaderFile,
|
|
String glfwNameMap,
|
|
) {
|
|
final Map<String, List<int>> nameToAndroidScanCodes = _readAndroidScanCodes(androidKeyboardLayout, androidNameMap);
|
|
final Map<String, List<int>> nameToGlfwKeyCodes = _readGlfwKeyCodes(glfwHeaderFile, glfwNameMap);
|
|
final Map<String, PhysicalKeyEntry> data = _readHidEntries(
|
|
chromiumHidCodes,
|
|
nameToAndroidScanCodes,
|
|
nameToGlfwKeyCodes,
|
|
);
|
|
final List<MapEntry<String, PhysicalKeyEntry>> sortedEntries = data.entries.toList()..sort(
|
|
(MapEntry<String, PhysicalKeyEntry> a, MapEntry<String, PhysicalKeyEntry> b) =>
|
|
PhysicalKeyEntry.compareByUsbHidCode(a.value, b.value),
|
|
);
|
|
data
|
|
..clear()
|
|
..addEntries(sortedEntries);
|
|
return PhysicalKeyData._(data);
|
|
}
|
|
|
|
/// Parses the given JSON data and populates the data structure from it.
|
|
factory PhysicalKeyData.fromJson(Map<String, dynamic> contentMap) {
|
|
final Map<String, PhysicalKeyEntry> data = <String, PhysicalKeyEntry>{};
|
|
for (final MapEntry<String, dynamic> jsonEntry in contentMap.entries) {
|
|
final PhysicalKeyEntry entry = PhysicalKeyEntry.fromJsonMapEntry(jsonEntry.value as Map<String, dynamic>);
|
|
data[entry.name] = entry;
|
|
}
|
|
return PhysicalKeyData._(data);
|
|
}
|
|
|
|
PhysicalKeyData._(this._data);
|
|
|
|
/// Find an entry from name, or null if not found.
|
|
PhysicalKeyEntry? tryEntryByName(String name) {
|
|
return _data[name];
|
|
}
|
|
|
|
/// Find an entry from name.
|
|
///
|
|
/// Asserts if the name is not found.
|
|
PhysicalKeyEntry entryByName(String name) {
|
|
final PhysicalKeyEntry? entry = tryEntryByName(name);
|
|
assert(entry != null,
|
|
'Unable to find logical entry by name $name.');
|
|
return entry!;
|
|
}
|
|
|
|
/// All entries.
|
|
Iterable<PhysicalKeyEntry> get entries => _data.values;
|
|
|
|
// Keys mapped from their names.
|
|
final Map<String, PhysicalKeyEntry> _data;
|
|
|
|
/// Converts the data structure into a JSON structure that can be parsed by
|
|
/// [PhysicalKeyData.fromJson].
|
|
Map<String, dynamic> toJson() {
|
|
final Map<String, dynamic> outputMap = <String, dynamic>{};
|
|
for (final PhysicalKeyEntry entry in _data.values) {
|
|
outputMap[entry.name] = entry.toJson();
|
|
}
|
|
return outputMap;
|
|
}
|
|
|
|
/// Parses entries from Androids Generic.kl scan code data file.
|
|
///
|
|
/// Lines in this file look like this (without the ///):
|
|
/// key 100 ALT_RIGHT
|
|
/// # key 101 "KEY_LINEFEED"
|
|
/// key 477 F12 FUNCTION
|
|
///
|
|
/// We parse the commented out lines as well as the non-commented lines, so
|
|
/// that we can get names for all of the available scan codes, not just ones
|
|
/// defined for the generic profile.
|
|
///
|
|
/// Also, note that some keys (notably MEDIA_EJECT) can be mapped to more than
|
|
/// one scan code, so the mapping can't just be 1:1, it has to be 1:many.
|
|
static Map<String, List<int>> _readAndroidScanCodes(String keyboardLayout, String nameMap) {
|
|
final RegExp keyEntry = RegExp(
|
|
r'#?\s*' // Optional comment mark
|
|
r'key\s+' // Literal "key"
|
|
r'(?<id>[0-9]+)\s*' // ID section
|
|
r'"?(?:KEY_)?(?<name>[0-9A-Z_]+|\(undefined\))"?\s*' // Name section
|
|
r'(?<function>FUNCTION)?' // Optional literal "FUNCTION"
|
|
);
|
|
final Map<String, List<int>> androidNameToScanCodes = <String, List<int>>{};
|
|
for (final RegExpMatch match in keyEntry.allMatches(keyboardLayout)) {
|
|
if (match.namedGroup('function') == 'FUNCTION') {
|
|
// Skip odd duplicate Android FUNCTION keys (F1-F12 are already defined).
|
|
continue;
|
|
}
|
|
final String name = match.namedGroup('name')!;
|
|
if (name == '(undefined)') {
|
|
// Skip undefined scan codes.
|
|
continue;
|
|
}
|
|
androidNameToScanCodes.putIfAbsent(name, () => <int>[])
|
|
.add(int.parse(match.namedGroup('id')!));
|
|
}
|
|
|
|
// Cast Android dom map
|
|
final Map<String, List<String>> nameToAndroidNames = (json.decode(nameMap) as Map<String, dynamic>)
|
|
.cast<String, List<dynamic>>()
|
|
.map<String, List<String>>((String key, List<dynamic> value) {
|
|
return MapEntry<String, List<String>>(key, value.cast<String>());
|
|
});
|
|
|
|
final Map<String, List<int>> result = nameToAndroidNames.map((String name, List<String> androidNames) {
|
|
final Set<int> scanCodes = <int>{};
|
|
for (final String androidName in androidNames) {
|
|
scanCodes.addAll(androidNameToScanCodes[androidName] ?? <int>[]);
|
|
}
|
|
return MapEntry<String, List<int>>(name, scanCodes.toList()..sort());
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Parses entries from GLFW's keycodes.h key code data file.
|
|
///
|
|
/// Lines in this file look like this (without the ///):
|
|
/// /** Space key. */
|
|
/// #define GLFW_KEY_SPACE 32,
|
|
/// #define GLFW_KEY_LAST GLFW_KEY_MENU
|
|
|
|
static Map<String, List<int>> _readGlfwKeyCodes(String headerFile, String nameMap) {
|
|
// Only get the KEY definitions, ignore the rest (mouse, joystick, etc).
|
|
final RegExp definedCodes = RegExp(
|
|
r'define\s+'
|
|
r'GLFW_KEY_(?<name>[A-Z0-9_]+)\s+'
|
|
r'(?<value>[A-Z0-9_]+),?',
|
|
);
|
|
final Map<String, dynamic> replaced = <String, dynamic>{};
|
|
for (final RegExpMatch match in definedCodes.allMatches(headerFile)) {
|
|
final String name = match.namedGroup('name')!;
|
|
final String value = match.namedGroup('value')!;
|
|
replaced[name] = int.tryParse(value) ?? value.replaceAll('GLFW_KEY_', '');
|
|
}
|
|
final Map<String, int> glfwNameToKeyCode = <String, int>{};
|
|
replaced.forEach((String key, dynamic value) {
|
|
// Some definition values point to other definitions (e.g #define GLFW_KEY_LAST GLFW_KEY_MENU).
|
|
if (value is String) {
|
|
glfwNameToKeyCode[key] = replaced[value] as int;
|
|
} else {
|
|
glfwNameToKeyCode[key] = value as int;
|
|
}
|
|
});
|
|
|
|
final Map<String, List<String>> nameToGlfwNames = (json.decode(nameMap) as Map<String, dynamic>)
|
|
.cast<String, List<dynamic>>()
|
|
.map<String, List<String>>((String key, List<dynamic> value) {
|
|
return MapEntry<String, List<String>>(key, value.cast<String>());
|
|
});
|
|
|
|
final Map<String, List<int>> result = nameToGlfwNames.map((String name, List<String> glfwNames) {
|
|
final Set<int> keyCodes = <int>{};
|
|
for (final String glfwName in glfwNames) {
|
|
if (glfwNameToKeyCode[glfwName] != null)
|
|
keyCodes.add(glfwNameToKeyCode[glfwName]!);
|
|
}
|
|
return MapEntry<String, List<int>>(name, keyCodes.toList()..sort());
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Parses entries from Chromium's HID code mapping header file.
|
|
///
|
|
/// Lines in this file look like this (without the ///):
|
|
/// USB evdev XKB Win Mac Code Enum
|
|
/// DOM_CODE(0x000010, 0x0000, 0x0000, 0x0000, 0xffff, "Hyper", HYPER),
|
|
static Map<String, PhysicalKeyEntry> _readHidEntries(
|
|
String input,
|
|
Map<String, List<int>> nameToAndroidScanCodes,
|
|
Map<String, List<int>> nameToGlfwKeyCodes,
|
|
) {
|
|
final Map<int, PhysicalKeyEntry> entries = <int, PhysicalKeyEntry>{};
|
|
final RegExp usbMapRegExp = RegExp(
|
|
r'DOM_CODE\s*\(\s*'
|
|
r'0[xX](?<usb>[a-fA-F0-9]+),\s*'
|
|
r'0[xX](?<evdev>[a-fA-F0-9]+),\s*'
|
|
r'0[xX](?<xkb>[a-fA-F0-9]+),\s*'
|
|
r'0[xX](?<win>[a-fA-F0-9]+),\s*'
|
|
r'0[xX](?<mac>[a-fA-F0-9]+),\s*'
|
|
r'(?:"(?<code>[^\s]+)")?[^")]*?,'
|
|
r'\s*(?<enum>[^\s]+?)\s*'
|
|
r'\)',
|
|
// Multiline is necessary because some definitions spread across
|
|
// multiple lines.
|
|
multiLine: true,
|
|
);
|
|
final RegExp commentRegExp = RegExp(r'//.*$', multiLine: true);
|
|
input = input.replaceAll(commentRegExp, '');
|
|
for (final RegExpMatch match in usbMapRegExp.allMatches(input)) {
|
|
final int usbHidCode = getHex(match.namedGroup('usb')!);
|
|
final int linuxScanCode = getHex(match.namedGroup('evdev')!);
|
|
final int xKbScanCode = getHex(match.namedGroup('xkb')!);
|
|
final int windowsScanCode = getHex(match.namedGroup('win')!);
|
|
final int macScanCode = getHex(match.namedGroup('mac')!);
|
|
final String? chromiumCode = match.namedGroup('code');
|
|
// The input data has a typo...
|
|
final String enumName = match.namedGroup('enum')!.replaceAll('MINIMIUM', 'MINIMUM');
|
|
|
|
final String name = chromiumCode ?? shoutingToUpperCamel(enumName);
|
|
if (name == 'IntlHash' || name == 'None') {
|
|
// Skip key that is not actually generated by any keyboard.
|
|
continue;
|
|
}
|
|
final PhysicalKeyEntry newEntry = PhysicalKeyEntry(
|
|
usbHidCode: usbHidCode,
|
|
androidScanCodes: nameToAndroidScanCodes[name] ?? <int>[],
|
|
glfwKeyCodes: nameToGlfwKeyCodes[name] ?? <int>[],
|
|
linuxScanCode: linuxScanCode == 0 ? null : linuxScanCode,
|
|
xKbScanCode: xKbScanCode == 0 ? null : xKbScanCode,
|
|
windowsScanCode: windowsScanCode == 0 ? null : windowsScanCode,
|
|
macOSScanCode: macScanCode == 0xffff ? null : macScanCode,
|
|
iOSScanCode: (usbHidCode & 0x070000) == 0x070000 ? (usbHidCode ^ 0x070000) : null,
|
|
name: name,
|
|
chromiumCode: chromiumCode,
|
|
);
|
|
// Remove duplicates: last one wins, so that supplemental codes
|
|
// override.
|
|
if (entries.containsKey(newEntry.usbHidCode)) {
|
|
// This is expected for Fn. Warn for other keys.
|
|
if (newEntry.name != 'Fn') {
|
|
print('Duplicate usbHidCode ${newEntry.usbHidCode} of key ${newEntry.name} '
|
|
'conflicts with existing ${entries[newEntry.usbHidCode]!.name}. Keeping the new one.');
|
|
}
|
|
}
|
|
entries[newEntry.usbHidCode] = newEntry;
|
|
}
|
|
return entries.map((int code, PhysicalKeyEntry entry) =>
|
|
MapEntry<String, PhysicalKeyEntry>(entry.name, entry));
|
|
}
|
|
}
|
|
|
|
/// A single entry in the key data structure.
|
|
///
|
|
/// Can be read from JSON with the [PhysicalKeyEntry.fromJsonMapEntry] constructor, or
|
|
/// written with the [toJson] method.
|
|
class PhysicalKeyEntry {
|
|
/// Creates a single key entry from available data.
|
|
///
|
|
/// The [usbHidCode] and [chromiumName] parameters must not be null.
|
|
PhysicalKeyEntry({
|
|
required this.usbHidCode,
|
|
required this.name,
|
|
required this.androidScanCodes,
|
|
required this.linuxScanCode,
|
|
required this.xKbScanCode,
|
|
required this.windowsScanCode,
|
|
required this.macOSScanCode,
|
|
required this.iOSScanCode,
|
|
required this.chromiumCode,
|
|
required this.glfwKeyCodes,
|
|
});
|
|
|
|
/// Populates the key from a JSON map.
|
|
factory PhysicalKeyEntry.fromJsonMapEntry(Map<String, dynamic> map) {
|
|
final Map<String, dynamic> names = map['names'] as Map<String, dynamic>;
|
|
final Map<String, dynamic> scanCodes = map['scanCodes'] as Map<String, dynamic>;
|
|
final Map<String, dynamic>? keyCodes = map['keyCodes'] as Map<String, dynamic>?;
|
|
return PhysicalKeyEntry(
|
|
name: names['name'] as String,
|
|
chromiumCode: names['chromium'] as String?,
|
|
usbHidCode: scanCodes['usb'] as int,
|
|
androidScanCodes: (scanCodes['android'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
|
linuxScanCode: scanCodes['linux'] as int?,
|
|
xKbScanCode: scanCodes['xkb'] as int?,
|
|
windowsScanCode: scanCodes['windows'] as int?,
|
|
macOSScanCode: scanCodes['macos'] as int?,
|
|
iOSScanCode: scanCodes['ios'] as int?,
|
|
glfwKeyCodes: (keyCodes?['glfw'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
|
);
|
|
}
|
|
|
|
/// The USB HID code of the key
|
|
final int usbHidCode;
|
|
|
|
/// The Linux scan code of the key, from Chromium's header file.
|
|
final int? linuxScanCode;
|
|
/// The XKb scan code of the key from Chromium's header file.
|
|
final int? xKbScanCode;
|
|
/// The Windows scan code of the key from Chromium's header file.
|
|
final int? windowsScanCode;
|
|
/// The macOS scan code of the key from Chromium's header file.
|
|
final int? macOSScanCode;
|
|
/// The iOS scan code of the key from UIKey's documentation (USB Hid table)
|
|
final int? iOSScanCode;
|
|
/// The list of Android scan codes matching this key, created by looking up
|
|
/// the Android name in the Chromium data, and substituting the Android scan
|
|
/// code value.
|
|
final List<int> androidScanCodes;
|
|
/// The list of GLFW key codes matching this key, created by looking up the
|
|
/// Linux name in the Chromium data, and substituting the GLFW key code
|
|
/// value.
|
|
final List<int> glfwKeyCodes;
|
|
/// The name of the key, mostly derived from the DomKey name in Chromium,
|
|
/// but where there was no DomKey representation, derived from the Chromium
|
|
/// symbol name.
|
|
final String name;
|
|
/// The Chromium event code for the key.
|
|
final String? chromiumCode;
|
|
|
|
/// Creates a JSON map from the key data.
|
|
Map<String, dynamic> toJson() {
|
|
return removeEmptyValues(<String, dynamic>{
|
|
'names': <String, dynamic>{
|
|
'name': name,
|
|
'chromium': chromiumCode,
|
|
},
|
|
'scanCodes': <String, dynamic>{
|
|
'android': androidScanCodes,
|
|
'usb': usbHidCode,
|
|
'linux': linuxScanCode,
|
|
'xkb': xKbScanCode,
|
|
'windows': windowsScanCode,
|
|
'macos': macOSScanCode,
|
|
'ios': iOSScanCode,
|
|
},
|
|
'keyCodes': <String, List<int>>{
|
|
'glfw': glfwKeyCodes,
|
|
},
|
|
});
|
|
}
|
|
|
|
static String getCommentName(String constantName) {
|
|
String upperCamel = lowerCamelToUpperCamel(constantName);
|
|
upperCamel = upperCamel.replaceAllMapped(
|
|
RegExp(r'(Digit|Numpad|Lang|Button|Left|Right)([0-9]+)'),
|
|
(Match match) => '${match.group(1)} ${match.group(2)}',
|
|
);
|
|
return upperCamel.replaceAllMapped(RegExp(r'([A-Z])'), (Match match) => ' ${match.group(1)}').trim();
|
|
}
|
|
|
|
/// Gets the name of the key suitable for placing in comments.
|
|
///
|
|
/// Takes the [constantName] and converts it from lower camel case to capitalized
|
|
/// separate words (e.g. "wakeUp" converts to "Wake Up").
|
|
String get commentName => getCommentName(constantName);
|
|
|
|
/// Gets the named used for the key constant in the definitions in
|
|
/// keyboard_key.dart.
|
|
///
|
|
/// If set by the constructor, returns the name set, but otherwise constructs
|
|
/// the name from the various different names available, making sure that the
|
|
/// name isn't a Dart reserved word (if it is, then it adds the word "Key" to
|
|
/// the end of the name).
|
|
late final String constantName = (() {
|
|
String? result;
|
|
if (name.isEmpty) {
|
|
// If it doesn't have a DomKey name then use the Chromium symbol name.
|
|
result = chromiumCode;
|
|
} else {
|
|
result = upperCamelToLowerCamel(name);
|
|
}
|
|
result ??= 'Key${toHex(usbHidCode)}';
|
|
if (kDartReservedWords.contains(result)) {
|
|
return '${result}Key';
|
|
}
|
|
return result;
|
|
})();
|
|
|
|
@override
|
|
String toString() {
|
|
return """'$constantName': (name: "$name", usbHidCode: ${toHex(usbHidCode)}, """
|
|
'linuxScanCode: ${toHex(linuxScanCode)}, xKbScanCode: ${toHex(xKbScanCode)}, '
|
|
'windowsKeyCode: ${toHex(windowsScanCode)}, macOSScanCode: ${toHex(macOSScanCode)}, '
|
|
'windowsScanCode: ${toHex(windowsScanCode)}, chromiumSymbolName: $chromiumCode '
|
|
'iOSScanCode: ${toHex(iOSScanCode)})';
|
|
}
|
|
|
|
static int compareByUsbHidCode(PhysicalKeyEntry a, PhysicalKeyEntry b) =>
|
|
a.usbHidCode.compareTo(b.usbHidCode);
|
|
}
|