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

[Keyboard] Make CharacterActivator support Ctrl and Meta modifiers, and repeats (#107195)

parent 74ac8678
...@@ -393,12 +393,13 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Int ...@@ -393,12 +393,13 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Int
class SingleActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator { class SingleActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
/// Triggered when the [trigger] key is pressed while 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 /// 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 /// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not be
/// a modifier key (sided or unsided). /// a modifier key (sided or unsided).
/// ///
/// The `control`, `shift`, `alt`, and `meta` flags represent whether /// The [control], [shift], [alt], and [meta] flags represent whether
/// the respect modifier keys should be held (true) or released (false) /// the respect modifier keys should be held (true) or released (false).
/// They default to false.
/// ///
/// By default, the activator is checked on all [RawKeyDownEvent] events for /// By default, the activator is checked on all [RawKeyDownEvent] events for
/// the [trigger] key. If `includeRepeats` is false, only the [trigger] key /// the [trigger] key. If `includeRepeats` is false, only the [trigger] key
...@@ -445,8 +446,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S ...@@ -445,8 +446,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// Whether either (or both) control keys should be held for [trigger] to /// Whether either (or both) control keys should be held for [trigger] to
/// activate the shortcut. /// activate the shortcut.
/// ///
/// If false, then all control keys must be released when the event is received /// It defaults to false, meaning all Control keys must be released when the
/// in order to activate the shortcut. /// event is received in order to activate the shortcut. If it's true, then
/// either or both Control keys must be pressed.
/// ///
/// See also: /// See also:
/// ///
...@@ -456,8 +458,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S ...@@ -456,8 +458,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// Whether either (or both) shift keys should be held for [trigger] to /// Whether either (or both) shift keys should be held for [trigger] to
/// activate the shortcut. /// activate the shortcut.
/// ///
/// If false, then all shift keys must be released when the event is received /// It defaults to false, meaning all Shift keys must be released when the
/// in order to activate the shortcut. /// event is received in order to activate the shortcut. If it's true, then
/// either or both Shift keys must be pressed.
/// ///
/// See also: /// See also:
/// ///
...@@ -467,8 +470,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S ...@@ -467,8 +470,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// Whether either (or both) alt keys should be held for [trigger] to /// Whether either (or both) alt keys should be held for [trigger] to
/// activate the shortcut. /// activate the shortcut.
/// ///
/// If false, then all alt keys must be released when the event is received /// It defaults to false, meaning all Alt keys must be released when the
/// in order to activate the shortcut. /// event is received in order to activate the shortcut. If it's true, then
/// either or both Alt keys must be pressed.
/// ///
/// See also: /// See also:
/// ///
...@@ -478,8 +482,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S ...@@ -478,8 +482,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// Whether either (or both) meta keys should be held for [trigger] to /// Whether either (or both) meta keys should be held for [trigger] to
/// activate the shortcut. /// activate the shortcut.
/// ///
/// If false, then all meta keys must be released when the event is received /// It defaults to false, meaning all Meta keys must be released when the
/// in order to activate the shortcut. /// event is received in order to activate the shortcut. If it's true, then
/// either or both Meta keys must be pressed.
/// ///
/// See also: /// See also:
/// ///
...@@ -545,7 +550,7 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S ...@@ -545,7 +550,7 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('keys', debugDescribeKeys())); properties.add(MessageProperty('keys', debugDescribeKeys()));
properties.add(FlagProperty('includeRepeats', value: includeRepeats, ifFalse: 'excluding repeats')); properties.add(FlagProperty('includeRepeats', value: includeRepeats, ifFalse: 'excluding repeats'));
} }
} }
...@@ -577,8 +582,54 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S ...@@ -577,8 +582,54 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// * [SingleActivator], an activator that represents a single key combined /// * [SingleActivator], an activator that represents a single key combined
/// with modifiers, such as `Ctrl+C`. /// with modifiers, such as `Ctrl+C`.
class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator { class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
/// Create a [CharacterActivator] from the triggering character. /// Triggered when the key event yields the given character.
const CharacterActivator(this.character); ///
/// The [control] and [meta] flags represent whether the respect modifier
/// keys should be held (true) or released (false). They default to false.
/// [CharacterActivator] can not check Shift keys or Alt keys yet, and will
/// accept whether they are pressed or not.
///
/// By default, the activator is checked on all [RawKeyDownEvent] events for
/// the [character]. If `includeRepeats` is false, only the [character]
/// events with a false [RawKeyDownEvent.repeat] attribute will be
/// considered.
const CharacterActivator(this.character, {
this.control = false,
this.meta = false,
this.includeRepeats = true,
});
/// Whether either (or both) control keys should be held for the [character]
/// to activate the shortcut.
///
/// It defaults to false, meaning all Control keys must be released when the
/// event is received in order to activate the shortcut. If it's true, then
/// either or both Control keys must be pressed.
///
/// See also:
///
/// * [LogicalKeyboardKey.controlLeft], [LogicalKeyboardKey.controlRight].
final bool control;
/// Whether either (or both) meta keys should be held for the [character] to
/// activate the shortcut.
///
/// It defaults to false, meaning all Meta keys must be released when the
/// event is received in order to activate the shortcut. If it's true, then
/// either or both Meta keys must be pressed.
///
/// See also:
///
/// * [LogicalKeyboardKey.metaLeft], [LogicalKeyboardKey.metaRight].
final bool meta;
/// Whether this activator accepts repeat events of the [character].
///
/// If [includeRepeats] is true, the activator is checked on all
/// [RawKeyDownEvent] events for the [character]. If `includeRepeats` is
/// false, only the [character] events with a false [RawKeyDownEvent.repeat]
/// attribute will be considered.
final bool includeRepeats;
/// The character of the triggering event. /// The character of the triggering event.
/// ///
...@@ -598,15 +649,24 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement ...@@ -598,15 +649,24 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
@override @override
bool accepts(RawKeyEvent event, RawKeyboard state) { bool accepts(RawKeyEvent event, RawKeyboard state) {
final Set<LogicalKeyboardKey> pressed = state.keysPressed;
return event is RawKeyDownEvent return event is RawKeyDownEvent
&& event.character == character; && event.character == character
&& (includeRepeats || !event.repeat)
&& (control == (pressed.contains(LogicalKeyboardKey.controlLeft) || pressed.contains(LogicalKeyboardKey.controlRight)))
&& (meta == (pressed.contains(LogicalKeyboardKey.metaLeft) || pressed.contains(LogicalKeyboardKey.metaRight)));
} }
@override @override
String debugDescribeKeys() { String debugDescribeKeys() {
String result = ''; String result = '';
assert(() { assert(() {
result = "'$character'"; final List<String> keys = <String>[
if (control) 'Control',
if (meta) 'Meta',
"'$character'",
];
result = keys.join(' + ');
return true; return true;
}()); }());
return result; return result;
...@@ -620,7 +680,8 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement ...@@ -620,7 +680,8 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(StringProperty('character', character)); properties.add(MessageProperty('character', debugDescribeKeys()));
properties.add(FlagProperty('includeRepeats', value: includeRepeats, ifFalse: 'excluding repeats'));
} }
} }
......
...@@ -1112,7 +1112,7 @@ void main() { ...@@ -1112,7 +1112,7 @@ void main() {
)); ));
await tester.pump(); await tester.pump();
// Press KeyC: Accepted by DumbLogicalActivator // Press Shift + /
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
expect(invoked, 1); expect(invoked, 1);
...@@ -1142,6 +1142,53 @@ void main() { ...@@ -1142,6 +1142,53 @@ void main() {
invoked = 0; invoked = 0;
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('rejects repeated events if requested', (WidgetTester tester) async {
int invoked = 0;
await tester.pumpWidget(activatorTester(
const CharacterActivator('?', includeRepeats: false),
(Intent intent) { invoked += 1; },
));
await tester.pump();
// Press Shift + /
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
expect(invoked, 1);
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?');
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 1);
invoked = 0;
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('handles Ctrl and Meta', (WidgetTester tester) async {
int invoked = 0;
await tester.pumpWidget(activatorTester(
const CharacterActivator('?', meta: true, control: true),
(Intent intent) { invoked += 1; },
));
await tester.pump();
// Press Shift + /
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
expect(invoked, 0);
// Press Ctrl + Meta + Shift + /
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 1);
invoked = 0;
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { testWidgets('isActivatedBy works as expected', (WidgetTester tester) async {
// Collect some key events to use for testing. // Collect some key events to use for testing.
...@@ -1163,6 +1210,52 @@ void main() { ...@@ -1163,6 +1210,52 @@ void main() {
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
expect(ShortcutActivator.isActivatedBy(characterActivator, events[0]), isTrue); expect(ShortcutActivator.isActivatedBy(characterActivator, events[0]), isTrue);
}); });
group('diagnostics.', () {
test('single key', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const CharacterActivator('A').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(1));
expect(description[0], equals("character: 'A'"));
});
test('no repeats', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const CharacterActivator('A', 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("character: 'A'"));
expect(description[1], equals('excluding repeats'));
});
test('combination', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const CharacterActivator('A',
control: true,
meta: true,
).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(1));
expect(description[0], equals("character: Control + Meta + 'A'"));
});
});
}); });
group('CallbackShortcuts', () { group('CallbackShortcuts', () {
......
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