| // 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, |
| ) : 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) as Map<String, List<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, List<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>()); |
| }); |
| 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, List<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]); |
| } |
| } |
| } |
| } |
| |
| 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 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 (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 enumEntry = RegExp(r'''define GLFW_KEY_([A-Z0-9_]+)\s*([A-Z0-9_]+),?'''); |
| final Map<String, dynamic> replaced = <String, dynamic>{}; |
| for (final 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] as int; |
| } else { |
| result[key] = value as int; |
| } |
| }); |
| 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 ''; |
| } |
| // 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.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'] 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, |
| macOsScanCode: map['scanCodes']['macos'] as int, |
| glfwKeyNames: (map['names']['glfw'] as List<dynamic>)?.cast<String>(), |
| glfwKeyCodes: (map['keyCodes']['glfw'] 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 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) 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; |
| } |