blob: 76348c318150262ec54b3eec7528eaa136718b05 [file] [log] [blame]
// 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 'dart:io';
import 'package:path/path.dart' as path;
import 'package:meta/meta.dart';
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
/// [KeyData.fromJson] constructor and [toJson] method, respectively.
class KeyData {
/// Parses the input data given in from the various data source files,
/// populating the data structure.
///
/// None of the parameters may be null.
KeyData(
String chromiumHidCodes,
String androidKeyboardLayout,
String androidKeyCodeHeader,
String androidNameMap,
String glfwKeyCodeHeader,
String glfwNameMap,
String gtkKeyCodeHeader,
String gtkNameMap,
String windowsKeyCodeHeader,
String windowsNameMap,
) : assert(chromiumHidCodes != null),
assert(androidKeyboardLayout != null),
assert(androidKeyCodeHeader != null),
assert(androidNameMap != null),
assert(glfwKeyCodeHeader != null),
assert(glfwNameMap != null),
assert(gtkKeyCodeHeader != null),
assert(gtkNameMap != null),
assert(windowsKeyCodeHeader != null),
assert(windowsNameMap != null) {
_nameToAndroidScanCodes = _readAndroidScanCodes(androidKeyboardLayout);
_nameToAndroidKeyCode = _readAndroidKeyCodes(androidKeyCodeHeader);
_nameToGlfwKeyCode = _readGlfwKeyCodes(glfwKeyCodeHeader);
_nameToGtkKeyCode = _readGtkKeyCodes(gtkKeyCodeHeader);
_nameToWindowsKeyCode = _readWindowsKeyCodes(windowsKeyCodeHeader);
// Cast Android dom map
final Map<String, List<dynamic>> dynamicAndroidNames = (json.decode(androidNameMap) as Map<String, dynamic>).cast<String, List<dynamic>>();
_nameToAndroidName = dynamicAndroidNames.map<String, List<String>>((String key, List<dynamic> value) {
return MapEntry<String, List<String>>(key, value.cast<String>());
});
// Cast GLFW dom map
final Map<String, List<dynamic>> dynamicGlfwNames = (json.decode(glfwNameMap) as Map<String, dynamic>).cast<String, List<dynamic>>();
_nameToGlfwName = dynamicGlfwNames.map<String, List<String>>((String key, List<dynamic> value) {
return MapEntry<String, List<String>>(key, value.cast<String>());
});
// Cast GTK dom map
final Map<String, List<dynamic>> dynamicGtkNames = (json.decode(gtkNameMap) as Map<String, dynamic>).cast<String, List<dynamic>>();
_nameToGtkName = dynamicGtkNames.map<String, List<String>>((String key, List<dynamic> value) {
return MapEntry<String, List<String>>(key, value.cast<String>());
});
// Cast Windows dom map
final Map<String, List<dynamic>> dynamicWindowsNames = (json.decode(windowsNameMap) as Map<String, dynamic>).cast<String, List<dynamic>>();
_nameToWindowsName = dynamicWindowsNames.map<String, List<String>>((String key, List<dynamic> value) {
return MapEntry<String, List<String>>(key, value.cast<String>());
});
data = _readHidEntries(chromiumHidCodes);
}
/// Parses the given JSON data and populates the data structure from it.
KeyData.fromJson(Map<String, dynamic> contentMap) {
data = <Key>[
for (final String key in contentMap.keys) Key.fromJsonMapEntry(key, contentMap[key] as Map<String, dynamic>),
];
}
/// Converts the data structure into a JSON structure that can be parsed by
/// [KeyData.fromJson].
Map<String, dynamic> toJson() {
for (final Key entry in data) {
// Android Key names
entry.androidKeyNames = _nameToAndroidName[entry.constantName]?.cast<String>();
if (entry.androidKeyNames != null && entry.androidKeyNames.isNotEmpty) {
for (final String androidKeyName in entry.androidKeyNames) {
if (_nameToAndroidKeyCode[androidKeyName] != null) {
entry.androidKeyCodes ??= <int>[];
entry.androidKeyCodes.add(_nameToAndroidKeyCode[androidKeyName]);
}
if (_nameToAndroidScanCodes[androidKeyName] != null && _nameToAndroidScanCodes[androidKeyName].isNotEmpty) {
entry.androidScanCodes ??= <int>[];
entry.androidScanCodes.addAll(_nameToAndroidScanCodes[androidKeyName]);
}
}
}
// GLFW key names
entry.glfwKeyNames = _nameToGlfwName[entry.constantName]?.cast<String>();
if (entry.glfwKeyNames != null && entry.glfwKeyNames.isNotEmpty) {
for (final String glfwKeyName in entry.glfwKeyNames) {
if (_nameToGlfwKeyCode[glfwKeyName] != null) {
entry.glfwKeyCodes ??= <int>[];
entry.glfwKeyCodes.add(_nameToGlfwKeyCode[glfwKeyName]);
}
}
}
// GTK key names
entry.gtkKeyNames = _nameToGtkName[entry.constantName]?.cast<String>();
if (entry.gtkKeyNames != null && entry.gtkKeyNames.isNotEmpty) {
for (final String gtkKeyName in entry.gtkKeyNames) {
if (_nameToGtkKeyCode[gtkKeyName] != null) {
entry.gtkKeyCodes ??= <int>[];
entry.gtkKeyCodes.add(_nameToGtkKeyCode[gtkKeyName]);
}
}
}
// Windows key names
entry.windowsKeyNames = _nameToWindowsName[entry.constantName]?.cast<String>();
if (entry.windowsKeyNames != null && entry.windowsKeyNames.isNotEmpty) {
for (final String windowsKeyName in entry.windowsKeyNames) {
if (_nameToWindowsKeyCode[windowsKeyName] != null) {
entry.windowsKeyCodes ??= <int>[];
entry.windowsKeyCodes.add(_nameToWindowsKeyCode[windowsKeyName]);
}
}
}
}
final Map<String, dynamic> outputMap = <String, dynamic>{};
for (final Key entry in data) {
outputMap[entry.constantName] = entry.toJson();
}
return outputMap;
}
/// The list of keys.
List<Key> data;
/// The mapping from the Flutter name (e.g. "eject") to the Android name (e.g.
/// "MEDIA_EJECT").
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, List<String>> _nameToAndroidName;
/// The mapping from the Flutter name (e.g. "eject") to the GLFW name (e.g.
/// "GLFW_MEDIA_EJECT").
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, List<String>> _nameToGlfwName;
/// The mapping from the Flutter name (e.g. "eject") to the GTK name (e.g.
/// "GDK_KEY_Eject").
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, List<String>> _nameToGtkName;
/// The mapping from the Android name (e.g. "MEDIA_EJECT") to the integer scan
/// code (physical location) of the key.
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, List<int>> _nameToAndroidScanCodes;
/// The mapping from Android name (e.g. "MEDIA_EJECT") to the integer key code
/// (logical meaning) of the key.
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, int> _nameToAndroidKeyCode;
/// The mapping from GLFW name (e.g. "GLFW_KEY_COMMA") to the integer key code
/// (logical meaning) of the key.
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, int> _nameToGlfwKeyCode;
/// The mapping from GTK name (e.g. "GTK_KEY_comma") to the integer key code
/// (logical meaning) of the key.
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, int> _nameToGtkKeyCode;
/// The mapping from Widows name (e.g. "RETURN") to the integer key code
/// (logical meaning) of the key.
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, int> _nameToWindowsKeyCode;
/// The mapping from the Flutter name (e.g. "enter") to the Windows name (e.g.
/// "RETURN").
///
/// Only populated if data is parsed from the source files, not if parsed from
/// JSON.
Map<String, List<String>> _nameToWindowsName;
/// 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"
///
/// We parse the commented out lines as well as the non-commented lines, so 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.
Map<String, List<int>> _readAndroidScanCodes(String keyboardLayout) {
final RegExp keyEntry = RegExp(r'#?\s*key\s+([0-9]+)\s*"?(?:KEY_)?([0-9A-Z_]+|\(undefined\))"?\s*(FUNCTION)?');
final Map<String, List<int>> result = <String, List<int>>{};
keyboardLayout.replaceAllMapped(keyEntry, (Match match) {
if (match.group(3) == 'FUNCTION') {
// Skip odd duplicate Android FUNCTION keys (F1-F12 are already defined).
return '';
}
final String name = match.group(2);
if (name == '(undefined)') {
// Skip undefined scan codes.
return '';
}
final String androidName = match.group(2);
result[androidName] ??= <int>[];
result[androidName].add(int.parse(match.group(1)));
return null;
});
return result;
}
/// Parses entries from Android's keycodes.h key code data file.
///
/// Lines in this file look like this (without the ///):
/// /** Left Control modifier key. */
/// AKEYCODE_CTRL_LEFT = 113,
Map<String, int> _readAndroidKeyCodes(String headerFile) {
final RegExp enumBlock = RegExp(r'enum\s*\{(.*)\};', multiLine: true);
// Eliminate everything outside of the enum block.
headerFile = headerFile.replaceAllMapped(enumBlock, (Match match) => match.group(1));
final RegExp enumEntry = RegExp(r'AKEYCODE_([A-Z0-9_]+)\s*=\s*([0-9]+),?');
final Map<String, int> result = <String, int>{};
for (final Match match in enumEntry.allMatches(headerFile)) {
result[match.group(1)] = int.parse(match.group(2));
}
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,
Map<String, int> _readGlfwKeyCodes(String headerFile) {
// Only get the KEY definitions, ignore the rest (mouse, joystick, etc).
final RegExp definedCodes = RegExp(r'define GLFW_KEY_([A-Z0-9_]+)\s*([A-Z0-9_]+),?');
final Map<String, dynamic> replaced = <String, dynamic>{};
for (final Match match in definedCodes.allMatches(headerFile)) {
replaced[match.group(1)] = int.tryParse(match.group(2)) ?? match.group(2).replaceAll('GLFW_KEY_', '');
}
final Map<String, int> result = <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) {
result[key] = replaced[value] as int;
} else {
result[key] = value as int;
}
});
return result;
}
/// Parses entries from GTK's gdkkeysyms.h key code data file.
///
/// Lines in this file look like this (without the ///):
/// /** Space key. */
/// #define GDK_KEY_space 0x020
Map<String, int> _readGtkKeyCodes(String headerFile) {
final RegExp definedCodes = RegExp(r'#define GDK_KEY_([a-zA-Z0-9_]+)\s*0x([0-9a-f]+),?');
final Map<String, int> replaced = <String, int>{};
for (final Match match in definedCodes.allMatches(headerFile)) {
replaced[match.group(1)] = int.parse(match.group(2), radix: 16);
}
return replaced;
}
Map<String, int> _readWindowsKeyCodes(String headerFile) {
final RegExp definedCodes = RegExp(r'define VK_([A-Z0-9_]+)\s*([A-Z0-9_x]+),?');
final Map<String, int> replaced = <String, int>{};
for (final Match match in definedCodes.allMatches(headerFile)) {
replaced[match.group(1)] = int.tryParse(match.group(2));
}
// The header doesn't explicitly define the [0-9] and [A-Z], but they mention that the range
// is equivalent to the ASCII value.
for (int i = 0x30; i <= 0x39; i++) {
replaced[String.fromCharCode(i)] = i;
}
for (int i = 0x41; i <= 0x5A; i++) {
replaced[String.fromCharCode(i)] = i;
}
return replaced;
}
/// 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),
List<Key> _readHidEntries(String input) {
final List<Key> entries = <Key>[];
final RegExp usbMapRegExp = RegExp(
r'DOM_CODE\s*\(\s*0x([a-fA-F0-9]+),\s*0x([a-fA-F0-9]+),'
r'\s*0x([a-fA-F0-9]+),\s*0x([a-fA-F0-9]+),\s*0x([a-fA-F0-9]+),\s*"?([^\s]+?)"?,\s*([^\s]+?)\s*\)',
multiLine: true);
final RegExp commentRegExp = RegExp(r'//.*$', multiLine: true);
input = input.replaceAll(commentRegExp, '');
input.replaceAllMapped(usbMapRegExp, (Match match) {
if (match != null) {
final int macScanCode = getHex(match.group(5));
final int linuxScanCode = getHex(match.group(2));
final int xKbScanCode = getHex(match.group(3));
final int windowsScanCode = getHex(match.group(4));
final Key newEntry = Key(
usbHidCode: getHex(match.group(1)),
linuxScanCode: linuxScanCode == 0 ? null : linuxScanCode,
xKbScanCode: xKbScanCode == 0 ? null : xKbScanCode,
windowsScanCode: windowsScanCode == 0 ? null : windowsScanCode,
macOsScanCode: macScanCode == 0xffff ? null : macScanCode,
name: match.group(6) == 'NULL' ? null : match.group(6),
// The input data has a typo...
chromiumName: shoutingToLowerCamel(match.group(7)).replaceAll('Minimium', 'Minimum'),
);
if (newEntry.chromiumName == 'none') {
newEntry.name = 'None';
}
if (newEntry.name == 'IntlHash') {
// Skip key that is not actually generated by any keyboard.
return '';
}
// Remove duplicates: last one wins, so that supplemental codes
// override.
entries.removeWhere((Key entry) => entry.usbHidCode == newEntry.usbHidCode);
entries.add(newEntry);
}
return match.group(0);
});
return entries;
}
}
/// A single entry in the key data structure.
///
/// Can be read from JSON with the [Key.fromJsonMapEntry] constructor, or
/// written with the [toJson] method.
class Key {
/// Creates a single key entry from available data.
///
/// The [usbHidCode] and [chromiumName] parameters must not be null.
Key({
String enumName,
this.name,
@required this.usbHidCode,
this.linuxScanCode,
this.xKbScanCode,
this.windowsScanCode,
this.windowsKeyNames,
this.windowsKeyCodes,
this.macOsScanCode,
@required this.chromiumName,
this.androidKeyNames,
this.androidScanCodes,
this.androidKeyCodes,
this.glfwKeyNames,
this.glfwKeyCodes,
this.gtkKeyNames,
this.gtkKeyCodes,
}) : assert(usbHidCode != null),
assert(chromiumName != null),
_constantName = enumName;
/// Populates the key from a JSON map.
factory Key.fromJsonMapEntry(String name, Map<String, dynamic> map) {
return Key(
enumName: name,
name: map['names']['domkey'] as String,
chromiumName: map['names']['chromium'] as String,
usbHidCode: map['scanCodes']['usb'] as int,
androidKeyNames: (map['names']['android'] as List<dynamic>)?.cast<String>(),
androidScanCodes: (map['scanCodes']['android'] as List<dynamic>)?.cast<int>(),
androidKeyCodes: (map['keyCodes']['android'] as List<dynamic>)?.cast<int>(),
linuxScanCode: map['scanCodes']['linux'] as int,
xKbScanCode: map['scanCodes']['xkb'] as int,
windowsScanCode: map['scanCodes']['windows'] as int,
windowsKeyCodes: (map['keyCodes']['windows'] as List<dynamic>)?.cast<int>(),
windowsKeyNames: (map['names']['windows'] as List<dynamic>)?.cast<String>(),
macOsScanCode: map['scanCodes']['macos'] as int,
glfwKeyNames: (map['names']['glfw'] as List<dynamic>)?.cast<String>(),
glfwKeyCodes: (map['keyCodes']['glfw'] as List<dynamic>)?.cast<int>(),
gtkKeyNames: (map['names']['gtk'] as List<dynamic>)?.cast<String>(),
gtkKeyCodes: (map['keyCodes']['gtk'] as List<dynamic>)?.cast<int>(),
);
}
/// The USB HID code of the key
int usbHidCode;
/// The Linux scan code of the key, from Chromium's header file.
int linuxScanCode;
/// The XKb scan code of the key from Chromium's header file.
int xKbScanCode;
/// The Windows scan code of the key from Chromium's header file.
int windowsScanCode;
/// The list of Windows key codes matching this key, created by looking up the
/// Windows name in the Chromium data, and substituting the Windows key code
/// value.
List<int> windowsKeyCodes;
/// The list of names that Windows gives to this key (symbol names minus the
/// prefix).
List<String> windowsKeyNames;
/// The macOS scan code of the key from Chromium's header file.
int macOsScanCode;
/// 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.
String name;
/// The Chromium symbol name for the key.
String chromiumName;
/// The list of names that Android gives to this key (symbol names minus the
/// prefix).
List<String> androidKeyNames;
/// The list of Android key codes matching this key, created by looking up the
/// Android name in the Chromium data, and substituting the Android key code
/// value.
List<int> androidKeyCodes;
/// 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.
List<int> androidScanCodes;
/// The list of names that GFLW gives to this key (symbol names minus the
/// prefix).
List<String> glfwKeyNames;
/// 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.
List<int> glfwKeyCodes;
/// The list of names that GTK gives to this key (symbol names minus the
/// prefix).
List<String> gtkKeyNames;
/// The list of GTK key codes matching this key, created by looking up the
/// Linux name in the GTK data, and substituting the GTK key code
/// value.
List<int> gtkKeyCodes;
/// Creates a JSON map from the key data.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'names': <String, dynamic>{
'domkey': name,
'android': androidKeyNames,
'english': commentName,
'chromium': chromiumName,
'glfw': glfwKeyNames,
'gtk': gtkKeyNames,
'windows': windowsKeyNames,
},
'scanCodes': <String, dynamic>{
'android': androidScanCodes,
'usb': usbHidCode,
'linux': linuxScanCode,
'xkb': xKbScanCode,
'windows': windowsScanCode,
'macos': macOsScanCode,
},
'keyCodes': <String, List<int>>{
'android': androidKeyCodes,
'glfw': glfwKeyCodes,
'gtk': gtkKeyCodes,
'windows': windowsKeyCodes,
},
};
}
/// Returns the printable representation of this key, if any.
///
/// If there is no printable representation, returns null.
String get keyLabel => printable[constantName];
int get flutterId {
if (printable.containsKey(constantName) && !constantName.startsWith('numpad')) {
return unicodePlane | (keyLabel.codeUnitAt(0) & valueMask);
}
return hidPlane | (usbHidCode & valueMask);
}
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_keys.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).
String get constantName {
if (_constantName == null) {
String result;
if (name == null || name.isEmpty) {
// If it doesn't have a DomKey name then use the Chromium symbol name.
result = chromiumName;
} else {
result = upperCamelToLowerCamel(name);
}
if (kDartReservedWords.contains(result)) {
return '${result}Key';
}
// Don't set enumName: we want it to regen each time if never set, but
// to stay set if set by the JSON loading.
return result;
}
return _constantName;
}
set constantName(String value) => _constantName = value;
String _constantName;
@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: $chromiumName';
}
/// Returns the static map of printable representations.
static Map<String, String> get printable {
if (_printable == null) {
final String printableKeys = File(path.join(flutterRoot.path, 'dev', 'tools', 'gen_keycodes', 'data', 'printable.json',)).readAsStringSync();
final Map<String, dynamic> printable = json.decode(printableKeys) as Map<String, dynamic>;
_printable = printable.cast<String, String>();
}
return _printable;
}
static Map<String, String> _printable;
/// Returns the static map of synonym representations.
///
/// These include synonyms for keys which don't have printable
/// representations, and appear in more than one place on the keyboard (e.g.
/// SHIFT, ALT, etc.).
static Map<String, List<dynamic>> get synonyms {
if (_synonym == null) {
final String synonymKeys = File(path.join(flutterRoot.path, 'dev', 'tools', 'gen_keycodes', 'data', 'synonyms.json',)).readAsStringSync();
final Map<String, dynamic> synonym = json.decode(synonymKeys) as Map<String, dynamic>;
_synonym = synonym.cast<String, List<dynamic>>();
}
return _synonym;
}
static Map<String, List<dynamic>> _synonym;
/// Mask for the 32-bit value portion of the code.
static const int valueMask = 0x000FFFFFFFF;
/// The code prefix for keys which have a Unicode representation.
static const int unicodePlane = 0x00000000000;
/// The code prefix for keys which do not have a Unicode representation, but
/// do have a USB HID ID.
static const int hidPlane = 0x00100000000;
/// The code prefix for pseudo-keys which represent collections of key synonyms.
static const int synonymPlane = 0x20000000000;
}