Simplify the Shortcuts widget diagnostic output (#48265)

This simplifies the diagnostic output for the Shortcuts widget so that if a debugLabel is supplied, then that is printed instead of the full list of shortcut keys in the map. Also, the output of the shortcut map is simplified to have the list of keys presented in more readable text.
diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart
index 71f4dbd..c7eab89 100644
--- a/packages/flutter/lib/src/widgets/app.dart
+++ b/packages/flutter/lib/src/widgets/app.dart
@@ -1364,6 +1364,7 @@
     assert(_debugCheckLocalizations(appLocale));
     return Shortcuts(
       shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
+      debugLabel: '<Default WidgetsApp Shortcuts>',
       child: Actions(
         actions: widget.actions ?? WidgetsApp.defaultActions,
         child: DefaultFocusTraversal(
diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart
index 351ef11..44caef6 100644
--- a/packages/flutter/lib/src/widgets/shortcuts.dart
+++ b/packages/flutter/lib/src/widgets/shortcuts.dart
@@ -27,7 +27,7 @@
 ///
 ///  * [ShortcutManager], which uses [LogicalKeySet] (a [KeySet] subclass) to
 ///    define its key map.
-class KeySet<T extends KeyboardKey> extends Diagnosticable {
+class KeySet<T extends KeyboardKey> {
   /// A constructor for making a [KeySet] of up to four keys.
   ///
   /// If you need a set of more than four keys, use [KeySet.fromSet].
@@ -97,12 +97,6 @@
   int get hashCode {
     return hashList(_keys);
   }
-
-  @override
-  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
-    super.debugFillProperties(properties);
-    properties.add(DiagnosticsProperty<Set<T>>('keys', _keys));
-  }
 }
 
 /// A set of [LogicalKeyboardKey]s that can be used as the keys in a map.
@@ -116,7 +110,7 @@
 /// This is a thin wrapper around a [Set], but changes the equality comparison
 /// from an identity comparison to a contents comparison so that non-identical
 /// sets with the same keys in them will compare as equal.
-class LogicalKeySet extends KeySet<LogicalKeyboardKey> {
+class LogicalKeySet extends KeySet<LogicalKeyboardKey> with DiagnosticableMixin {
   /// A constructor for making a [LogicalKeySet] of up to four keys.
   ///
   /// If you need a set of more than four keys, use [LogicalKeySet.fromSet].
@@ -136,6 +130,71 @@
   ///
   /// The `keys` must not be null.
   LogicalKeySet.fromSet(Set<LogicalKeyboardKey> keys) : super.fromSet(keys);
+
+  static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{
+    LogicalKeyboardKey.alt,
+    LogicalKeyboardKey.control,
+    LogicalKeyboardKey.meta,
+    LogicalKeyboardKey.shift,
+  };
+
+  /// Returns a description of the key set that is short and readable.
+  ///
+  /// Intended to be used in debug mode for logging purposes.
+  String debugDescribeKeys() {
+    final List<LogicalKeyboardKey> sortedKeys = keys.toList()..sort(
+            (LogicalKeyboardKey a, LogicalKeyboardKey b) {
+          // Put the modifiers first. If it has a synonym, then it's something
+          // like shiftLeft, altRight, etc.
+          final bool aIsModifier = a.synonyms.isNotEmpty || _modifiers.contains(a);
+          final bool bIsModifier = b.synonyms.isNotEmpty || _modifiers.contains(b);
+          if (aIsModifier && !bIsModifier) {
+            return -1;
+          } else if (bIsModifier && !aIsModifier) {
+            return 1;
+          }
+          return a.debugName.compareTo(b.debugName);
+        }
+    );
+    return sortedKeys.map<String>((LogicalKeyboardKey key) => '${key.debugName}').join(' + ');
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('keys', _keys, description: debugDescribeKeys()));
+  }
+}
+
+/// Diagnostics property which handles formatting a `Map<LogicalKeySet, Intent>`
+/// (the same type as the [Shortcuts.shortcuts] property) so that it is human-readable.
+class ShortcutMapProperty extends DiagnosticsProperty<Map<LogicalKeySet, Intent>> {
+  /// Create a diagnostics property for `Map<LogicalKeySet, Intent>` objects,
+  /// which are the same type as the [Shortcuts.shortcuts] property.
+  ///
+  /// The [showName] and [level] arguments must not be null.
+  ShortcutMapProperty(
+    String name,
+    Map<LogicalKeySet, Intent> value, {
+    bool showName = true,
+    Object defaultValue = kNoDefaultValue,
+    DiagnosticLevel level = DiagnosticLevel.info,
+    String description,
+  }) : assert(showName != null),
+       assert(level != null),
+       super(
+         name,
+         value,
+         showName: showName,
+         defaultValue: defaultValue,
+         level: level,
+         description: description,
+       );
+
+  @override
+  String valueToString({ TextTreeConfiguration parentConfiguration }) {
+    return '{${value.keys.map<String>((LogicalKeySet keySet) => '{${keySet.debugDescribeKeys()}}: ${value[keySet]}').join(', ')}}';
+  }
 }
 
 /// A manager of keyboard shortcut bindings.
@@ -243,6 +302,7 @@
     this.manager,
     this.shortcuts,
     this.child,
+    this.debugLabel,
   }) : super(key: key);
 
   /// The [ShortcutManager] that will manage the mapping between key
@@ -268,6 +328,15 @@
   /// {@macro flutter.widgets.child}
   final Widget child;
 
+  /// The debug label that is printed for this node when logged.
+  ///
+  /// If this label is set, then it will be displayed instead of the shortcut
+  /// map when logged.
+  ///
+  /// This allows simplifying the diagnostic output to avoid cluttering it
+  /// unnecessarily with the default shortcut map.
+  final String debugLabel;
+
   /// Returns the [ActionDispatcher] that most tightly encloses the given
   /// [BuildContext].
   ///
@@ -299,8 +368,8 @@
   @override
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     super.debugFillProperties(properties);
-    properties.add(DiagnosticsProperty<ShortcutManager>('manager', manager));
-    properties.add(DiagnosticsProperty<Map<LogicalKeySet, Intent>>('shortcuts', shortcuts));
+    properties.add(DiagnosticsProperty<ShortcutManager>('manager', manager, defaultValue: null));
+    properties.add(ShortcutMapProperty('shortcuts', shortcuts, description: debugLabel?.isNotEmpty ?? false ? debugLabel : null));
   }
 }
 
diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart
index 5b87be4..e1f2098 100644
--- a/packages/flutter/test/widgets/shortcuts_test.dart
+++ b/packages/flutter/test/widgets/shortcuts_test.dart
@@ -51,7 +51,7 @@
 
 void main() {
   group(LogicalKeySet, () {
-    test('$LogicalKeySet passes parameters correctly.', () {
+    test('LogicalKeySet passes parameters correctly.', () {
       final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA);
       final LogicalKeySet set2 = LogicalKeySet(
         LogicalKeyboardKey.keyA,
@@ -109,7 +109,7 @@
             LogicalKeyboardKey.keyD,
           }));
     });
-    test('$LogicalKeySet works as a map key.', () {
+    test('LogicalKeySet works as a map key.', () {
       final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA);
       final LogicalKeySet set2 = LogicalKeySet(
         LogicalKeyboardKey.keyA,
@@ -146,7 +146,7 @@
           })),
       );
     });
-    test('$KeySet diagnostics work.', () {
+    test('LogicalKeySet diagnostics work.', () {
       final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
 
       LogicalKeySet(
@@ -162,14 +162,11 @@
           .toList();
 
       expect(description.length, equals(1));
-      expect(
-          description[0],
-          equalsIgnoringHashCodes(
-              'keys: {LogicalKeyboardKey#00000(keyId: "0x00000061", keyLabel: "a", debugName: "Key A"), LogicalKeyboardKey#00000(keyId: "0x00000062", keyLabel: "b", debugName: "Key B")}'));
+      expect(description[0], equals('keys: Key A + Key B'));
     });
   });
   group(Shortcuts, () {
-    testWidgets('$ShortcutManager handles shortcuts', (WidgetTester tester) async {
+    testWidgets('ShortcutManager handles shortcuts', (WidgetTester tester) async {
       final GlobalKey containerKey = GlobalKey();
       final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
       final TestShortcutManager testManager = TestShortcutManager(pressedKeys);
@@ -202,7 +199,7 @@
       expect(invoked, isTrue);
       expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
     });
-    testWidgets("$Shortcuts passes to the next $Shortcuts widget if it doesn't map the key", (WidgetTester tester) async {
+    testWidgets("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", (WidgetTester tester) async {
       final GlobalKey containerKey = GlobalKey();
       final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
       final TestShortcutManager testManager = TestShortcutManager(pressedKeys);
@@ -240,7 +237,7 @@
       expect(invoked, isTrue);
       expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
     });
-    testWidgets('$Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async {
+    testWidgets('Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async {
       final GlobalKey containerKey = GlobalKey();
       final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
       final TestShortcutManager testManager = TestShortcutManager(pressedKeys);
@@ -280,5 +277,53 @@
       expect(invoked, isFalse);
       expect(pressedKeys, isEmpty);
     });
+    test('Shortcuts diagnostics work.', () {
+      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
+
+      Shortcuts(shortcuts: <LogicalKeySet, Intent>{LogicalKeySet(
+        LogicalKeyboardKey.shift,
+        LogicalKeyboardKey.keyA,
+      ) : const Intent(ActivateAction.key),
+        LogicalKeySet(
+        LogicalKeyboardKey.shift,
+        LogicalKeyboardKey.arrowRight,
+      ) : const DirectionalFocusIntent(TraversalDirection.right)}).debugFillProperties(builder);
+
+      final List<String> description = builder.properties
+          .where((DiagnosticsNode node) {
+        return !node.isFiltered(DiagnosticLevel.info);
+      })
+          .map((DiagnosticsNode node) => node.toString())
+          .toList();
+
+      expect(description.length, equals(1));
+      expect(
+          description[0],
+          equalsIgnoringHashCodes(
+              'shortcuts: {{Shift + Key A}: Intent#00000(key: [<ActivateAction>]), {Shift + Arrow Right}: DirectionalFocusIntent#00000(key: [<DirectionalFocusAction>])}'));
+    });
+    test('Shortcuts diagnostics work when debugLabel specified.', () {
+      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
+
+      Shortcuts(
+        debugLabel: '<Debug Label>',
+        shortcuts: <LogicalKeySet, Intent>{
+          LogicalKeySet(
+            LogicalKeyboardKey.keyA,
+            LogicalKeyboardKey.keyB,
+          ): const Intent(ActivateAction.key)
+        },
+      ).debugFillProperties(builder);
+
+      final List<String> description = builder.properties
+          .where((DiagnosticsNode node) {
+        return !node.isFiltered(DiagnosticLevel.info);
+      })
+          .map((DiagnosticsNode node) => node.toString())
+          .toList();
+
+      expect(description.length, equals(1));
+      expect(description[0], equals('shortcuts: <Debug Label>'));
+    });
   });
 }