Skip to content

Commit cda8041

Browse files
authored
[Keyboard] Make CharacterActivator support Ctrl and Meta modifiers, and repeats (#107195)
1 parent 74ac867 commit cda8041

File tree

2 files changed

+172
-18
lines changed

2 files changed

+172
-18
lines changed

packages/flutter/lib/src/widgets/shortcuts.dart

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,13 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Int
393393
class SingleActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
394394
/// Triggered when the [trigger] key is pressed while the modifiers are held.
395395
///
396-
/// The `trigger` should be the non-modifier key that is pressed after all the
396+
/// The [trigger] should be the non-modifier key that is pressed after all the
397397
/// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not be
398398
/// a modifier key (sided or unsided).
399399
///
400-
/// The `control`, `shift`, `alt`, and `meta` flags represent whether
401-
/// the respect modifier keys should be held (true) or released (false)
400+
/// The [control], [shift], [alt], and [meta] flags represent whether
401+
/// the respect modifier keys should be held (true) or released (false).
402+
/// They default to false.
402403
///
403404
/// By default, the activator is checked on all [RawKeyDownEvent] events for
404405
/// the [trigger] key. If `includeRepeats` is false, only the [trigger] key
@@ -445,8 +446,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
445446
/// Whether either (or both) control keys should be held for [trigger] to
446447
/// activate the shortcut.
447448
///
448-
/// If false, then all control keys must be released when the event is received
449-
/// in order to activate the shortcut.
449+
/// It defaults to false, meaning all Control keys must be released when the
450+
/// event is received in order to activate the shortcut. If it's true, then
451+
/// either or both Control keys must be pressed.
450452
///
451453
/// See also:
452454
///
@@ -456,8 +458,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
456458
/// Whether either (or both) shift keys should be held for [trigger] to
457459
/// activate the shortcut.
458460
///
459-
/// If false, then all shift keys must be released when the event is received
460-
/// in order to activate the shortcut.
461+
/// It defaults to false, meaning all Shift keys must be released when the
462+
/// event is received in order to activate the shortcut. If it's true, then
463+
/// either or both Shift keys must be pressed.
461464
///
462465
/// See also:
463466
///
@@ -467,8 +470,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
467470
/// Whether either (or both) alt keys should be held for [trigger] to
468471
/// activate the shortcut.
469472
///
470-
/// If false, then all alt keys must be released when the event is received
471-
/// in order to activate the shortcut.
473+
/// It defaults to false, meaning all Alt keys must be released when the
474+
/// event is received in order to activate the shortcut. If it's true, then
475+
/// either or both Alt keys must be pressed.
472476
///
473477
/// See also:
474478
///
@@ -478,8 +482,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
478482
/// Whether either (or both) meta keys should be held for [trigger] to
479483
/// activate the shortcut.
480484
///
481-
/// If false, then all meta keys must be released when the event is received
482-
/// in order to activate the shortcut.
485+
/// It defaults to false, meaning all Meta keys must be released when the
486+
/// event is received in order to activate the shortcut. If it's true, then
487+
/// either or both Meta keys must be pressed.
483488
///
484489
/// See also:
485490
///
@@ -545,7 +550,7 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
545550
@override
546551
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
547552
super.debugFillProperties(properties);
548-
properties.add(DiagnosticsProperty<String>('keys', debugDescribeKeys()));
553+
properties.add(MessageProperty('keys', debugDescribeKeys()));
549554
properties.add(FlagProperty('includeRepeats', value: includeRepeats, ifFalse: 'excluding repeats'));
550555
}
551556
}
@@ -577,8 +582,54 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
577582
/// * [SingleActivator], an activator that represents a single key combined
578583
/// with modifiers, such as `Ctrl+C`.
579584
class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
580-
/// Create a [CharacterActivator] from the triggering character.
581-
const CharacterActivator(this.character);
585+
/// Triggered when the key event yields the given character.
586+
///
587+
/// The [control] and [meta] flags represent whether the respect modifier
588+
/// keys should be held (true) or released (false). They default to false.
589+
/// [CharacterActivator] can not check Shift keys or Alt keys yet, and will
590+
/// accept whether they are pressed or not.
591+
///
592+
/// By default, the activator is checked on all [RawKeyDownEvent] events for
593+
/// the [character]. If `includeRepeats` is false, only the [character]
594+
/// events with a false [RawKeyDownEvent.repeat] attribute will be
595+
/// considered.
596+
const CharacterActivator(this.character, {
597+
this.control = false,
598+
this.meta = false,
599+
this.includeRepeats = true,
600+
});
601+
602+
/// Whether either (or both) control keys should be held for the [character]
603+
/// to activate the shortcut.
604+
///
605+
/// It defaults to false, meaning all Control keys must be released when the
606+
/// event is received in order to activate the shortcut. If it's true, then
607+
/// either or both Control keys must be pressed.
608+
///
609+
/// See also:
610+
///
611+
/// * [LogicalKeyboardKey.controlLeft], [LogicalKeyboardKey.controlRight].
612+
final bool control;
613+
614+
/// Whether either (or both) meta keys should be held for the [character] to
615+
/// activate the shortcut.
616+
///
617+
/// It defaults to false, meaning all Meta keys must be released when the
618+
/// event is received in order to activate the shortcut. If it's true, then
619+
/// either or both Meta keys must be pressed.
620+
///
621+
/// See also:
622+
///
623+
/// * [LogicalKeyboardKey.metaLeft], [LogicalKeyboardKey.metaRight].
624+
final bool meta;
625+
626+
/// Whether this activator accepts repeat events of the [character].
627+
///
628+
/// If [includeRepeats] is true, the activator is checked on all
629+
/// [RawKeyDownEvent] events for the [character]. If `includeRepeats` is
630+
/// false, only the [character] events with a false [RawKeyDownEvent.repeat]
631+
/// attribute will be considered.
632+
final bool includeRepeats;
582633

