-
Notifications
You must be signed in to change notification settings - Fork 6k
[Web, keyboard] Locale layout mapping #34625
Changes from 89 commits
8cf086a
21c50f0
0883f87
22d2ccf
3c05c3d
becbf08
e82bc25
416f574
5ed152d
cb3b06d
e7a6473
b88d06f
2175a4a
4f1083f
0a85ed9
4b0d8e4
d4920f4
249542c
3c4cac8
84703c7
1a7b3d9
8798bbc
f05dc33
f5704c5
c1a964b
35d42b1
162e790
0227c4c
66eb8a8
9d8e1ce
ca87473
f1f5b80
f329b68
b894f71
49198b0
0a40d4d
ae05f6b
a6eb42d
1a80988
7f985ee
013ea9a
9cec217
0010a73
ca82c56
d0a7c45
5638316
3a68127
4953e1f
d2636a3
4d78abf
d089597
1343f01
d0cd62c
d4fbac1
cf2df77
af178f6
b2a63bf
0c51f44
7675615
f594a0d
c4d0aab
5dc792a
0c5dffe
6450c92
0ab2d9c
ec0a719
f1334ce
c0ba71e
2809d26
8b36170
3754b08
f7de833
7857246
3b938c3
1582aa2
b1bce40
edfdd0f
13e6fb4
f970ce1
de98688
fdd3225
0e32e71
0d570e2
a507c33
3cfaf33
c33c9f2
cedb9bf
fe356c0
884e783
3c48e3d
e27af08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
Signature: 027af91b165acaa447651bfca8c7c704 | ||
Signature: f6d8146c82d268e2e2549bf5019ebf07 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
|
||
import 'package:meta/meta.dart'; | ||
import 'package:ui/ui.dart' as ui; | ||
import 'package:web_locale_keymap/web_locale_keymap.dart' as locale_keymap; | ||
|
||
import '../engine.dart' show registerHotRestartListener; | ||
import 'browser_detection.dart'; | ||
|
@@ -54,16 +55,6 @@ final Map<int, _ModifierGetter> _kLogicalKeyToModifierGetter = <int, _ModifierGe | |
_kLogicalMetaRight: (FlutterHtmlKeyboardEvent event) => event.metaKey, | ||
}; | ||
|
||
// ASCII for a, z, A, and Z | ||
const int _kCharLowerA = 0x61; | ||
const int _kCharLowerZ = 0x7a; | ||
const int _kCharUpperA = 0x41; | ||
const int _kCharUpperZ = 0x5a; | ||
bool isAlphabet(int charCode) { | ||
return (charCode >= _kCharLowerA && charCode <= _kCharLowerZ) | ||
|| (charCode >= _kCharUpperA && charCode <= _kCharUpperZ); | ||
} | ||
|
||
const String _kPhysicalCapsLock = 'CapsLock'; | ||
|
||
const String _kLogicalDead = 'Dead'; | ||
|
@@ -98,9 +89,24 @@ Duration _eventTimeStampToDuration(num milliseconds) { | |
return Duration(milliseconds: ms, microseconds: micro); | ||
} | ||
|
||
// Returns a function that caches the result of `body`, ensuring that `body` is | ||
// only run once. | ||
ValueGetter<T> _cached<T>(ValueGetter<T> body) { | ||
T? cache; | ||
return () { | ||
return cache ??= body(); | ||
}; | ||
} | ||
|
||
class KeyboardBinding { | ||
KeyboardBinding._() { | ||
_setup(); | ||
_addEventListener('keydown', allowInterop((DomEvent domEvent) { | ||
final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent); | ||
return _converter.handleEvent(event); | ||
})); | ||
_addEventListener('keyup', allowInterop((DomEvent event) { | ||
return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent)); | ||
})); | ||
} | ||
|
||
/// The singleton instance of this object. | ||
|
@@ -117,8 +123,23 @@ class KeyboardBinding { | |
} | ||
} | ||
|
||
static void debugClearInstance() { | ||
_instance = null; | ||
} | ||
|
||
/// The platform as used in the initialization. | ||
/// | ||
/// By default it is derived from [operatingSystem]. | ||
@protected | ||
OperatingSystem get localPlatform { | ||
return operatingSystem; | ||
} | ||
|
||
KeyboardConverter get converter => _converter; | ||
late final KeyboardConverter _converter; | ||
late final KeyboardConverter _converter = KeyboardConverter( | ||
_onKeyData, | ||
localPlatform, | ||
); | ||
final Map<String, DomEventListener> _listeners = <String, DomEventListener>{}; | ||
|
||
void _addEventListener(String eventName, DomEventListener handler) { | ||
|
@@ -154,16 +175,6 @@ class KeyboardBinding { | |
return result!; | ||
} | ||
|
||
void _setup() { | ||
_addEventListener('keydown', allowInterop((DomEvent event) { | ||
return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent)); | ||
})); | ||
_addEventListener('keyup', allowInterop((DomEvent event) { | ||
return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent)); | ||
})); | ||
_converter = KeyboardConverter(_onKeyData, onMacOs: operatingSystem == OperatingSystem.macOs); | ||
} | ||
|
||
void _reset() { | ||
_clearListeners(); | ||
_converter.dispose(); | ||
|
@@ -211,10 +222,30 @@ class FlutterHtmlKeyboardEvent { | |
// [dispatchKeyData] as given in the constructor. Some key data might be | ||
// dispatched asynchronously. | ||
class KeyboardConverter { | ||
KeyboardConverter(this.performDispatchKeyData, {this.onMacOs = false}); | ||
KeyboardConverter(this.performDispatchKeyData, OperatingSystem platform) | ||
: onMacOs = platform == OperatingSystem.macOs, | ||
_mapping = _mappingFromPlatform(platform); | ||
|
||
final DispatchKeyData performDispatchKeyData; | ||
// Whether the current platform is macOS, which affects how certain key events | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it the case that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice find. I've tested it on my iPad and I confirm that they also present on iOS. I'm changing it to onDarwin. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me make it a separate PR after this PR. |
||
// are comprehended. | ||
dkwingsmt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
final bool onMacOs; | ||
// Maps logical keys from key event properties. | ||
dkwingsmt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
final locale_keymap.LocaleKeymap _mapping; | ||
|
||
static locale_keymap.LocaleKeymap _mappingFromPlatform(OperatingSystem platform) { | ||
switch (platform) { | ||
case OperatingSystem.iOs: | ||
case OperatingSystem.macOs: | ||
return locale_keymap.LocaleKeymap.darwin(); | ||
case OperatingSystem.windows: | ||
return locale_keymap.LocaleKeymap.win(); | ||
case OperatingSystem.android: | ||
case OperatingSystem.linux: | ||
case OperatingSystem.unknown: | ||
return locale_keymap.LocaleKeymap.linux(); | ||
} | ||
} | ||
|
||
// The `performDispatchKeyData` wrapped with tracking logic. | ||
// | ||
|
@@ -273,29 +304,14 @@ class KeyboardConverter { | |
(metaDown ? _kDeadKeyMeta : 0); | ||
} | ||
|
||
// Whether `event.key` should be considered a key name. | ||
// Whether `event.key` is a key name, such as "Shift", or otherwise a | ||
// character, such as "S" or "ж". | ||
// | ||
// The `event.key` can either be a key name or the printable character. If the | ||
// first character is an alphabet, it must be either 'A' to 'Z' ( and return | ||
// true), or be a key name (and return false). Otherwise, return true. | ||
static bool _eventKeyIsKeyname(String key) { | ||
assert(key.isNotEmpty); | ||
return isAlphabet(key.codeUnitAt(0)) && key.length > 1; | ||
} | ||
|
||
static int _characterToLogicalKey(String key) { | ||
// Assume the length being <= 2 to be sufficient in all cases. If not, | ||
// extend the algorithm. | ||
assert(key.length <= 2); | ||
int result = key.codeUnitAt(0) & 0xffff; | ||
if (key.length == 2) { | ||
result += key.codeUnitAt(1) << 16; | ||
} | ||
// Convert upper letters to lower letters | ||
if (result >= _kCharUpperA && result <= _kCharUpperZ) { | ||
result = result + _kCharLowerA - _kCharUpperA; | ||
} | ||
return result; | ||
// A key name always has more than 1 code unit, and they are all alnums. | ||
// Character keys, however, can also have more than 1 code unit: en-in | ||
// maps KeyL to L̥/l̥. To resolve this, we check the second code unit. | ||
static bool _eventKeyIsKeyName(String key) { | ||
return key.length > 1 && key.codeUnitAt(0) < 0x7F && key.codeUnitAt(1) < 0x7F; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't you just say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, such as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh, OK. LGTM then. |
||
} | ||
|
||
static int _deadKeyToLogicalKey(int physicalKey, FlutterHtmlKeyboardEvent event) { | ||
|
@@ -307,10 +323,6 @@ class KeyboardConverter { | |
return physicalKey + _getModifierMask(event) + _kWebKeyIdPlane; | ||
} | ||
|
||
static int _otherLogicalKey(String key) { | ||
return kWebToLogicalKey[key] ?? (key.hashCode + _kWebKeyIdPlane); | ||
} | ||
|
||
// Map from pressed physical key to corresponding pressed logical key. | ||
// | ||
// Multiple physical keys can be mapped to the same logical key, usually due | ||
|
@@ -369,22 +381,33 @@ class KeyboardConverter { | |
final String eventKey = event.key!; | ||
|
||
final int physicalKey = _getPhysicalCode(event.code!); | ||
final bool logicalKeyIsCharacter = !_eventKeyIsKeyname(eventKey); | ||
final String? character = logicalKeyIsCharacter ? eventKey : null; | ||
final int logicalKey = () { | ||
final bool logicalKeyIsCharacter = !_eventKeyIsKeyName(eventKey); | ||
final ValueGetter<int> logicalKey = _cached<int>(() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for using late final int logicalKey = () {
// ...
}(); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function might not be evaluated: if the event is a key up event, the logical key is simply the pressed logical key, and this is not needed at all. In this case, I'm hoping not to run the function body, since it's not a trivial amount of work. And as far as I understand, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the docs, the https://dart.dev/null-safety/understanding-null-safety#lazy-initialization |
||
// Mapped logical keys, such as ArrowLeft, Escape, AudioVolumeDown. | ||
final int? mappedLogicalKey = kWebToLogicalKey[eventKey]; | ||
if (mappedLogicalKey != null) { | ||
return mappedLogicalKey; | ||
} | ||
// Keys with locations, such as modifier keys (Shift) or numpad keys. | ||
if (kWebLogicalLocationMap.containsKey(event.key)) { | ||
final int? result = kWebLogicalLocationMap[event.key!]?[event.location!]; | ||
assert(result != null, 'Invalid modifier location: ${event.key}, ${event.location}'); | ||
return result!; | ||
} | ||
if (character != null) { | ||
return _characterToLogicalKey(character); | ||
// Locale-sensitive keys: letters, digits, and certain symbols. | ||
if (logicalKeyIsCharacter) { | ||
final int? localeLogicalKeys = _mapping.getLogicalKey(event.code, event.key, event.keyCode); | ||
if (localeLogicalKeys != null) { | ||
return localeLogicalKeys; | ||
} | ||
} | ||
// Dead keys that are not handled by the locale mapping. | ||
if (eventKey == _kLogicalDead) { | ||
return _deadKeyToLogicalKey(physicalKey, event); | ||
} | ||
return _otherLogicalKey(eventKey); | ||
}(); | ||
// Minted logical keys. | ||
return eventKey.hashCode + _kWebKeyIdPlane; | ||
}); | ||
|
||
assert(event.type == 'keydown' || event.type == 'keyup'); | ||
final bool isPhysicalDown = event.type == 'keydown' || | ||
|
@@ -406,7 +429,7 @@ class KeyboardConverter { | |
timeStamp: timeStamp, | ||
type: ui.KeyEventType.up, | ||
physical: physicalKey, | ||
logical: logicalKey, | ||
logical: logicalKey(), | ||
character: null, | ||
synthesized: true, | ||
), | ||
|
@@ -441,7 +464,7 @@ class KeyboardConverter { | |
timeStamp: timeStamp, | ||
type: ui.KeyEventType.up, | ||
physical: physicalKey, | ||
logical: logicalKey, | ||
logical: logicalKey(), | ||
character: null, | ||
synthesized: true, | ||
)); | ||
|
@@ -474,7 +497,7 @@ class KeyboardConverter { | |
switch (type) { | ||
case ui.KeyEventType.down: | ||
assert(lastLogicalRecord == null); | ||
nextLogicalRecord = logicalKey; | ||
nextLogicalRecord = logicalKey(); | ||
break; | ||
case ui.KeyEventType.up: | ||
assert(lastLogicalRecord != null); | ||
|
@@ -499,7 +522,7 @@ class KeyboardConverter { | |
_kLogicalKeyToModifierGetter.forEach((int testeeLogicalKey, _ModifierGetter getModifier) { | ||
// Do not synthesize for the key of the current event. The event is the | ||
// ground truth. | ||
if (logicalKey == testeeLogicalKey) { | ||
if (logicalKey() == testeeLogicalKey) { | ||
return; | ||
} | ||
if (_pressingRecords.containsValue(testeeLogicalKey) && !getModifier(event)) { | ||
|
@@ -525,17 +548,18 @@ class KeyboardConverter { | |
// Update key guards | ||
if (logicalKeyIsCharacter) { | ||
if (nextLogicalRecord != null) { | ||
_startGuardingKey(physicalKey, logicalKey, timeStamp); | ||
_startGuardingKey(physicalKey, logicalKey(), timeStamp); | ||
} else { | ||
_stopGuardingKey(physicalKey); | ||
} | ||
} | ||
|
||
final String? character = logicalKeyIsCharacter ? eventKey : null; | ||
final ui.KeyData keyData = ui.KeyData( | ||
timeStamp: timeStamp, | ||
type: type, | ||
physical: physicalKey, | ||
logical: lastLogicalRecord ?? logicalKey, | ||
logical: lastLogicalRecord ?? logicalKey(), | ||
character: type == ui.KeyEventType.up ? null : character, | ||
synthesized: false, | ||
); | ||
|
Uh oh!
There was an error while loading. Please reload this page.