Add support for Alt to CharacterActivator, add tests (#113466)

diff --git a/packages/flutter/lib/src/widgets/platform_menu_bar.dart b/packages/flutter/lib/src/widgets/platform_menu_bar.dart
index f5ded7a..b670c96 100644
--- a/packages/flutter/lib/src/widgets/platform_menu_bar.dart
+++ b/packages/flutter/lib/src/widgets/platform_menu_bar.dart
@@ -45,10 +45,23 @@
   /// Creates a [ShortcutSerialization] representing a single character.
   ///
   /// This is used by a [CharacterActivator] to serialize itself.
-  ShortcutSerialization.character(String character)
-      : _internal = <String, Object?>{_kShortcutCharacter: character},
+  ShortcutSerialization.character(String character, {
+    bool alt = false,
+    bool control = false,
+    bool meta = false,
+  })  : assert(character.length == 1),
         _character = character,
-        assert(character.length == 1);
+        _trigger = null,
+        _alt = alt,
+        _control = control,
+        _meta = meta,
+        _shift = null,
+        _internal = <String, Object?>{
+          _kShortcutCharacter: character,
+          _kShortcutModifiers: (control ? _shortcutModifierControl : 0) |
+              (alt ? _shortcutModifierAlt : 0) |
+              (meta ? _shortcutModifierMeta : 0),
+        };
 
   /// Creates a [ShortcutSerialization] representing a specific
   /// [LogicalKeyboardKey] and modifiers.
@@ -56,14 +69,11 @@
   /// This is used by a [SingleActivator] to serialize itself.
   ShortcutSerialization.modifier(
     LogicalKeyboardKey trigger, {
-    bool control = false,
-    bool shift = false,
     bool alt = false,
+    bool control = false,
     bool meta = false,
-  })  : assert(trigger != LogicalKeyboardKey.shift &&
-               trigger != LogicalKeyboardKey.shiftLeft &&
-               trigger != LogicalKeyboardKey.shiftRight &&
-               trigger != LogicalKeyboardKey.alt &&
+    bool shift = false,
+  })  : assert(trigger != LogicalKeyboardKey.alt &&
                trigger != LogicalKeyboardKey.altLeft &&
                trigger != LogicalKeyboardKey.altRight &&
                trigger != LogicalKeyboardKey.control &&
@@ -71,52 +81,64 @@
                trigger != LogicalKeyboardKey.controlRight &&
                trigger != LogicalKeyboardKey.meta &&
                trigger != LogicalKeyboardKey.metaLeft &&
-               trigger != LogicalKeyboardKey.metaRight,
+               trigger != LogicalKeyboardKey.metaRight &&
+               trigger != LogicalKeyboardKey.shift &&
+               trigger != LogicalKeyboardKey.shiftLeft &&
+               trigger != LogicalKeyboardKey.shiftRight,
                'Specifying a modifier key as a trigger is not allowed. '
                'Use provided boolean parameters instead.'),
         _trigger = trigger,
-        _control = control,
-        _shift = shift,
+        _character = null,
         _alt = alt,
+        _control = control,
         _meta = meta,
+        _shift = shift,
         _internal = <String, Object?>{
           _kShortcutTrigger: trigger.keyId,
-          _kShortcutModifiers: (control ? _shortcutModifierControl : 0) |
-              (alt ? _shortcutModifierAlt : 0) |
-              (shift ? _shortcutModifierShift : 0) |
-              (meta ? _shortcutModifierMeta : 0),
+          _kShortcutModifiers: (alt ? _shortcutModifierAlt : 0) |
+            (control ? _shortcutModifierControl : 0) |
+            (meta ? _shortcutModifierMeta : 0) |
+            (shift ? _shortcutModifierShift : 0),
         };
 
   final Map<String, Object?> _internal;
 
   /// The keyboard key that triggers this shortcut, if any.
   LogicalKeyboardKey? get trigger => _trigger;
-  LogicalKeyboardKey? _trigger;
+  final LogicalKeyboardKey? _trigger;
 
   /// The character that triggers this shortcut, if any.
   String? get character => _character;
-  String? _character;
-
-  /// If this shortcut has a [trigger], this indicates whether or not the
-  /// control modifier needs to be down or not.
-  bool? get control => _control;
-  bool? _control;
-
-  /// If this shortcut has a [trigger], this indicates whether or not the
-  /// shift modifier needs to be down or not.
-  bool? get shift => _shift;
-  bool? _shift;
+  final String? _character;
 
   /// If this shortcut has a [trigger], this indicates whether or not the
   /// alt modifier needs to be down or not.
   bool? get alt => _alt;
