Unverified Commit a70b020e authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add pseudo-key synonyms for keys like shift, meta, alt, and control. (#33695)

This adds a list of key synonyms for non-printable keyboard keys that appear in more than one place So keys like LogicalKeyboardKey.shiftLeft and LogicalKeyboardKey.shiftRight now can be mapped to just LogicalKeyboardKey.shift.

I also fixed a bug in the gen_keycodes tool where GLFW entries would get removed if they weren't parsed from the source on the web.
parent a5a5595c
......@@ -27,6 +27,9 @@ used to generate the source files.
generated data will be inserted.
- `data/printable.json`: contains a mapping between Flutter key name and its
printable character. This character is used as the key label.
- `data/synonyms.json`: contains a mapping between pseudo-keys that represent
other keys, and the sets of keys they represent. For example, this contains
the "shift" key that represents either a "shiftLeft" or "shiftRight" key.
## Running the tool
......@@ -134,6 +137,13 @@ define. It has values in the following ranges.
that their version of Flutter doesn’t support yet. The prefix for this code
is the platform prefix from the previous sections, plus 0x100.
- **0x200 0000 0000 - 0x2FF FFFF FFFF**: For pseudo-keys which represent
combinations of other keys, and conceptual keys which don't have a physical
representation. This is where things like key synonyms are defined (e.g.
"shiftLeft" is a synonym for "shift": the "shift" key is a pseudo-key
representing either the left or right shift key).
**This is intended to get us out of the business of defining key codes where
possible.** We still have to have mapping tables, but at least the actual minting
of codes is deferred to other organizations to a large extent. Coming up with a
......@@ -167,4 +177,4 @@ HID codes, these are not necessarily the same HID codes produced by the hardware
and presented to the driver, since on most platforms we have to map the platform
representation back to a HID code because we don’t have access to the original
HID code. USB HID is simply a conveniently well-defined standard that includes
many of the keys we would want.
\ No newline at end of file
many of the keys we would want.
......@@ -166,14 +166,6 @@ class LogicalKeyboardKey extends Diagnosticable {
/// null, if not found.
static LogicalKeyboardKey findKeyByKeyId(int keyId) => _knownLogicalKeys[keyId];
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('keyId', '0x${keyId.toRadixString(16).padLeft(8, '0')}', showName: true));
properties.add(StringProperty('keyLabel', keyLabel, showName: true));
properties.add(StringProperty('debugName', debugName, showName: true, defaultValue: null));
}
/// Returns true if the given label represents a Unicode control character.
///
/// Examples of control characters are characters like "U+000A LINE FEED (LF)"
......@@ -215,10 +207,35 @@ class LogicalKeyboardKey extends Diagnosticable {
/// platforms that had a "do what I mean" key from then on.
bool get isAutogenerated => (keyId & autogeneratedMask) != 0;
/// Returns a set of pseudo-key synonyms for the given `key`.
///
/// This allows finding the pseudo-keys that also represents a concrete
/// `key` so that a class with a key map can match pseudo-keys as well as the
/// actual generated keys.
///
/// The pseudo-keys returned in the set are typically used to represent keys
/// which appear in multiple places on the keyboard, such as the [shift],
/// [alt], [control], and [meta] keys. The keys in the returned set won't ever
/// be generated directly, but if a more specific key event is received, then
/// this set can be used to find the more general pseudo-key. For example, if
/// this is a [shiftLeft] key, this accessor will return the set
/// `<LogicalKeyboardKey>{ shift }`.
Set<LogicalKeyboardKey> get synonyms {
final LogicalKeyboardKey result = _synonyms[this];
return result == null ? <LogicalKeyboardKey>{} : <LogicalKeyboardKey>{result};
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('keyId', '0x${keyId.toRadixString(16).padLeft(8, '0')}', showName: true));
properties.add(StringProperty('keyLabel', keyLabel, showName: true));
properties.add(StringProperty('debugName', debugName, showName: true, defaultValue: null));
}
/// Mask for the 32-bit value portion of the key code.
///
/// This is used by
/// platform-specific code to generate Flutter key codes.
/// This is used by platform-specific code to generate Flutter key codes.
static const int valueMask = 0x000FFFFFFFF;
/// Mask for the platform prefix portion of the key code.
......@@ -248,6 +265,10 @@ class LogicalKeyboardKey extends Diagnosticable {
static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{
@@@LOGICAL_KEY_MAP@@@
};
// A map of keys to the pseudo-key synonym for that key. Used by getSynonyms.
static final Map<LogicalKeyboardKey, LogicalKeyboardKey> _synonyms = <LogicalKeyboardKey, LogicalKeyboardKey>{
@@@LOGICAL_KEY_SYNONYMS@@@ };
}
/// A class with static values that describe the keys that are returned from
......
{
"shift": ["ShiftLeft", "ShiftRight"],
"meta": ["MetaLeft", "MetaRight"],
"alt": ["AltLeft", "AltRight"],
"control": ["ControlLeft", "ControlRight"]
}
......@@ -55,26 +55,59 @@ $otherComments static const PhysicalKeyboardKey ${entry.constantName} = Physica
String get logicalDefinitions {
String escapeLabel(String label) => label.contains("'") ? 'r"$label"' : "r'$label'";
final StringBuffer definitions = StringBuffer();
for (Key entry in keyData.data) {
final String firstComment = wrapString('Represents the logical "${entry.commentName}" key on the keyboard.');
final String otherComments = wrapString('See the function [RawKeyEvent.logicalKey] for more information.');
if (entry.keyLabel == null) {
void printKey(int flutterId, String keyLabel, String constantName, String commentName, {String otherComments}) {
final String firstComment = wrapString('Represents the logical "$commentName" key on the keyboard.');
otherComments ??= wrapString('See the function [RawKeyEvent.logicalKey] for more information.');
if (keyLabel == null) {
definitions.write('''
$firstComment ///
$otherComments static const LogicalKeyboardKey ${entry.constantName} = LogicalKeyboardKey(${toHex(entry.flutterId, digits: 11)}, debugName: kReleaseMode ? null : '${entry.commentName}');
$otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardKey(${toHex(flutterId, digits: 11)}, debugName: kReleaseMode ? null : '$commentName');
''');
} else {
definitions.write('''
$firstComment ///
$otherComments static const LogicalKeyboardKey ${entry.constantName} = LogicalKeyboardKey(${toHex(entry.flutterId, digits: 11)}, keyLabel: ${escapeLabel(entry.keyLabel)}, debugName: kReleaseMode ? null : '${entry.commentName}');
$otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardKey(${toHex(flutterId, digits: 11)}, keyLabel: ${escapeLabel(keyLabel)}, debugName: kReleaseMode ? null : '$commentName');
''');
}
}
for (Key entry in keyData.data) {
printKey(
entry.flutterId,
entry.keyLabel,
entry.constantName,
entry.commentName,
);
}
for (String name in Key.synonyms.keys) {
// Use the first item in the synonyms as a template for the ID to use.
// It won't end up being the same value because it'll be in the pseudo-key
// plane.
final Key entry = keyData.data.firstWhere((Key item) => item.name == Key.synonyms[name][0]);
final Set<String> unionNames = Key.synonyms[name].map<String>((dynamic name) {
return upperCamelToLowerCamel(name);
}).toSet();
printKey(Key.synonymPlane | entry.flutterId, entry.keyLabel, name, Key.getCommentName(name),
otherComments: wrapString('This key represents the union of the keys '
'$unionNames when comparing keys. This key will never be generated '
'directly, its main use is in defining key maps.'));
}
return definitions.toString();
}
String get logicalSynonyms {
final StringBuffer synonyms = StringBuffer();
for (String name in Key.synonyms.keys) {
for (String synonym in Key.synonyms[name]) {
final String keyName = upperCamelToLowerCamel(synonym);
synonyms.writeln(' $keyName: $name,');
}
}
return synonyms.toString();
}
/// This generates the map of USB HID codes to physical keys.
String get predefinedHidCodeMap {
final StringBuffer scanCodeMap = StringBuffer();
......@@ -90,6 +123,16 @@ $otherComments static const LogicalKeyboardKey ${entry.constantName} = LogicalK
for (Key entry in keyData.data) {
keyCodeMap.writeln(' ${toHex(entry.flutterId, digits: 10)}: ${entry.constantName},');
}
for (String entry in Key.synonyms.keys) {
// Use the first item in the synonyms as a template for the ID to use.
// It won't end up being the same value because it'll be in the pseudo-key
// plane.
final Key primaryKey = keyData.data.firstWhere((Key item) {
return item.name == Key.synonyms[entry][0];
}, orElse: () => null);
assert(primaryKey != null);
keyCodeMap.writeln(' ${toHex(Key.synonymPlane | primaryKey.flutterId, digits: 10)}: $entry,');
}
return keyCodeMap.toString().trimRight();
}
......@@ -229,6 +272,7 @@ $otherComments static const LogicalKeyboardKey ${entry.constantName} = LogicalK
'PHYSICAL_KEY_MAP': predefinedHidCodeMap,
'LOGICAL_KEY_MAP': predefinedKeyCodeMap,
'LOGICAL_KEY_DEFINITIONS': logicalDefinitions,
'LOGICAL_KEY_SYNONYMS': logicalSynonyms,
'PHYSICAL_KEY_DEFINITIONS': physicalDefinitions,
};
......
......@@ -206,7 +206,7 @@ class KeyData {
result[key] = value;
}
});
return result;
return result;
}
/// Parses entries from Chromium's HID code mapping header file.
......@@ -293,6 +293,8 @@ class Key {
xKbScanCode: map['scanCodes']['xkb'],
windowsScanCode: map['scanCodes']['windows'],
macOsScanCode: map['scanCodes']['macos'],
glfwKeyNames: map['names']['glfw']?.cast<String>(),
glfwKeyCodes: map['keyCodes']['glfw']?.cast<int>(),
);
}
......@@ -371,16 +373,18 @@ class Key {
return hidPlane | (usbHidCode & valueMask);
}
/// 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 {
static String getCommentName(String constantName) {
String upperCamel = lowerCamelToUpperCamel(constantName);
upperCamel = upperCamel.replaceAllMapped(RegExp(r'(Digit|Numpad|Lang)([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.
///
......@@ -428,6 +432,21 @@ class Key {
}
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;
......@@ -437,4 +456,7 @@ class Key {
/// 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;
}
......@@ -166,14 +166,6 @@ class LogicalKeyboardKey extends Diagnosticable {
/// null, if not found.
static LogicalKeyboardKey findKeyByKeyId(int keyId) => _knownLogicalKeys[keyId];
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('keyId', '0x${keyId.toRadixString(16).padLeft(8, '0')}', showName: true));
properties.add(StringProperty('keyLabel', keyLabel, showName: true));
properties.add(StringProperty('debugName', debugName, showName: true, defaultValue: null));
}
/// Returns true if the given label represents a Unicode control character.
///
/// Examples of control characters are characters like "U+000A LINE FEED (LF)"
......@@ -215,10 +207,35 @@ class LogicalKeyboardKey extends Diagnosticable {
/// platforms that had a "do what I mean" key from then on.
bool get isAutogenerated => (keyId & autogeneratedMask) != 0;
/// Returns a set of pseudo-key synonyms for the given `key`.
///
/// This allows finding the pseudo-keys that also represents a concrete
/// `key` so that a class with a key map can match pseudo-keys as well as the
/// actual generated keys.
///
/// The pseudo-keys returned in the set are typically used to represent keys
/// which appear in multiple places on the keyboard, such as the [shift],
/// [alt], [control], and [meta] keys. The keys in the returned set won't ever
/// be generated directly, but if a more specific key event is received, then
/// this set can be used to find the more general pseudo-key. For example, if
/// this is a [shiftLeft] key, this accessor will return the set
/// `<LogicalKeyboardKey>{ shift }`.
Set<LogicalKeyboardKey> get synonyms {
final LogicalKeyboardKey result = _synonyms[this];
return result == null ? <LogicalKeyboardKey>{} : <LogicalKeyboardKey>{result};
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('keyId', '0x${keyId.toRadixString(16).padLeft(8, '0')}', showName: true));
properties.add(StringProperty('keyLabel', keyLabel, showName: true));
properties.add(StringProperty('debugName', debugName, showName: true, defaultValue: null));
}
/// Mask for the 32-bit value portion of the key code.
///
/// This is used by
/// platform-specific code to generate Flutter key codes.
/// This is used by platform-specific code to generate Flutter key codes.
static const int valueMask = 0x000FFFFFFFF;
/// Mask for the platform prefix portion of the key code.
......@@ -1413,6 +1430,34 @@ class LogicalKeyboardKey extends Diagnosticable {
/// See the function [RawKeyEvent.logicalKey] for more information.
static const LogicalKeyboardKey showAllWindows = LogicalKeyboardKey(0x001000c029f, debugName: kReleaseMode ? null : 'Show All Windows');
/// Represents the logical "Shift" key on the keyboard.
///
/// This key represents the union of the keys {shiftLeft, shiftRight} when
/// comparing keys. This key will never be generated directly, its main use is
/// in defining key maps.
static const LogicalKeyboardKey shift = LogicalKeyboardKey(0x201000700e1, debugName: kReleaseMode ? null : 'Shift');
/// Represents the logical "Meta" key on the keyboard.
///
/// This key represents the union of the keys {metaLeft, metaRight} when
/// comparing keys. This key will never be generated directly, its main use is
/// in defining key maps.
static const LogicalKeyboardKey meta = LogicalKeyboardKey(0x201000700e3, debugName: kReleaseMode ? null : 'Meta');
/// Represents the logical "Alt" key on the keyboard.
///
/// This key represents the union of the keys {altLeft, altRight} when
/// comparing keys. This key will never be generated directly, its main use is
/// in defining key maps.
static const LogicalKeyboardKey alt = LogicalKeyboardKey(0x201000700e2, debugName: kReleaseMode ? null : 'Alt');
/// Represents the logical "Control" key on the keyboard.
///
/// This key represents the union of the keys {controlLeft, controlRight} when
/// comparing keys. This key will never be generated directly, its main use is
/// in defining key maps.
static const LogicalKeyboardKey control = LogicalKeyboardKey(0x201000700e0, debugName: kReleaseMode ? null : 'Control');
// A list of all predefined constant LogicalKeyboardKeys so they can be
// searched.
static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{
......@@ -1650,6 +1695,22 @@ class LogicalKeyboardKey extends Diagnosticable {
0x01000c028c: mailSend,
0x01000c029d: keyboardLayoutSelect,
0x01000c029f: showAllWindows,
0x201000700e1: shift,
0x201000700e3: meta,
0x201000700e2: alt,
0x201000700e0: control,
};
// A map of keys to the pseudo-key synonym for that key. Used by getSynonyms.
static final Map<LogicalKeyboardKey, LogicalKeyboardKey> _synonyms = <LogicalKeyboardKey, LogicalKeyboardKey>{
shiftLeft: shift,
shiftRight: shift,
metaLeft: meta,
metaRight: meta,
altLeft: alt,
altRight: alt,
controlLeft: control,
controlRight: control,
};
}
......
......@@ -50,5 +50,15 @@ void main() {
expect(key1, equals(key1));
expect(key1, equals(key2));
});
test('Basic synonyms can be looked up.', () async {
expect(LogicalKeyboardKey.shiftLeft.synonyms.first, equals(LogicalKeyboardKey.shift));
expect(LogicalKeyboardKey.controlLeft.synonyms.first, equals(LogicalKeyboardKey.control));
expect(LogicalKeyboardKey.altLeft.synonyms.first, equals(LogicalKeyboardKey.alt));
expect(LogicalKeyboardKey.metaLeft.synonyms.first, equals(LogicalKeyboardKey.meta));
expect(LogicalKeyboardKey.shiftRight.synonyms.first, equals(LogicalKeyboardKey.shift));
expect(LogicalKeyboardKey.controlRight.synonyms.first, equals(LogicalKeyboardKey.control));
expect(LogicalKeyboardKey.altRight.synonyms.first, equals(LogicalKeyboardKey.alt));
expect(LogicalKeyboardKey.metaRight.synonyms.first, equals(LogicalKeyboardKey.meta));
});
});
}
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