Unverified Commit efe76a53 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Update key examples to use `Focus` widgets instead of `RawKeyboardListener` (#101537)

This updates the examples for PhysicalKeyboardKey and LogicalKeyboardKey to use Focus widgets that handle the keys instead of using RawKeyboardListener, since that usually leads people down the wrong path. Updated the See Also and added tests as well. Also exposed the `physicalKey` attribute for `tester.sendKeyEvent`.
parent cae740b9
......@@ -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<MyStatefulWidget> createState() => _MyStatefulWidgetState();
State<MyKeyExample> createState() => _MyKeyExampleState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
// The node used to request the keyboard focus.
class _MyKeyExampleState extends State<MyKeyExample> {
// 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<MyStatefulWidget> {
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<MyStatefulWidget> {
onTap: () {
FocusScope.of(context).requestFocus(_focusNode);
},
child: const Text('Tap to focus'),
child: const Text('Click to focus'),
);
}
return Text(_message ?? 'Press a key');
......
......@@ -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<MyStatefulWidget> createState() => _MyStatefulWidgetState();
State<MyPhysicalKeyExample> createState() => _MyPhysicalKeyExampleState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
class _MyPhysicalKeyExampleState extends State<MyPhysicalKeyExample> {
// The node used to request the keyboard focus.
final FocusNode _focusNode = FocusNode();
// The message to display.
......@@ -48,14 +46,21 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
// 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<MyStatefulWidget> {
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<MyStatefulWidget> {
onTap: () {
FocusScope.of(context).requestFocus(_focusNode);
},
child: const Text('Tap to focus'),
child: const Text('Click to focus'),
);
}
return Text(_message ?? 'Press a key');
......
// 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);
});
}
// 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);
});
}
......@@ -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.
......
......@@ -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<bool> sendKeyEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
Future<bool> 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<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
Future<bool> 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<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
Future<bool> 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<bool> sendKeyRepeatEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
Future<bool> 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
......
......@@ -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<KeyDownEvent>(events[0], PhysicalKeyboardKey.keyQ, LogicalKeyboardKey.keyA, 'a');
expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyQ}));
expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
events.clear();
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA, physicalKey: PhysicalKeyboardKey.keyQ);
_verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.keyQ, LogicalKeyboardKey.keyA, 'a');
expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyQ}));
expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
events.clear();
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA, physicalKey: PhysicalKeyboardKey.keyQ);
_verifyKeyEvent<KeyUpEvent>(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<KeyDownEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
......
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