-  bool? _alt;
+  final bool? _alt;
+
+  /// If this shortcut has a [trigger], this indicates whether or not the
+  /// control modifier needs to be down or not.
+  bool? get control => _control;
+  final bool? _control;
 
   /// If this shortcut has a [trigger], this indicates whether or not the meta
   /// (also known as the Windows or Command key) modifier needs to be down or
   /// not.
   bool? get meta => _meta;
-  bool? _meta;
+  final bool? _meta;
+
+  /// If this shortcut has a [trigger], this indicates whether or not the
+  /// shift modifier needs to be down or not.
+  bool? get shift => _shift;
+  final bool? _shift;
+
+  /// The bit mask for the [LogicalKeyboardKey.alt] key (or it's left/right
+  /// equivalents) being down.
+  static const int _shortcutModifierAlt = 1 << 2;
+
+  /// The bit mask for the [LogicalKeyboardKey.control] key (or it's left/right
+  /// equivalents) being down.
+  static const int _shortcutModifierControl = 1 << 3;
 
   /// The bit mask for the [LogicalKeyboardKey.meta] key (or it's left/right
   /// equivalents) being down.
@@ -126,14 +148,6 @@
   /// equivalents) being down.
   static const int _shortcutModifierShift = 1 << 1;
 
-  /// The bit mask for the [LogicalKeyboardKey.alt] key (or it's left/right
-  /// equivalents) being down.
-  static const int _shortcutModifierAlt = 1 << 2;
-
-  /// The bit mask for the [LogicalKeyboardKey.alt] key (or it's left/right
-  /// equivalents) being down.
-  static const int _shortcutModifierControl = 1 << 3;
-
   /// Converts the internal representation to the format needed for a
   /// [PlatformMenuItem] to include it in its serialized form for sending to the
   /// platform.
diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart
index 7625284..fec74e8 100644
--- a/packages/flutter/lib/src/widgets/shortcuts.dart
+++ b/packages/flutter/lib/src/widgets/shortcuts.dart
@@ -580,25 +580,40 @@
 /// See also:
 ///
 ///  * [SingleActivator], an activator that represents a single key combined
-///    with modifiers, such as `Ctrl+C`.
+///    with modifiers, such as `Ctrl+C` or `Ctrl-Right Arrow`.
 class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
   /// Triggered when the key event yields the given character.
   ///
-  /// The [control] and [meta] flags represent whether the respect modifier
-  /// keys should be held (true) or released (false). They default to false.
-  /// [CharacterActivator] can not check Shift keys or Alt keys yet, and will
-  /// accept whether they are pressed or not.
+  /// The [alt], [control], and [meta] flags represent whether the respective
+  /// modifier keys should be held (true) or released (false). They default to
+  /// false. [CharacterActivator] cannot check Shift keys, since the shift key
+  /// affects the resulting character, and will accept whether either of the
+  /// Shift keys are pressed or not, as long as the key event produces the
+  /// correct character.
   ///
   /// By default, the activator is checked on all [RawKeyDownEvent] events for
-  /// the [character]. If `includeRepeats` is false, only the [character]
-  /// events with a false [RawKeyDownEvent.repeat] attribute will be
-  /// considered.
+  /// the [character] in combination with the requested modifier keys. If
+  /// `includeRepeats` is false, only the [character] events with a false
+  /// [RawKeyDownEvent.repeat] attribute will be considered.
   const CharacterActivator(this.character, {
+    this.alt = false,
     this.control = false,
     this.meta = false,
     this.includeRepeats = true,
   });
 
+  /// Whether either (or both) alt keys should be held for the [character] to
+  /// activate the shortcut.
+  ///
+  /// It defaults to false, meaning all Alt keys must be released when the event
+  /// is received in order to activate the shortcut. If it's true, then either
+  /// or both Alt keys must be pressed.
+ ///
+  /// See also:
+  ///
+  /// * [LogicalKeyboardKey.altLeft], [LogicalKeyboardKey.altRight].
+  final bool alt;
+
   /// Whether either (or both) control keys should be held for the [character]
   /// to activate the shortcut.
   ///
@@ -631,7 +646,7 @@
   /// attribute will be considered.
   final bool includeRepeats;
 
-  /// The character of the triggering event.
+  /// The character which triggers the shortcut.
   ///
   /// This is typically a single-character string, such as '?' or 'œ', although
   /// [CharacterActivator] doesn't check the length of [character] or whether it
@@ -653,6 +668,7 @@
     return event is RawKeyDownEvent
       && event.character == character
       && (includeRepeats || !event.repeat)
