flutter_flutter/dev/tools/gen_keycodes/lib/physical_key_data.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

371 lines
13 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 '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,
) {
final Map<String, List<int>> nameToAndroidScanCodes = _readAndroidScanCodes(
androidKeyboardLayout,
androidNameMap,
);
final Map<String, PhysicalKeyEntry> data = _readHidEntries(
chromiumHidCodes,
nameToAndroidScanCodes,
);
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 data = <String, PhysicalKeyEntry>{};
for (final MapEntry<String, dynamic> jsonEntry in contentMap.entries) {
final 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 outputMap = <String, dynamic>{};
for (final PhysicalKeyEntry entry in _data.values) {
outputMap[entry.name] = entry.toJson();
}
return outputMap;
}
/// Parses entries from Android's `Generic.kl` scan code data file.
///
/// Lines in this file look like this (without the ///):
///
/// ```none
/// 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.
///
/// 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 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 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 scanCodes = <int>{};
for (final androidName in androidNames) {
scanCodes.addAll(androidNameToScanCodes[androidName] ?? <int>[]);
}
return MapEntry<String, List<int>>(name, scanCodes.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,
) {
final entries = <int, PhysicalKeyEntry>{};
final 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 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 evdevCode = 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? existing = entries[usbHidCode];
// Allow duplicate entries for Fn, which overwrites.
if (existing != null && existing.name != 'Fn') {
// If it's an existing entry, the only thing we currently support is
// to insert an extra DOMKey. The other entries must be empty.
assert(
evdevCode == 0 &&
xKbScanCode == 0 &&
windowsScanCode == 0 &&
macScanCode == 0xffff &&
chromiumCode != null &&
chromiumCode.isNotEmpty,
'Duplicate usbHidCode ${existing.usbHidCode} of key ${existing.name} '
'conflicts with existing ${entries[existing.usbHidCode]!.name}.',
);
existing.otherWebCodes.add(chromiumCode!);
continue;
}
final newEntry = PhysicalKeyEntry(
usbHidCode: usbHidCode,
androidScanCodes: nameToAndroidScanCodes[name] ?? <int>[],
evdevCode: evdevCode == 0 ? null : evdevCode,
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,
);
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.
PhysicalKeyEntry({
required this.usbHidCode,
required this.name,
required this.androidScanCodes,
required this.evdevCode,
required this.xKbScanCode,
required this.windowsScanCode,
required this.macOSScanCode,
required this.iOSScanCode,
required this.chromiumCode,
List<String>? otherWebCodes,
}) : otherWebCodes = otherWebCodes ?? <String>[];
/// Populates the key from a JSON map.
factory PhysicalKeyEntry.fromJsonMapEntry(Map<String, dynamic> map) {
final names = map['names'] as Map<String, dynamic>;
final scanCodes = map['scanCodes'] 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>[],
evdevCode: scanCodes['linux'] as int?,
xKbScanCode: scanCodes['xkb'] as int?,
windowsScanCode: scanCodes['windows'] as int?,
macOSScanCode: scanCodes['macos'] as int?,
iOSScanCode: scanCodes['ios'] as int?,
otherWebCodes: (map['otherWebCodes'] as List<dynamic>?)?.cast<String>(),
);
}
/// The USB HID code of the key
final int usbHidCode;
/// The Evdev scan code of the key, from Chromium's header file.
final int? evdevCode;
/// 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 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;
/// Other codes used by Web besides chromiumCode.
final List<String> otherWebCodes;
Iterable<String> webCodes() sync* {
if (chromiumCode != null) {
yield chromiumCode!;
}
yield* otherWebCodes;
}
/// Creates a JSON map from the key data.
Map<String, dynamic> toJson() {
return removeEmptyValues(<String, dynamic>{
'names': <String, dynamic>{'name': name, 'chromium': chromiumCode},
'otherWebCodes': otherWebCodes,
'scanCodes': <String, dynamic>{
'android': androidScanCodes,
'usb': usbHidCode,
'linux': evdevCode,
'xkb': xKbScanCode,
'windows': windowsScanCode,
'macos': macOSScanCode,
'ios': iOSScanCode,
},
});
}
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.g.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() {
final otherWebStr = otherWebCodes.isEmpty
? ''
: ', otherWebCodes: [${otherWebCodes.join(', ')}]';
return """'$constantName': (name: "$name", usbHidCode: ${toHex(usbHidCode)}, """
'linuxScanCode: ${toHex(evdevCode)}, xKbScanCode: ${toHex(xKbScanCode)}, '
'windowsKeyCode: ${toHex(windowsScanCode)}, macOSScanCode: ${toHex(macOSScanCode)}, '
'windowsScanCode: ${toHex(windowsScanCode)}, chromiumSymbolName: $chromiumCode '
'iOSScanCode: ${toHex(iOSScanCode)})$otherWebStr';
}
static int compareByUsbHidCode(PhysicalKeyEntry a, PhysicalKeyEntry b) =>
a.usbHidCode.compareTo(b.usbHidCode);
}