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
class SingleActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
/// 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
/// a modifier key (sided or unsided).
///
/// The `control`, `shift`, `alt`, and `meta` flags represent whether
/// the respect modifier keys should be held (true) or released (false)
/// The [control], [shift], [alt], and [meta] flags represent whether
/// 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
/// the [trigger] key. If `includeRepeats` is false, only the [trigger] key
......@@ -445,8 +446,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// Whether either (or both) control keys should be held for [trigger] to
/// activate the shortcut.
///
/// If false, then all control keys must be released when the event is received
/// in order 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:
///
......@@ -456,8 +458,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// Whether either (or both) shift keys should be held for [trigger] to
/// activate the shortcut.
///
/// If false, then all shift keys must be released when the event is received
/// in order to activate the shortcut.
/// It defaults to false, meaning all Shift keys must be released when the
/// event is received in order to activate the shortcut. If it's true, then
/// either or both Shift keys must be pressed.
///
/// See also:
///
......@@ -467,8 +470,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// Whether either (or both) alt keys should be held for [trigger] to
/// activate the shortcut.
///
/// If false, then all alt keys must be released when the event is received
/// in order to activate the shortcut.
/// It defaults to false, meaning all Alt keys must be released when the
/// event is received in order to activate the shortcut. If it's true, then
/// either or both Alt keys must be pressed.
///
/// See also:
///
......@@ -478,8 +482,9 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// Whether either (or both) meta keys should be held for [trigger] to
/// activate the shortcut.
///
/// If false, then all meta keys must be released when the event is received
/// in order 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:
///
......@@ -545,7 +550,7 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('keys', debugDescribeKeys()));
properties.add(MessageProperty('keys', debugDescribeKeys()));
properties.add(FlagProperty('includeRepeats', value: includeRepeats, ifFalse: 'excluding repeats'));
}
}
......@@ -577,8 +582,54 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// * [SingleActivator], an activator that represents a single key combined
/// with modifiers, such as `Ctrl+C`.
class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
/// Create a [CharacterActivator] from the triggering character.
const CharacterActivator(this.character);
/// Triggered when the key event yields the given 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.
///
......@@ -598,15 +649,24 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
@override
bool accepts(RawKeyEvent event, RawKeyboard state) {
final Set<LogicalKeyboardKey> pressed = state.keysPressed;
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
String debugDescribeKeys() {
String result = '';
assert(() {
result = "'$character'";
final List<String> keys = <String>[
if (control) 'Control',
if (meta) 'Meta',
"'$character'",
];
result = keys.join(' + ');
return true;
}());
return result;
......@@ -620,7 +680,8 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
@override
void debugFillProperties(DiagnosticPropertiesBuilder 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() {
));
await tester.pump();
// Press KeyC: Accepted by DumbLogicalActivator
// Press Shift + /
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
expect(invoked, 1);
......@@ -1142,6 +1142,53 @@ void main() {
invoked = 0;
}, 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 {
// Collect some key events to use for testing.
......@@ -1163,6 +1210,52 @@ void main() {
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
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', () {
......
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