+      && (alt == (pressed.contains(LogicalKeyboardKey.altLeft) || pressed.contains(LogicalKeyboardKey.altRight)))
       && (control == (pressed.contains(LogicalKeyboardKey.controlLeft) || pressed.contains(LogicalKeyboardKey.controlRight)))
       && (meta == (pressed.contains(LogicalKeyboardKey.metaLeft) || pressed.contains(LogicalKeyboardKey.metaRight)));
   }
@@ -662,6 +678,7 @@
     String result = '';
     assert(() {
       final List<String> keys = <String>[
+        if (alt) 'Alt',
         if (control) 'Control',
         if (meta) 'Meta',
         "'$character'",
@@ -674,7 +691,7 @@
 
   @override
   ShortcutSerialization serializeForMenu() {
-    return ShortcutSerialization.character(character);
+    return ShortcutSerialization.character(character, alt: alt, control: control, meta: meta);
   }
 
   @override
diff --git a/packages/flutter/test/widgets/platform_menu_bar_test.dart b/packages/flutter/test/widgets/platform_menu_bar_test.dart
index 429e116..c8209c0 100644
--- a/packages/flutter/test/widgets/platform_menu_bar_test.dart
+++ b/packages/flutter/test/widgets/platform_menu_bar_test.dart
@@ -228,6 +228,34 @@
       ]);
     });
   });
+
+  group('ShortcutSerialization', () {
+    testWidgets('character constructor', (WidgetTester tester) async {
+      final ShortcutSerialization serialization = ShortcutSerialization.character('?');
+      expect(serialization.toChannelRepresentation(), equals(<String, Object?>{
+        'shortcutCharacter': '?',
+        'shortcutModifiers': 0,
+      }));
+      final ShortcutSerialization serializationWithModifiers = ShortcutSerialization.character('?', alt: true, control: true, meta: true);
+      expect(serializationWithModifiers.toChannelRepresentation(), equals(<String, Object?>{
+        'shortcutCharacter': '?',
+        'shortcutModifiers': 13,
+      }));
+    });
+
+    testWidgets('modifier constructor', (WidgetTester tester) async {
+      final ShortcutSerialization serialization = ShortcutSerialization.modifier(LogicalKeyboardKey.home);
+      expect(serialization.toChannelRepresentation(), equals(<String, Object?>{
+        'shortcutTrigger': LogicalKeyboardKey.home.keyId,
+        'shortcutModifiers': 0,
+      }));
+      final ShortcutSerialization serializationWithModifiers = ShortcutSerialization.modifier(LogicalKeyboardKey.home, alt: true, control: true, meta: true, shift: true);
+      expect(serializationWithModifiers.toChannelRepresentation(), equals(<String, Object?>{
+        'shortcutTrigger': LogicalKeyboardKey.home.keyId,
+        'shortcutModifiers': 15,
+      }));
+    });
+  });
 }
 
 const List<String> mainMenu = <String>[
diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart
index d0bb609..c566945 100644
--- a/packages/flutter/test/widgets/shortcuts_test.dart
+++ b/packages/flutter/test/widgets/shortcuts_test.dart
@@ -1162,10 +1162,10 @@
       invoked = 0;
     }, variant: KeySimulatorTransitModeVariant.all());
 
-    testWidgets('handles Ctrl and Meta', (WidgetTester tester) async {
+    testWidgets('handles Alt, Ctrl and Meta', (WidgetTester tester) async {
       int invoked = 0;
       await tester.pumpWidget(activatorTester(
-        const CharacterActivator('?', meta: true, control: true),
+        const CharacterActivator('?', alt: true, meta: true, control: true),
         (Intent intent) { invoked += 1; },
       ));
       await tester.pump();
@@ -1176,7 +1176,8 @@
       await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
       expect(invoked, 0);
 
-      // Press Ctrl + Meta + Shift + /
+      // Press Left Alt + Ctrl + Meta + Shift + /
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
       await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
       await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
       expect(invoked, 0);
@@ -1185,9 +1186,26 @@
       await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
       await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
       await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
       await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
       expect(invoked, 1);
       invoked = 0;
+
+      // Press Right Alt + Ctrl + Meta + Shift + /
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftRight);
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.altRight);
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.metaRight);
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
+      expect(invoked, 0);
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
+      expect(invoked, 1);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftRight);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.metaRight);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.altRight);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
+      expect(invoked, 1);
+      invoked = 0;
     }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('isActivatedBy works as expected', (WidgetTester tester) async {