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';
typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, BuildContext? context, ActionDispatcher dispatcher});
class TestAction extends CallbackAction<TestIntent> {
class TestAction extends CallbackAction<Intent> {
TestAction({
required OnInvokeCallback onInvoke,
}) : assert(onInvoke != null),
......@@ -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 {
const TestIntent();
}
class TestIntent2 extends Intent {
const TestIntent2();
}
class TestShortcutManager extends ShortcutManager {
TestShortcutManager(this.keys);
......@@ -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(
key: GlobalKey(),
actions: <Type, Action<Intent>>{
......@@ -57,10 +100,16 @@ Widget activatorTester(ShortcutActivator activator, ValueSetter<Intent> onInvoke
onInvoke(intent);
return true;
}),
if (hasSecond)
TestIntent2: TestAction(onInvoke: (Intent intent) {
onInvoke2(intent);
}),
},
child: Shortcuts(
shortcuts: <ShortcutActivator, Intent>{
activator: const TestIntent(),
if (hasSecond)
activator2: const TestIntent2(),
},
child: const Focus(
autofocus: true,
......@@ -967,5 +1016,65 @@ void main() {
expect(value, isTrue);
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 {
///
/// - [sendKeyUpEvent] to simulate the corresponding key up event.
/// - [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);
// 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.
......
......@@ -194,6 +194,7 @@ class KeyEventSimulator {
required String platform,
bool isDown = true,
PhysicalKeyboardKey? physicalKey,
String? character,
}) {
assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation');
......@@ -211,27 +212,31 @@ class KeyEventSimulator {
'keymap': platform,
};
if (kIsWeb) {
final String resultCharacter = character ?? _keyLabel(key);
void assignWeb() {
result['code'] = _getWebKeyCode(key);
result['key'] = _keyLabel(key);
result['key'] = resultCharacter;
result['metaState'] = _getWebModifierFlags(key, isDown);
}
if (kIsWeb) {
assignWeb();
return result;
}
switch (platform) {
case 'android':
result['keyCode'] = keyCode;
if (_keyLabel(key).isNotEmpty) {
result['codePoint'] = _keyLabel(key).codeUnitAt(0);
result['character'] = _keyLabel(key);
if (resultCharacter.isNotEmpty) {
result['codePoint'] = resultCharacter.codeUnitAt(0);
result['character'] = resultCharacter;
}
result['scanCode'] = scanCode;
result['metaState'] = _getAndroidModifierFlags(key, isDown);
break;
case 'fuchsia':
result['hidUsage'] = physicalKey.usbHidUsage;
if (_keyLabel(key).isNotEmpty) {
result['codePoint'] = _keyLabel(key).codeUnitAt(0);
if (resultCharacter.isNotEmpty) {
result['codePoint'] = resultCharacter.codeUnitAt(0);
}
result['modifiers'] = _getFuchsiaModifierFlags(key, isDown);
break;
......@@ -240,34 +245,33 @@ class KeyEventSimulator {
result['keyCode'] = keyCode;
result['scanCode'] = scanCode;
result['modifiers'] = _getGlfwModifierFlags(key, isDown);
result['unicodeScalarValues'] = _keyLabel(key).isNotEmpty ? _keyLabel(key).codeUnitAt(0) : 0;
result['unicodeScalarValues'] = resultCharacter.isNotEmpty ? resultCharacter.codeUnitAt(0) : 0;
break;
case 'macos':
result['keyCode'] = scanCode;
if (_keyLabel(key).isNotEmpty) {
result['characters'] = _keyLabel(key);
result['charactersIgnoringModifiers'] = _keyLabel(key);
if (resultCharacter.isNotEmpty) {
result['characters'] = resultCharacter;
result['charactersIgnoringModifiers'] = resultCharacter;
}
result['modifiers'] = _getMacOsModifierFlags(key, isDown);
break;
case 'ios':
result['keyCode'] = scanCode;
result['characters'] = _keyLabel(key);
result['charactersIgnoringModifiers'] = _keyLabel(key);
result['characters'] = resultCharacter;
result['charactersIgnoringModifiers'] = resultCharacter;
result['modifiers'] = _getIOSModifierFlags(key, isDown);
break;
case 'web':
result['code'] = _getWebKeyCode(key);
result['key'] = _keyLabel(key);
result['metaState'] = _getWebModifierFlags(key, isDown);
break;
case 'windows':
result['keyCode'] = keyCode;
result['scanCode'] = scanCode;
if (_keyLabel(key).isNotEmpty) {
result['characterCodePoint'] = _keyLabel(key).codeUnitAt(0);
if (resultCharacter.isNotEmpty) {
result['characterCodePoint'] = resultCharacter.codeUnitAt(0);
}
result['modifiers'] = _getWindowsModifierFlags(key, isDown);
break;
case 'web':
assignWeb();
break;
}
return result;
}
......@@ -631,12 +635,12 @@ class KeyEventSimulator {
/// See also:
///
/// - [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 {
platform ??= Platform.operatingSystem;
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;
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
......@@ -715,8 +719,8 @@ class KeyEventSimulator {
/// See also:
///
/// - [simulateKeyUpEvent] to simulate the corresponding key up event.
Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey}) {
return KeyEventSimulator.simulateKeyDownEvent(key, platform: platform, physicalKey: physicalKey);
Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey, String? character}) {
return KeyEventSimulator.simulateKeyDownEvent(key, platform: platform, physicalKey: physicalKey, character: character);
}
/// 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