[flutter_releases] Flutter beta 2.10.0-0.2.pre Framework Cherrypicks (#96872)

* RawKeyboard repeat events, and SingleActivator.includeRepeats (#96154)

* Reland "Ensure the engineLayer is disposed when an OpacityLayer is disabled"" (#96295)

* Update Flutter desktop doc URL in error message (#96559)

When desktop support is not present in an existing project, certain
flutter tool commands raise an error that direct the user to
documentation on how to add desktop support to an existing Flutter
project. In a recent revamp of the webside, the URL was very slightly
changed (flutter.dev -> docs.flutter.dev).

This updates the error message to output the new URL.

Issue: https://github.com/flutter/flutter/issues/94398

* 'add branch flutter-2.8-candidate.16 to enabled_branches in .ci.yaml'

* 'Update Engine revision to 74b74b10ea6764e373ca2bcb420fdb4a6f6b5918 for beta release 2.10.0-0.2.pre'

* Removed unnecessary reference to enabled_branches

Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
Co-authored-by: Dan Field <dnfield@google.com>
Co-authored-by: Chris Bracken <chris@bracken.jp>
Co-authored-by: Kevin Chisholm <kevinjchisholm@google.com>
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index b913705..9c1fbfe 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-63ca99584a1aef79722b2a7c6414570b54416bab
+74b74b10ea6764e373ca2bcb420fdb4a6f6b5918
diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart
index d56f4aa..868bd5a 100644
--- a/packages/flutter/lib/src/rendering/layer.dart
+++ b/packages/flutter/lib/src/rendering/layer.dart
@@ -1763,11 +1763,16 @@
   @override
   void addToScene(ui.SceneBuilder builder) {
     assert(alpha != null);
-    bool enabled = firstChild != null;  // don't add this layer if there's no child
+
+    // Don't add this layer if there's no child.
+    bool enabled = firstChild != null;
     if (!enabled) {
+      // Ensure the engineLayer is disposed.
+      engineLayer = null;
       // TODO(dnfield): Remove this if/when we can fix https://github.com/flutter/flutter/issues/90004
       return;
     }
+
     assert(() {
       enabled = enabled && !debugDisableOpacityLayers;
       return true;
diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart
index 515261d..188b1e8 100644
--- a/packages/flutter/lib/src/services/raw_keyboard.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard.dart
@@ -280,10 +280,14 @@
   const RawKeyEvent({
     required this.data,
     this.character,
+    this.repeat = false,
   });
 
   /// Creates a concrete [RawKeyEvent] class from a message in the form received
   /// on the [SystemChannels.keyEvent] channel.
+  ///
+  /// [RawKeyEvent.repeat] will be derived from the current keyboard state,
+  /// instead of using the message information.
   factory RawKeyEvent.fromMessage(Map<String, Object?> message) {
     String? character;
     RawKeyEventData _dataFromWeb() {
@@ -388,10 +392,11 @@
           throw FlutterError('Unknown keymap for key events: $keymap');
       }
     }
+    final bool repeat = RawKeyboard.instance.physicalKeysPressed.contains(data.physicalKey);
     final String type = message['type']! as String;
     switch (type) {
       case 'keydown':
-        return RawKeyDownEvent(data: data, character: character);
+        return RawKeyDownEvent(data: data, character: character, repeat: repeat);
       case 'keyup':
         return RawKeyUpEvent(data: data);
       default:
@@ -504,6 +509,15 @@
   /// input.
   final String? character;
 
+  /// Whether this is a repeated down event.
+  ///
+  /// When a key is held down, the systems usually fire a down event and then
+  /// a series of repeated down events. The [repeat] is false for the
+  /// first event and true for the following events.
+  ///
+  /// The [repeat] attribute is always false for [RawKeyUpEvent]s.
+  final bool repeat;
+
   /// Platform-specific information about the key event.
   final RawKeyEventData data;
 
@@ -512,6 +526,8 @@
     super.debugFillProperties(properties);
     properties.add(DiagnosticsProperty<LogicalKeyboardKey>('logicalKey', logicalKey));
     properties.add(DiagnosticsProperty<PhysicalKeyboardKey>('physicalKey', physicalKey));
+    if (this is RawKeyDownEvent)
+      properties.add(DiagnosticsProperty<bool>('repeat', repeat));
   }
 }
 
@@ -525,7 +541,8 @@
   const RawKeyDownEvent({
     required RawKeyEventData data,
     String? character,
-  }) : super(data: data, character: character);
+    bool repeat = false,
+  }) : super(data: data, character: character, repeat: repeat);
 }
 
 /// The user has released a key on the keyboard.
@@ -538,7 +555,7 @@
   const RawKeyUpEvent({
     required RawKeyEventData data,
     String? character,
-  }) : super(data: data, character: character);
+  }) : super(data: data, character: character, repeat: false);
 }
 
 /// A callback type used by [RawKeyboard.keyEventHandler] to send key events to
diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart
index 7dcedeb..9f1b5e9 100644
--- a/packages/flutter/lib/src/widgets/shortcuts.dart
+++ b/packages/flutter/lib/src/widgets/shortcuts.dart
@@ -388,8 +388,7 @@
 ///  * [CharacterActivator], an activator that represents key combinations
 ///    that result in the specified character, such as question mark.
 class SingleActivator with Diagnosticable implements ShortcutActivator {
-  /// Triggered when the [trigger] key is pressed or repeated when the
-  /// modifiers are held.
+  /// Triggered when the [trigger] key is pressed while the modifiers are held.
   ///
   /// The `trigger` should be the non-modifier key that is pressed after all the
   /// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not be
@@ -398,8 +397,9 @@
   /// The `control`, `shift`, `alt`, and `meta` flags represent whether
   /// the respect modifier keys should be held (true) or released (false)
   ///
-  /// On each [RawKeyDownEvent] of the [trigger] key, this activator checks
-  /// whether the specified modifier conditions are met.
+  /// By default, the activator is checked on all [RawKeyDownEvent] events for
+  /// the [trigger] key. If `includeRepeats` is false, only the [trigger] key
+  /// events with a false [RawKeyDownEvent.repeat] attribute will be considered.
   ///
   /// {@tool dartpad}
   /// In the following example, the shortcut `Control + C` increases the counter:
@@ -412,6 +412,7 @@
     this.shift = false,
     this.alt = false,
     this.meta = false,
+    this.includeRepeats = true,
   }) : // The enumerated check with `identical` is cumbersome but the only way
        // since const constructors can not call functions such as `==` or
        // `Set.contains`. Checking with `identical` might not work when the
@@ -482,6 +483,14 @@
   ///  * [LogicalKeyboardKey.metaLeft], [LogicalKeyboardKey.metaRight].
   final bool meta;
 
+  /// Whether this activator accepts repeat events of the [trigger] key.
+  ///
+  /// If [includeRepeats] is true, the activator is checked on all
+  /// [RawKeyDownEvent] events for the [trigger] key. If `includeRepeats` is
+  /// false, only the [trigger] key events with a false [RawKeyDownEvent.repeat]
+  /// attribute will be considered.
+  final bool includeRepeats;
+
   @override
   Iterable<LogicalKeyboardKey> get triggers {
     return <LogicalKeyboardKey>[trigger];
@@ -491,6 +500,7 @@
   bool accepts(RawKeyEvent event, RawKeyboard state) {
     final Set<LogicalKeyboardKey> pressed = state.keysPressed;
     return event is RawKeyDownEvent
+      && (includeRepeats || !event.repeat)
       && (control == (pressed.contains(LogicalKeyboardKey.controlLeft) || pressed.contains(LogicalKeyboardKey.controlRight)))
       && (shift == (pressed.contains(LogicalKeyboardKey.shiftLeft) || pressed.contains(LogicalKeyboardKey.shiftRight)))
       && (alt == (pressed.contains(LogicalKeyboardKey.altLeft) || pressed.contains(LogicalKeyboardKey.altRight)))
@@ -522,6 +532,7 @@
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     super.debugFillProperties(properties);
     properties.add(DiagnosticsProperty<String>('keys', debugDescribeKeys()));
+    properties.add(FlagProperty('includeRepeats', value: includeRepeats, ifFalse: 'excluding repeats'));
   }
 }
 
diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart
index 8d4e2dc..1d9aa4a 100644
--- a/packages/flutter/test/rendering/layers_test.dart
+++ b/packages/flutter/test/rendering/layers_test.dart
@@ -637,6 +637,21 @@
     expect(builder.addedPicture, true);
     expect(layer.engineLayer, isA<FakeOpacityEngineLayer>());
   });
