Unverified Commit eb735167 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Shortcut activator (#78522)

This PR introduced a new class, ShortcutActivator, and refactored the the definition and lookup system of shortcuts, in order to solve a few issues of the previous algorithm.
parent 600e4893
...@@ -132,8 +132,8 @@ abstract class RawKeyEventData { ...@@ -132,8 +132,8 @@ abstract class RawKeyEventData {
/// ///
/// If the modifier key wasn't pressed at the time of this event, returns /// 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 /// null. If the given key only appears in one place on the keyboard, returns
/// [KeyboardSide.all] if pressed. Never returns [KeyboardSide.any], because /// [KeyboardSide.all] if pressed. If the given platform does not specify
/// that doesn't make sense in this context. /// the side, return [KeyboardSide.any].
KeyboardSide? getModifierSide(ModifierKey key); KeyboardSide? getModifierSide(ModifierKey key);
/// Returns true if a CTRL modifier key was pressed at the time of this event, /// Returns true if a CTRL modifier key was pressed at the time of this event,
...@@ -734,15 +734,38 @@ class RawKeyboard { ...@@ -734,15 +734,38 @@ class RawKeyboard {
}; };
void _synchronizeModifiers(RawKeyEvent event) { void _synchronizeModifiers(RawKeyEvent event) {
// Don't send any key events for these changes, since there *should* be // Compare modifier states to the ground truth as specified by
// separate events for each modifier key down/up that occurs while the app // [RawKeyEvent.data.modifiersPressed] and update unsynchronized ones.
// has focus. This is just to synchronize the modifier keys when they are //
// pressed/released while the app doesn't have focus, to make sure that // This function will update the state of modifier keys in `_keysPressed` so
// _keysPressed reflects reality at all times. // that they match the ones given by [RawKeyEvent.data.modifiersPressed].
// For a `modifiersPressed` result of anything but [KeyboardSide.any], the
// states in `_keysPressed` will be updated to exactly match the result,
// i.e. exactly one of "no down", "left down", "right down" or "both down".
//
// If `modifiersPressed` returns [KeyboardSide.any], the states in
// `_keysPressed` will be updated to a rough match, i.e. "either side down"
// or "no down". If `_keysPressed` has no modifier down, a
// [KeyboardSide.any] will synchronize by forcing the left modifier down. If
// `_keysPressed` has any modifier down, a [KeyboardSide.any] will not cause
// a state change.
final Map<ModifierKey, KeyboardSide?> modifiersPressed = event.data.modifiersPressed; final Map<ModifierKey, KeyboardSide?> modifiersPressed = event.data.modifiersPressed;
final Map<PhysicalKeyboardKey, LogicalKeyboardKey> modifierKeys = <PhysicalKeyboardKey, LogicalKeyboardKey>{}; final Map<PhysicalKeyboardKey, LogicalKeyboardKey> modifierKeys = <PhysicalKeyboardKey, LogicalKeyboardKey>{};
// Physical keys that whose modifiers are pressed at any side.
final Set<PhysicalKeyboardKey> anySideKeys = <PhysicalKeyboardKey>{};
final Set<PhysicalKeyboardKey> keysPressedAfterEvent = <PhysicalKeyboardKey>{
..._keysPressed.keys,
if (event is RawKeyDownEvent) event.physicalKey,
};
for (final ModifierKey key in modifiersPressed.keys) { for (final ModifierKey key in modifiersPressed.keys) {
if (modifiersPressed[key] == KeyboardSide.any) {
final Set<PhysicalKeyboardKey>? thisModifierKeys = _modifierKeyMap[_ModifierSidePair(key, KeyboardSide.all)];
anySideKeys.addAll(thisModifierKeys!);
if (thisModifierKeys.any(keysPressedAfterEvent.contains)) {
continue;
}
}
final Set<PhysicalKeyboardKey>? mappedKeys = _modifierKeyMap[_ModifierSidePair(key, modifiersPressed[key])]; final Set<PhysicalKeyboardKey>? mappedKeys = _modifierKeyMap[_ModifierSidePair(key, modifiersPressed[key])];
assert((){ assert((){
if (mappedKeys == null) { if (mappedKeys == null) {
...@@ -764,7 +787,9 @@ class RawKeyboard { ...@@ -764,7 +787,9 @@ class RawKeyboard {
modifierKeys[physicalModifier] = _allModifiers[physicalModifier]!; modifierKeys[physicalModifier] = _allModifiers[physicalModifier]!;
} }
} }
_allModifiersExceptFn.keys.forEach(_keysPressed.remove); _allModifiersExceptFn.keys
.where((PhysicalKeyboardKey key) => !anySideKeys.contains(key))
.forEach(_keysPressed.remove);
if (event.data is! RawKeyEventDataFuchsia && event.data is! RawKeyEventDataMacOs) { if (event.data is! RawKeyEventDataFuchsia && event.data is! RawKeyEventDataMacOs) {
// On Fuchsia and macOS, the Fn key is not considered a modifier key. // On Fuchsia and macOS, the Fn key is not considered a modifier key.
_keysPressed.remove(PhysicalKeyboardKey.fn); _keysPressed.remove(PhysicalKeyboardKey.fn);
......
...@@ -119,7 +119,7 @@ class RawKeyEventDataWeb extends RawKeyEventData { ...@@ -119,7 +119,7 @@ class RawKeyEventDataWeb extends RawKeyEventData {
// //
// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState> // See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState>
// for more information. // for more information.
return KeyboardSide.all; return KeyboardSide.any;
} }
// Modifier key masks. // Modifier key masks.
......
...@@ -354,16 +354,15 @@ void main() { ...@@ -354,16 +354,15 @@ void main() {
testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async { testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async {
expect(RawKeyboard.instance.keysPressed, isEmpty); expect(RawKeyboard.instance.keysPressed, isEmpty);
// Generate the data for a regular key down event. // Generate the data for a regular key down event. Change the modifiers so
// that they show the shift key as already down when this event is
// received, but it's not in keysPressed yet.
final Map<String, dynamic> data = KeyEventSimulator.getKeyData( final Map<String, dynamic> data = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyA,
platform: 'web', platform: 'web',
isDown: true, isDown: true,
); )..['metaState'] |= RawKeyEventDataWeb.modifierShift;
// Change the modifiers so that they show the shift key as already down // Dispatch the modified data.
// when this event is received, but it's not in keysPressed yet.
data['metaState'] |= RawKeyEventDataWeb.modifierShift;
// dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name, SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data), SystemChannels.keyEvent.codec.encodeMessage(data),
...@@ -374,13 +373,71 @@ void main() { ...@@ -374,13 +373,71 @@ void main() {
equals( equals(
<LogicalKeyboardKey>{ <LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft,
// Web doesn't distinguish between left and right keys, so they're
// all shown as down when either is pressed.
LogicalKeyboardKey.shiftRight,
LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyA,
}, },
), ),
); );
// Generate the data for a regular key up event. Don't set the modifiers
// for shift so that they show the shift key as already up when this event
// is received, and it's in keysPressed.
final Map<String, dynamic> data2 = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.keyA,
platform: 'web',
isDown: false,
)..['metaState'] = 0;
// Dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data2),
(ByteData? data) {},
);
expect(
RawKeyboard.instance.keysPressed,
equals(
<LogicalKeyboardKey>{},
),
);
// Press right modifier key
final Map<String, dynamic> data3 = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.shiftRight,
platform: 'web',
isDown: true,
)..['metaState'] |= RawKeyEventDataWeb.modifierShift;
// Dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data3),
(ByteData? data) {},
);
expect(
RawKeyboard.instance.keysPressed,
equals(
<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftRight,
},
),
);
// Release the key
final Map<String, dynamic> data4 = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.shiftRight,
platform: 'web',
isDown: false,
)..['metaState'] = 0;
// Dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data4),
(ByteData? data) {},
);
expect(
RawKeyboard.instance.keysPressed,
equals(
<LogicalKeyboardKey>{},
),
);
}); });
testWidgets('sided modifiers without a side set return all sides on Android', (WidgetTester tester) async { testWidgets('sided modifiers without a side set return all sides on Android', (WidgetTester tester) async {
...@@ -572,7 +629,7 @@ void main() { ...@@ -572,7 +629,7 @@ void main() {
); );
}, skip: isBrowser); // This is a GLFW-specific test. }, skip: isBrowser); // This is a GLFW-specific test.
testWidgets('sided modifiers without a side set return all sides on web', (WidgetTester tester) async { testWidgets('sided modifiers without a side set return left sides on web', (WidgetTester tester) async {
expect(RawKeyboard.instance.keysPressed, isEmpty); expect(RawKeyboard.instance.keysPressed, isEmpty);
// Generate the data for a regular key down event. // Generate the data for a regular key down event.
final Map<String, dynamic> data = KeyEventSimulator.getKeyData( final Map<String, dynamic> data = KeyEventSimulator.getKeyData(
...@@ -597,13 +654,9 @@ void main() { ...@@ -597,13 +654,9 @@ void main() {
equals( equals(
<LogicalKeyboardKey>{ <LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
LogicalKeyboardKey.altLeft, LogicalKeyboardKey.altLeft,
LogicalKeyboardKey.altRight,
LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.controlRight,
LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaLeft,
LogicalKeyboardKey.metaRight,
LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyA,
}, },
), ),
......
...@@ -21,6 +21,8 @@ import 'test_pointer.dart'; ...@@ -21,6 +21,8 @@ import 'test_pointer.dart';
/// This value must be greater than [kTouchSlop]. /// This value must be greater than [kTouchSlop].
const double kDragSlopDefault = 20.0; const double kDragSlopDefault = 20.0;
const String _defaultPlatform = kIsWeb ? 'web' : 'android';
/// Class that programmatically interacts with widgets. /// Class that programmatically interacts with widgets.
/// ///
/// For a variant of this class suited specifically for unit tests, see /// For a variant of this class suited specifically for unit tests, see
...@@ -976,8 +978,8 @@ abstract class WidgetController { ...@@ -976,8 +978,8 @@ abstract class WidgetController {
/// ///
/// Specify `platform` as one of the platforms allowed in /// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type /// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". Must not be null. Some platforms (e.g. /// of system. Defaults to "web" on web, and "android" everywhere else. Must not be
/// Windows, iOS) are not yet supported. /// null. Some platforms (e.g. Windows, iOS) are not yet supported.
/// ///
/// Keys that are down when the test completes are cleared after each test. /// Keys that are down when the test completes are cleared after each test.
/// ///
...@@ -991,7 +993,7 @@ abstract class WidgetController { ...@@ -991,7 +993,7 @@ abstract class WidgetController {
/// ///
/// - [sendKeyDownEvent] to simulate only a key down event. /// - [sendKeyDownEvent] to simulate only a key down event.
/// - [sendKeyUpEvent] to simulate only a key up event. /// - [sendKeyUpEvent] to simulate only a key up event.
Future<bool> sendKeyEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { Future<bool> sendKeyEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
assert(platform != null); assert(platform != null);
final bool handled = await simulateKeyDownEvent(key, platform: platform); final bool handled = await simulateKeyDownEvent(key, platform: platform);
// Internally wrapped in async guard. // Internally wrapped in async guard.
...@@ -1006,8 +1008,8 @@ abstract class WidgetController { ...@@ -1006,8 +1008,8 @@ abstract class WidgetController {
/// ///
/// Specify `platform` as one of the platforms allowed in /// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type /// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". Must not be null. Some platforms (e.g. /// of system. Defaults to "web" on web, and "android" everywhere else. Must not be
/// Windows, iOS) are not yet supported. /// null. Some platforms (e.g. Windows, iOS) are not yet supported.
/// ///
/// Keys that are down when the test completes are cleared after each test. /// Keys that are down when the test completes are cleared after each test.
/// ///
...@@ -1017,7 +1019,7 @@ abstract class WidgetController { ...@@ -1017,7 +1019,7 @@ abstract class WidgetController {
/// ///
/// - [sendKeyUpEvent] to simulate the corresponding key up event. /// - [sendKeyUpEvent] to simulate the corresponding key up event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call. /// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
assert(platform != null); assert(platform != null);
// Internally wrapped in async guard. // Internally wrapped in async guard.
return simulateKeyDownEvent(key, platform: platform); return simulateKeyDownEvent(key, platform: platform);
...@@ -1029,8 +1031,8 @@ abstract class WidgetController { ...@@ -1029,8 +1031,8 @@ abstract class WidgetController {
/// not from a soft keyboard. /// not from a soft keyboard.
/// ///
/// Specify `platform` as one of the platforms allowed in /// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type /// [Platform.operatingSystem] to make the event appear to be from that type of
/// of system. Defaults to "android". May not be null. /// system. Defaults to "web" on web, and "android" everywhere else. May not be null.
/// ///
/// Returns true if the key event was handled by the framework. /// Returns true if the key event was handled by the framework.
/// ///
...@@ -1038,7 +1040,7 @@ abstract class WidgetController { ...@@ -1038,7 +1040,7 @@ abstract class WidgetController {
/// ///
/// - [sendKeyDownEvent] to simulate the corresponding key down event. /// - [sendKeyDownEvent] to simulate the corresponding key down event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call. /// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
assert(platform != null); assert(platform != null);
// Internally wrapped in async guard. // Internally wrapped in async guard.
return simulateKeyUpEvent(key, platform: platform); return simulateKeyUpEvent(key, platform: platform);
......
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