Unverified Commit 01c98fa9 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Character activator (#81807)

parent 7cdd33fe
...@@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, BuildContext? context, ActionDispatcher dispatcher}); typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, BuildContext? context, ActionDispatcher dispatcher});
class TestAction extends CallbackAction<TestIntent> { class TestAction extends CallbackAction<Intent> {
TestAction({ TestAction({
required OnInvokeCallback onInvoke, required OnInvokeCallback onInvoke,
}) : assert(onInvoke != null), }) : assert(onInvoke != null),
...@@ -31,10 +31,47 @@ class TestDispatcher extends ActionDispatcher { ...@@ -31,10 +31,47 @@ class TestDispatcher extends ActionDispatcher {
} }
} }
/// An activator that accepts down events that has [key] as the logical key.
///
/// This class is used only to tests. It is intentionally designed poorly by
/// returning null in [triggers], and checks [key] in [accepts].
class DumbLogicalActivator extends ShortcutActivator {
const DumbLogicalActivator(this.key);
final LogicalKeyboardKey key;
@override
Iterable<LogicalKeyboardKey>? get triggers => null;
@override
bool accepts(RawKeyEvent event, RawKeyboard state) {
return event is RawKeyDownEvent
&& event.logicalKey == key;
}
/// Returns a short and readable description of the key combination.
///
/// Intended to be used in debug mode for logging purposes. In release mode,
/// [debugDescribeKeys] returns an empty string.
@override
String debugDescribeKeys() {
String result = '';
assert(() {
result = key.keyLabel;
return true;
}());
return result;
}
}
class TestIntent extends Intent { class TestIntent extends Intent {
const TestIntent(); const TestIntent();
} }
class TestIntent2 extends Intent {
const TestIntent2();
}
class TestShortcutManager extends ShortcutManager { class TestShortcutManager extends ShortcutManager {
TestShortcutManager(this.keys); TestShortcutManager(this.keys);
...@@ -49,7 +86,13 @@ class TestShortcutManager extends ShortcutManager { ...@@ -49,7 +86,13 @@ class TestShortcutManager extends ShortcutManager {
} }
} }
Widget activatorTester(ShortcutActivator activator, ValueSetter<Intent> onInvoke) { Widget activatorTester(
ShortcutActivator activator,
ValueSetter<Intent> onInvoke, [
ShortcutActivator? activator2,
ValueSetter<Intent>? onInvoke2,
]) {
final bool hasSecond = activator2 != null && onInvoke2 != null;
return Actions( return Actions(
key: GlobalKey(), key: GlobalKey(),
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
...@@ -57,10 +100,16 @@ Widget activatorTester(ShortcutActivator activator, ValueSetter<Intent> onInvoke ...@@ -57,10 +100,16 @@ Widget activatorTester(ShortcutActivator activator, ValueSetter<Intent> onInvoke
onInvoke(intent); onInvoke(intent);
return true; return true;
}), }),
if (hasSecond)
TestIntent2: TestAction(onInvoke: (Intent intent) {
onInvoke2(intent);
}),
}, },
child: Shortcuts( child: Shortcuts(
shortcuts: <ShortcutActivator, Intent>{ shortcuts: <ShortcutActivator, Intent>{
activator: const TestIntent(), activator: const TestIntent(),
if (hasSecond)
activator2: const TestIntent2(),
}, },
child: const Focus( child: const Focus(
autofocus: true, autofocus: true,
...@@ -967,5 +1016,65 @@ void main() { ...@@ -967,5 +1016,65 @@ void main() {
expect(value, isTrue); expect(value, isTrue);
expect(controller.position.pixels, 0.0); expect(controller.position.pixels, 0.0);
}); });
testWidgets('Shortcuts support activators that returns null in triggers', (WidgetTester tester) async {
int invoked = 0;
await tester.pumpWidget(activatorTester(
const DumbLogicalActivator(LogicalKeyboardKey.keyC),
(Intent intent) { invoked += 1; },
const SingleActivator(LogicalKeyboardKey.keyC, control: true),
(Intent intent) { invoked += 10; },
));
await tester.pump();
// Press KeyC: Accepted by DumbLogicalActivator
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
invoked = 0;
// Press ControlLeft + KeyC: Accepted by SingleActivator
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 10);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 10);
invoked = 0;
// Press ControlLeft + ShiftLeft + KeyC: Accepted by DumbLogicalActivator
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
expect(invoked, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 1);
invoked = 0;
});
});
group('CharacterActivator', () {
testWidgets('is triggered on events with correct character', (WidgetTester tester) async {
int invoked = 0;
await tester.pumpWidget(activatorTester(
const CharacterActivator('?'),
(Intent intent) { invoked += 1; },
));
await tester.pump();
// Press KeyC: Accepted by DumbLogicalActivator
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
expect(invoked, 1);
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, 1);
invoked = 0;
});
}); });
} }
...@@ -1021,10 +1021,10 @@ abstract class WidgetController { ...@@ -1021,10 +1021,10 @@ abstract class WidgetController {
/// ///
/// - [sendKeyUpEvent] to simulate the corresponding key up event. /// - [sendKeyUpEvent] to simulate the corresponding key up event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call. /// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async { Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
assert(platform != null); assert(platform != null);
// Internally wrapped in async guard. // Internally wrapped in async guard.
return simulateKeyDownEvent(key, platform: platform); return simulateKeyDownEvent(key, character: character, platform: platform);
} }
/// Simulates sending a physical key up event through the system channel. /// Simulates sending a physical key up event through the system channel.
......
...@@ -194,6 +194,7 @@ class KeyEventSimulator { ...@@ -194,6 +194,7 @@ class KeyEventSimulator {
required String platform, required String platform,
bool isDown = true, bool isDown = true,
PhysicalKeyboardKey? physicalKey, PhysicalKeyboardKey? physicalKey,
String? character,
}) { }) {
assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation'); assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation');
...@@ -211,27 +212,31 @@ class KeyEventSimulator { ...@@ -211,27 +212,31 @@ class KeyEventSimulator {
'keymap': platform, 'keymap': platform,
}; };
if (kIsWeb) { final String resultCharacter = character ?? _keyLabel(key);
void assignWeb() {
result['code'] = _getWebKeyCode(key); result['code'] = _getWebKeyCode(key);
result['key'] = _keyLabel(key); result['key'] = resultCharacter;
result['metaState'] = _getWebModifierFlags(key, isDown); result['metaState'] = _getWebModifierFlags(key, isDown);
}
if (kIsWeb) {
assignWeb();
return result; return result;
} }
switch (platform) { switch (platform) {
case 'android': case 'android':
result['keyCode'] = keyCode; result['keyCode'] = keyCode;
if (_keyLabel(key).isNotEmpty) { if (resultCharacter.isNotEmpty) {
result['codePoint'] = _keyLabel(key).codeUnitAt(0); result['codePoint'] = resultCharacter.codeUnitAt(0);
result['character'] = _keyLabel(key); result['character'] = resultCharacter;
} }
result['scanCode'] = scanCode; result['scanCode'] = scanCode;
result['metaState'] = _getAndroidModifierFlags(key, isDown); result['metaState'] = _getAndroidModifierFlags(key, isDown);
break; break;
case 'fuchsia': case 'fuchsia':
result['hidUsage'] = physicalKey.usbHidUsage; result['hidUsage'] = physicalKey.usbHidUsage;
if (_keyLabel(key).isNotEmpty) { if (resultCharacter.isNotEmpty) {
result['codePoint'] = _keyLabel(key).codeUnitAt(0); result['codePoint'] = resultCharacter.codeUnitAt(0);
} }
result['modifiers'] = _getFuchsiaModifierFlags(key, isDown); result['modifiers'] = _getFuchsiaModifierFlags(key, isDown);
break; break;
...@@ -240,34 +245,33 @@ class KeyEventSimulator { ...@@ -240,34 +245,33 @@ class KeyEventSimulator {
result['keyCode'] = keyCode; result['keyCode'] = keyCode;
result['scanCode'] = scanCode; result['scanCode'] = scanCode;
result['modifiers'] = _getGlfwModifierFlags(key, isDown); result['modifiers'] = _getGlfwModifierFlags(key, isDown);
result['unicodeScalarValues'] = _keyLabel(key).isNotEmpty ? _keyLabel(key).codeUnitAt(0) : 0; result['unicodeScalarValues'] = resultCharacter.isNotEmpty ? resultCharacter.codeUnitAt(0) : 0;
break; break;
case 'macos': case 'macos':
result['keyCode'] = scanCode; result['keyCode'] = scanCode;
if (_keyLabel(key).isNotEmpty) { if (resultCharacter.isNotEmpty) {
result['characters'] = _keyLabel(key); result['characters'] = resultCharacter;
result['charactersIgnoringModifiers'] = _keyLabel(key); result['charactersIgnoringModifiers'] = resultCharacter;
} }
result['modifiers'] = _getMacOsModifierFlags(key, isDown); result['modifiers'] = _getMacOsModifierFlags(key, isDown);
break; break;
case 'ios': case 'ios':
result['keyCode'] = scanCode; result['keyCode'] = scanCode;
result['characters'] = _keyLabel(key); result['characters'] = resultCharacter;
result['charactersIgnoringModifiers'] = _keyLabel(key); result['charactersIgnoringModifiers'] = resultCharacter;
result['modifiers'] = _getIOSModifierFlags(key, isDown); result['modifiers'] = _getIOSModifierFlags(key, isDown);
break; break;
case 'web':
result['code'] = _getWebKeyCode(key);
result['key'] = _keyLabel(key);
result['metaState'] = _getWebModifierFlags(key, isDown);
break;
case 'windows': case 'windows':
result['keyCode'] = keyCode; result['keyCode'] = keyCode;
result['scanCode'] = scanCode; result['scanCode'] = scanCode;
if (_keyLabel(key).isNotEmpty) { if (resultCharacter.isNotEmpty) {
result['characterCodePoint'] = _keyLabel(key).codeUnitAt(0); result['characterCodePoint'] = resultCharacter.codeUnitAt(0);
} }
result['modifiers'] = _getWindowsModifierFlags(key, isDown); result['modifiers'] = _getWindowsModifierFlags(key, isDown);
break;
case 'web':
assignWeb();
break;
} }
return result; return result;
} }
...@@ -631,12 +635,12 @@ class KeyEventSimulator { ...@@ -631,12 +635,12 @@ class KeyEventSimulator {
/// See also: /// See also:
/// ///
/// - [simulateKeyUpEvent] to simulate the corresponding key up event. /// - [simulateKeyUpEvent] to simulate the corresponding key up event.
static Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey}) async { static Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey, String? character}) async {
return TestAsyncUtils.guard<bool>(() async { return TestAsyncUtils.guard<bool>(() async {
platform ??= Platform.operatingSystem; platform ??= Platform.operatingSystem;
assert(_osIsSupported(platform!), 'Platform $platform not supported for key simulation'); assert(_osIsSupported(platform!), 'Platform $platform not supported for key simulation');
final Map<String, dynamic> data = getKeyData(key, platform: platform!, isDown: true, physicalKey: physicalKey); final Map<String, dynamic> data = getKeyData(key, platform: platform!, isDown: true, physicalKey: physicalKey, character: character);
bool result = false; bool result = false;
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name, SystemChannels.keyEvent.name,
...@@ -715,8 +719,8 @@ class KeyEventSimulator { ...@@ -715,8 +719,8 @@ class KeyEventSimulator {
/// See also: /// See also:
/// ///
/// - [simulateKeyUpEvent] to simulate the corresponding key up event. /// - [simulateKeyUpEvent] to simulate the corresponding key up event.
Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey}) { Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey, String? character}) {
return KeyEventSimulator.simulateKeyDownEvent(key, platform: platform, physicalKey: physicalKey); return KeyEventSimulator.simulateKeyDownEvent(key, platform: platform, physicalKey: physicalKey, character: character);
} }
/// Simulates sending a hardware key up event through the system channel. /// Simulates sending a hardware key up event through the system channel.
......
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