Created
July 16, 2021 15:20
-
-
Save venkatd/fbbf17eda538538e9ff3ed92a3ca29de to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/services.dart'; | |
import 'package:flutter/widgets.dart'; | |
/// This class smooths over the platform differences for a RawKeyEvent, | |
/// encapsulates some workarounds for Flutter keyboard handling bugs, | |
/// and makes it easier to match up [LogicalKeyEvent] to a keyboard shortcut | |
/// combo. | |
/// | |
/// It accounts for edge cases & bugs such as: | |
/// - Ensuring the event is only triggered on a non-modifier key. For example | |
/// cmd+x would trigger a cut event while x+cmd would not | |
/// - Normalizing modifier key such as shiftLeft and shiftRight to mean just mean shift | |
/// - On Flutter web ensure modifier keys don't get stuck. On this platform, | |
/// modifier keys are not marked as released immediately. | |
/// - Normalize the [LogicalKeyEvent.character] to correspond to the actual | |
/// character that is intended to be typed. This means filtering out control | |
/// character, on web UI event keys, and ensuring the quote character is properly | |
/// interpreted as '"' on international keyboards. | |
/// - On MacOS ensure the shift-key doesn't modify the case of letter trigger keys | |
/// For example cmd+shift+v on Mac gets interpreted as cmd+shift+V (upper case) | |
/// while on other platforms it is correctly interpreted as cmd+shift+v | |
class LogicalKeyEvent { | |
LogicalKeyEvent({ | |
required this.trigger, | |
required Set<LogicalKeyboardKey> keysPressed, | |
this.character, | |
}) : _keySet = LogicalKeySet.fromSet(keysPressed); | |
factory LogicalKeyEvent.fromRawKeyEvent( | |
RawKeyEvent keyEvent, Set<LogicalKeyboardKey> keysPressed) { | |
final adjustedKeysPressed = _adjustKeysPressed( | |
_collapseKeyboardKeySynonyms(keysPressed), | |
keyEvent, | |
); | |
return LogicalKeyEvent( | |
trigger: keyEvent.logicalKey, | |
keysPressed: adjustedKeysPressed, | |
character: _logicalCharacterForKeyEvent( | |
trigger: keyEvent.logicalKey, | |
keysPressed: adjustedKeysPressed, | |
character: keyEvent.character, | |
), | |
); | |
} | |
/// The non-modifier key that possibly triggers the event | |
/// For example, if trigger is [LogicalKeyboardKey.keyX], cmd+x would be triggered | |
/// while x+cmd would not trigger anything. | |
final LogicalKeyboardKey trigger; | |
Set<LogicalKeyboardKey> get keysPressed => _keySet.keys; | |
final String? character; | |
final LogicalKeySet _keySet; | |
bool isKeyPressed(LogicalKeyboardKey key) => keysPressed.contains(key); | |
LogicalKeySet toKeySet() => _keySet; | |
@override | |
bool operator ==(Object other) { | |
if (identical(this, other)) return true; | |
if (other is! LogicalKeyEvent) return false; | |
return trigger == other.trigger && _keySet == other._keySet; | |
} | |
@override | |
int get hashCode => trigger.hashCode; | |
@override | |
String toString() { | |
return toKeySet().toString(); | |
} | |
} | |
String? _logicalCharacterForKeyEvent({ | |
required LogicalKeyboardKey trigger, | |
required Set<LogicalKeyboardKey> keysPressed, | |
required String? character, | |
}) { | |
if (_isQuoteKey(trigger)) { | |
return keysPressed.contains(LogicalKeyboardKey.shift) ? '"' : "'"; | |
} | |
if (character == null || character == '') return null; | |
if (keysPressed.contains(LogicalKeyboardKey.meta) || | |
keysPressed.contains(LogicalKeyboardKey.control)) { | |
return null; | |
} | |
if (LogicalKeyboardKey.isControlCharacter(character)) return null; | |
if (kIsWeb && _isWebUIEventKey(character)) return null; | |
return character; | |
} | |
const _kMacDeadKey = LogicalKeyboardKey(0x100070034); | |
// This is the accent key aka dead key (https://en.wikipedia.org/wiki/Dead_key) | |
// also doubles as a quote in some keyboard layouts. To account for this, we have | |
// to manually generate a single or double quote | |
bool _isQuoteKey(LogicalKeyboardKey key) { | |
return key == LogicalKeyboardKey.quote || key == _kMacDeadKey; | |
} | |
// Limitation of Flutter web which returns chars that map to UI events | |
// Further reading: https://www.w3.org/TR/uievents-key | |
// The equivalent regex would look like [A-Z][A-Za-z0-9]+ | |
bool _isWebUIEventKey(String chars) { | |
const _kA = 0x41; | |
const _kZ = 0x5a; | |
const _ka = 0x61; | |
const _kz = 0x7a; | |
const _k0 = 0x30; | |
const _k9 = 0x39; | |
bool isUpperAlpha(int codeUnit) => codeUnit >= _kA && codeUnit <= _kZ; | |
bool isAlphaNumeric(int codeUnit) { | |
return codeUnit >= _kA && codeUnit <= _kZ || | |
codeUnit >= _ka && codeUnit <= _kz || | |
codeUnit >= _k0 && codeUnit <= _k9; | |
} | |
if (chars.length <= 1) return false; | |
final codeUnits = chars.codeUnits; | |
if (!isUpperAlpha(codeUnits[0])) return false; | |
for (var i = 1; i < codeUnits.length; i++) { | |
if (!isAlphaNumeric(codeUnits[i])) return false; | |
} | |
return true; | |
} | |
/// This is a workaround to a bug on some platforms | |
/// | |
/// Bug 1: Sticky web keys (kIsWeb): | |
/// Modifier keys are "sticky". Meaning they are incorrectly marked as pressed | |
/// for some delay after they have already been released. | |
/// For example hitting cmd+right, then cmd+left, incorrectly reports | |
/// [RawKeyboard.instance.keysPressed] to be cmd+left+right when really right | |
/// is no longer pressed. | |
/// The workaround assumes we only have 1 non-modifier key. | |
/// On a [RawKeyDownEvent], we filter out any previous non-modifier keys and | |
/// replace them with the most recent non modifier key. | |
/// | |
/// Bug 2: Shift keys affecting MacOS key case | |
Set<LogicalKeyboardKey> _adjustKeysPressed( | |
Set<LogicalKeyboardKey> keys, RawKeyEvent keyEvent) { | |
if (keyEvent is! RawKeyDownEvent) return keys; | |
if (keyEvent.logicalKey.isModifierKey) return keys; | |
final modifierKeys = keys.where((k) => k.isModifierKey); | |
// In some systems, letters gets transformed to the upper case variant due to | |
// the shift modifier key. Here we normalize it so all letters are lower-case/ | |
// Meanwhile in most platforms, the lower case letter come through. | |
final triggerKey = modifierKeys.contains(LogicalKeyboardKey.shift) | |
? keyEvent.logicalKey.toLowerCase() | |
: keyEvent.logicalKey; | |
return {triggerKey, ...modifierKeys}; | |
} | |
LogicalKeyboardKey _collapseKeyboardSynonym(LogicalKeyboardKey key) { | |
return _keyboardKeySynonyms[key] ?? key; | |
} | |
Set<LogicalKeyboardKey> _collapseKeyboardKeySynonyms( | |
Set<LogicalKeyboardKey> keys) { | |
final collapsed = <LogicalKeyboardKey>{}; | |
for (final k in keys) { | |
collapsed.add(_collapseKeyboardSynonym(k)); | |
} | |
return collapsed; | |
} | |
extension LogicalKeyboardKeyExt on LogicalKeyboardKey { | |
bool get isModifierKey => _modifierKeys.contains(this); | |
} | |
final _modifierKeys = { | |
LogicalKeyboardKey.shiftLeft, | |
LogicalKeyboardKey.shiftRight, | |
LogicalKeyboardKey.shift, | |
LogicalKeyboardKey.metaLeft, | |
LogicalKeyboardKey.metaRight, | |
LogicalKeyboardKey.meta, | |
LogicalKeyboardKey.altLeft, | |
LogicalKeyboardKey.altRight, | |
LogicalKeyboardKey.alt, | |
LogicalKeyboardKey.controlLeft, | |
LogicalKeyboardKey.controlRight, | |
LogicalKeyboardKey.control, | |
LogicalKeyboardKey.fn, | |
}; | |
final _keyboardKeySynonyms = <LogicalKeyboardKey, LogicalKeyboardKey>{ | |
LogicalKeyboardKey.shiftLeft: LogicalKeyboardKey.shift, | |
LogicalKeyboardKey.shiftRight: LogicalKeyboardKey.shift, | |
LogicalKeyboardKey.metaLeft: LogicalKeyboardKey.meta, | |
LogicalKeyboardKey.metaRight: LogicalKeyboardKey.meta, | |
LogicalKeyboardKey.altLeft: LogicalKeyboardKey.alt, | |
LogicalKeyboardKey.altRight: LogicalKeyboardKey.alt, | |
LogicalKeyboardKey.controlLeft: LogicalKeyboardKey.control, | |
LogicalKeyboardKey.controlRight: LogicalKeyboardKey.control, | |
LogicalKeyboardKey.numpadEnter: LogicalKeyboardKey.enter, | |
}; | |
extension _LogicalKeyboardKeyCaseExt on LogicalKeyboardKey { | |
static final _kUpperToLowerDist = 0x20; | |
static final _kLowerCaseA = LogicalKeyboardKey.keyA.keyId; | |
static final _kLowerCaseZ = LogicalKeyboardKey.keyZ.keyId; | |
static final _kUpperCaseA = _kLowerCaseA - _kUpperToLowerDist; | |
static final _kUpperCaseZ = _kLowerCaseZ - _kUpperToLowerDist; | |
LogicalKeyboardKey toLowerCase() { | |
if (keyId < _kUpperCaseA || keyId > _kUpperCaseZ) return this; | |
return LogicalKeyboardKey(keyId + _kUpperToLowerDist); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment