diff --git a/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart b/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart index f8bb12dbde2..200c23295f2 100644 --- a/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart +++ b/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart @@ -8,61 +8,59 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -void main() => runApp(const MyApp()); +void main() => runApp(const KeyExampleApp()); -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - static const String _title = 'Flutter Code Sample'; +class KeyExampleApp extends StatelessWidget { + const KeyExampleApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( - title: _title, home: Scaffold( - appBar: AppBar(title: const Text(_title)), - body: const MyStatefulWidget(), + appBar: AppBar(title: const Text('Key Handling Example')), + body: const MyKeyExample(), ), ); } } -class MyStatefulWidget extends StatefulWidget { - const MyStatefulWidget({Key? key}) : super(key: key); +class MyKeyExample extends StatefulWidget { + const MyKeyExample({Key? key}) : super(key: key); @override - State createState() => _MyStatefulWidgetState(); + State createState() => _MyKeyExampleState(); } -class _MyStatefulWidgetState extends State { -// The node used to request the keyboard focus. +class _MyKeyExampleState extends State { + // The node used to request the keyboard focus. final FocusNode _focusNode = FocusNode(); -// The message to display. + // The message to display. String? _message; -// Focus nodes need to be disposed. + // Focus nodes need to be disposed. @override void dispose() { _focusNode.dispose(); super.dispose(); } -// Handles the key events from the RawKeyboardListener and update the -// _message. - void _handleKeyEvent(RawKeyEvent event) { + // Handles the key events from the Focus widget and updates the + // _message. + KeyEventResult _handleKeyEvent(FocusNode node, RawKeyEvent event) { setState(() { if (event.logicalKey == LogicalKeyboardKey.keyQ) { _message = 'Pressed the "Q" key!'; } else { if (kReleaseMode) { - _message = - 'Not a Q: Pressed 0x${event.logicalKey.keyId.toRadixString(16)}'; + _message = 'Not a Q: Pressed 0x${event.logicalKey.keyId.toRadixString(16)}'; } else { - // The debugName will only print useful information in debug mode. + // As the name implies, the debugName will only print useful + // information in debug mode. _message = 'Not a Q: Pressed ${event.logicalKey.debugName}'; } } }); + return event.logicalKey == LogicalKeyboardKey.keyQ ? KeyEventResult.handled : KeyEventResult.ignored; } @override @@ -73,7 +71,7 @@ class _MyStatefulWidgetState extends State { alignment: Alignment.center, child: DefaultTextStyle( style: textTheme.headline4!, - child: RawKeyboardListener( + child: Focus( focusNode: _focusNode, onKey: _handleKeyEvent, child: AnimatedBuilder( @@ -84,7 +82,7 @@ class _MyStatefulWidgetState extends State { onTap: () { FocusScope.of(context).requestFocus(_focusNode); }, - child: const Text('Tap to focus'), + child: const Text('Click to focus'), ); } return Text(_message ?? 'Press a key'); diff --git a/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart b/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart index 3fb07d76ec6..282ba9d5f06 100644 --- a/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart +++ b/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart @@ -4,36 +4,34 @@ // Flutter code sample for PhysicalKeyboardKey +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -void main() => runApp(const MyApp()); +void main() => runApp(const KeyExampleApp()); -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - static const String _title = 'Flutter Code Sample'; +class KeyExampleApp extends StatelessWidget { + const KeyExampleApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( - title: _title, home: Scaffold( - appBar: AppBar(title: const Text(_title)), - body: const MyStatefulWidget(), + appBar: AppBar(title: const Text('PhysicalKeyboardKey Example')), + body: const MyPhysicalKeyExample(), ), ); } } -class MyStatefulWidget extends StatefulWidget { - const MyStatefulWidget({Key? key}) : super(key: key); +class MyPhysicalKeyExample extends StatefulWidget { + const MyPhysicalKeyExample({Key? key}) : super(key: key); @override - State createState() => _MyStatefulWidgetState(); + State createState() => _MyPhysicalKeyExampleState(); } -class _MyStatefulWidgetState extends State { +class _MyPhysicalKeyExampleState extends State { // The node used to request the keyboard focus. final FocusNode _focusNode = FocusNode(); // The message to display. @@ -48,14 +46,21 @@ class _MyStatefulWidgetState extends State { // Handles the key events from the RawKeyboardListener and update the // _message. - void _handleKeyEvent(RawKeyEvent event) { + KeyEventResult _handleKeyEvent(FocusNode node, RawKeyEvent event) { setState(() { if (event.physicalKey == PhysicalKeyboardKey.keyA) { _message = 'Pressed the key next to CAPS LOCK!'; } else { - _message = 'Wrong key.'; + if (kReleaseMode) { + _message = 'Not the key next to CAPS LOCK: Pressed 0x${event.physicalKey.usbHidUsage.toRadixString(16)}'; + } else { + // As the name implies, the debugName will only print useful + // information in debug mode. + _message = 'Not the key next to CAPS LOCK: Pressed ${event.physicalKey.debugName}'; + } } }); + return event.physicalKey == PhysicalKeyboardKey.keyA ? KeyEventResult.handled : KeyEventResult.ignored; } @override @@ -66,7 +71,7 @@ class _MyStatefulWidgetState extends State { alignment: Alignment.center, child: DefaultTextStyle( style: textTheme.headline4!, - child: RawKeyboardListener( + child: Focus( focusNode: _focusNode, onKey: _handleKeyEvent, child: AnimatedBuilder( @@ -77,7 +82,7 @@ class _MyStatefulWidgetState extends State { onTap: () { FocusScope.of(context).requestFocus(_focusNode); }, - child: const Text('Tap to focus'), + child: const Text('Click to focus'), ); } return Text(_message ?? 'Press a key'); diff --git a/examples/api/test/services/keyboard_key/logical_keyboard_key.0_test.dart b/examples/api/test/services/keyboard_key/logical_keyboard_key.0_test.dart new file mode 100644 index 00000000000..0500c109b90 --- /dev/null +++ b/examples/api/test/services/keyboard_key/logical_keyboard_key.0_test.dart @@ -0,0 +1,27 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_api_samples/services/keyboard_key/logical_keyboard_key.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Responds to key', (WidgetTester tester) async { + await tester.pumpWidget( + const example.KeyExampleApp(), + ); + + await tester.tap(find.text('Click to focus')); + await tester.pumpAndSettle(); + expect(find.text('Press a key'), findsOneWidget); + + await tester.sendKeyEvent(LogicalKeyboardKey.keyQ); + await tester.pumpAndSettle(); + expect(find.text('Pressed the "Q" key!'), findsOneWidget); + + await tester.sendKeyEvent(LogicalKeyboardKey.keyB); + await tester.pumpAndSettle(); + expect(find.text('Not a Q: Pressed Key B'), findsOneWidget); + }); +} diff --git a/examples/api/test/services/keyboard_key/physical_keyboard_key.0_test.dart b/examples/api/test/services/keyboard_key/physical_keyboard_key.0_test.dart new file mode 100644 index 00000000000..4969c0651bc --- /dev/null +++ b/examples/api/test/services/keyboard_key/physical_keyboard_key.0_test.dart @@ -0,0 +1,26 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_api_samples/services/keyboard_key/physical_keyboard_key.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Responds to key', (WidgetTester tester) async { + await tester.pumpWidget( + const example.KeyExampleApp(), + ); + + await tester.tap(find.text('Click to focus')); + await tester.pumpAndSettle(); + expect(find.text('Press a key'), findsOneWidget); + await tester.sendKeyEvent(LogicalKeyboardKey.keyQ, physicalKey: PhysicalKeyboardKey.keyA); + await tester.pumpAndSettle(); + expect(find.text('Pressed the key next to CAPS LOCK!'), findsOneWidget); + + await tester.sendKeyEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyB); + await tester.pumpAndSettle(); + expect(find.text('Not the key next to CAPS LOCK: Pressed Key B'), findsOneWidget); + }); +} diff --git a/packages/flutter/lib/src/services/keyboard_key.dart b/packages/flutter/lib/src/services/keyboard_key.dart index ca3b4232ec1..c745acec467 100644 --- a/packages/flutter/lib/src/services/keyboard_key.dart +++ b/packages/flutter/lib/src/services/keyboard_key.dart @@ -48,7 +48,7 @@ abstract class KeyboardKey with Diagnosticable { /// /// {@tool dartpad} /// This example shows how to detect if the user has selected the logical "Q" -/// key. +/// key and handle the key if they have. /// /// ** See code in examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart ** /// {@end-tool} @@ -56,8 +56,9 @@ abstract class KeyboardKey with Diagnosticable { /// /// * [RawKeyEvent], the keyboard event object received by widgets that listen /// to keyboard events. -/// * [RawKeyboardListener], a widget used to listen to and supply handlers for -/// keyboard events. +/// * [Focus.onKey], the handler on a widget that lets you handle key events. +/// * [RawKeyboardListener], a widget used to listen to keyboard events (but +/// not handle them). @immutable class LogicalKeyboardKey extends KeyboardKey { /// Creates a new LogicalKeyboardKey object for a key ID. @@ -3503,8 +3504,9 @@ class LogicalKeyboardKey extends KeyboardKey { /// /// * [RawKeyEvent], the keyboard event object received by widgets that listen /// to keyboard events. -/// * [RawKeyboardListener], a widget used to listen to and supply handlers for -/// keyboard events. +/// * [Focus.onKey], the handler on a widget that lets you handle key events. +/// * [RawKeyboardListener], a widget used to listen to keyboard events (but +/// not handle them). @immutable class PhysicalKeyboardKey extends KeyboardKey { /// Creates a new PhysicalKeyboardKey object for a USB HID usage. diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index f605eb90ada..c208559fb8c 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -982,6 +982,14 @@ abstract class WidgetController { /// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet /// supported. /// + /// Specify the `physicalKey` for the event to override what is included in + /// the simulated event. If not specified, it uses a default from the US + /// keyboard layout for the corresponding logical `key`. + /// + /// Specify the `character` for the event to override what is included in the + /// simulated event. If not specified, it uses a default derived from the + /// logical `key`. + /// /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is /// controlled by [debugKeyEventSimulatorTransitModeOverride]. /// @@ -997,11 +1005,16 @@ abstract class WidgetController { /// /// - [sendKeyDownEvent] to simulate only a key down event. /// - [sendKeyUpEvent] to simulate only a key up event. - Future sendKeyEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async { + Future sendKeyEvent( + LogicalKeyboardKey key, { + String platform = _defaultPlatform, + String? character, + PhysicalKeyboardKey? physicalKey + }) async { assert(platform != null); - final bool handled = await simulateKeyDownEvent(key, platform: platform); + final bool handled = await simulateKeyDownEvent(key, platform: platform, character: character, physicalKey: physicalKey); // Internally wrapped in async guard. - await simulateKeyUpEvent(key, platform: platform); + await simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey); return handled; } @@ -1016,6 +1029,14 @@ abstract class WidgetController { /// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet /// supported. /// + /// Specify the `physicalKey` for the event to override what is included in + /// the simulated event. If not specified, it uses a default from the US + /// keyboard layout for the corresponding logical `key`. + /// + /// Specify the `character` for the event to override what is included in the + /// simulated event. If not specified, it uses a default derived from the + /// logical `key`. + /// /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is /// controlled by [debugKeyEventSimulatorTransitModeOverride]. /// @@ -1028,10 +1049,15 @@ abstract class WidgetController { /// - [sendKeyUpEvent] and [sendKeyRepeatEvent] to simulate the corresponding /// key up and repeat event. /// - [sendKeyEvent] to simulate both the key up and key down in the same call. - Future sendKeyDownEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async { + Future sendKeyDownEvent( + LogicalKeyboardKey key, { + String platform = _defaultPlatform, + String? character, + PhysicalKeyboardKey? physicalKey + }) async { assert(platform != null); // Internally wrapped in async guard. - return simulateKeyDownEvent(key, character: character, platform: platform); + return simulateKeyDownEvent(key, platform: platform, character: character, physicalKey: physicalKey); } /// Simulates sending a physical key up event through the system channel. @@ -1044,6 +1070,10 @@ abstract class WidgetController { /// that type of system. Defaults to "web" on web, and "android" everywhere /// else. May not be null. /// + /// Specify the `physicalKey` for the event to override what is included in + /// the simulated event. If not specified, it uses a default from the US + /// keyboard layout for the corresponding logical `key`. + /// /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is /// controlled by [debugKeyEventSimulatorTransitModeOverride]. /// @@ -1054,13 +1084,17 @@ abstract class WidgetController { /// - [sendKeyDownEvent] and [sendKeyRepeatEvent] to simulate the /// corresponding key down and repeat event. /// - [sendKeyEvent] to simulate both the key up and key down in the same call. - Future sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async { + Future sendKeyUpEvent( + LogicalKeyboardKey key, { + String platform = _defaultPlatform, + PhysicalKeyboardKey? physicalKey + }) async { assert(platform != null); // Internally wrapped in async guard. - return simulateKeyUpEvent(key, platform: platform); + return simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey); } - /// Simulates sending a physical key repeat event. + /// Simulates sending a key repeat event from a physical keyboard. /// /// This only simulates key repeat events coming from a physical keyboard, not /// from a soft keyboard. @@ -1070,6 +1104,14 @@ abstract class WidgetController { /// of system. Defaults to "web" on web, and "android" everywhere else. Must not be /// null. Some platforms (e.g. Windows, iOS) are not yet supported. /// + /// Specify the `physicalKey` for the event to override what is included in + /// the simulated event. If not specified, it uses a default from the US + /// keyboard layout for the corresponding logical `key`. + /// + /// Specify the `character` for the event to override what is included in the + /// simulated event. If not specified, it uses a default derived from the + /// logical `key`. + /// /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is /// controlled by [debugKeyEventSimulatorTransitModeOverride]. If through [RawKeyEvent], /// this method is equivalent to [sendKeyDownEvent]. @@ -1083,10 +1125,15 @@ abstract class WidgetController { /// - [sendKeyDownEvent] and [sendKeyUpEvent] to simulate the corresponding /// key down and up event. /// - [sendKeyEvent] to simulate both the key up and key down in the same call. - Future sendKeyRepeatEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async { + Future sendKeyRepeatEvent( + LogicalKeyboardKey key, { + String platform = _defaultPlatform, + String? character, + PhysicalKeyboardKey? physicalKey + }) async { assert(platform != null); // Internally wrapped in async guard. - return simulateKeyRepeatEvent(key, character: character, platform: platform); + return simulateKeyRepeatEvent(key, platform: platform, character: character, physicalKey: physicalKey); } /// Returns the rect of the given widget. This is only valid once diff --git a/packages/flutter_test/test/event_simulation_test.dart b/packages/flutter_test/test/event_simulation_test.dart index 0dfc774e157..344f77b22d4 100644 --- a/packages/flutter_test/test/event_simulation_test.dart +++ b/packages/flutter_test/test/event_simulation_test.dart @@ -151,6 +151,29 @@ void main() { expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty); events.clear(); + // Key press keyA with physical keyQ + await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA, physicalKey: PhysicalKeyboardKey.keyQ); + expect(events.length, 1); + _verifyKeyEvent(events[0], PhysicalKeyboardKey.keyQ, LogicalKeyboardKey.keyA, 'a'); + expect(HardwareKeyboard.instance.physicalKeysPressed, equals({PhysicalKeyboardKey.keyQ})); + expect(HardwareKeyboard.instance.logicalKeysPressed, equals({LogicalKeyboardKey.keyA})); + expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty); + events.clear(); + + await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA, physicalKey: PhysicalKeyboardKey.keyQ); + _verifyKeyEvent(events[0], PhysicalKeyboardKey.keyQ, LogicalKeyboardKey.keyA, 'a'); + expect(HardwareKeyboard.instance.physicalKeysPressed, equals({PhysicalKeyboardKey.keyQ})); + expect(HardwareKeyboard.instance.logicalKeysPressed, equals({LogicalKeyboardKey.keyA})); + expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty); + events.clear(); + + await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA, physicalKey: PhysicalKeyboardKey.keyQ); + _verifyKeyEvent(events[0], PhysicalKeyboardKey.keyQ, LogicalKeyboardKey.keyA, null); + expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty); + expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty); + expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty); + events.clear(); + // Key press numpad1 await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad1); _verifyKeyEvent(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);