+
+  test('OpacityLayer dispose its engineLayer if there are no children', () {
+    final OpacityLayer layer = OpacityLayer(alpha: 128);
+    final FakeSceneBuilder builder = FakeSceneBuilder();
+    layer.addToScene(builder);
+    expect(layer.engineLayer, null);
+
+    layer.append(PictureLayer(Rect.largest)..picture = FakePicture());
+    layer.addToScene(builder);
+    expect(layer.engineLayer, isA<FakeOpacityEngineLayer>());
+
+    layer.removeAllChildren();
+    layer.addToScene(builder);
+    expect(layer.engineLayer, null);
+  });
 }
 
 class FakeEngineLayer extends Fake implements EngineLayer {
diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart
index 8a7703d..c55d616 100644
--- a/packages/flutter/test/services/raw_keyboard_test.dart
+++ b/packages/flutter/test/services/raw_keyboard_test.dart
@@ -598,6 +598,49 @@
       );
     }, skip: isBrowser); // [intended] This is an iOS-specific test.
 
+    testWidgets('repeat events', (WidgetTester tester) async {
+      expect(RawKeyboard.instance.keysPressed, isEmpty);
+      late RawKeyEvent receivedEvent;
+      RawKeyboard.instance.keyEventHandler = (RawKeyEvent event) {
+        receivedEvent = event;
+        return true;
+      };
+
+      // Dispatch a down event.
+      final Map<String, dynamic> downData = KeyEventSimulator.getKeyData(
+        LogicalKeyboardKey.keyA,
+        platform: 'windows',
+      );
+      await ServicesBinding.instance?.defaultBinaryMessenger.handlePlatformMessage(
+        SystemChannels.keyEvent.name,
+        SystemChannels.keyEvent.codec.encodeMessage(downData),
+        (ByteData? data) {},
+      );
+      expect(receivedEvent.repeat, false);
+
+      // Dispatch another down event, which should be recognized as a repeat.
+      await ServicesBinding.instance?.defaultBinaryMessenger.handlePlatformMessage(
+        SystemChannels.keyEvent.name,
+        SystemChannels.keyEvent.codec.encodeMessage(downData),
+        (ByteData? data) {},
+      );
+      expect(receivedEvent.repeat, true);
+
+      // Dispatch an up event.
+      await ServicesBinding.instance?.defaultBinaryMessenger.handlePlatformMessage(
+        SystemChannels.keyEvent.name,
+        SystemChannels.keyEvent.codec.encodeMessage(KeyEventSimulator.getKeyData(
+          LogicalKeyboardKey.keyA,
+          isDown: false,
+          platform: 'windows',
+        )),
+        (ByteData? data) {},
+      );
+      expect(receivedEvent.repeat, false);
+
+      RawKeyboard.instance.keyEventHandler = null;
+    }, skip: isBrowser); // [intended] This is a Windows-specific test.
+
     testWidgets('sided modifiers without a side set return all sides on Windows', (WidgetTester tester) async {
       expect(RawKeyboard.instance.keysPressed, isEmpty);
       // Generate the data for a regular key down event.
diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart
index 01ef201..39f5090 100644
--- a/packages/flutter/test/widgets/shortcuts_test.dart
+++ b/packages/flutter/test/widgets/shortcuts_test.dart
@@ -464,6 +464,33 @@
       expect(RawKeyboard.instance.keysPressed, isEmpty);
     }, variant: KeySimulatorTransitModeVariant.all());
 
