flutter/dev/manual_tests/lib/raw_keyboard.dart
Greg Spencer c5dd3ec47a
Fix stuck keys when shift is released before the letter. (#51095)
This fixes a problem where if you press "Shift" and then "A", then release "Shift" and then "a", then the "A" key will be "stuck" on because the logical key for the key down message is different (capital "A") from the logical key for the key up message (lowercase "a").

This PR changes the pressed keys logic so that it uses the physical key to add/remove keys from the list of pressed keys, but keeps the associated logical key.

This does mean that after the "Shift" key goes up, the pressed keys contains a capital "A" and it doesn't switch to be a lowercase "a", but there isn't currently any mechanism we can use to do that remapping. This is far less surprising than the current behavior, but is still not quite correct.

I fixed the event simulation code to take a physicalKey so that it could be matched with the logical key, but the event simulation code isn't up to the task, since it can only simulate keys that appear in the key maps. The new platform key event design should fix that (added TODOs).
2020-02-21 14:13:38 -08:00

156 lines
6.2 KiB
Dart

// 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 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// Sets a platform override for desktop to avoid exceptions. See
// https://flutter.dev/desktop#target-platform-override for more info.
// TODO(gspencergoog): Remove once TargetPlatform includes all desktop platforms.
void _enablePlatformOverrideForDesktop() {
if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) {
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
}
}
void main() {
_enablePlatformOverrideForDesktop();
runApp(MaterialApp(
title: 'Hardware Key Demo',
home: Scaffold(
appBar: AppBar(
title: const Text('Hardware Key Demo'),
),
body: const Center(
child: RawKeyboardDemo(),
),
),
));
}
class RawKeyboardDemo extends StatefulWidget {
const RawKeyboardDemo({Key key}) : super(key: key);
@override
_HardwareKeyDemoState createState() => _HardwareKeyDemoState();
}
class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
final FocusNode _focusNode = FocusNode();
RawKeyEvent _event;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
bool _handleKeyEvent(FocusNode node, RawKeyEvent event) {
setState(() {
_event = event;
});
return false;
}
String _asHex(int value) => value != null ? '0x${value.toRadixString(16)}' : 'null';
String _getEnumName(dynamic enumItem) {
final String name = '$enumItem';
final int index = name.indexOf('.');
return index == -1 ? name : name.substring(index + 1);
}
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return Focus(
focusNode: _focusNode,
onKey: _handleKeyEvent,
autofocus: true,
child: AnimatedBuilder(
animation: _focusNode,
builder: (BuildContext context, Widget child) {
if (!_focusNode.hasFocus) {
return GestureDetector(
onTap: () {
_focusNode.requestFocus();
},
child: Text('Tap to focus', style: textTheme.headline4),
);
}
if (_event == null) {
return Text('Press a key', style: textTheme.headline4);
}
final RawKeyEventData data = _event.data;
final String modifierList = data.modifiersPressed.keys.map<String>(_getEnumName).join(', ').replaceAll('Modifier', '');
final List<Widget> dataText = <Widget>[
Text('${_event.runtimeType}'),
Text('modifiers set: $modifierList'),
];
if (data is RawKeyEventDataAndroid) {
const int combiningCharacterMask = 0x7fffffff;
final String codePointChar = String.fromCharCode(combiningCharacterMask & data.codePoint);
dataText.add(Text('codePoint: ${data.codePoint} (${_asHex(data.codePoint)}: $codePointChar)'));
final String plainCodePointChar = String.fromCharCode(combiningCharacterMask & data.plainCodePoint);
dataText.add(Text('plainCodePoint: ${data.plainCodePoint} (${_asHex(data.plainCodePoint)}: $plainCodePointChar)'));
dataText.add(Text('keyCode: ${data.keyCode} (${_asHex(data.keyCode)})'));
dataText.add(Text('scanCode: ${data.scanCode} (${_asHex(data.scanCode)})'));
dataText.add(Text('metaState: ${data.metaState} (${_asHex(data.metaState)})'));
dataText.add(Text('source: ${data.eventSource} (${_asHex(data.eventSource)})'));
dataText.add(Text('vendorId: ${data.vendorId} (${_asHex(data.vendorId)})'));
dataText.add(Text('productId: ${data.productId} (${_asHex(data.productId)})'));
dataText.add(Text('flags: ${data.flags} (${_asHex(data.flags)})'));
dataText.add(Text('repeatCount: ${data.repeatCount} (${_asHex(data.repeatCount)})'));
} else if (data is RawKeyEventDataFuchsia) {
dataText.add(Text('codePoint: ${data.codePoint} (${_asHex(data.codePoint)})'));
dataText.add(Text('hidUsage: ${data.hidUsage} (${_asHex(data.hidUsage)})'));
dataText.add(Text('modifiers: ${data.modifiers} (${_asHex(data.modifiers)})'));
} else if (data is RawKeyEventDataMacOs) {
dataText.add(Text('keyCode: ${data.keyCode} (${_asHex(data.keyCode)})'));
dataText.add(Text('characters: ${data.characters}'));
dataText.add(Text('charactersIgnoringModifiers: ${data.charactersIgnoringModifiers}'));
dataText.add(Text('modifiers: ${data.modifiers} (${_asHex(data.modifiers)})'));
} else if (data is RawKeyEventDataLinux) {
dataText.add(Text('keyCode: ${data.keyCode} (${_asHex(data.keyCode)})'));
dataText.add(Text('scanCode: ${data.scanCode}'));
dataText.add(Text('unicodeScalarValues: ${data.unicodeScalarValues}'));
dataText.add(Text('modifiers: ${data.modifiers} (${_asHex(data.modifiers)})'));
}
dataText.add(Text('logical: ${_event.logicalKey}'));
dataText.add(Text('physical: ${_event.physicalKey}'));
if (_event.character != null) {
dataText.add(Text('character: ${_event.character}'));
}
for (final ModifierKey modifier in data.modifiersPressed.keys) {
for (final KeyboardSide side in KeyboardSide.values) {
if (data.isModifierPressed(modifier, side: side)) {
dataText.add(
Text('${_getEnumName(side)} ${_getEnumName(modifier).replaceAll('Modifier', '')} pressed'),
);
}
}
}
final List<String> pressed = <String>['Pressed:'];
for (final LogicalKeyboardKey key in RawKeyboard.instance.keysPressed) {
pressed.add(key.debugName);
}
dataText.add(Text(pressed.join(' ')));
return DefaultTextStyle(
style: textTheme.subtitle1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: dataText,
),
);
},
),
);
}
}