| // Copyright 2019 The Chromium 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, | |
| ) : assert(chromiumHidCodes != null), | |
| assert(androidKeyboardLayout != null), | |
| assert(androidKeyCodeHeader != null), | |
| assert(androidNameMap != null), | |
| assert(glfwKeyCodeHeader != null), | |
| assert(glfwNameMap != null) { | |
| _nameToAndroidScanCodes = _readAndroidScanCodes(androidKeyboardLayout); | |
| _nameToAndroidKeyCode = _readAndroidKeyCodes(androidKeyCodeHeader); | |
| _nameToGlfwKeyCode = _readGlfwKeyCodes(glfwKeyCodeHeader); | |
| // Cast Android dom map | |
| final Map<String, List<dynamic>> dynamicAndroidNames = json.decode(androidNameMap).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).cast<String, List<dynamic>>(); | |
| _nameToGlfwName = dynamicGlfwNames.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 (String key in contentMap.keys) Key.fromJsonMapEntry(key, contentMap[key]), | |
| ]; | |
| } | |
| /// Converts the data structure into a JSON structure that can be parsed by | |
| /// [KeyData.fromJson]. | |
| Map<String, dynamic> toJson() { | |
| for (Key entry in data) { | |
| // Android Key names | |
| entry.androidKeyNames = _nameToAndroidName[entry.constantName]?.cast<String>(); | |
| if (entry.androidKeyNames != null && entry.androidKeyNames.isNotEmpty) { | |
| for (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 (String glfwKeyName in entry.glfwKeyNames) { | |
| if (_nameToGlfwKeyCode[glfwKeyName] != null) { | |
| entry.glfwKeyCodes ??= <int>[]; | |
| entry.glfwKeyCodes.add(_nameToGlfwKeyCode[glfwKeyName]); | |
| } | |
| } | |
| } | |
| } | |
| final Map<String, dynamic> outputMap = <String, dynamic>{}; | |
| for (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 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; | |
| /// 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 (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 enumEntry = RegExp(r'''define GLFW_KEY_([A-Z0-9_]+)\s*([A-Z0-9_]+),?'''); | |
| final Map<String, dynamic> replaced = <String, dynamic>{}; | |
| for (Match match in enumEntry.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]; | |
| } else { | |
| result[key] = value; | |
| } | |
| }); | |
| 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 | |
| /// USB_KEYMAP(0x000010, 0x0000, 0x0000, 0x0000, 0xffff, "Hyper", HYPER), | |
| List<Key> _readHidEntries(String input) { | |
| final List<Key> entries = <Key>[]; | |
| final RegExp usbMapRegExp = RegExp( | |
| r'''USB_KEYMAP\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 ''; | |
| } | |
| 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.macOsScanCode, | |
| @required this.chromiumName, | |
| this.androidKeyNames, | |
| this.androidScanCodes, | |
| this.androidKeyCodes, | |
| this.glfwKeyNames, | |
| this.glfwKeyCodes, | |
| }) : 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'], | |
| chromiumName: map['names']['chromium'], | |
| usbHidCode: map['scanCodes']['usb'], | |
| androidKeyNames: map['names']['android']?.cast<String>(), | |
| androidScanCodes: map['scanCodes']['android']?.cast<int>(), | |
| androidKeyCodes: map['keyCodes']['android']?.cast<int>(), | |
| linuxScanCode: map['scanCodes']['linux'], | |
| xKbScanCode: map['scanCodes']['xkb'], | |
| windowsScanCode: map['scanCodes']['windows'], | |
| macOsScanCode: map['scanCodes']['macos'], | |
| glfwKeyNames: map['names']['glfw']?.cast<String>(), | |
| glfwKeyCodes: map['keyCodes']['glfw']?.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 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; | |
| /// 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, | |
| }, | |
| 'scanCodes': <String, dynamic>{ | |
| 'android': androidScanCodes, | |
| 'usb': usbHidCode, | |
| 'linux': linuxScanCode, | |
| 'xkb': xKbScanCode, | |
| 'windows': windowsScanCode, | |
| 'macos': macOsScanCode, | |
| }, | |
| 'keyCodes': <String, List<int>>{ | |
| 'android': androidKeyCodes, | |
| 'glfw': glfwKeyCodes, | |
| }, | |
| }; | |
| } | |
| /// 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)}, ''' | |
| '''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); | |
| _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); | |
| _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; | |
| } |