Unverified Commit 431cfdaf authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Adding support for logical and physical key events (#27627)

This adds support for logical and physical key information inside of RawKeyEvent. This allows developers to differentiate keys in a platform-agnostic way. They are able to tell the physical location of a key (PhysicalKeyboardKey) and a logical meaning of the key (LogicalKeyboardKey), as well as get notified of the character generated by the keypress. All of which is useful for handling keyboard shortcuts.

This PR builds on the previous PR (#27620) which generated the key code mappings and definitions.
parent b7807ce8
......@@ -89,6 +89,8 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
dataText.add(Text('hidUsage: ${data.hidUsage} (${_asHex(data.hidUsage)})'));
dataText.add(Text('modifiers: ${data.modifiers} (${_asHex(data.modifiers)})'));
}
dataText.add(Text('logical: ${_event.logicalKey}'));
dataText.add(Text('physical: ${_event.physicalKey}'));
for (ModifierKey modifier in data.modifiersPressed.keys) {
for (KeyboardSide side in KeyboardSide.values) {
if (data.isModifierPressed(modifier, side: side)) {
......
......@@ -6,7 +6,7 @@
// This file is generated by dev/tools/gen_keycodes/bin/gen_keycodes.dart and
// should not be edited directly.
//
// Edit dev/tools/gen_keycodes/data/keyboard_maps.tmpl instead.
// Edit the template dev/tools/gen_keycodes/data/keyboard_maps.tmpl instead.
// See dev/tools/gen_keycodes/README.md for more information.
import 'keyboard_key.dart';
......
......@@ -15,32 +15,32 @@
"digit8": "8",
"digit9": "9",
"equal": "=",
"keyA": "A",
"keyB": "B",
"keyC": "C",
"keyD": "D",
"keyE": "E",
"keyF": "F",
"keyG": "G",
"keyH": "H",
"keyI": "I",
"keyJ": "J",
"keyK": "K",
"keyL": "L",
"keyM": "M",
"keyN": "N",
"keyO": "O",
"keyP": "P",
"keyQ": "Q",
"keyR": "R",
"keyS": "S",
"keyT": "T",
"keyU": "U",
"keyV": "V",
"keyW": "W",
"keyX": "X",
"keyY": "Y",
"keyZ": "Z",
"keyA": "a",
"keyB": "b",
"keyC": "c",
"keyD": "d",
"keyE": "e",
"keyF": "f",
"keyG": "g",
"keyH": "h",
"keyI": "i",
"keyJ": "j",
"keyK": "k",
"keyL": "l",
"keyM": "m",
"keyN": "n",
"keyO": "o",
"keyP": "p",
"keyQ": "q",
"keyR": "r",
"keyS": "s",
"keyT": "t",
"keyU": "u",
"keyV": "v",
"keyW": "w",
"keyX": "x",
"keyY": "y",
"keyZ": "z",
"minus": "-",
"numpad0": "0",
"numpad1": "1",
......
......@@ -15,7 +15,7 @@ class CodeGenerator {
/// Given an [input] string, wraps the text at 80 characters and prepends each
/// line with the [prefix] string. Use for generated comments.
String wrapString(String input, String prefix) {
String wrapString(String input, {String prefix = ' /// '}) {
final int wrapWidth = 80 - prefix.length;
final StringBuffer result = StringBuffer();
final List<String> words = input.split(RegExp(r'\s+'));
......@@ -38,12 +38,14 @@ class CodeGenerator {
String get physicalDefinitions {
final StringBuffer definitions = StringBuffer();
for (Key entry in keyData.data) {
final String comment = wrapString('Represents the location of a '
'"${entry.commentName}" key on a generalized keyboard. See the function '
'[RawKeyEvent.physicalKey] for more information.', ' /// ');
final String firstComment = wrapString('Represents the location of the '
'"${entry.commentName}" key on a generalized keyboard.');
final String otherComments = wrapString('See the function '
'[RawKeyEvent.physicalKey] for more information.');
definitions.write('''
$comment static const PhysicalKeyboardKey ${entry.constantName} = PhysicalKeyboardKey(${toHex(entry.usbHidCode, digits: 8)}, debugName: kReleaseMode ? null : '${entry.commentName}');
$firstComment ///
$otherComments static const PhysicalKeyboardKey ${entry.constantName} = PhysicalKeyboardKey(${toHex(entry.usbHidCode, digits: 8)}, debugName: kReleaseMode ? null : '${entry.commentName}');
''');
}
return definitions.toString();
......@@ -54,17 +56,19 @@ $comment static const PhysicalKeyboardKey ${entry.constantName} = PhysicalKeybo
String escapeLabel(String label) => label.contains("'") ? 'r"$label"' : "r'$label'";
final StringBuffer definitions = StringBuffer();
for (Key entry in keyData.data) {
final String comment = wrapString('Represents a logical "${entry.commentName}" key on the '
'keyboard. See the function [RawKeyEvent.logicalKey] for more information.', ' /// ');
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) {
definitions.write('''
$comment static const LogicalKeyboardKey ${entry.constantName} = LogicalKeyboardKey(${toHex(entry.flutterId, digits: 11)}, debugName: kReleaseMode ? null : '${entry.commentName}');
$firstComment ///
$otherComments static const LogicalKeyboardKey ${entry.constantName} = LogicalKeyboardKey(${toHex(entry.flutterId, digits: 11)}, debugName: kReleaseMode ? null : '${entry.commentName}');
''');
} else {
definitions.write('''
$comment static const LogicalKeyboardKey ${entry.constantName} = LogicalKeyboardKey(${toHex(entry.flutterId, digits: 11)}, keyLabel: ${escapeLabel(entry.keyLabel)}, debugName: kReleaseMode ? null : '${entry.commentName}');
$firstComment ///
$otherComments static const LogicalKeyboardKey ${entry.constantName} = LogicalKeyboardKey(${toHex(entry.flutterId, digits: 11)}, keyLabel: ${escapeLabel(entry.keyLabel)}, debugName: kReleaseMode ? null : '${entry.commentName}');
''');
}
}
......
......@@ -193,7 +193,7 @@ class KeyData {
/// A single entry in the key data structure.
///
/// Can be read from JSON with the [Key..fromJsonMapEntry] constructor, or
/// 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.
......
......@@ -15,6 +15,7 @@ export 'src/services/binding.dart';
export 'src/services/clipboard.dart';
export 'src/services/font_loader.dart';
export 'src/services/haptic_feedback.dart';
export 'src/services/keyboard_key.dart';
export 'src/services/message_codec.dart';
export 'src/services/message_codecs.dart';
export 'src/services/platform_channel.dart';
......
......@@ -152,6 +152,7 @@ abstract class DeletableChipAttributes {
/// have to do something similar to the following sample:
///
/// {@tool snippet --template=stateful_widget}
///
/// This sample shows how to use [onDeleted] to remove an entry when the
/// delete button is tapped.
///
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
......@@ -2,8 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'keyboard_key.dart';
import 'keyboard_maps.dart';
import 'raw_keyboard.dart';
// Android sets the 0x80000000 bit on a character to indicate that it is a
// combining character, so we use this mask to remove that bit to make it a
// valid Unicode character again.
const int _kCombiningCharacterMask = 0x7fffffff;
/// Platform-specific key event data for Android.
///
/// This object contains information about key events obtained from Android's
......@@ -82,6 +91,60 @@ class RawKeyEventDataAndroid extends RawKeyEventData {
/// * [isMetaPressed], to see if a META key is pressed.
final int metaState;
// Android only reports a single code point for the key label.
@override
String get keyLabel => codePoint == 0 ? null : String.fromCharCode(_combinedCodePoint).toLowerCase();
// Handles the logic for removing Android's "combining character" flag on the
// codePoint.
int get _combinedCodePoint => codePoint & _kCombiningCharacterMask;
// In order for letter keys to match the corresponding constant, they need to
// be a consistent case.
int get _lowercaseCodePoint => String.fromCharCode(_combinedCodePoint).toLowerCase().codeUnitAt(0);
@override
PhysicalKeyboardKey get physicalKey => kAndroidToPhysicalKey[scanCode] ?? PhysicalKeyboardKey.none;
@override
LogicalKeyboardKey get logicalKey {
// 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.
final LogicalKeyboardKey numPadKey = kAndroidNumPadMap[keyCode];
if (numPadKey != null) {
return numPadKey;
}
// If it has a non-control-character label, then either return the existing
// constant, or construct a new Unicode-based key from it. Don't mark it as
// autogenerated, since the label uniquely identifies an ID from the Unicode
// plane.
if (keyLabel != null && keyLabel.isNotEmpty && !LogicalKeyboardKey.isControlCharacter(keyLabel)) {
final int keyId = LogicalKeyboardKey.unicodePlane | (_lowercaseCodePoint & LogicalKeyboardKey.valueMask);
return LogicalKeyboardKey.findKeyByKeyId(keyId) ?? LogicalKeyboardKey(
keyId,
keyLabel: keyLabel,
debugName: kReleaseMode ? null : 'Key ${keyLabel.toUpperCase()}',
);
}
// Look to see if the keyCode is one we know about and have a mapping for.
LogicalKeyboardKey newKey = kAndroidToLogicalKey[keyCode];
if (newKey != null) {
return newKey;
}
// This is a non-printable key that we don't know about, so we mint a new
// code with the autogenerated bit set.
const int androidKeyIdPlane = 0x00200000000;
newKey ??= LogicalKeyboardKey(
androidKeyIdPlane | keyCode | LogicalKeyboardKey.autogeneratedMask,
debugName: kReleaseMode ? null : 'Unknown Android key code $keyCode',
);
return newKey;
}
bool _isLeftRightModifierPressed(KeyboardSide side, int anyMask, int leftMask, int rightMask) {
if (metaState & anyMask == 0) {
return false;
......@@ -308,7 +371,8 @@ class RawKeyEventDataAndroid extends RawKeyEventData {
@override
String toString() {
return '$runtimeType(flags: $flags, codePoint: $codePoint, keyCode: $keyCode, '
'scanCode: $scanCode, metaState: $metaState, modifiers down: $modifiersPressed)';
return '$runtimeType(keyLabel: $keyLabel flags: $flags, codePoint: $codePoint, '
'keyCode: $keyCode, scanCode: $scanCode, metaState: $metaState, '
'modifiers down: $modifiersPressed)';
}
}
......@@ -2,6 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'keyboard_key.dart';
import 'keyboard_maps.dart';
import 'raw_keyboard.dart';
/// Platform-specific key event data for Fuchsia.
......@@ -54,6 +58,41 @@ class RawKeyEventDataFuchsia extends RawKeyEventData {
/// * [isMetaPressed], to see if a META key is pressed.
final int modifiers;
// Fuchsia only reports a single code point for the key label.
@override
String get keyLabel => codePoint == 0 ? null : String.fromCharCode(codePoint);
@override
LogicalKeyboardKey get logicalKey {
// If the key has a printable representation, then make a logical key based
// on that.
if (codePoint != 0) {
return LogicalKeyboardKey(
LogicalKeyboardKey.unicodePlane | codePoint & LogicalKeyboardKey.valueMask,
keyLabel: keyLabel,
debugName: kReleaseMode ? null : 'Key $keyLabel',
);
}
// Look to see if the hidUsage is one we know about and have a mapping for.
LogicalKeyboardKey newKey = kFuchsiaToLogicalKey[hidUsage | LogicalKeyboardKey.hidPlane];
if (newKey != null) {
return newKey;
}
// This is a non-printable key that we don't know about, so we mint a new
// code with the autogenerated bit set.
const int fuchsiaKeyIdPlane = 0x00300000000;
newKey ??= LogicalKeyboardKey(
fuchsiaKeyIdPlane | hidUsage | LogicalKeyboardKey.autogeneratedMask,
debugName: kReleaseMode ? null : 'Ephemeral Fuchsia key code $hidUsage',
);
return newKey;
}
@override
PhysicalKeyboardKey get physicalKey => kFuchsiaToPhysicalKey[hidUsage] ?? PhysicalKeyboardKey.none;
bool _isLeftRightModifierPressed(KeyboardSide side, int anyMask, int leftMask, int rightMask) {
if (modifiers & anyMask == 0) {
return false;
......
// 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 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group(PhysicalKeyboardKey, () {
test('Various classes of keys can be looked up by code.', () async {
// Check a letter key
expect(PhysicalKeyboardKey.findKeyByCode(0x00070004), equals(PhysicalKeyboardKey.keyA));
// Check a control key
expect(PhysicalKeyboardKey.findKeyByCode(0x00070029), equals(PhysicalKeyboardKey.escape));
// Check a modifier key
expect(PhysicalKeyboardKey.findKeyByCode(0x000700e1), equals(PhysicalKeyboardKey.shiftLeft));
});
test('Equality is only based on HID code.', () async {
const PhysicalKeyboardKey key1 = PhysicalKeyboardKey(0x01, debugName: 'key1');
const PhysicalKeyboardKey key2 = PhysicalKeyboardKey(0x01, debugName: 'key2');
expect(key1, equals(key1));
expect(key1, equals(key2));
});
});
group(LogicalKeyboardKey, () {
test('Various classes of keys can be looked up by code', () async {
// Check a letter key
expect(LogicalKeyboardKey.findKeyByKeyId(0x0000000061), equals(LogicalKeyboardKey.keyA));
// Check a control key
expect(LogicalKeyboardKey.findKeyByKeyId(0x0100070029), equals(LogicalKeyboardKey.escape));
// Check a modifier key
expect(LogicalKeyboardKey.findKeyByKeyId(0x01000700e1), equals(LogicalKeyboardKey.shiftLeft));
});
test('Control characters are recognized as such', () async {
// Check some common control characters
expect(LogicalKeyboardKey.isControlCharacter('\x08'), isTrue); // BACKSPACE
expect(LogicalKeyboardKey.isControlCharacter('\x0a'), isTrue); // LINE FEED
expect(LogicalKeyboardKey.isControlCharacter('\x0d'), isTrue); // RETURN
expect(LogicalKeyboardKey.isControlCharacter('\x1b'), isTrue); // ESC
expect(LogicalKeyboardKey.isControlCharacter('\x7f'), isTrue); // DELETE
// Check non-control characters
expect(LogicalKeyboardKey.isControlCharacter('A'), isFalse);
expect(LogicalKeyboardKey.isControlCharacter(' '), isFalse);
expect(LogicalKeyboardKey.isControlCharacter('~'), isFalse);
expect(LogicalKeyboardKey.isControlCharacter('\xa0'), isFalse); // NO-BREAK SPACE
});
test('Equality is only based on ID.', () async {
const LogicalKeyboardKey key1 = LogicalKeyboardKey(0x01, keyLabel: 'label1', debugName: 'key1');
const LogicalKeyboardKey key2 = LogicalKeyboardKey(0x01, keyLabel: 'label2', debugName: 'key2');
expect(key1, equals(key1));
expect(key1, equals(key2));
});
});
}
\ No newline at end of file
......@@ -97,6 +97,51 @@ void main() {
}
}
});
test('Printable keyboard keys are correctly translated', () {
final RawKeyEvent keyAEvent = RawKeyEvent.fromMessage(<String, dynamic>{
'type': 'keydown',
'keymap': 'android',
'keyCode': 29,
'codePoint': 'A'.codeUnitAt(0),
'character': 'A',
'scanCode': 30,
'metaState': 0x0,
});
final RawKeyEventDataAndroid data = keyAEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.keyA));
expect(data.logicalKey, equals(LogicalKeyboardKey.keyA));
expect(data.keyLabel, equals('a'));
});
test('Control keyboard keys are correctly translated', () {
final RawKeyEvent escapeKeyEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'android',
'keyCode': 111,
'codePoint': null,
'character': null,
'scanCode': 1,
'metaState': 0x0,
});
final RawKeyEventDataAndroid data = escapeKeyEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.escape));
expect(data.logicalKey, equals(LogicalKeyboardKey.escape));
expect(data.keyLabel, isNull);
});
test('Modifier keyboard keys are correctly translated', () {
final RawKeyEvent shiftLeftKeyEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'android',
'keyCode': 59,
'codePoint': null,
'character': null,
'scanCode': 42,
'metaState': RawKeyEventDataAndroid.modifierLeftShift,
});
final RawKeyEventDataAndroid data = shiftLeftKeyEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.shiftLeft));
expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
expect(data.keyLabel, isNull);
});
});
group('RawKeyEventDataFuchsia', () {
const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{
......@@ -175,5 +220,44 @@ void main() {
}
}
});
test('Printable keyboard keys are correctly translated', () {
final RawKeyEvent keyAEvent = RawKeyEvent.fromMessage(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x00070004,
'codePoint': 'a'.codeUnitAt(0),
'character': 'a',
});
final RawKeyEventDataFuchsia data = keyAEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.keyA));
expect(data.logicalKey, equals(LogicalKeyboardKey.keyA));
expect(data.keyLabel, equals('a'));
});
test('Control keyboard keys are correctly translated', () {
final RawKeyEvent escapeKeyEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x00070029,
'codePoint': null,
'character': null,
});
final RawKeyEventDataFuchsia data = escapeKeyEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.escape));
expect(data.logicalKey, equals(LogicalKeyboardKey.escape));
expect(data.keyLabel, isNull);
});
test('Modifier keyboard keys are correctly translated', () {
final RawKeyEvent shiftLeftKeyEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x000700e1,
'codePoint': null,
'character': null,
});
final RawKeyEventDataFuchsia data = shiftLeftKeyEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.shiftLeft));
expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
expect(data.keyLabel, isNull);
});
});
}
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