Unverified Commit 141d6e13 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add support for detecting which modifier keys have been pressed on RawKeyboardEvents (#26265)

This adds some functions to the interface for RawKeyEventData and all subclasses that allow the recipient of an event to determine which modifier keys are currently being pressed without needing to know the specific modifier bitmasks for the platform.

Also adds constants for the modifier bitmasks for each platform, for completeness (and because I needed them anyhow to implement the above).

Added tests for the RawKeyEventData subclasses, and modified the raw_keyboard manual test app to show modifier keys being pressed. I also separated the different platform-specific subclasses into separate files.

Fixes #26155.
parent 8af3e480
...@@ -20,7 +20,7 @@ void main() { ...@@ -20,7 +20,7 @@ void main() {
} }
class RawKeyboardDemo extends StatefulWidget { class RawKeyboardDemo extends StatefulWidget {
const RawKeyboardDemo({ Key key }) : super(key: key); const RawKeyboardDemo({Key key}) : super(key: key);
@override @override
_HardwareKeyDemoState createState() => _HardwareKeyDemoState(); _HardwareKeyDemoState createState() => _HardwareKeyDemoState();
...@@ -42,6 +42,14 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> { ...@@ -42,6 +42,14 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
}); });
} }
String _asHex(int value) => value != null ? '0x${value.toRadixString(16)}' : 'null';
String _getEnumName(dynamic enumItem) {
final String name = '$enumItem';
final int index = name.indexOf('.');
return index == -1 ? name : name.substring(index + 1);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme; final TextTheme textTheme = Theme.of(context).textTheme;
...@@ -60,28 +68,42 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> { ...@@ -60,28 +68,42 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
); );
} }
if (_event == null) if (_event == null) {
return Text('Press a key', style: textTheme.display1); return Text('Press a key', style: textTheme.display1);
}
int codePoint;
int keyCode;
int hidUsage;
final RawKeyEventData data = _event.data; final RawKeyEventData data = _event.data;
final String modifierList = data.modifiersPressed.keys.map<String>(_getEnumName).join(', ').replaceAll('Modifier', '');
final List<Widget> dataText = <Widget>[
Text('${_event.runtimeType}'),
Text('modifiers set: $modifierList'),
];
if (data is RawKeyEventDataAndroid) { if (data is RawKeyEventDataAndroid) {
codePoint = data.codePoint; dataText.add(Text('codePoint: ${data.codePoint} (${_asHex(data.codePoint)})'));
keyCode = data.keyCode; dataText.add(Text('keyCode: ${data.keyCode} (${_asHex(data.keyCode)})'));
dataText.add(Text('scanCode: ${data.scanCode} (${_asHex(data.scanCode)})'));
dataText.add(Text('metaState: ${data.metaState} (${_asHex(data.metaState)})'));
dataText.add(Text('flags: ${data.flags} (${_asHex(data.flags)})'));
} else if (data is RawKeyEventDataFuchsia) { } else if (data is RawKeyEventDataFuchsia) {
codePoint = data.codePoint; dataText.add(Text('codePoint: ${data.codePoint} (${_asHex(data.codePoint)})'));
hidUsage = data.hidUsage; dataText.add(Text('hidUsage: ${data.hidUsage} (${_asHex(data.hidUsage)})'));
dataText.add(Text('modifiers: ${data.modifiers} (${_asHex(data.modifiers)})'));
} }
return Column( for (ModifierKey modifier in data.modifiersPressed.keys) {
for (KeyboardSide side in KeyboardSide.values) {
if (data.isModifierPressed(modifier, side: side)) {
dataText.add(
Text('${_getEnumName(side)} ${_getEnumName(modifier).replaceAll('Modifier', '')} pressed'),
);
}
}
}
return DefaultTextStyle(
style: textTheme.headline,
child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: dataText,
Text('${_event.runtimeType}', style: textTheme.body2), ),
Text('codePoint: $codePoint', style: textTheme.display4),
Text('keyCode: $keyCode', style: textTheme.display4),
Text('hidUsage: $hidUsage', style: textTheme.display4),
],
); );
}, },
), ),
......
...@@ -21,6 +21,8 @@ export 'src/services/platform_channel.dart'; ...@@ -21,6 +21,8 @@ export 'src/services/platform_channel.dart';
export 'src/services/platform_messages.dart'; export 'src/services/platform_messages.dart';
export 'src/services/platform_views.dart'; export 'src/services/platform_views.dart';
export 'src/services/raw_keyboard.dart'; export 'src/services/raw_keyboard.dart';
export 'src/services/raw_keyboard_android.dart';
export 'src/services/raw_keyboard_fuschia.dart';
export 'src/services/system_channels.dart'; export 'src/services/system_channels.dart';
export 'src/services/system_chrome.dart'; export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart'; export 'src/services/system_navigator.dart';
......
This diff is collapsed.
This diff is collapsed.
// 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';
class _ModifierCheck {
const _ModifierCheck(this.key, this.side);
final ModifierKey key;
final KeyboardSide side;
}
void main() {
group('RawKeyEventDataAndroid', () {
const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{
RawKeyEventDataAndroid.modifierAlt | RawKeyEventDataAndroid.modifierLeftAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.left),
RawKeyEventDataAndroid.modifierAlt | RawKeyEventDataAndroid.modifierRightAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.right),
RawKeyEventDataAndroid.modifierShift | RawKeyEventDataAndroid.modifierLeftShift: _ModifierCheck(ModifierKey.shiftModifier, KeyboardSide.left),
RawKeyEventDataAndroid.modifierShift | RawKeyEventDataAndroid.modifierRightShift: _ModifierCheck(ModifierKey.shiftModifier, KeyboardSide.right),
RawKeyEventDataAndroid.modifierSym: _ModifierCheck(ModifierKey.symbolModifier, KeyboardSide.all),
RawKeyEventDataAndroid.modifierFunction: _ModifierCheck(ModifierKey.functionModifier, KeyboardSide.all),
RawKeyEventDataAndroid.modifierControl | RawKeyEventDataAndroid.modifierLeftControl: _ModifierCheck(ModifierKey.controlModifier, KeyboardSide.left),
RawKeyEventDataAndroid.modifierControl | RawKeyEventDataAndroid.modifierRightControl: _ModifierCheck(ModifierKey.controlModifier, KeyboardSide.right),
RawKeyEventDataAndroid.modifierMeta | RawKeyEventDataAndroid.modifierLeftMeta: _ModifierCheck(ModifierKey.metaModifier, KeyboardSide.left),
RawKeyEventDataAndroid.modifierMeta | RawKeyEventDataAndroid.modifierRightMeta: _ModifierCheck(ModifierKey.metaModifier, KeyboardSide.right),
RawKeyEventDataAndroid.modifierCapsLock: _ModifierCheck(ModifierKey.capsLockModifier, KeyboardSide.all),
RawKeyEventDataAndroid.modifierNumLock: _ModifierCheck(ModifierKey.numLockModifier, KeyboardSide.all),
RawKeyEventDataAndroid.modifierScrollLock: _ModifierCheck(ModifierKey.scrollLockModifier, KeyboardSide.all),
};
test('modifier keys are recognized individually', () {
for (int modifier in modifierTests.keys) {
final RawKeyEvent event = RawKeyEvent.fromMessage(<String, dynamic>{
'type': 'keydown',
'keymap': 'android',
'keyCode': 0x04,
'codePoint': 0x64,
'scanCode': 0x64,
'metaState': modifier,
});
final RawKeyEventDataAndroid data = event.data;
for (ModifierKey key in ModifierKey.values) {
if (modifierTests[modifier].key == key) {
expect(
data.isModifierPressed(key, side: modifierTests[modifier].side),
isTrue,
reason: "$key should be pressed with metaState $modifier, but isn't.",
);
expect(data.getModifierSide(key), equals(modifierTests[modifier].side));
} else {
expect(
data.isModifierPressed(key, side: modifierTests[modifier].side),
isFalse,
reason: '$key should not be pressed with metaState $modifier.',
);
}
}
}
});
test('modifier keys are recognized when combined', () {
for (int modifier in modifierTests.keys) {
if (modifier == RawKeyEventDataAndroid.modifierFunction) {
// No need to combine function key with itself.
continue;
}
final RawKeyEvent event = RawKeyEvent.fromMessage(<String, dynamic>{
'type': 'keydown',
'keymap': 'android',
'keyCode': 0x04,
'codePoint': 0x64,
'scanCode': 0x64,
'metaState': modifier | RawKeyEventDataAndroid.modifierFunction,
});
final RawKeyEventDataAndroid data = event.data;
for (ModifierKey key in ModifierKey.values) {
if (modifierTests[modifier].key == key || key == ModifierKey.functionModifier) {
expect(
data.isModifierPressed(key, side: modifierTests[modifier].side),
isTrue,
reason: '$key should be pressed with metaState $modifier '
"and additional key ${RawKeyEventDataAndroid.modifierFunction}, but isn't.",
);
if (key != ModifierKey.functionModifier) {
expect(data.getModifierSide(key), equals(modifierTests[modifier].side));
} else {
expect(data.getModifierSide(key), equals(KeyboardSide.all));
}
} else {
expect(
data.isModifierPressed(key, side: modifierTests[modifier].side),
isFalse,
reason: '$key should not be pressed with metaState $modifier with metaState $modifier '
'and additional key ${RawKeyEventDataAndroid.modifierFunction}.',
);
}
}
}
});
});
group('RawKeyEventDataFuchsia', () {
const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{
RawKeyEventDataFuchsia.modifierAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.any),
RawKeyEventDataFuchsia.modifierLeftAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.left),
RawKeyEventDataFuchsia.modifierRightAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.right),
RawKeyEventDataFuchsia.modifierShift: _ModifierCheck(ModifierKey.shiftModifier, KeyboardSide.any),
RawKeyEventDataFuchsia.modifierLeftShift: _ModifierCheck(ModifierKey.shiftModifier, KeyboardSide.left),
RawKeyEventDataFuchsia.modifierRightShift: _ModifierCheck(ModifierKey.shiftModifier, KeyboardSide.right),
RawKeyEventDataFuchsia.modifierControl: _ModifierCheck(ModifierKey.controlModifier, KeyboardSide.any),
RawKeyEventDataFuchsia.modifierLeftControl: _ModifierCheck(ModifierKey.controlModifier, KeyboardSide.left),
RawKeyEventDataFuchsia.modifierRightControl: _ModifierCheck(ModifierKey.controlModifier, KeyboardSide.right),
RawKeyEventDataFuchsia.modifierMeta: _ModifierCheck(ModifierKey.metaModifier, KeyboardSide.any),
RawKeyEventDataFuchsia.modifierLeftMeta: _ModifierCheck(ModifierKey.metaModifier, KeyboardSide.left),
RawKeyEventDataFuchsia.modifierRightMeta: _ModifierCheck(ModifierKey.metaModifier, KeyboardSide.right),
RawKeyEventDataFuchsia.modifierCapsLock: _ModifierCheck(ModifierKey.capsLockModifier, KeyboardSide.any),
};
test('modifier keys are recognized individually', () {
for (int modifier in modifierTests.keys) {
final RawKeyEvent event = RawKeyEvent.fromMessage(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': modifier,
});
final RawKeyEventDataFuchsia data = event.data;
for (ModifierKey key in ModifierKey.values) {
if (modifierTests[modifier].key == key) {
expect(
data.isModifierPressed(key, side: modifierTests[modifier].side),
isTrue,
reason: "$key should be pressed with metaState $modifier, but isn't.",
);
} else {
expect(
data.isModifierPressed(key, side: modifierTests[modifier].side),
isFalse,
reason: '$key should not be pressed with metaState $modifier.',
);
}
}
}
});
test('modifier keys are recognized when combined', () {
for (int modifier in modifierTests.keys) {
if (modifier == RawKeyEventDataFuchsia.modifierCapsLock) {
// No need to combine caps lock key with itself.
continue;
}
final RawKeyEvent event = RawKeyEvent.fromMessage(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': modifier | RawKeyEventDataFuchsia.modifierCapsLock,
});
final RawKeyEventDataFuchsia data = event.data;
for (ModifierKey key in ModifierKey.values) {
if (modifierTests[modifier].key == key || key == ModifierKey.capsLockModifier) {
expect(
data.isModifierPressed(key, side: modifierTests[modifier].side),
isTrue,
reason: '$key should be pressed with metaState $modifier '
"and additional key ${RawKeyEventDataFuchsia.modifierCapsLock}, but isn't.",
);
} else {
expect(
data.isModifierPressed(key, side: modifierTests[modifier].side),
isFalse,
reason: '$key should not be pressed with metaState $modifier '
'and additional key ${RawKeyEventDataFuchsia.modifierCapsLock}.',
);
}
}
}
});
});
}
...@@ -43,7 +43,7 @@ void main() { ...@@ -43,7 +43,7 @@ void main() {
'keymap': 'fuchsia', 'keymap': 'fuchsia',
'hidUsage': 0x04, 'hidUsage': 0x04,
'codePoint': 0x64, 'codePoint': 0x64,
'modifiers': 0x08, 'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
}); });
await tester.idle(); await tester.idle();
...@@ -53,7 +53,8 @@ void main() { ...@@ -53,7 +53,8 @@ void main() {
final RawKeyEventDataFuchsia typedData = events[0].data; final RawKeyEventDataFuchsia typedData = events[0].data;
expect(typedData.hidUsage, 0x04); expect(typedData.hidUsage, 0x04);
expect(typedData.codePoint, 0x64); expect(typedData.codePoint, 0x64);
expect(typedData.modifiers, 0x08); expect(typedData.modifiers, RawKeyEventDataFuchsia.modifierLeftMeta);
expect(typedData.isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.left), isTrue);
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
focusNode.dispose(); focusNode.dispose();
...@@ -79,7 +80,7 @@ void main() { ...@@ -79,7 +80,7 @@ void main() {
'keymap': 'fuchsia', 'keymap': 'fuchsia',
'hidUsage': 0x04, 'hidUsage': 0x04,
'codePoint': 0x64, 'codePoint': 0x64,
'modifiers': 0x08, 'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
}); });
await tester.idle(); await tester.idle();
...@@ -93,7 +94,7 @@ void main() { ...@@ -93,7 +94,7 @@ void main() {
'keymap': 'fuchsia', 'keymap': 'fuchsia',
'hidUsage': 0x04, 'hidUsage': 0x04,
'codePoint': 0x64, 'codePoint': 0x64,
'modifiers': 0x08, 'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
}); });
await tester.idle(); await tester.idle();
......
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