+    testWidgets('rejects repeated events if requested', (WidgetTester tester) async {
+      int invoked = 0;
+      await tester.pumpWidget(activatorTester(
+        const SingleActivator(
+          LogicalKeyboardKey.keyC,
+          control: true,
+          includeRepeats: false,
+        ),
+        (Intent intent) { invoked += 1; },
+      ));
+      await tester.pump();
+
+      // LCtrl -> KeyC: Accept
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+      expect(invoked, 0);
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
+      expect(invoked, 1);
+      await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC);
+      expect(invoked, 1);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+      expect(invoked, 1);
+      invoked = 0;
+
+      expect(RawKeyboard.instance.keysPressed, isEmpty);
+    }, variant: KeySimulatorTransitModeVariant.all());
+
     testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async {
       int invoked = 0;
       await tester.pumpWidget(activatorTester(
@@ -514,8 +541,8 @@
       expect(RawKeyboard.instance.keysPressed, isEmpty);
     });
 
-    test('diagnostics.', () {
-      {
+    group('diagnostics.', () {
+      test('single key', () {
         final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
 
         const SingleActivator(
@@ -528,9 +555,26 @@
 
         expect(description.length, equals(1));
         expect(description[0], equals('keys: Key A'));
-      }
+      });
 
-      {
+      test('no repeats', () {
+        final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
+
+        const SingleActivator(
+          LogicalKeyboardKey.keyA,
+          includeRepeats: false,
+        ).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(2));
+        expect(description[0], equals('keys: Key A'));
+        expect(description[1], equals('excluding repeats'));
+      });
+
+      test('combination', () {
         final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
 
         const SingleActivator(
@@ -547,7 +591,7 @@
 
         expect(description.length, equals(1));
         expect(description[0], equals('keys: Control + Alt + Meta + Shift + Key A'));
-      }
+      });
     });
   });
 
diff --git a/packages/flutter_tools/lib/src/linux/build_linux.dart b/packages/flutter_tools/lib/src/linux/build_linux.dart
index 49f14a1..81c86d5 100644
--- a/packages/flutter_tools/lib/src/linux/build_linux.dart
+++ b/packages/flutter_tools/lib/src/linux/build_linux.dart
@@ -38,7 +38,7 @@
   target ??= 'lib/main.dart';
   if (!linuxProject.cmakeFile.existsSync()) {
     throwToolExit('No Linux desktop project configured. See '
-      'https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
+      'https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
       'to learn about adding Linux support to a project.');
   }
 
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 335f634..c217750 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -31,7 +31,7 @@
 }) async {
   if (!flutterProject.macos.xcodeWorkspace.existsSync()) {
     throwToolExit('No macOS desktop project configured. '
-      'See https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
+      'See https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
       'to learn about adding macOS support to a project.');
   }
 
diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart
index 1d7a269..6422d39 100644
--- a/packages/flutter_tools/lib/src/windows/build_windows.dart
+++ b/packages/flutter_tools/lib/src/windows/build_windows.dart
@@ -32,7 +32,7 @@
   if (!windowsProject.cmakeFile.existsSync()) {
     throwToolExit(
       'No Windows desktop project configured. See '
-      'https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
+      'https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
       'to learn about adding Windows support to a project.');
   }
 
@@ -124,7 +124,7 @@
   if (!windowsProject.existsSync()) {
     throwToolExit(
       'No Windows UWP desktop project configured. See '
-      'https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
+      'https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
       'to learn about adding Windows support to a project.',
     );
   }
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart
index 9714f74..0d73157 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart
@@ -114,7 +114,7 @@
     expect(createTestCommandRunner(command).run(
       const <String>['build', 'linux', '--no-pub']
     ), throwsToolExit(message: 'No Linux desktop project configured. See '
-      'https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
+      'https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
       'to learn about adding Linux support to a project.'));
   }, overrides: <Type, Generator>{
     Platform: () => linuxPlatform,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart
index febf951..4e5cc45 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart
@@ -131,7 +131,7 @@
     expect(createTestCommandRunner(command).run(
       const <String>['build', 'macos', '--no-pub']
     ), throwsToolExit(message: 'No macOS desktop project configured. See '
-      'https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
+      'https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
       'to learn about adding macOS support to a project.'));
   }, overrides: <Type, Generator>{
     Platform: () => macosPlatform,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart
index 1244d02..a85bec4 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart
@@ -159,7 +159,7 @@
     expect(createTestCommandRunner(command).run(
       const <String>['windows', '--no-pub']
     ), throwsToolExit(message: 'No Windows desktop project configured. See '
-      'https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
+      'https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
       'to learn about adding Windows support to a project.'));
   }, overrides: <Type, Generator>{
     Platform: () => windowsPlatform,
@@ -489,7 +489,7 @@
     expect(createTestCommandRunner(command).run(
       const <String>['winuwp', '--no-pub']
     ), throwsToolExit(message: 'No Windows UWP desktop project configured. See '
-      'https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
+      'https://docs.flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
       'to learn about adding Windows support to a project.'));
   }, overrides: <Type, Generator>{
     Platform: () => windowsPlatform,