[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 {
|
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
|
|||||||
/// 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
|
|||||||
/// 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
|
|||||||
/// 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
|
|||||||
/// 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
|
|||||||
@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
|
|||||||
/// * [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
|
|||||||
|
|
||||||
@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
|
|||||||
@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() {
|
|||||||
));
|
));
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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', () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user