583634
/// The character of the triggering event.
584635
///
@@ -598,15 +649,24 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
598649

599650
@override
600651
bool accepts(RawKeyEvent event, RawKeyboard state) {
652+
final Set<LogicalKeyboardKey> pressed = state.keysPressed;
601653
return event is RawKeyDownEvent
602-
&& event.character == character;
654+
&& event.character == character
655+
&& (includeRepeats || !event.repeat)
656+
&& (control == (pressed.contains(LogicalKeyboardKey.controlLeft) || pressed.contains(LogicalKeyboardKey.controlRight)))
657+
&& (meta == (pressed.contains(LogicalKeyboardKey.metaLeft) || pressed.contains(LogicalKeyboardKey.metaRight)));
603658
}
604659

605660
@override
606661
String debugDescribeKeys() {
607662
String result = '';
608663
assert(() {
609-
result = "'$character'";
664+
final List<String> keys = <String>[
665+
if (control) 'Control',
666+
if (meta) 'Meta',
667+
"'$character'",
668+
];
669+
result = keys.join(' + ');
610670
return true;
611671
}());
612672
return result;
@@ -620,7 +680,8 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
620680
@override
621681
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
622682
super.debugFillProperties(properties);
623-
properties.add(StringProperty('character', character));
683+
properties.add(MessageProperty('character', debugDescribeKeys()));
684+
properties.add(FlagProperty('includeRepeats', value: includeRepeats, ifFalse: 'excluding repeats'));
624685
}
625686
}
626687

packages/flutter/test/widgets/shortcuts_test.dart

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1112,7 +1112,7 @@ void main() {
11121112
));
11131113
await tester.pump();
11141114

