diff --git a/packages/flutter/lib/src/services/debug.dart b/packages/flutter/lib/src/services/debug.dart index b3581119f8..518c29c535 100644 --- a/packages/flutter/lib/src/services/debug.dart +++ b/packages/flutter/lib/src/services/debug.dart @@ -27,6 +27,14 @@ KeyDataTransitMode? debugKeyEventSimulatorTransitModeOverride; /// Flutter to the host platform, "down" is the host platform to flutter. 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. /// /// This function is used by the test framework to ensure that debug variables @@ -38,7 +46,7 @@ bool debugAssertAllServicesVarsUnset(String reason) { if (debugKeyEventSimulatorTransitModeOverride != null) { throw FlutterError(reason); } - if (debugProfilePlatformChannels) { + if (debugProfilePlatformChannels || debugPrintKeyboardEvents) { throw FlutterError(reason); } return true; diff --git a/packages/flutter/lib/src/services/hardware_keyboard.dart b/packages/flutter/lib/src/services/hardware_keyboard.dart index 414b9c8c21..9f12f723c6 100644 --- a/packages/flutter/lib/src/services/hardware_keyboard.dart +++ b/packages/flutter/lib/src/services/hardware_keyboard.dart @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'binding.dart'; +import 'debug.dart'; import 'raw_keyboard.dart'; import 'system_channels.dart'; @@ -17,6 +18,41 @@ export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder; export 'keyboard_key.g.dart' show LogicalKeyboardKey, PhysicalKeyboardKey; 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 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 details = detailsFunc?.call() ?? const []; + 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]. /// /// A lock mode locks some of a keyboard's keys into a distinct mode of operation, @@ -546,9 +582,22 @@ class HardwareKeyboard { return handled; } + List _debugPressedKeysDetails() { + if (_pressedKeys.isEmpty) { + return ['Empty']; + } + final List details = []; + 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 /// to handlers. bool handleKeyEvent(KeyEvent event) { + assert(_keyboardDebug(() => 'Key event received: $event')); + assert(_keyboardDebug(() => 'Pressed state before processing the event:', _debugPressedKeysDetails)); _assertEventIsRegular(event); final PhysicalKeyboardKey physicalKey = event.physicalKey; final LogicalKeyboardKey logicalKey = event.logicalKey; @@ -568,6 +617,7 @@ class HardwareKeyboard { // Empty } + assert(_keyboardDebug(() => 'Pressed state after processing the event:', _debugPressedKeysDetails)); return _dispatchKeyEvent(event); } diff --git a/packages/flutter/test/services/hardware_keyboard_test.dart b/packages/flutter/test/services/hardware_keyboard_test.dart index 6c6b13f081..b9522292a7 100644 --- a/packages/flutter/test/services/hardware_keyboard_test.dart +++ b/packages/flutter/test/services/hardware_keyboard_test.dart @@ -465,10 +465,28 @@ void main() { // trigger assertions. expect(record, isNull); }, 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 _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async { final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError; FlutterError.onError = onError;