blob: 8f58243c143f4fe4bef48912c3d7af92e4f468ba [file] [log] [blame]
// Copyright 2014 The Flutter 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/foundation.dart';
import 'keyboard_maps.dart';
import 'raw_keyboard.dart';
export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder;
export 'keyboard_key.dart' show LogicalKeyboardKey, PhysicalKeyboardKey;
export 'raw_keyboard.dart' show KeyboardSide, ModifierKey;
/// Platform-specific key event data for Linux.
///
/// Different window toolkit implementations can map to different key codes. This class
/// will use the correct mapping depending on the [keyHelper] provided.
///
/// See also:
///
/// * [RawKeyboard], which uses this interface to expose key data.
class RawKeyEventDataLinux extends RawKeyEventData {
/// Creates a key event data structure specific for Linux.
///
/// The [keyHelper], [scanCode], [unicodeScalarValues], [keyCode], and [modifiers],
/// arguments must not be null.
const RawKeyEventDataLinux({
required this.keyHelper,
this.unicodeScalarValues = 0,
this.scanCode = 0,
this.keyCode = 0,
this.modifiers = 0,
required this.isDown,
this.specifiedLogicalKey,
}) : assert(scanCode != null),
assert(unicodeScalarValues != null),
assert((unicodeScalarValues & ~LogicalKeyboardKey.valueMask) == 0),
assert(keyCode != null),
assert(modifiers != null),
assert(keyHelper != null);
/// A helper class that abstracts the fetching of the toolkit-specific mappings.
///
/// There is no real concept of a "native" window toolkit on Linux, and each implementation
/// (GLFW, GTK, QT, etc) may have a different key code mapping.
final KeyHelper keyHelper;
/// An int with up to two Unicode scalar values generated by a single keystroke. An assertion
/// will fire if more than two values are encoded in a single keystroke.
///
/// This is typically the character that [keyCode] would produce without any modifier keys.
/// For dead keys, it is typically the diacritic it would add to a character. Defaults to 0,
/// asserted to be not null.
final int unicodeScalarValues;
/// 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.
final int scanCode;
/// The hardware key code corresponding to this key event.
///
/// This is the physical key that was pressed, not the Unicode character.
/// This value may be different depending on the window toolkit used. See [KeyHelper].
final int keyCode;
/// A mask of the current modifiers using the values in Modifier Flags.
/// This value may be different depending on the window toolkit used. See [KeyHelper].
final int modifiers;
/// Whether or not this key event is a key down (true) or key up (false).
final bool isDown;
/// A logical key specified by the embedding that should be used instead of
/// deriving from raw data.
///
/// The GTK embedding detects the keyboard layout and maps some keys to
/// logical keys in a way that can not be derived from per-key information.
///
/// This is not part of the native GTK key event.
final int? specifiedLogicalKey;
@override
String get keyLabel => unicodeScalarValues == 0 ? '' : String.fromCharCode(unicodeScalarValues);
@override
PhysicalKeyboardKey get physicalKey => kLinuxToPhysicalKey[scanCode] ?? PhysicalKeyboardKey(LogicalKeyboardKey.webPlane + scanCode);
@override
LogicalKeyboardKey get logicalKey {
if (specifiedLogicalKey != null) {
final int key = specifiedLogicalKey!;
return LogicalKeyboardKey.findKeyByKeyId(key) ?? LogicalKeyboardKey(key);
}
// Look to see if the keyCode is a printable number pad key, so that a
// difference between regular keys (e.g. "=") and the number pad version
// (e.g. the "=" on the number pad) can be determined.
final LogicalKeyboardKey? numPadKey = keyHelper.numpadKey(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.isNotEmpty &&
!LogicalKeyboardKey.isControlCharacter(keyLabel)) {
final int keyId = LogicalKeyboardKey.unicodePlane | (unicodeScalarValues & LogicalKeyboardKey.valueMask);
return LogicalKeyboardKey.findKeyByKeyId(keyId) ?? LogicalKeyboardKey(keyId);
}
// Look to see if the keyCode is one we know about and have a mapping for.
final LogicalKeyboardKey? newKey = keyHelper.logicalKey(keyCode);
if (newKey != null) {
return newKey;
}
// This is a non-printable key that we don't know about, so we mint a new
// code.
return LogicalKeyboardKey(keyCode | keyHelper.platformPlane);
}
@override
bool isModifierPressed(ModifierKey key, {KeyboardSide side = KeyboardSide.any}) {
return keyHelper.isModifierPressed(key, modifiers, side: side, keyCode: keyCode, isDown: isDown);
}
@override
KeyboardSide getModifierSide(ModifierKey key) {
return keyHelper.getModifierSide(key);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('toolkit', keyHelper.debugToolkit));
properties.add(DiagnosticsProperty<int>('unicodeScalarValues', unicodeScalarValues));
properties.add(DiagnosticsProperty<int>('scanCode', scanCode));
properties.add(DiagnosticsProperty<int>('keyCode', keyCode));
properties.add(DiagnosticsProperty<int>('modifiers', modifiers));
properties.add(DiagnosticsProperty<bool>('isDown', isDown));
properties.add(DiagnosticsProperty<int?>('specifiedLogicalKey', specifiedLogicalKey, defaultValue: null));
}
@override
bool operator==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is RawKeyEventDataLinux
&& other.keyHelper.runtimeType == keyHelper.runtimeType
&& other.unicodeScalarValues == unicodeScalarValues
&& other.scanCode == scanCode
&& other.keyCode == keyCode
&& other.modifiers == modifiers
&& other.isDown == isDown;
}
@override
int get hashCode => Object.hash(
keyHelper.runtimeType,
unicodeScalarValues,
scanCode,
keyCode,
modifiers,
isDown,
);
}
/// Abstract class for window-specific key mappings.
///
/// Given that there might be multiple window toolkit implementations (GLFW,
/// GTK, QT, etc), this creates a common interface for each of the
/// different toolkits.
abstract class KeyHelper {
/// Create a KeyHelper implementation depending on the given toolkit.
factory KeyHelper(String toolkit) {
if (toolkit == 'glfw') {
return GLFWKeyHelper();
} else if (toolkit == 'gtk') {
return GtkKeyHelper();
} else {
throw FlutterError('Window toolkit not recognized: $toolkit');
}
}
/// Returns the name for the toolkit.
///
/// This is used in debug mode to generate readable string.
String get debugToolkit;
/// 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.
KeyboardSide getModifierSide(ModifierKey key);
/// Returns true if the given [ModifierKey] was pressed at the time of this
/// event.
bool isModifierPressed(ModifierKey key, int modifiers, {KeyboardSide side = KeyboardSide.any, required int keyCode, required bool isDown});
/// The numpad key from the specific key code mapping.
LogicalKeyboardKey? numpadKey(int keyCode);
/// The logical key from the specific key code mapping.
LogicalKeyboardKey? logicalKey(int keyCode);
/// The platform plane mask value of this platform.
int get platformPlane;
}
/// Helper class that uses GLFW-specific key mappings.
class GLFWKeyHelper implements KeyHelper {
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether the CAPS LOCK modifier key is on.
///
/// {@template flutter.services.GLFWKeyHelper.modifierCapsLock}
/// Use this value if you need to decode the [RawKeyEventDataLinux.modifiers]
/// field yourself, but it's much easier to use [isModifierPressed] if you
/// just want to know if a modifier is pressed. This is especially true on
/// GLFW, since its modifiers don't include the effects of the current key
/// event.
/// {@endtemplate}
static const int modifierCapsLock = 0x0010;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether one of the SHIFT modifier keys is pressed.
///
/// {@macro flutter.services.GLFWKeyHelper.modifierCapsLock}
static const int modifierShift = 0x0001;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether one of the CTRL modifier keys is pressed.
///
/// {@macro flutter.services.GLFWKeyHelper.modifierCapsLock}
static const int modifierControl = 0x0002;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether one of the ALT modifier keys is pressed.
///
/// {@macro flutter.services.GLFWKeyHelper.modifierCapsLock}
static const int modifierAlt = 0x0004;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether one of the Meta(SUPER) modifier keys is pressed.
///
/// {@macro flutter.services.GLFWKeyHelper.modifierCapsLock}
static const int modifierMeta = 0x0008;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether any key in the numeric keypad is pressed.
///
/// {@macro flutter.services.GLFWKeyHelper.modifierCapsLock}
static const int modifierNumericPad = 0x0020;
@override
String get debugToolkit => 'GLFW';
int _mergeModifiers({required int modifiers, required int keyCode, required bool isDown}) {
// GLFW Key codes for modifier keys.
const int shiftLeftKeyCode = 340;
const int shiftRightKeyCode = 344;
const int controlLeftKeyCode = 341;
const int controlRightKeyCode = 345;
const int altLeftKeyCode = 342;
const int altRightKeyCode = 346;
const int metaLeftKeyCode = 343;
const int metaRightKeyCode = 347;
const int capsLockKeyCode = 280;
const int numLockKeyCode = 282;
// On GLFW, the "modifiers" bitfield is the state as it is BEFORE this event
// happened, not AFTER, like every other platform. Consequently, if this is
// a key down, then we need to add the correct modifier bits, and if it's a
// key up, we need to remove them.
int modifierChange = 0;
switch (keyCode) {
case shiftLeftKeyCode:
case shiftRightKeyCode:
modifierChange = modifierShift;
break;
case controlLeftKeyCode:
case controlRightKeyCode:
modifierChange = modifierControl;
break;
case altLeftKeyCode:
case altRightKeyCode:
modifierChange = modifierAlt;
break;
case metaLeftKeyCode:
case metaRightKeyCode:
modifierChange = modifierMeta;
break;
case capsLockKeyCode:
modifierChange = modifierCapsLock;
break;
case numLockKeyCode:
modifierChange = modifierNumericPad;
break;
default:
break;
}
return isDown ? modifiers | modifierChange : modifiers & ~modifierChange;
}
@override
bool isModifierPressed(ModifierKey key, int modifiers, {KeyboardSide side = KeyboardSide.any, required int keyCode, required bool isDown}) {
modifiers = _mergeModifiers(modifiers: modifiers, keyCode: keyCode, isDown: isDown);
switch (key) {
case ModifierKey.controlModifier:
return modifiers & modifierControl != 0;
case ModifierKey.shiftModifier:
return modifiers & modifierShift != 0;
case ModifierKey.altModifier:
return modifiers & modifierAlt != 0;
case ModifierKey.metaModifier:
return modifiers & modifierMeta != 0;
case ModifierKey.capsLockModifier:
return modifiers & modifierCapsLock != 0;
case ModifierKey.numLockModifier:
return modifiers & modifierNumericPad != 0;
case ModifierKey.functionModifier:
case ModifierKey.symbolModifier:
case ModifierKey.scrollLockModifier:
// These are not used in GLFW keyboards.
return false;
}
}
@override
KeyboardSide getModifierSide(ModifierKey key) {
// Neither GLFW nor X11 provide a distinction between left and right
// modifiers, so defaults to KeyboardSide.all.
// https://code.woboq.org/qt5/include/X11/X.h.html#_M/ShiftMask
return KeyboardSide.all;
}
@override
LogicalKeyboardKey? numpadKey(int keyCode) {
return kGlfwNumpadMap[keyCode];
}
@override
LogicalKeyboardKey? logicalKey(int keyCode) {
return kGlfwToLogicalKey[keyCode];
}
@override
int get platformPlane => LogicalKeyboardKey.glfwPlane;
}
/// Helper class that uses GTK-specific key mappings.
class GtkKeyHelper implements KeyHelper {
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether one of the SHIFT modifier keys is pressed.
///
/// {@template flutter.services.GtkKeyHelper.modifierShift}
/// Use this value if you need to decode the [RawKeyEventDataLinux.modifiers] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if a
/// modifier is pressed. This is especially true on GTK, since its modifiers
/// don't include the effects of the current key event.
/// {@endtemplate}
static const int modifierShift = 1 << 0;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether the CAPS LOCK modifier key is on.
///
/// {@macro flutter.services.GtkKeyHelper.modifierShift}
static const int modifierCapsLock = 1 << 1;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether one of the CTRL modifier keys is pressed.
///
/// {@macro flutter.services.GtkKeyHelper.modifierShift}
static const int modifierControl = 1 << 2;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether the first modifier key is pressed (usually mapped to alt).
///
/// {@macro flutter.services.GtkKeyHelper.modifierShift}
static const int modifierMod1 = 1 << 3;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether the second modifier key is pressed (assumed to be mapped to
/// num lock).
///
/// {@macro flutter.services.GtkKeyHelper.modifierShift}
static const int modifierMod2 = 1 << 4;
/// This mask is used to check the [RawKeyEventDataLinux.modifiers] field to
/// test whether one of the Meta(SUPER) modifier keys is pressed.
///
/// {@macro flutter.services.GtkKeyHelper.modifierShift}
static const int modifierMeta = 1 << 26;
@override
String get debugToolkit => 'GTK';
int _mergeModifiers({required int modifiers, required int keyCode, required bool isDown}) {
// GTK Key codes for modifier keys.
const int shiftLeftKeyCode = 0xffe1;
const int shiftRightKeyCode = 0xffe2;
const int controlLeftKeyCode = 0xffe3;
const int controlRightKeyCode = 0xffe4;
const int capsLockKeyCode = 0xffe5;
const int shiftLockKeyCode = 0xffe6;
const int altLeftKeyCode = 0xffe9;
const int altRightKeyCode = 0xffea;
const int metaLeftKeyCode = 0xffeb;
const int metaRightKeyCode = 0xffec;
const int numLockKeyCode = 0xff7f;
// On GTK, the "modifiers" bitfield is the state as it is BEFORE this event
// happened, not AFTER, like every other platform. Consequently, if this is
// a key down, then we need to add the correct modifier bits, and if it's a
// key up, we need to remove them.
int modifierChange = 0;
switch (keyCode) {
case shiftLeftKeyCode:
case shiftRightKeyCode:
modifierChange = modifierShift;
break;
case controlLeftKeyCode:
case controlRightKeyCode:
modifierChange = modifierControl;
break;
case altLeftKeyCode:
case altRightKeyCode:
modifierChange = modifierMod1;
break;
case metaLeftKeyCode:
case metaRightKeyCode:
modifierChange = modifierMeta;
break;
case capsLockKeyCode:
case shiftLockKeyCode:
modifierChange = modifierCapsLock;
break;
case numLockKeyCode:
modifierChange = modifierMod2;
break;
default:
break;
}
return isDown ? modifiers | modifierChange : modifiers & ~modifierChange;
}
@override
bool isModifierPressed(ModifierKey key, int modifiers, {KeyboardSide side = KeyboardSide.any, required int keyCode, required bool isDown}) {
modifiers = _mergeModifiers(modifiers: modifiers, keyCode: keyCode, isDown: isDown);
switch (key) {
case ModifierKey.controlModifier:
return modifiers & modifierControl != 0;
case ModifierKey.shiftModifier:
return modifiers & modifierShift != 0;
case ModifierKey.altModifier:
return modifiers & modifierMod1 != 0;
case ModifierKey.metaModifier:
return modifiers & modifierMeta != 0;
case ModifierKey.capsLockModifier:
return modifiers & modifierCapsLock != 0;
case ModifierKey.numLockModifier:
return modifiers & modifierMod2 != 0;
case ModifierKey.functionModifier:
case ModifierKey.symbolModifier:
case ModifierKey.scrollLockModifier:
// These are not used in GTK keyboards.
return false;
}
}
@override
KeyboardSide getModifierSide(ModifierKey key) {
// Neither GTK nor X11 provide a distinction between left and right
// modifiers, so defaults to KeyboardSide.all.
// https://code.woboq.org/qt5/include/X11/X.h.html#_M/ShiftMask
return KeyboardSide.all;
}
@override
LogicalKeyboardKey? numpadKey(int keyCode) {
return kGtkNumpadMap[keyCode];
}
@override
LogicalKeyboardKey? logicalKey(int keyCode) {
return kGtkToLogicalKey[keyCode];
}
@override
int get platformPlane => LogicalKeyboardKey.gtkPlane;
}