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 {
///
/// 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.all] if pressed. If the given platform does not specify
/// the side, return [KeyboardSide.any].
KeyboardSide? getModifierSide(ModifierKey key);
/// Returns true if a CTRL modifier key was pressed at the time of this event,
......@@ -734,15 +734,38 @@ class RawKeyboard {
};
void _synchronizeModifiers(RawKeyEvent event) {
// Don't send any key events for these changes, since there *should* be
// separate events for each modifier key down/up that occurs while the app
// 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
// _keysPressed reflects reality at all times.
// Compare modifier states to the ground truth as specified by
// [RawKeyEvent.data.modifiersPressed] and update unsynchronized ones.
//
// This function will update the state of modifier keys in `_keysPressed` so
// 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<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) {
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])];
assert((){
if (mappedKeys == null) {
......@@ -764,7 +787,9 @@ class RawKeyboard {
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) {
// On Fuchsia and macOS, the Fn key is not considered a modifier key.
_keysPressed.remove(PhysicalKeyboardKey.fn);
......
......@@ -119,7 +119,7 @@ class RawKeyEventDataWeb extends RawKeyEventData {
//
// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState>
// for more information.
return KeyboardSide.all;
return KeyboardSide.any;
}
// Modifier key masks.
......
......@@ -354,16 +354,15 @@ void main() {
testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async {
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(
LogicalKeyboardKey.keyA,
platform: 'web',
isDown: true,
);
// 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.
data['metaState'] |= RawKeyEventDataWeb.modifierShift;
// dispatch the modified data.
)..['metaState'] |= RawKeyEventDataWeb.modifierShift;
// Dispatch the modified data.
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data),
......@@ -374,13 +373,71 @@ void main() {
equals(
<LogicalKeyboardKey>{
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,
},
),
);
// 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 {
......@@ -572,7 +629,7 @@ void main() {
);
}, 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);
// Generate the data for a regular key down event.
final Map<String, dynamic> data = KeyEventSimulator.getKeyData(
......@@ -597,13 +654,9 @@ void main() {
equals(
<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
LogicalKeyboardKey.altLeft,
LogicalKeyboardKey.altRight,
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.controlRight,
LogicalKeyboardKey.metaLeft,
LogicalKeyboardKey.metaRight,
LogicalKeyboardKey.keyA,
},
),
......
......@@ -21,6 +21,8 @@ import 'test_pointer.dart';
/// This value must be greater than [kTouchSlop].
const double kDragSlopDefault = 20.0;
const String _defaultPlatform = kIsWeb ? 'web' : 'android';
/// Class that programmatically interacts with widgets.
///
/// For a variant of this class suited specifically for unit tests, see
......@@ -976,8 +978,8 @@ abstract class WidgetController {
///
/// Specify `platform` as one of the platforms allowed in
/// [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.
/// Windows, iOS) are not yet supported.
/// of system. Defaults to "web" on web, and "android" everywhere else. Must not be
/// null. Some platforms (e.g. Windows, iOS) are not yet supported.
///
/// Keys that are down when the test completes are cleared after each test.
///
......@@ -991,7 +993,7 @@ abstract class WidgetController {
///
/// - [sendKeyDownEvent] to simulate only a key down 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);
final bool handled = await simulateKeyDownEvent(key, platform: platform);
// Internally wrapped in async guard.
......@@ -1006,8 +1008,8 @@ abstract class WidgetController {
///
/// Specify `platform` as one of the platforms allowed in
/// [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.
/// Windows, iOS) are not yet supported.
/// of system. Defaults to "web" on web, and "android" everywhere else. Must not be
/// null. Some platforms (e.g. Windows, iOS) are not yet supported.
///
/// Keys that are down when the test completes are cleared after each test.
///
......@@ -1017,7 +1019,7 @@ abstract class WidgetController {
///
/// - [sendKeyUpEvent] to simulate the corresponding key up event.
/// - [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);
// Internally wrapped in async guard.
return simulateKeyDownEvent(key, platform: platform);
......@@ -1029,8 +1031,8 @@ abstract class WidgetController {
/// not from a soft keyboard.
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". May not be null.
/// [Platform.operatingSystem] to make the event appear to be from that type of
/// 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.
///
......@@ -1038,7 +1040,7 @@ abstract class WidgetController {
///
/// - [sendKeyDownEvent] to simulate the corresponding key down event.
/// - [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);
// Internally wrapped in async guard.
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