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

RawKeyboard repeat events, and SingleActivator.includeRepeats (#96154)

parent 3e5ddffb
......@@ -280,10 +280,14 @@ abstract class RawKeyEvent with Diagnosticable {
const RawKeyEvent({
required this.data,
this.character,
this.repeat = false,
});
/// Creates a concrete [RawKeyEvent] class from a message in the form received
/// on the [SystemChannels.keyEvent] channel.
///
/// [RawKeyEvent.repeat] will be derived from the current keyboard state,
/// instead of using the message information.
factory RawKeyEvent.fromMessage(Map<String, Object?> message) {
String? character;
RawKeyEventData _dataFromWeb() {
......@@ -388,10 +392,11 @@ abstract class RawKeyEvent with Diagnosticable {
throw FlutterError('Unknown keymap for key events: $keymap');
}
}
final bool repeat = RawKeyboard.instance.physicalKeysPressed.contains(data.physicalKey);
final String type = message['type']! as String;
switch (type) {
case 'keydown':
return RawKeyDownEvent(data: data, character: character);
return RawKeyDownEvent(data: data, character: character, repeat: repeat);
case 'keyup':
return RawKeyUpEvent(data: data);
default:
......@@ -504,6 +509,15 @@ abstract class RawKeyEvent with Diagnosticable {
/// input.
final String? character;
/// Whether this is a repeated down event.
///
/// When a key is held down, the systems usually fire a down event and then
/// a series of repeated down events. The [repeat] is false for the
/// first event and true for the following events.
///
/// The [repeat] attribute is always false for [RawKeyUpEvent]s.
final bool repeat;
/// Platform-specific information about the key event.
final RawKeyEventData data;
......@@ -512,6 +526,8 @@ abstract class RawKeyEvent with Diagnosticable {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LogicalKeyboardKey>('logicalKey', logicalKey));
properties.add(DiagnosticsProperty<PhysicalKeyboardKey>('physicalKey', physicalKey));
if (this is RawKeyDownEvent)
properties.add(DiagnosticsProperty<bool>('repeat', repeat));
}
}
......@@ -525,7 +541,8 @@ class RawKeyDownEvent extends RawKeyEvent {
const RawKeyDownEvent({
required RawKeyEventData data,
String? character,
}) : super(data: data, character: character);
bool repeat = false,
}) : super(data: data, character: character, repeat: repeat);
}
/// The user has released a key on the keyboard.
......@@ -538,7 +555,7 @@ class RawKeyUpEvent extends RawKeyEvent {
const RawKeyUpEvent({
required RawKeyEventData data,
String? character,
}) : super(data: data, character: character);
}) : super(data: data, character: character, repeat: false);
}
/// A callback type used by [RawKeyboard.keyEventHandler] to send key events to
......
......@@ -388,8 +388,7 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Int
/// * [CharacterActivator], an activator that represents key combinations
/// that result in the specified character, such as question mark.
class SingleActivator with Diagnosticable implements ShortcutActivator {
/// Triggered when the [trigger] key is pressed or repeated when the
/// modifiers are held.
/// Triggered when the [trigger] key is pressed while the modifiers are held.
///
/// The `trigger` should be the non-modifier key that is pressed after all the
/// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not be
......@@ -398,8 +397,9 @@ class SingleActivator with Diagnosticable implements ShortcutActivator {
/// The `control`, `shift`, `alt`, and `meta` flags represent whether
/// the respect modifier keys should be held (true) or released (false)
///
/// On each [RawKeyDownEvent] of the [trigger] key, this activator checks
/// whether the specified modifier conditions are met.
/// By default, the activator is checked on all [RawKeyDownEvent] events for
/// the [trigger] key. If `includeRepeats` is false, only the [trigger] key
/// events with a false [RawKeyDownEvent.repeat] attribute will be considered.
///
/// {@tool dartpad}
/// In the following example, the shortcut `Control + C` increases the counter:
......@@ -412,6 +412,7 @@ class SingleActivator with Diagnosticable implements ShortcutActivator {
this.shift = false,
this.alt = false,
this.meta = false,
this.includeRepeats = true,
}) : // The enumerated check with `identical` is cumbersome but the only way
// since const constructors can not call functions such as `==` or
// `Set.contains`. Checking with `identical` might not work when the
......@@ -482,6 +483,14 @@ class SingleActivator with Diagnosticable implements ShortcutActivator {
/// * [LogicalKeyboardKey.metaLeft], [LogicalKeyboardKey.metaRight].
final bool meta;
/// Whether this activator accepts repeat events of the [trigger] key.
///
/// If [includeRepeats] is true, the activator is checked on all
/// [RawKeyDownEvent] events for the [trigger] key. If `includeRepeats` is
/// false, only the [trigger] key events with a false [RawKeyDownEvent.repeat]
/// attribute will be considered.
final bool includeRepeats;
@override
Iterable<LogicalKeyboardKey> get triggers {
return <LogicalKeyboardKey>[trigger];
......@@ -491,6 +500,7 @@ class SingleActivator with Diagnosticable implements ShortcutActivator {
bool accepts(RawKeyEvent event, RawKeyboard state) {
final Set<LogicalKeyboardKey> pressed = state.keysPressed;
return event is RawKeyDownEvent
&& (includeRepeats || !event.repeat)
&& (control == (pressed.contains(LogicalKeyboardKey.controlLeft) || pressed.contains(LogicalKeyboardKey.controlRight)))
&& (shift == (pressed.contains(LogicalKeyboardKey.shiftLeft) || pressed.contains(LogicalKeyboardKey.shiftRight)))
&& (alt == (pressed.contains(LogicalKeyboardKey.altLeft) || pressed.contains(LogicalKeyboardKey.altRight)))
......@@ -522,6 +532,7 @@ class SingleActivator with Diagnosticable implements ShortcutActivator {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('keys', debugDescribeKeys()));
properties.add(FlagProperty('includeRepeats', value: includeRepeats, ifFalse: 'excluding repeats'));
}
}
......
......@@ -598,6 +598,49 @@ void main() {
);
}, skip: isBrowser); // [intended] This is an iOS-specific test.
testWidgets('repeat events', (WidgetTester tester) async {
expect(RawKeyboard.instance.keysPressed, isEmpty);
late RawKeyEvent receivedEvent;
RawKeyboard.instance.keyEventHandler = (RawKeyEvent event) {
receivedEvent = event;
return true;
};
// Dispatch a down event.
final Map<String, dynamic> downData = KeyEventSimulator.getKeyData(
LogicalKeyboardKey.keyA,
platform: 'windows',
);
await ServicesBinding.instance?.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(downData),
(ByteData? data) {},
);
expect(receivedEvent.repeat, false);
// Dispatch another down event, which should be recognized as a repeat.
await ServicesBinding.instance?.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(downData),
(ByteData? data) {},
);
expect(receivedEvent.repeat, true);
// Dispatch an up event.
await ServicesBinding.instance?.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(KeyEventSimulator.getKeyData(
LogicalKeyboardKey.keyA,
isDown: false,
platform: 'windows',
)),
(ByteData? data) {},
);
expect(receivedEvent.repeat, false);
RawKeyboard.instance.keyEventHandler = null;
}, skip: isBrowser); // [intended] This is a Windows-specific test.
testWidgets('sided modifiers without a side set return all sides on Windows', (WidgetTester tester) async {
expect(RawKeyboard.instance.keysPressed, isEmpty);
// Generate the data for a regular key down event.
......
......@@ -464,6 +464,33 @@ void main() {
expect(RawKeyboard.instance.keysPressed, isEmpty);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('rejects repeated events if requested', (WidgetTester tester) async {
int invoked = 0;
await tester.pumpWidget(activatorTester(
const SingleActivator(
LogicalKeyboardKey.keyC,
control: true,
includeRepeats: false,
),
(Intent intent) { invoked += 1; },
));
await tester.pump();
// LCtrl -> KeyC: Accept
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 1);
invoked = 0;
expect(RawKeyboard.instance.keysPressed, isEmpty);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async {
int invoked = 0;
await tester.pumpWidget(activatorTester(
......@@ -514,8 +541,8 @@ void main() {
expect(RawKeyboard.instance.keysPressed, isEmpty);
});
test('diagnostics.', () {
{
group('diagnostics.', () {
test('single key', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SingleActivator(
......@@ -528,9 +555,26 @@ void main() {
expect(description.length, equals(1));
expect(description[0], equals('keys: Key A'));
}
});
{
test('no repeats', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SingleActivator(
LogicalKeyboardKey.keyA,
includeRepeats: false,
).debugFillProperties(builder);
final List<String> description = builder.properties.where((DiagnosticsNode node) {
return !node.isFiltered(DiagnosticLevel.info);
}).map((DiagnosticsNode node) => node.toString()).toList();
expect(description.length, equals(2));
expect(description[0], equals('keys: Key A'));
expect(description[1], equals('excluding repeats'));
});
test('combination', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SingleActivator(
......@@ -547,7 +591,7 @@ void main() {
expect(description.length, equals(1));
expect(description[0], equals('keys: Control + Alt + Meta + Shift + Key A'));
}
});
});
});
......
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