Unverified Commit e435b1ae authored by Bruno Leroux's avatar Bruno Leroux Committed by GitHub

Add debugPrintKeyboardEvents flag (#125629)

## Description

This PR adds a new debug flag named `debugPrintKeyboardEvents` to help debugging keyboard issues.

Keyboard code has some useful asserts but sometimes an assertion failure is related to the handling of previous key events. This debug flag will help understanding the flow of key events which leads to an assertion failure.

## Related Issue

Fixes https://github.com/flutter/flutter/issues/125627

## Tests

Adds 1 test.
parent 794c2e0a
...@@ -27,6 +27,14 @@ KeyDataTransitMode? debugKeyEventSimulatorTransitModeOverride; ...@@ -27,6 +27,14 @@ KeyDataTransitMode? debugKeyEventSimulatorTransitModeOverride;
/// Flutter to the host platform, "down" is the host platform to flutter. /// Flutter to the host platform, "down" is the host platform to flutter.
bool debugProfilePlatformChannels = false; bool debugProfilePlatformChannels = false;
/// Setting to true will cause extensive logging to occur when key events are
/// received.
///
/// Can be used to debug keyboard issues: each time a key event is received on
/// the framework side, the event details and the current pressed state will
/// be printed.
bool debugPrintKeyboardEvents = false;
/// Returns true if none of the widget library debug variables have been changed. /// Returns true if none of the widget library debug variables have been changed.
/// ///
/// This function is used by the test framework to ensure that debug variables /// This function is used by the test framework to ensure that debug variables
...@@ -38,7 +46,7 @@ bool debugAssertAllServicesVarsUnset(String reason) { ...@@ -38,7 +46,7 @@ bool debugAssertAllServicesVarsUnset(String reason) {
if (debugKeyEventSimulatorTransitModeOverride != null) { if (debugKeyEventSimulatorTransitModeOverride != null) {
throw FlutterError(reason); throw FlutterError(reason);
} }
if (debugProfilePlatformChannels) { if (debugProfilePlatformChannels || debugPrintKeyboardEvents) {
throw FlutterError(reason); throw FlutterError(reason);
} }
return true; return true;
......
...@@ -7,6 +7,7 @@ import 'dart:ui' as ui; ...@@ -7,6 +7,7 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'binding.dart'; import 'binding.dart';
import 'debug.dart';
import 'raw_keyboard.dart'; import 'raw_keyboard.dart';
import 'system_channels.dart'; import 'system_channels.dart';
...@@ -17,6 +18,41 @@ export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder; ...@@ -17,6 +18,41 @@ export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder;
export 'keyboard_key.g.dart' show LogicalKeyboardKey, PhysicalKeyboardKey; export 'keyboard_key.g.dart' show LogicalKeyboardKey, PhysicalKeyboardKey;
export 'raw_keyboard.dart' show RawKeyEvent, RawKeyboard; export 'raw_keyboard.dart' show RawKeyEvent, RawKeyboard;
// When using _keyboardDebug, always call it like so:
//
// assert(_keyboardDebug(() => 'Blah $foo'));
//
// It needs to be inside the assert in order to be removed in release mode, and
// it needs to use a closure to generate the string in order to avoid string
// interpolation when debugPrintKeyboardEvents is false.
//
// It will throw a StateError if you try to call it when the app is in release
// mode.
bool _keyboardDebug(
String Function() messageFunc, [
Iterable<Object> Function()? detailsFunc,
]) {
if (kReleaseMode) {
throw StateError(
'_keyboardDebug was called in Release mode, which means they are called '
'without being wrapped in an assert. Always call _keyboardDebug like so:\n'
r" assert(_keyboardDebug(() => 'Blah $foo'));"
);
}
if (!debugPrintKeyboardEvents) {
return true;
}
debugPrint('KEYBOARD: ${messageFunc()}');
final Iterable<Object> details = detailsFunc?.call() ?? const <Object>[];
if (details.isNotEmpty) {
for (final Object detail in details) {
debugPrint(' $detail');
}
}
// Return true so that it can be used inside of an assert.
return true;
}
/// Represents a lock mode of a keyboard, such as [KeyboardLockMode.capsLock]. /// Represents a lock mode of a keyboard, such as [KeyboardLockMode.capsLock].
/// ///
/// A lock mode locks some of a keyboard's keys into a distinct mode of operation, /// A lock mode locks some of a keyboard's keys into a distinct mode of operation,
...@@ -546,9 +582,22 @@ class HardwareKeyboard { ...@@ -546,9 +582,22 @@ class HardwareKeyboard {
return handled; return handled;
} }
List<String> _debugPressedKeysDetails() {
if (_pressedKeys.isEmpty) {
return <String>['Empty'];
}
final List<String> details = <String>[];
for (final PhysicalKeyboardKey physicalKey in _pressedKeys.keys) {
details.add('$physicalKey: ${_pressedKeys[physicalKey]}');
}
return details;
}
/// Process a new [KeyEvent] by recording the state changes and dispatching /// Process a new [KeyEvent] by recording the state changes and dispatching
/// to handlers. /// to handlers.
bool handleKeyEvent(KeyEvent event) { bool handleKeyEvent(KeyEvent event) {
assert(_keyboardDebug(() => 'Key event received: $event'));
assert(_keyboardDebug(() => 'Pressed state before processing the event:', _debugPressedKeysDetails));
_assertEventIsRegular(event); _assertEventIsRegular(event);
final PhysicalKeyboardKey physicalKey = event.physicalKey; final PhysicalKeyboardKey physicalKey = event.physicalKey;
final LogicalKeyboardKey logicalKey = event.logicalKey; final LogicalKeyboardKey logicalKey = event.logicalKey;
...@@ -568,6 +617,7 @@ class HardwareKeyboard { ...@@ -568,6 +617,7 @@ class HardwareKeyboard {
// Empty // Empty
} }
assert(_keyboardDebug(() => 'Pressed state after processing the event:', _debugPressedKeysDetails));
return _dispatchKeyEvent(event); return _dispatchKeyEvent(event);
} }
......
...@@ -465,9 +465,27 @@ void main() { ...@@ -465,9 +465,27 @@ void main() {
// trigger assertions. // trigger assertions.
expect(record, isNull); expect(record, isNull);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
}
testWidgets('debugPrintKeyboardEvents causes logging of key events', (WidgetTester tester) async {
final bool oldDebugPrintKeyboardEvents = debugPrintKeyboardEvents;
final DebugPrintCallback oldDebugPrint = debugPrint;
final StringBuffer messages = StringBuffer();
debugPrint = (String? message, {int? wrapWidth}) {
messages.writeln(message ?? '');
};
debugPrintKeyboardEvents = true;
try {
await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
} finally {
debugPrintKeyboardEvents = oldDebugPrintKeyboardEvents;
debugPrint = oldDebugPrint;
}
final String messagesStr = messages.toString();
expect(messagesStr, contains('KEYBOARD: Key event received: '));
expect(messagesStr, contains('KEYBOARD: Pressed state before processing the event:'));
expect(messagesStr, contains('KEYBOARD: Pressed state after processing the event:'));
});
}
Future<void> _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async { Future<void> _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async {
final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError; final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError;
......
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