[Keyboard] Make CharacterActivator support Ctrl and Meta modifiers, and repeats (#107195)
This commit is contained in:
parent
74ac867882
commit
cda8041e52
@ -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', () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user