1115-
// Press KeyC: Accepted by DumbLogicalActivator
1115+
// Press Shift + /
11161116
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
11171117
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
11181118
expect(invoked, 1);
@@ -1142,6 +1142,53 @@ void main() {
11421142
invoked = 0;
11431143
}, variant: KeySimulatorTransitModeVariant.all());
11441144

1145+
testWidgets('rejects repeated events if requested', (WidgetTester tester) async {
1146+
int invoked = 0;
1147+
await tester.pumpWidget(activatorTester(
1148+
const CharacterActivator('?', includeRepeats: false),
1149+
(Intent intent) { invoked += 1; },
1150+
));
1151+
await tester.pump();
1152+
1153+
// Press Shift + /
1154+
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
1155+
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
1156+
expect(invoked, 1);
1157+
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?');
1158+
expect(invoked, 1);
1159+
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
1160+
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
1161+
expect(invoked, 1);
1162+
invoked = 0;
1163+
}, variant: KeySimulatorTransitModeVariant.all());
1164+
1165+
testWidgets('handles Ctrl and Meta', (WidgetTester tester) async {
1166+
int invoked = 0;
1167+
await tester.pumpWidget(activatorTester(
1168+
const CharacterActivator('?', meta: true, control: true),
1169+
(Intent intent) { invoked += 1; },
1170+
));
1171+
await tester.pump();
1172+
1173+
// Press Shift + /
1174+
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
1175+
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
1176+
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
1177+
expect(invoked, 0);
1178+
1179+
// Press Ctrl + Meta + Shift + /
1180+
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
1181+
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
1182+
expect(invoked, 0);
1183+
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
1184+
expect(invoked, 1);
1185+
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
1186+
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
1187+
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
1188+
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
1189+
expect(invoked, 1);
1190+
invoked = 0;
1191+
}, variant: KeySimulatorTransitModeVariant.all());
11451192

11461193
testWidgets('isActivatedBy works as expected', (WidgetTester tester) async {
11471194
// Collect some key events to use for testing.
@@ -1163,6 +1210,52 @@ void main() {
11631210
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
11641211
expect(ShortcutActivator.isActivatedBy(characterActivator, events[0]), isTrue);
11651212
});
1213+
1214+
group('diagnostics.', () {
1215+
test('single key', () {
1216+
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
1217+
1218+
const CharacterActivator('A').debugFillProperties(builder);
1219+
1220+
final List<String> description = builder.properties.where((DiagnosticsNode node) {
1221+
return !node.isFiltered(DiagnosticLevel.info);
1222+
}).map((DiagnosticsNode node) => node.toString()).toList();
1223+
1224+
expect(description.length, equals(1));
1225+
expect(description[0], equals("character: 'A'"));
1226+
});
1227+
1228+
test('no repeats', () {
1229+
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
1230+
1231+
const CharacterActivator('A', includeRepeats: false)
1232+
.debugFillProperties(builder);
1233+
1234+
final List<String> description = builder.properties.where((DiagnosticsNode node) {
1235+
return !node.isFiltered(DiagnosticLevel.info);
1236+
}).map((DiagnosticsNode node) => node.toString()).toList();
1237+
1238+
expect(description.length, equals(2));
1239+
expect(description[0], equals("character: 'A'"));
1240+
expect(description[1], equals('excluding repeats'));
1241+
});
1242+
1243+
test('combination', () {
1244+
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
1245+
1246+
const CharacterActivator('A',
1247+
control: true,
1248+
meta: true,
1249+
).debugFillProperties(builder);
1250+
1251+
final List<String> description = builder.properties.where((DiagnosticsNode node) {
1252+
return !node.isFiltered(DiagnosticLevel.info);
1253+
}).map((DiagnosticsNode node) => node.toString()).toList();
1254+
1255+
expect(description.length, equals(1));
1256+
expect(description[0], equals("character: Control + Meta + 'A'"));
1257+
});
1258+
});
11661259
});
11671260

11681261
group('CallbackShortcuts', () {

0 commit comments

Comments
 (0)