diff --git a/dev/manual_tests/lib/raw_keyboard.dart b/dev/manual_tests/lib/raw_keyboard.dart index cabbfe090e8..a916b80a6d0 100644 --- a/dev/manual_tests/lib/raw_keyboard.dart +++ b/dev/manual_tests/lib/raw_keyboard.dart @@ -20,7 +20,7 @@ void main() { } class RawKeyboardDemo extends StatefulWidget { - const RawKeyboardDemo({ Key key }) : super(key: key); + const RawKeyboardDemo({Key key}) : super(key: key); @override _HardwareKeyDemoState createState() => _HardwareKeyDemoState(); @@ -42,6 +42,14 @@ class _HardwareKeyDemoState extends State { }); } + 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 Widget build(BuildContext context) { final TextTheme textTheme = Theme.of(context).textTheme; @@ -60,28 +68,42 @@ class _HardwareKeyDemoState extends State { ); } - if (_event == null) + if (_event == null) { return Text('Press a key', style: textTheme.display1); - - int codePoint; - int keyCode; - int hidUsage; - final RawKeyEventData data = _event.data; - if (data is RawKeyEventDataAndroid) { - codePoint = data.codePoint; - keyCode = data.keyCode; - } else if (data is RawKeyEventDataFuchsia) { - codePoint = data.codePoint; - hidUsage = data.hidUsage; } - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('${_event.runtimeType}', style: textTheme.body2), - Text('codePoint: $codePoint', style: textTheme.display4), - Text('keyCode: $keyCode', style: textTheme.display4), - Text('hidUsage: $hidUsage', style: textTheme.display4), - ], + + final RawKeyEventData data = _event.data; + final String modifierList = data.modifiersPressed.keys.map(_getEnumName).join(', ').replaceAll('Modifier', ''); + final List dataText = [ + Text('${_event.runtimeType}'), + Text('modifiers set: $modifierList'), + ]; + if (data is RawKeyEventDataAndroid) { + dataText.add(Text('codePoint: ${data.codePoint} (${_asHex(data.codePoint)})')); + 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) { + dataText.add(Text('codePoint: ${data.codePoint} (${_asHex(data.codePoint)})')); + dataText.add(Text('hidUsage: ${data.hidUsage} (${_asHex(data.hidUsage)})')); + dataText.add(Text('modifiers: ${data.modifiers} (${_asHex(data.modifiers)})')); + } + 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, + children: dataText, + ), ); }, ), diff --git a/packages/flutter/lib/services.dart b/packages/flutter/lib/services.dart index 03aab5ca2f6..6c2ca8120d4 100644 --- a/packages/flutter/lib/services.dart +++ b/packages/flutter/lib/services.dart @@ -21,6 +21,8 @@ export 'src/services/platform_channel.dart'; export 'src/services/platform_messages.dart'; export 'src/services/platform_views.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_chrome.dart'; export 'src/services/system_navigator.dart'; diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart index 359d9dff3ad..9788c2488af 100644 --- a/packages/flutter/lib/src/services/raw_keyboard.dart +++ b/packages/flutter/lib/src/services/raw_keyboard.dart @@ -6,9 +6,94 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'raw_keyboard_android.dart'; +import 'raw_keyboard_fuschia.dart'; import 'system_channels.dart'; -/// Base class for platform specific key event data. +/// An enum describing the side of the keyboard that a key is on, to allow +/// discrimination between which key is pressed (e.g. the left or right SHIFT +/// key). +/// +/// See also: +/// +/// * [RawKeyEventData.isModifierPressed], which accepts this enum as an +/// argument. +enum KeyboardSide { + /// Matches if either the left, right or both versions of the key are pressed. + any, + + /// Matches the left version of the key. + left, + + /// Matches the right version of the key. + right, + + /// Matches the left and right version of the key pressed simultaneously. + all, +} + +/// An enum describing the type of modifier key that is being pressed. +/// +/// See also: +/// +/// * [RawKeyEventData.isModifierPressed], which accepts this enum as an +/// argument. +enum ModifierKey { + /// The CTRL modifier key. + /// + /// Typically, there are two of these. + controlModifier, + + /// The SHIFT modifier key. + /// + /// Typically, there are two of these. + shiftModifier, + + /// The ALT modifier key. + /// + /// Typically, there are two of these. + altModifier, + + /// The META modifier key. + /// + /// Typically, there are two of these. This is, for example, the Windows key + /// on Windows (⊞), the Command (⌘) key on macOS and iOS, and the Search (🔍) + /// key on Android. + metaModifier, + + /// The CAPS LOCK modifier key. + /// + /// Typically, there is one of these. Only shown as "pressed" when the caps + /// lock is on, so on a key up when the mode is turned on, on each key press + /// when it's enabled, and on a key down when it is turned off. + capsLockModifier, + + /// The NUM LOCK modifier key. + /// + /// Typically, there is one of these. Only shown as "pressed" when the num + /// lock is on, so on a key up when the mode is turned on, on each key press + /// when it's enabled, and on a key down when it is turned off. + numLockModifier, + + /// The SCROLL LOCK modifier key. + /// + /// Typically, there is one of these. Only shown as "pressed" when the scroll + /// lock is on, so on a key up when the mode is turned on, on each key press + /// when it's enabled, and on a key down when it is turned off. + scrollLockModifier, + + /// The FUNCTION (Fn) modifier key. + /// + /// Typically, there is one of these. + functionModifier, + + /// The SYMBOL modifier key. + /// + /// Typically, there is one of these. + symbolModifier, +} + +/// Base class for platform-specific key event data. /// /// This base class exists to have a common type to use for each of the /// target platform's key event data structures. @@ -22,99 +107,79 @@ import 'system_channels.dart'; /// * [RawKeyboard], which uses these interfaces to expose key data. @immutable abstract class RawKeyEventData { - /// Abstract const constructor. This constructor enables subclasses to provide - /// const constructors so that they can be used in const expressions. + /// Abstract const constructor. + /// + /// This constructor enables subclasses to provide const constructors so that + /// they can be used in const expressions. const RawKeyEventData(); -} -/// Platform-specific key event data for Android. -/// -/// This object contains information about key events obtained from Android's -/// `KeyEvent` interface. -/// -/// See also: -/// -/// * [RawKeyboard], which uses this interface to expose key data. -class RawKeyEventDataAndroid extends RawKeyEventData { - /// Creates a key event data structure specific for Android. + /// Returns true if the given [ModifierKey] was pressed at the time of this + /// event. /// - /// The [flags], [codePoint], [keyCode], [scanCode], and [metaState] arguments - /// must not be null. - const RawKeyEventDataAndroid({ - this.flags = 0, - this.codePoint = 0, - this.keyCode = 0, - this.scanCode = 0, - this.metaState = 0, - }) : assert(flags != null), - assert(codePoint != null), - assert(keyCode != null), - assert(scanCode != null), - assert(metaState != null); + /// If [side] is specified, then this restricts its check to the specified + /// side of the keyboard. Defaults to checking for the key being down on + /// either side of the keyboard. If there is only one instance of the key on + /// the keyboard, then [side] is ignored. + bool isModifierPressed(ModifierKey key, {KeyboardSide side = KeyboardSide.any}); - /// See - final int flags; - - /// See - final int codePoint; - - /// See - final int keyCode; - - /// See - final int scanCode; - - /// See - final int metaState; -} - -/// Platform-specific key event data for Fuchsia. -/// -/// This object contains information about key events obtained from Fuchsia's -/// `KeyData` interface. -/// -/// See also: -/// -/// * [RawKeyboard], which uses this interface to expose key data. -class RawKeyEventDataFuchsia extends RawKeyEventData { - /// Creates a key event data structure specific for Fuchsia. + /// Returns a [KeyboardSide] enum value that describes which side or sides of + /// the given keyboard modifier key were pressed at the time of this event. /// - /// The [hidUsage], [codePoint], and [modifiers] arguments must not be null. - const RawKeyEventDataFuchsia({ - this.hidUsage = 0, - this.codePoint = 0, - this.modifiers = 0, - }) : assert(hidUsage != null), - assert(codePoint != null), - assert(modifiers != null); + /// If the modifier key wasn't pressed at the time of this event, returns + /// null. If the given key only appears in one place on the keyboard, returns + /// [KeyboardSide.all] if pressed. Never returns [KeyboardSide.any], because + /// that doesn't make sense in this context. + KeyboardSide getModifierSide(ModifierKey key); - /// The USB HID usage. + /// Returns true if a CTRL modifier key was pressed at the time of this event, + /// regardless of which side of the keyboard it is on. /// - /// See - final int hidUsage; + /// Use [isModifierPressed] if you need to know which control key was pressed. + bool get isControlPressed => isModifierPressed(ModifierKey.controlModifier, side: KeyboardSide.any); - /// The Unicode code point represented by the key event, if any. + /// Returns true if a SHIFT modifier key was pressed at the time of this + /// event, regardless of which side of the keyboard it is on. /// - /// If there is no Unicode code point, this value is zero. - final int codePoint; + /// Use [isModifierPressed] if you need to know which shift key was pressed. + bool get isShiftPressed => isModifierPressed(ModifierKey.shiftModifier, side: KeyboardSide.any); - /// The modifiers that we present when the key event occurred. + /// Returns true if a ALT modifier key was pressed at the time of this event, + /// regardless of which side of the keyboard it is on. /// - /// See - /// for the numerical values of the modifiers. - final int modifiers; + /// Use [isModifierPressed] if you need to know which alt key was pressed. + bool get isAltPressed => isModifierPressed(ModifierKey.altModifier, side: KeyboardSide.any); + + /// Returns true if a META modifier key was pressed at the time of this event, + /// regardless of which side of the keyboard it is on. + /// + /// Use [isModifierPressed] if you need to know which meta key was pressed. + bool get isMetaPressed => isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.any); + + /// Returns a map of modifier keys that were pressed at the time of this + /// event, and the keyboard side or sides that the key was on. + Map get modifiersPressed { + final Map result = {}; + for (ModifierKey key in ModifierKey.values) { + if (isModifierPressed(key)) { + result[key] = getModifierSide(key); + } + } + return result; + } } /// Base class for raw key events. /// /// Raw key events pass through as much information as possible from the -/// underlying platform's key events, which makes they provide a high level of -/// fidelity but a low level of portability. +/// underlying platform's key events, which allows them to provide a high level +/// of fidelity but a low level of portability. /// /// See also: /// -/// * [RawKeyDownEvent], a specialization for events representing the user pressing a key. -/// * [RawKeyUpEvent], a specialization for events representing the user releasing a key. +/// * [RawKeyDownEvent], a specialization for events representing the user +/// pressing a key. +/// * [RawKeyUpEvent], a specialization for events representing the user +/// releasing a key. /// * [RawKeyboard], which uses this interface to expose key data. /// * [RawKeyboardListener], a widget that listens for raw key events. @immutable @@ -148,8 +213,9 @@ abstract class RawKeyEvent { ); break; default: - // We don't yet implement raw key events on iOS, but we don't hit this - // exception because the engine never sends us these messages. + // We don't yet implement raw key events on iOS or other platforms, but + // we don't hit this exception because the engine never sends us these + // messages. throw FlutterError('Unknown keymap for key events: $keymap'); } @@ -234,13 +300,17 @@ class RawKeyboard { } Future _handleKeyEvent(dynamic message) async { - if (_listeners.isEmpty) + if (_listeners.isEmpty) { return; + } final RawKeyEvent event = RawKeyEvent.fromMessage(message); - if (event == null) + if (event == null) { return; - for (ValueChanged listener in List>.from(_listeners)) - if (_listeners.contains(listener)) + } + for (ValueChanged listener in List>.from(_listeners)) { + if (_listeners.contains(listener)) { listener(event); + } + } } } diff --git a/packages/flutter/lib/src/services/raw_keyboard_android.dart b/packages/flutter/lib/src/services/raw_keyboard_android.dart new file mode 100644 index 00000000000..fbcf168e1c4 --- /dev/null +++ b/packages/flutter/lib/src/services/raw_keyboard_android.dart @@ -0,0 +1,314 @@ +// 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 'raw_keyboard.dart'; + +/// Platform-specific key event data for Android. +/// +/// This object contains information about key events obtained from Android's +/// `KeyEvent` interface. +/// +/// See also: +/// +/// * [RawKeyboard], which uses this interface to expose key data. +class RawKeyEventDataAndroid extends RawKeyEventData { + /// Creates a key event data structure specific for Android. + /// + /// The [flags], [codePoint], [keyCode], [scanCode], and [metaState] arguments + /// must not be null. + const RawKeyEventDataAndroid({ + this.flags = 0, + this.codePoint = 0, + this.keyCode = 0, + this.scanCode = 0, + this.metaState = 0, + }) : assert(flags != null), + assert(codePoint != null), + assert(keyCode != null), + assert(scanCode != null), + assert(metaState != null); + + /// The current set of additional flags for this event. + /// + /// Flags indicate things like repeat state, etc. + /// + /// See + /// for more information. + final int flags; + + /// The Unicode code point represented by the key event, if any. + /// + /// If there is no Unicode code point, this value is zero. + /// + /// Dead keys are represented as Unicode combining characters. + /// + /// See + /// for more information. + final int codePoint; + + /// The hardware key code corresponding to this key event. + /// + /// This is the physical key that was pressed, not the Unicode character. + /// See [codePoint] for the Unicode character. + /// + /// See + /// for more information. + final int keyCode; + + /// The hardware scan code id corresponding to this key event. + /// + /// These values are not reliable and vary from device to device, so this + /// information is mainly useful for debugging. + /// + /// See + /// for more information. + final int scanCode; + + /// The modifiers that were present when the key event occurred. + /// + /// See + /// for the numerical values of the `metaState`. Many of these constants are + /// also replicated as static constants in this class. + /// + /// See also: + /// + /// * [modifiersPressed], which returns a Map of currently pressed modifiers + /// and their keyboard side. + /// * [isModifierPressed], to see if a specific modifier is pressed. + /// * [isControlPressed], to see if a CTRL key is pressed. + /// * [isShiftPressed], to see if a SHIFT key is pressed. + /// * [isAltPressed], to see if an ALT key is pressed. + /// * [isMetaPressed], to see if a META key is pressed. + final int metaState; + + bool _isLeftRightModifierPressed(KeyboardSide side, int anyMask, int leftMask, int rightMask) { + if (metaState & anyMask == 0) { + return false; + } + switch (side) { + case KeyboardSide.any: + return true; + case KeyboardSide.all: + return metaState & leftMask != 0 && metaState & rightMask != 0; + case KeyboardSide.left: + return metaState & leftMask != 0; + case KeyboardSide.right: + return metaState & rightMask != 0; + } + return false; + } + + @override + bool isModifierPressed(ModifierKey key, {KeyboardSide side = KeyboardSide.any}) { + assert(side != null); + switch (key) { + case ModifierKey.controlModifier: + return _isLeftRightModifierPressed(side, modifierControl, modifierLeftControl, modifierRightControl); + case ModifierKey.shiftModifier: + return _isLeftRightModifierPressed(side, modifierShift, modifierLeftShift, modifierRightShift); + case ModifierKey.altModifier: + return _isLeftRightModifierPressed(side, modifierAlt, modifierLeftAlt, modifierRightAlt); + case ModifierKey.metaModifier: + return _isLeftRightModifierPressed(side, modifierMeta, modifierLeftMeta, modifierRightMeta); + case ModifierKey.capsLockModifier: + return metaState & modifierCapsLock != 0; + case ModifierKey.numLockModifier: + return metaState & modifierNumLock != 0; + case ModifierKey.scrollLockModifier: + return metaState & modifierScrollLock != 0; + case ModifierKey.functionModifier: + return metaState & modifierFunction != 0; + case ModifierKey.symbolModifier: + return metaState & modifierSym != 0; + } + return false; + } + + @override + KeyboardSide getModifierSide(ModifierKey key) { + KeyboardSide findSide(int leftMask, int rightMask) { + final int combinedMask = leftMask | rightMask; + final int combined = metaState & combinedMask; + if (combined == leftMask) { + return KeyboardSide.left; + } else if (combined == rightMask) { + return KeyboardSide.right; + } else if (combined == combinedMask) { + return KeyboardSide.all; + } + return null; + } + + switch (key) { + case ModifierKey.controlModifier: + return findSide(modifierLeftControl, modifierRightControl); + case ModifierKey.shiftModifier: + return findSide(modifierLeftShift, modifierRightShift); + case ModifierKey.altModifier: + return findSide(modifierLeftAlt, modifierRightAlt); + case ModifierKey.metaModifier: + return findSide(modifierLeftMeta, modifierRightMeta); + case ModifierKey.capsLockModifier: + case ModifierKey.numLockModifier: + case ModifierKey.scrollLockModifier: + case ModifierKey.functionModifier: + case ModifierKey.symbolModifier: + return KeyboardSide.all; + } + + assert(false, 'Not handling $key type properly.'); + return null; + } + + // Modifier key masks. + + /// No modifier keys are pressed in the [metaState] field. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierNone = 0; + + /// This mask is used to check the [metaState] field to test whether one of + /// the ALT modifier keys is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierAlt = 0x02; + + /// This mask is used to check the [metaState] field to test whether the left + /// ALT modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierLeftAlt = 0x10; + + /// This mask is used to check the [metaState] field to test whether the right + /// ALT modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierRightAlt = 0x20; + + /// This mask is used to check the [metaState] field to test whether one of + /// the SHIFT modifier keys is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierShift = 0x01; + + /// This mask is used to check the [metaState] field to test whether the left + /// SHIFT modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierLeftShift = 0x40; + + /// This mask is used to check the [metaState] field to test whether the right + /// SHIFT modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierRightShift = 0x80; + + /// This mask is used to check the [metaState] field to test whether the SYM + /// modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierSym = 0x04; + + /// This mask is used to check the [metaState] field to test whether the + /// Function modifier key (Fn) is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierFunction = 0x08; + + /// This mask is used to check the [metaState] field to test whether one of + /// the CTRL modifier keys is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierControl = 0x1000; + + /// This mask is used to check the [metaState] field to test whether the left + /// CTRL modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierLeftControl = 0x2000; + + /// This mask is used to check the [metaState] field to test whether the right + /// CTRL modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierRightControl = 0x4000; + + /// This mask is used to check the [metaState] field to test whether one of + /// the META modifier keys is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierMeta = 0x10000; + + /// This mask is used to check the [metaState] field to test whether the left + /// META modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierLeftMeta = 0x20000; + + /// This mask is used to check the [metaState] field to test whether the right + /// META modifier key is pressed. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierRightMeta = 0x40000; + + /// This mask is used to check the [metaState] field to test whether the CAPS + /// LOCK modifier key is on. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierCapsLock = 0x100000; + + /// This mask is used to check the [metaState] field to test whether the NUM + /// LOCK modifier key is on. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierNumLock = 0x200000; + + /// This mask is used to check the [metaState] field to test whether the + /// SCROLL LOCK modifier key is on. + /// + /// Use this value if you need to decode the [metaState] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierScrollLock = 0x400000; + + @override + String toString() { + return '$runtimeType(flags: $flags, codePoint: $codePoint, keyCode: $keyCode, ' + 'scanCode: $scanCode, metaState: $metaState, modifiers down: $modifiersPressed)'; + } +} diff --git a/packages/flutter/lib/src/services/raw_keyboard_fuschia.dart b/packages/flutter/lib/src/services/raw_keyboard_fuschia.dart new file mode 100644 index 00000000000..52d2638f5a9 --- /dev/null +++ b/packages/flutter/lib/src/services/raw_keyboard_fuschia.dart @@ -0,0 +1,254 @@ +// 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 'raw_keyboard.dart'; + +/// Platform-specific key event data for Fuchsia. +/// +/// This object contains information about key events obtained from Fuchsia's +/// `KeyData` interface. +/// +/// See also: +/// +/// * [RawKeyboard], which uses this interface to expose key data. +class RawKeyEventDataFuchsia extends RawKeyEventData { + /// Creates a key event data structure specific for Fuchsia. + /// + /// The [hidUsage], [codePoint], and [modifiers] arguments must not be null. + const RawKeyEventDataFuchsia({ + this.hidUsage = 0, + this.codePoint = 0, + this.modifiers = 0, + }) : assert(hidUsage != null), + assert(codePoint != null), + assert(modifiers != null); + + /// The USB HID usage. + /// + /// See for more + /// information. + final int hidUsage; + + /// The Unicode code point represented by the key event, if any. + /// + /// If there is no Unicode code point, this value is zero. + /// + /// Dead keys are represented as Unicode combining characters. + final int codePoint; + + /// The modifiers that were present when the key event occurred. + /// + /// See + /// for the numerical values of the modifiers. Many of these are also + /// replicated as static constants in this class. + /// + /// See also: + /// + /// * [modifiersPressed], which returns a Map of currently pressed modifiers + /// and their keyboard side. + /// * [isModifierPressed], to see if a specific modifier is pressed. + /// * [isControlPressed], to see if a CTRL key is pressed. + /// * [isShiftPressed], to see if a SHIFT key is pressed. + /// * [isAltPressed], to see if an ALT key is pressed. + /// * [isMetaPressed], to see if a META key is pressed. + final int modifiers; + + bool _isLeftRightModifierPressed(KeyboardSide side, int anyMask, int leftMask, int rightMask) { + if (modifiers & anyMask == 0) { + return false; + } + switch (side) { + case KeyboardSide.any: + return true; + case KeyboardSide.all: + return modifiers & leftMask != 0 && modifiers & rightMask != 0; + case KeyboardSide.left: + return modifiers & leftMask != 0; + case KeyboardSide.right: + return modifiers & rightMask != 0; + } + return false; + } + + @override + bool isModifierPressed(ModifierKey key, {KeyboardSide side = KeyboardSide.any}) { + assert(side != null); + switch (key) { + case ModifierKey.controlModifier: + return _isLeftRightModifierPressed(side, modifierControl, modifierLeftControl, modifierRightControl); + case ModifierKey.shiftModifier: + return _isLeftRightModifierPressed(side, modifierShift, modifierLeftShift, modifierRightShift); + case ModifierKey.altModifier: + return _isLeftRightModifierPressed(side, modifierAlt, modifierLeftAlt, modifierRightAlt); + case ModifierKey.metaModifier: + return _isLeftRightModifierPressed(side, modifierMeta, modifierLeftMeta, modifierRightMeta); + case ModifierKey.capsLockModifier: + return modifiers & modifierCapsLock != 0; + case ModifierKey.numLockModifier: + case ModifierKey.scrollLockModifier: + case ModifierKey.functionModifier: + case ModifierKey.symbolModifier: + // Fuschia doesn't have masks for these keys (yet). + return false; + } + return false; + } + + @override + KeyboardSide getModifierSide(ModifierKey key) { + KeyboardSide findSide(int leftMask, int rightMask, int combinedMask) { + final int combined = modifiers & combinedMask; + if (combined == leftMask) { + return KeyboardSide.left; + } else if (combined == rightMask) { + return KeyboardSide.right; + } else if (combined == combinedMask) { + return KeyboardSide.all; + } + return null; + } + + switch (key) { + case ModifierKey.controlModifier: + return findSide(modifierLeftControl, modifierRightControl, modifierControl); + case ModifierKey.shiftModifier: + return findSide(modifierLeftShift, modifierRightShift, modifierShift); + case ModifierKey.altModifier: + return findSide(modifierLeftAlt, modifierRightAlt, modifierAlt); + case ModifierKey.metaModifier: + return findSide(modifierLeftMeta, modifierRightMeta, modifierMeta); + case ModifierKey.capsLockModifier: + return (modifiers & modifierCapsLock == 0) ? null : KeyboardSide.all; + case ModifierKey.numLockModifier: + case ModifierKey.scrollLockModifier: + case ModifierKey.functionModifier: + case ModifierKey.symbolModifier: + // Fuchsia doesn't support these modifiers, so they can't be pressed. + return null; + } + + assert(false, 'Not handling $key type properly.'); + return null; + } + + // Keyboard modifier masks for Fuschia modifiers. + + /// The [modifiers] field indicates that no modifier keys are pressed if it + /// equals this value. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierNone = 0x0; + + /// This mask is used to check the [modifiers] field to test whether the CAPS + /// LOCK modifier key is on. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierCapsLock = 0x1; + + /// This mask is used to check the [modifiers] field to test whether the left + /// SHIFT modifier key is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierLeftShift = 0x2; + + /// This mask is used to check the [modifiers] field to test whether the right + /// SHIFT modifier key is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierRightShift = 0x4; + + /// This mask is used to check the [modifiers] field to test whether one of + /// the SHIFT modifier keys is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierShift = modifierLeftShift | modifierRightShift; + + /// This mask is used to check the [modifiers] field to test whether the left + /// CTRL modifier key is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierLeftControl = 0x8; + + /// This mask is used to check the [modifiers] field to test whether the right + /// CTRL modifier key is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierRightControl = 0x10; + + /// This mask is used to check the [modifiers] field to test whether one of + /// the CTRL modifier keys is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierControl = modifierLeftControl | modifierRightControl; + + /// This mask is used to check the [modifiers] field to test whether the left + /// ALT modifier key is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierLeftAlt = 0x20; + + /// This mask is used to check the [modifiers] field to test whether the right + /// ALT modifier key is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierRightAlt = 0x40; + + /// This mask is used to check the [modifiers] field to test whether one of + /// the ALT modifier keys is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierAlt = modifierLeftAlt | modifierRightAlt; + + /// This mask is used to check the [modifiers] field to test whether the left + /// META modifier key is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierLeftMeta = 0x80; + + /// This mask is used to check the [modifiers] field to test whether the right + /// META modifier key is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierRightMeta = 0x100; + + /// This mask is used to check the [modifiers] field to test whether one of + /// the META modifier keys is pressed. + /// + /// Use this value if you need to decode the [modifiers] field yourself, but + /// it's much easier to use [isModifierPressed] if you just want to know if + /// a modifier is pressed. + static const int modifierMeta = modifierLeftMeta | modifierRightMeta; + + @override + String toString() { + return '$runtimeType(hidUsage: $hidUsage, codePoint: $codePoint, modifiers: $modifiers, ' + 'modifiers down: $modifiersPressed)'; + } +} diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart new file mode 100644 index 00000000000..d246439b035 --- /dev/null +++ b/packages/flutter/test/services/raw_keyboard_test.dart @@ -0,0 +1,179 @@ +// 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 modifierTests = { + 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({ + '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({ + '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 modifierTests = { + 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({ + '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({ + '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}.', + ); + } + } + } + }); + }); +} diff --git a/packages/flutter/test/widgets/raw_keyboard_listener_test.dart b/packages/flutter/test/widgets/raw_keyboard_listener_test.dart index 7a6d6cd1d46..dd02459a83a 100644 --- a/packages/flutter/test/widgets/raw_keyboard_listener_test.dart +++ b/packages/flutter/test/widgets/raw_keyboard_listener_test.dart @@ -43,7 +43,7 @@ void main() { 'keymap': 'fuchsia', 'hidUsage': 0x04, 'codePoint': 0x64, - 'modifiers': 0x08, + 'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta, }); await tester.idle(); @@ -53,7 +53,8 @@ void main() { final RawKeyEventDataFuchsia typedData = events[0].data; expect(typedData.hidUsage, 0x04); 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()); focusNode.dispose(); @@ -79,7 +80,7 @@ void main() { 'keymap': 'fuchsia', 'hidUsage': 0x04, 'codePoint': 0x64, - 'modifiers': 0x08, + 'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta, }); await tester.idle(); @@ -93,7 +94,7 @@ void main() { 'keymap': 'fuchsia', 'hidUsage': 0x04, 'codePoint': 0x64, - 'modifiers': 0x08, + 'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta, }); await tester.idle();