Unverified Commit 65ea7671 authored by Tong Mu's avatar Tong Mu Committed by GitHub

[gen_keycode, RawKeyboard] Apply derived keyboard layout from Linux (#102709)

parent 5288ff8f
......@@ -206,6 +206,8 @@ Future<void> main(List<String> rawArguments) async {
logicalData = LogicalKeyData.fromJson(json.decode(await File(parsedArguments['logical-data'] as String).readAsString()) as Map<String, dynamic>);
}
final Map<String, bool> layoutGoals = parseMapOfBool(readDataFile('layout_goals.json'));
final File codeFile = File(parsedArguments['code'] as String);
if (!codeFile.existsSync()) {
codeFile.createSync(recursive: true);
......@@ -236,6 +238,7 @@ Future<void> main(List<String> rawArguments) async {
'macos': MacOSCodeGenerator(
physicalData,
logicalData,
layoutGoals,
),
'ios': IOSCodeGenerator(
physicalData,
......@@ -251,6 +254,7 @@ Future<void> main(List<String> rawArguments) async {
logicalData,
readDataFile('gtk_modifier_bit_mapping.json'),
readDataFile('gtk_lock_bit_mapping.json'),
layoutGoals,
),
'web': WebCodeGenerator(
physicalData,
......
......@@ -11,6 +11,7 @@
| [`chromium_modifiers.json`](chromium_modifiers.json) | Maps the web's `key` for modifier keys to the names of the logical keys for these keys' left and right variations.This is used when generating logical keys to provide independent values for sided logical keys. Web uses the same `key` for modifier keys of different sides, but Flutter's logical key model treats them as different keys.|
| [`printable.json`](printable.json) | Maps Flutter key name to its printable character. This character is used as the key label.|
| [`synonyms.json`](synonyms.json) | Maps pseudo-keys that represent other keys to the sets of keys they represent. For example, this contains the "shift" key that represents either a "shiftLeft" or "shiftRight" key.|
| [`layout_goals.json`](layout_goals.json) | A list of layout goals, keys that the platform keyboard manager should find mappings for. Each key in this file is the key name of the goal, both logical and physical simultaneously, while its value represents whether the goal is mandatory. A mandatory goal must be fulfilled, and the manager will use the default value from this file if a mapping can not be found. A non-mandatory goal is suggestive, only used if the key mapping information is malformed (e.g. contains no ASCII characters.) |
### Framework
......
......@@ -35,4 +35,8 @@ void initialize_lock_bit_to_checked_keys(GHashTable* table) {
@@@GTK_MODE_BIT_MAP@@@
}
const std::vector<LayoutGoal> layout_goals = {
@@@LAYOUT_GOALS@@@
};
@@@MASK_CONSTANTS@@@
{
"KeyA": true,
"KeyB": true,
"KeyC": true,
"KeyD": true,
"KeyE": true,
"KeyF": true,
"KeyG": true,
"KeyH": true,
"KeyI": true,
"KeyJ": true,
"KeyK": true,
"KeyL": true,
"KeyM": true,
"KeyN": true,
"KeyO": true,
"KeyP": true,
"KeyQ": true,
"KeyR": true,
"KeyS": true,
"KeyT": true,
"KeyU": true,
"KeyV": true,
"KeyW": true,
"KeyX": true,
"KeyY": true,
"KeyZ": true,
"Digit1": true,
"Digit2": true,
"Digit3": true,
"Digit4": true,
"Digit5": true,
"Digit6": true,
"Digit7": true,
"Digit8": true,
"Digit9": true,
"Digit0": true,
"Quote": false,
"Comma": false,
"Minus": false,
"Period": false,
"Slash": false,
"Semicolon": false,
"Equal": false,
"BracketLeft": false,
"Backslash": false,
"BracketRight": false,
"Backquote": false,
"IntlBackslash": false
}
......@@ -19,6 +19,7 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
super.logicalData,
String modifierBitMapping,
String lockBitMapping,
this._layoutGoals,
) : _modifierBitMapping = parseMapOfListOfString(modifierBitMapping),
_lockBitMapping = parseMapOfListOfString(lockBitMapping);
......@@ -91,6 +92,24 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
}
final Map<String, List<String>> _lockBitMapping;
final Map<String, bool> _layoutGoals;
String get _layoutGoalsString {
final OutputLines<int> lines = OutputLines<int>('GTK layout goals');
_layoutGoals.forEach((String name, bool mandatory) {
final PhysicalKeyEntry physicalEntry = keyData.entryByName(name);
final LogicalKeyEntry logicalEntry = logicalData.entryByName(name);
final String line = 'LayoutGoal{'
'${toHex(physicalEntry.xKbScanCode, digits: 2)}, '
'${toHex(logicalEntry.value, digits: 2)}, '
'${mandatory ? 'true' : 'false'}'
'},';
lines.add(logicalEntry.value,
' ${line.padRight(39)}'
'// ${logicalEntry.name}');
});
return lines.sortedJoin().trimRight();
}
/// This generates the mask values for the part of a key code that defines its plane.
String get _maskConstants {
final StringBuffer buffer = StringBuffer();
......@@ -120,6 +139,7 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
'GTK_MODIFIER_BIT_MAP': _gtkModifierBitMap,
'GTK_MODE_BIT_MAP': _gtkModeBitMap,
'MASK_CONSTANTS': _maskConstants,
'LAYOUT_GOALS': _layoutGoalsString,
};
}
}
......@@ -89,7 +89,7 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
String get _keyToModifierFlagMap {
final StringBuffer modifierKeyMap = StringBuffer();
for (final String name in kModifiersOfInterest) {
final String line =' {${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}, kModifierFlag${lowerCamelToUpperCamel(name)}},';
final String line = '{${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}, kModifierFlag${lowerCamelToUpperCamel(name)}},';
modifierKeyMap.writeln(' ${line.padRight(42)}// $name');
}
return modifierKeyMap.toString().trimRight();
......@@ -99,7 +99,7 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
String get _modifierFlagToKeyMap {
final StringBuffer modifierKeyMap = StringBuffer();
for (final String name in kModifiersOfInterest) {
final String line =' {kModifierFlag${lowerCamelToUpperCamel(name)}, ${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}},';
final String line = '{kModifierFlag${lowerCamelToUpperCamel(name)}, ${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}},';
modifierKeyMap.writeln(' ${line.padRight(42)}// $name');
}
return modifierKeyMap.toString().trimRight();
......
......@@ -28,7 +28,7 @@ const List<String> kSpecialLogicalKeys = <String>['CapsLock'];
/// Generates the key mapping for macOS, based on the information in the key
/// data structure given to it.
class MacOSCodeGenerator extends PlatformCodeGenerator {
MacOSCodeGenerator(super.keyData, super.logicalData);
MacOSCodeGenerator(super.keyData, super.logicalData, this._layoutGoals);
/// This generates the map of macOS key codes to physical keys.
String get _scanCodeMap {
......@@ -96,24 +96,21 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
return specialKeyConstants.toString().trimRight();
}
String get _layoutGoals {
final Map<String, bool> _layoutGoals;
String get _layoutGoalsString {
final OutputLines<int> lines = OutputLines<int>('macOS layout goals');
final Iterable<LogicalKeyEntry> asciiEntries = logicalData.entries.where(
(LogicalKeyEntry entry) => entry.value <= 128);
for (final LogicalKeyEntry logicalEntry in asciiEntries) {
final int value = logicalEntry.value;
final PhysicalKeyEntry? physicalEntry = keyData.tryEntryByName(logicalEntry.name);
if (physicalEntry == null) {
continue;
}
final bool mandatory = (value >= '0'.codeUnitAt(0) && value <= '9'.codeUnitAt(0))
|| (value >= 'a'.codeUnitAt(0) && value <= 'z'.codeUnitAt(0));
lines.add(value,
' LayoutGoal{${toHex(physicalEntry.macOSScanCode, digits: 2)}, '
'${toHex(value, digits: 2)}, '
'${mandatory ? 'true}, ' : 'false},'}'
' // ${logicalEntry.name}');
}
_layoutGoals.forEach((String name, bool mandatory) {
final PhysicalKeyEntry physicalEntry = keyData.entryByName(name);
final LogicalKeyEntry logicalEntry = logicalData.entryByName(name);
final String line = 'LayoutGoal{'
'${toHex(physicalEntry.macOSScanCode, digits: 2)}, '
'${toHex(logicalEntry.value, digits: 2)}, '
'${mandatory ? 'true' : 'false'}'
'},';
lines.add(logicalEntry.value,
' ${line.padRight(39)}'
'// ${logicalEntry.name}');
});
return lines.sortedJoin().trimRight();
}
......@@ -136,7 +133,7 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
'KEYCODE_TO_MODIFIER_FLAG_MAP': _keyToModifierFlagMap,
'MODIFIER_FLAG_TO_KEYCODE_MAP': _modifierFlagToKeyMap,
'SPECIAL_KEY_CONSTANTS': _specialKeyConstants,
'LAYOUT_GOALS': _layoutGoals,
'LAYOUT_GOALS': _layoutGoalsString,
};
}
}
......@@ -158,7 +158,7 @@ class PhysicalKeyData {
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 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')!);
......@@ -174,7 +174,7 @@ class PhysicalKeyData {
final PhysicalKeyEntry newEntry = PhysicalKeyEntry(
usbHidCode: usbHidCode,
androidScanCodes: nameToAndroidScanCodes[name] ?? <int>[],
linuxScanCode: linuxScanCode == 0 ? null : linuxScanCode,
evdevCode: evdevCode == 0 ? null : evdevCode,
xKbScanCode: xKbScanCode == 0 ? null : xKbScanCode,
windowsScanCode: windowsScanCode == 0 ? null : windowsScanCode,
macOSScanCode: macScanCode == 0xffff ? null : macScanCode,
......@@ -210,7 +210,7 @@ class PhysicalKeyEntry {
required this.usbHidCode,
required this.name,
required this.androidScanCodes,
required this.linuxScanCode,
required this.evdevCode,
required this.xKbScanCode,
required this.windowsScanCode,
required this.macOSScanCode,
......@@ -227,7 +227,7 @@ class PhysicalKeyEntry {
chromiumCode: names['chromium'] as String?,
usbHidCode: scanCodes['usb'] as int,
androidScanCodes: (scanCodes['android'] as List<dynamic>?)?.cast<int>() ?? <int>[],
linuxScanCode: scanCodes['linux'] as int?,
evdevCode: scanCodes['linux'] as int?,
xKbScanCode: scanCodes['xkb'] as int?,
windowsScanCode: scanCodes['windows'] as int?,
macOSScanCode: scanCodes['macos'] as int?,
......@@ -238,8 +238,8 @@ class PhysicalKeyEntry {
/// 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 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.
......@@ -269,7 +269,7 @@ class PhysicalKeyEntry {
'scanCodes': <String, dynamic>{
'android': androidScanCodes,
'usb': usbHidCode,
'linux': linuxScanCode,
'linux': evdevCode,
'xkb': xKbScanCode,
'windows': windowsScanCode,
'macos': macOSScanCode,
......@@ -318,7 +318,7 @@ class PhysicalKeyEntry {
@override
String toString() {
return """'$constantName': (name: "$name", usbHidCode: ${toHex(usbHidCode)}, """
'linuxScanCode: ${toHex(linuxScanCode)}, xKbScanCode: ${toHex(xKbScanCode)}, '
'linuxScanCode: ${toHex(evdevCode)}, xKbScanCode: ${toHex(xKbScanCode)}, '
'windowsKeyCode: ${toHex(windowsScanCode)}, macOSScanCode: ${toHex(macOSScanCode)}, '
'windowsScanCode: ${toHex(windowsScanCode)}, chromiumSymbolName: $chromiumCode '
'iOSScanCode: ${toHex(iOSScanCode)})';
......
......@@ -184,6 +184,10 @@ Map<String, List<String?>> parseMapOfListOfNullableString(String jsonString) {
});
}
Map<String, bool> parseMapOfBool(String jsonString) {
return (json.decode(jsonString) as Map<String, dynamic>).cast<String, bool>();
}
/// Reverse the map of { fromValue -> list of toValue } to { toValue -> fromValue } and return.
Map<String, String> reverseMapOfListOfString(Map<String, List<String>> inMap, void Function(String fromValue, String newToValue) onDuplicate) {
final Map<String, String> result = <String, String>{};
......
......@@ -2,7 +2,7 @@ name: gen_keycodes
description: Generates keycode source files from various resources.
environment:
sdk: ">=2.17.0-0 <3.0.0"
sdk: ">=2.18.0-0 <3.0.0"
dependencies:
args: 2.3.0
......
......@@ -26,6 +26,8 @@ final PhysicalKeyData physicalData = PhysicalKeyData.fromJson(
json.decode(readDataFile('physical_key_data.json')) as Map<String, dynamic>);
final LogicalKeyData logicalData = LogicalKeyData.fromJson(
json.decode(readDataFile('logical_key_data.json')) as Map<String, dynamic>);
final Map<String, bool> keyGoals = parseMapOfBool(
readDataFile('layout_goals.json'));
void main() {
setUp(() {
......@@ -65,6 +67,7 @@ void main() {
final PlatformCodeGenerator codeGenerator = MacOSCodeGenerator(
physicalData,
logicalData,
keyGoals,
);
final String output = codeGenerator.generate();
......@@ -119,6 +122,7 @@ void main() {
logicalData,
readDataFile(path.join(dataRoot, 'gtk_modifier_bit_mapping.json')),
readDataFile(path.join(dataRoot, 'gtk_lock_bit_mapping.json')),
keyGoals,
);
final String output = codeGenerator.generate();
......
......@@ -364,6 +364,7 @@ abstract class RawKeyEvent with Diagnosticable {
scanCode: message['scanCode'] as int? ?? 0,
modifiers: message['modifiers'] as int? ?? 0,
isDown: message['type'] == 'keydown',
specifiedLogicalKey: message['specifiedLogicalKey'] as int?,
);
if (unicodeScalarValues != 0) {
character = String.fromCharCode(unicodeScalarValues);
......
......@@ -28,6 +28,7 @@ class RawKeyEventDataLinux extends RawKeyEventData {
this.keyCode = 0,
this.modifiers = 0,
required this.isDown,
this.specifiedLogicalKey,
}) : assert(scanCode != null),
assert(unicodeScalarValues != null),
assert((unicodeScalarValues & ~LogicalKeyboardKey.valueMask) == 0),
......@@ -68,6 +69,15 @@ class RawKeyEventDataLinux extends RawKeyEventData {
/// Whether or not this key event is a key down (true) or key up (false).
final bool isDown;
/// A logical key specified by the embedding that should be used instead of
/// deriving from raw data.
///
/// The GTK embedding detects the keyboard layout and maps some keys to
/// logical keys in a way that can not be derived from per-key information.
///
/// This is not part of the native GTK key event.
final int? specifiedLogicalKey;
@override
String get keyLabel => unicodeScalarValues == 0 ? '' : String.fromCharCode(unicodeScalarValues);
......@@ -76,6 +86,10 @@ class RawKeyEventDataLinux extends RawKeyEventData {
@override
LogicalKeyboardKey get logicalKey {
if (specifiedLogicalKey != null) {
final int key = specifiedLogicalKey!;
return LogicalKeyboardKey.findKeyByKeyId(key) ?? LogicalKeyboardKey(key);
}
// Look to see if the keyCode is a printable number pad key, so that a
// difference between regular keys (e.g. "=") and the number pad version
// (e.g. the "=" on the number pad) can be determined.
......@@ -124,6 +138,7 @@ class RawKeyEventDataLinux extends RawKeyEventData {
properties.add(DiagnosticsProperty<int>('keyCode', keyCode));
properties.add(DiagnosticsProperty<int>('modifiers', modifiers));
properties.add(DiagnosticsProperty<bool>('isDown', isDown));
properties.add(DiagnosticsProperty<int?>('specifiedLogicalKey', specifiedLogicalKey, defaultValue: null));
}
@override
......
......@@ -246,6 +246,7 @@ class RawKeyEventDataMacOs extends RawKeyEventData {
properties.add(DiagnosticsProperty<String>('charactersIgnoringModifiers', charactersIgnoringModifiers));
properties.add(DiagnosticsProperty<int>('keyCode', keyCode));
properties.add(DiagnosticsProperty<int>('modifiers', modifiers));
properties.add(DiagnosticsProperty<int?>('specifiedLogicalKey', specifiedLogicalKey, defaultValue: null));
}
@override
......
......@@ -2421,6 +2421,21 @@ void main() {
expect(data.keyLabel, isEmpty);
});
test('Prioritize logical key from specifiedLogicalKey', () {
final RawKeyEvent digit1FromFrench = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'linux',
'toolkit': 'gtk',
'keyCode': 0x6c6,
'scanCode': 0x26,
'unicodeScalarValues': 0x424,
'specifiedLogicalKey': 0x61,
});
final RawKeyEventDataLinux data = digit1FromFrench.data as RawKeyEventDataLinux;
expect(data.physicalKey, equals(PhysicalKeyboardKey.keyA));
expect(data.logicalKey, equals(LogicalKeyboardKey.keyA));
});
test('data.toString', () {
expect(RawKeyEvent.fromMessage(const <String, Object?>{
'type': 'keydown',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment