From 014ae7fcf650f20d3d12614ac947cf013270a89f Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Tue, 21 May 2024 07:10:28 -0700 Subject: [PATCH 01/13] [CP-stable]Fixes incorrect read/write permissions on Flutter.framework and FlutterMacOS.framework (#148717) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? https://github.com/flutter/flutter/issues/148354 https://github.com/flutter/flutter/issues/147142 ### Changelog Description: Correct read/write permisions for FlutterMacOS.framework to allow MacOS developers to publish to the app store ### Impact Description: MacOS developers. ### Workaround: Is there a workaround for this issue? Run chmod locally after building https://github.com/flutter/flutter/issues/148354#issuecomment-2116391118 ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? 1. `flutter build macos --release` any macos project 2. `ls -la macos/Build/Products/Release/test_create.app/Contents/Frameworks/` 3. Note that the permissions for FlutterMacOS.framework is `drwxr-xr-x`, NOT `dr-------` (note that this is validated to work here https://github.com/flutter/flutter/issues/148354#issuecomment-2116447391) --- .../lib/src/build_system/targets/ios.dart | 1 + .../lib/src/build_system/targets/macos.dart | 1 + .../build_system/targets/ios_test.dart | 2 ++ .../build_system/targets/macos_test.dart | 2 ++ .../ios_content_validation_test.dart | 17 ++++++++++++++--- .../macos_content_validation_test.dart | 5 +++++ 6 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 9ca6e543a20..56d73f7cfa4 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -314,6 +314,7 @@ abstract class UnpackIOS extends Target { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', basePath, environment.outputDir.path, ]); diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index 9996664a0a1..e9f48cc88e4 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -60,6 +60,7 @@ abstract class UnpackMacOS extends Target { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', basePath, environment.outputDir.path, ]); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart index 0d37f90460a..e98a71eacf6 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart @@ -542,6 +542,7 @@ void main() { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', 'Artifact.flutterFramework.TargetPlatform.ios.debug.EnvironmentType.physical', outputDir.path, ]); @@ -598,6 +599,7 @@ void main() { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', 'Artifact.flutterFramework.TargetPlatform.ios.debug.EnvironmentType.simulator', outputDir.path, ], diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index 680d826b496..21898bd3dcf 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -73,6 +73,7 @@ void main() { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', 'Artifact.flutterMacOSFramework.debug', environment.outputDir.path, ], @@ -133,6 +134,7 @@ void main() { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', // source 'Artifact.flutterMacOSFramework.debug', // destination diff --git a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart index 2e0587524e6..0f7befcac57 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart @@ -422,10 +422,16 @@ void main() { .whereType() .singleWhere((Directory directory) => fileSystem.path.extension(directory.path) == '.app'); + final Directory flutterFrameworkDir = fileSystem.directory( + fileSystem.path.join( + appBundle.path, + 'Frameworks', + 'Flutter.framework', + ), + ); + final String flutterFramework = fileSystem.path.join( - appBundle.path, - 'Frameworks', - 'Flutter.framework', + flutterFrameworkDir.path, 'Flutter', ); @@ -456,6 +462,11 @@ void main() { ], ); expect(appCodesign, const ProcessResultMatcher()); + + // Check read/write permissions are being correctly set + final String rawStatString = flutterFrameworkDir.statSync().modeString(); + final String statString = rawStatString.substring(rawStatString.length - 9); + expect(statString, 'rwxr-xr-x'); }); }, skip: !platform.isMacOS, // [intended] only makes sense for macos platform. timeout: const Timeout(Duration(minutes: 10)) diff --git a/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart index 5568ad2f9a0..2ff219c7641 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart @@ -164,6 +164,11 @@ void main() { ), ); + // Check read/write permissions are being correctly set + final String rawStatString = outputFlutterFramework.statSync().modeString(); + final String statString = rawStatString.substring(rawStatString.length - 9); + expect(statString, 'rwxr-xr-x'); + // Check complicated macOS framework symlink structure. final Link current = outputFlutterFramework.childDirectory('Versions').childLink('Current'); -- Gitee From 6ae220666f83940fab1f208c3ba9141879ee64a9 Mon Sep 17 00:00:00 2001 From: Kevin Chisholm Date: Wed, 22 May 2024 11:08:21 -0500 Subject: [PATCH 02/13] [CP] [devicelab] explicitly enable vulkan validation in test. (#147382) (#148863) Fix for release build failure https://ci.chromium.org/ui/p/flutter/builders/prod/Linux_pixel_7pro%20hello_world_impeller/3962/overview Part of https://github.com/flutter/flutter/issues/142659 this test expects validation layers on, so we need to explicitly enable them before we can turn them off by default. --- dev/devicelab/bin/tasks/hello_world_impeller.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/devicelab/bin/tasks/hello_world_impeller.dart b/dev/devicelab/bin/tasks/hello_world_impeller.dart index e67a25b48e6..d094882fdaa 100644 --- a/dev/devicelab/bin/tasks/hello_world_impeller.dart +++ b/dev/devicelab/bin/tasks/hello_world_impeller.dart @@ -54,6 +54,7 @@ Future run() async { 'run', options: [ '--enable-impeller', + '--enable-vulkan-validation', '-d', device.deviceId, ], -- Gitee From 315c05d04c252b48e7cc54af9e13951f8308de36 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Fri, 31 May 2024 12:43:17 -0700 Subject: [PATCH 03/13] [CP-stable]Refactor route focus node creation (#149378) ### Issue Link: https://github.com/flutter/flutter/issues/148867 Cherry-picking https://github.com/flutter/flutter/pull/147390 ### Changelog Description: Fixes a focus issue that causes TextFields to not function after cupertino back swipes ### Impact Description: iOS, macos, iOS web, macos web app that uses TextField ### Workaround: disable cupertino back swipe ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? TextField should not be broken after a cupertino back swipe --- .../transitions/listenable_builder.1_test.dart | 2 +- packages/flutter/lib/src/widgets/routes.dart | 18 +++++++++--------- packages/flutter/test/widgets/routes_test.dart | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/api/test/widgets/transitions/listenable_builder.1_test.dart b/examples/api/test/widgets/transitions/listenable_builder.1_test.dart index 00f18d1c28b..9b33fb6f021 100644 --- a/examples/api/test/widgets/transitions/listenable_builder.1_test.dart +++ b/examples/api/test/widgets/transitions/listenable_builder.1_test.dart @@ -10,7 +10,7 @@ void main() { testWidgets('Tapping FAB increments counter', (WidgetTester tester) async { await tester.pumpWidget(const example.ListenableBuilderExample()); - String getCount() => (tester.widget(find.descendant(of: find.byType(ListenableBuilder), matching: find.byType(Text))) as Text).data!; + String getCount() => (tester.widget(find.descendant(of: find.byType(ListenableBuilder).last, matching: find.byType(Text))) as Text).data!; expect(find.text('Current counter value:'), findsOneWidget); expect(find.text('0'), findsOneWidget); diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 277a5fb6c55..890d320e935 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -1022,6 +1022,8 @@ class _ModalScopeState extends State<_ModalScope> { @override Widget build(BuildContext context) { + // Only top most route can participate in focus traversal. + focusScopeNode.skipTraversal = !widget.route.isCurrent; return AnimatedBuilder( animation: widget.route.restorationScopeId, builder: (BuildContext context, Widget? child) { @@ -1048,24 +1050,22 @@ class _ModalScopeState extends State<_ModalScope> { }, child: PrimaryScrollController( controller: primaryScrollController, - child: FocusScope( - node: focusScopeNode, // immutable - // Only top most route can participate in focus traversal. - skipTraversal: !widget.route.isCurrent, + child: FocusScope.withExternalFocusNode( + focusScopeNode: focusScopeNode, // immutable child: RepaintBoundary( - child: AnimatedBuilder( - animation: _listenable, // immutable + child: ListenableBuilder( + listenable: _listenable, // immutable builder: (BuildContext context, Widget? child) { return widget.route.buildTransitions( context, widget.route.animation!, widget.route.secondaryAnimation!, - // This additional AnimatedBuilder is include because if the + // This additional ListenableBuilder is include because if the // value of the userGestureInProgressNotifier changes, it's // only necessary to rebuild the IgnorePointer widget and set // the focus node's ability to focus. - AnimatedBuilder( - animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier(false), + ListenableBuilder( + listenable: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier(false), builder: (BuildContext context, Widget? child) { final bool ignoreEvents = _shouldIgnoreFocusRequest; focusScopeNode.canRequestFocus = !ignoreEvents; diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart index 49508884e74..3c8cd459b1d 100644 --- a/packages/flutter/test/widgets/routes_test.dart +++ b/packages/flutter/test/widgets/routes_test.dart @@ -1736,7 +1736,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); - testWidgets('focus traverse correct when pop multiple page simultaneously', (WidgetTester tester) async { + testWidgets('focus traversal is correct when popping multiple pages simultaneously', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/48903 final GlobalKey navigatorKey = GlobalKey(); await tester.pumpWidget(MaterialApp( -- Gitee From fb72bca933a6f3be2d497c9d439b12dde536ed4d Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:34:06 -0700 Subject: [PATCH 04/13] [CP-stable]Enhance ColorScheme.fromSeed with a new `variant` parameter (#148916) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? https://github.com/flutter/flutter/issues/148359 ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/wiki/Hotfix-Documentation-Best-Practices) for examples Provide an option in `ColorScheme.fromSeed` method to respect the seed color even if the seed color is very bright. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) The updated algorithm in `ColorScheme.fromSeed` creates a darker version of `ColorScheme`. This may result in apps looking different from their original color theme. ### Workaround: Is there a workaround for this issue? Manually construct a desired `ColorScheme` instead of using `ColorScheme.fromSeed`, or use the deprecated `Scheme` class in [MCU package](https://github.com/material-foundation/material-color-utilities/blob/main/dart/lib/scheme/scheme.dart) to build the `ColorScheme` ### Risk: What is the risk level of this cherry-pick? This cherry-pick does not break the current implementation but only provides an option if users want the `ColorScheme` built by `ColorScheme.fromSeed` respect the seed color. ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? With `dynamicSchemeVariant: DynamicSchemeVariant.fidelity`, the color scheme constructed by `ColorScheme.fromSeed()` respects the seed color. --- .../material/color_scheme/color_scheme.0.dart | 192 +++++++++++------- .../color_scheme/color_scheme.0_test.dart | 7 +- .../lib/src/material/color_scheme.dart | 109 ++++++++-- .../test/material/color_scheme_test.dart | 144 ++++++++++++- 4 files changed, 352 insertions(+), 100 deletions(-) diff --git a/examples/api/lib/material/color_scheme/color_scheme.0.dart b/examples/api/lib/material/color_scheme/color_scheme.0.dart index 51ce183f9e2..78a6af44399 100644 --- a/examples/api/lib/material/color_scheme/color_scheme.0.dart +++ b/examples/api/lib/material/color_scheme/color_scheme.0.dart @@ -19,95 +19,93 @@ class ColorSchemeExample extends StatefulWidget { class _ColorSchemeExampleState extends State { Color selectedColor = ColorSeed.baseColor.color; + Brightness selectedBrightness = Brightness.light; + static const List schemeVariants = DynamicSchemeVariant.values; @override Widget build(BuildContext context) { - final Color? colorSeed = selectedColor == ColorSeed.baseColor.color ? null : selectedColor; - final ThemeData lightTheme = ThemeData( - colorSchemeSeed: colorSeed, - brightness: Brightness.light, - ); - final ThemeData darkTheme = ThemeData( - colorSchemeSeed: colorSeed, - brightness: Brightness.dark, - ); - - Widget schemeLabel(String brightness) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 15), - child: Text( - brightness, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ); - } - - Widget schemeView(ThemeData theme) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: ColorSchemeView(colorScheme: theme.colorScheme), - ); - } - return MaterialApp( - theme: ThemeData(colorSchemeSeed: selectedColor), + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: selectedColor, + brightness: selectedBrightness, + ) + ), home: Builder( builder: (BuildContext context) => Scaffold( appBar: AppBar( title: const Text('ColorScheme'), - leading: MenuAnchor( - builder: (BuildContext context, MenuController controller, Widget? widget) { - return IconButton( - icon: Icon(Icons.circle, color: selectedColor), - onPressed: () { - setState(() { - if (!controller.isOpen) { - controller.open(); - } - }); - }, - ); - }, - menuChildren: List.generate(ColorSeed.values.length, (int index) { - final Color itemColor = ColorSeed.values[index].color; - return MenuItemButton( - leadingIcon: selectedColor == ColorSeed.values[index].color - ? Icon(Icons.circle, color: itemColor) - : Icon(Icons.circle_outlined, color: itemColor), - onPressed: () { - setState(() { - selectedColor = itemColor; - }); - }, - child: Text(ColorSeed.values[index].label), - ); - }), - ), + actions: [ + Row( + children: [ + const Text('Color Seed'), + MenuAnchor( + builder: (BuildContext context, MenuController controller, Widget? widget) { + return IconButton( + icon: Icon(Icons.circle, color: selectedColor), + onPressed: () { + setState(() { + if (!controller.isOpen) { + controller.open(); + } + }); + }, + ); + }, + menuChildren: List.generate(ColorSeed.values.length, (int index) { + final Color itemColor = ColorSeed.values[index].color; + return MenuItemButton( + leadingIcon: selectedColor == ColorSeed.values[index].color + ? Icon(Icons.circle, color: itemColor) + : Icon(Icons.circle_outlined, color: itemColor), + onPressed: () { + setState(() { + selectedColor = itemColor; + }); + }, + child: Text(ColorSeed.values[index].label), + ); + }), + ), + ], + ), + ], ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only(top: 5), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Column( - children: [ - schemeLabel('Light ColorScheme'), - schemeView(lightTheme), - ], - ), - ), - Expanded( - child: Column( - children: [ - schemeLabel('Dark ColorScheme'), - schemeView(darkTheme), - ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Row( + children: [ + const Text('Brightness'), + const SizedBox(width: 10), + Switch( + value: selectedBrightness == Brightness.light, + onChanged: (bool value) { + setState(() { + selectedBrightness = value ? Brightness.light : Brightness.dark; + }); + }, ), - ), - ], + ], + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(schemeVariants.length, (int index) { + return ColorSchemeVariantColumn( + selectedColor: selectedColor, + brightness: selectedBrightness, + schemeVariant: schemeVariants[index], + ); + }).toList(), + ), ), ], ), @@ -119,6 +117,47 @@ class _ColorSchemeExampleState extends State { } } +class ColorSchemeVariantColumn extends StatelessWidget { + const ColorSchemeVariantColumn({ + super.key, + this.schemeVariant = DynamicSchemeVariant.tonalSpot, + this.brightness = Brightness.light, + required this.selectedColor, + }); + + final DynamicSchemeVariant schemeVariant; + final Brightness brightness; + final Color selectedColor; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 250), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 15), + child: Text( + schemeVariant.name == 'tonalSpot' ? '${schemeVariant.name} (Default)' : schemeVariant.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: ColorSchemeView( + colorScheme: ColorScheme.fromSeed( + seedColor: selectedColor, + brightness: brightness, + dynamicSchemeVariant: schemeVariant, + ), + ), + ), + ], + ) + ); + } +} + class ColorSchemeView extends StatelessWidget { const ColorSchemeView({super.key, required this.colorScheme}); @@ -301,7 +340,10 @@ enum ColorSeed { yellow('Yellow', Colors.yellow), orange('Orange', Colors.orange), deepOrange('Deep Orange', Colors.deepOrange), - pink('Pink', Colors.pink); + pink('Pink', Colors.pink), + brightBlue('Bright Blue', Color(0xFF0000FF)), + brightGreen('Bright Green', Color(0xFF00FF00)), + brightRed('Bright Red', Color(0xFFFF0000)); const ColorSeed(this.label, this.color); final String label; diff --git a/examples/api/test/material/color_scheme/color_scheme.0_test.dart b/examples/api/test/material/color_scheme/color_scheme.0_test.dart index 492a71884b9..58e0e142ff0 100644 --- a/examples/api/test/material/color_scheme/color_scheme.0_test.dart +++ b/examples/api/test/material/color_scheme/color_scheme.0_test.dart @@ -11,10 +11,9 @@ void main() { await tester.pumpWidget( const example.ColorSchemeExample(), ); - expect(find.text('Light ColorScheme'), findsOneWidget); - expect(find.text('Dark ColorScheme'), findsOneWidget); + expect(find.text('tonalSpot (Default)'), findsOneWidget); - expect(find.byType(example.ColorChip), findsNWidgets(86)); + expect(find.byType(example.ColorChip), findsNWidgets(43 * 9)); }); testWidgets('Change color seed', (WidgetTester tester) async { @@ -30,7 +29,7 @@ void main() { ) ); } - expect(coloredBox().color, const Color(0xFF6750A4)); + expect(coloredBox().color, const Color(0xff65558f)); await tester.tap(find.byType(MenuAnchor)); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(MenuItemButton, 'Yellow')); diff --git a/packages/flutter/lib/src/material/color_scheme.dart b/packages/flutter/lib/src/material/color_scheme.dart index eaa01ace6ba..76387afbb02 100644 --- a/packages/flutter/lib/src/material/color_scheme.dart +++ b/packages/flutter/lib/src/material/color_scheme.dart @@ -8,10 +8,58 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; +import 'package:material_color_utilities/scheme/scheme_fruit_salad.dart'; +import 'package:material_color_utilities/scheme/scheme_rainbow.dart'; import 'colors.dart'; import 'theme_data.dart'; +/// The algorithm used to construct a [ColorScheme] in [ColorScheme.fromSeed]. +/// +/// The `tonalSpot` variant builds default Material scheme colors. These colors are +/// mapped to light or dark tones to achieve visually accessible color +/// pairings with sufficient contrast between foreground and background elements. +/// +/// In some cases, the tones can prevent colors from appearing as intended, +/// such as when a color is too light to offer enough contrast for accessibility. +/// Color fidelity (`DynamicSchemeVariant.fidelity`) is a feature that adjusts +/// tones in these cases to produce the intended visual results without harming +/// visual contrast. +enum DynamicSchemeVariant { + /// Default for Material theme colors. Builds pastel palettes with a low chroma. + tonalSpot, + + /// The resulting color palettes match seed color, even if the seed color + /// is very bright (high chroma). + fidelity, + + /// All colors are grayscale, no chroma. + monochrome, + + /// Close to grayscale, a hint of chroma. + neutral, + + /// Pastel colors, high chroma palettes. The primary palette's chroma is at + /// maximum. Use `fidelity` instead if tokens should alter their tone to match + /// the palette vibrancy. + vibrant, + + /// Pastel colors, medium chroma palettes. The primary palette's hue is + /// different from the seed color, for variety. + expressive, + + /// Almost identical to `fidelity`. Tokens and palettes match the seed color. + /// [ColorScheme.primaryContainer] is the seed color, adjusted to ensure + /// contrast with surfaces. The tertiary palette is analogue of the seed color. + content, + + /// A playful theme - the seed color's hue does not appear in the theme. + rainbow, + + /// A playful theme - the seed color's hue does not appear in the theme. + fruitSalad, +} + /// {@template flutter.material.color_scheme.ColorScheme} /// A set of 45 colors based on the /// [Material spec](https://m3.material.io/styles/color/the-color-system/color-roles) @@ -215,20 +263,35 @@ class ColorScheme with Diagnosticable { /// Generate a [ColorScheme] derived from the given `seedColor`. /// - /// Using the seedColor as a starting point, a set of tonal palettes are - /// constructed. These tonal palettes are based on the Material 3 Color - /// system and provide all the needed colors for a [ColorScheme]. These - /// colors are designed to work well together and meet contrast - /// requirements for accessibility. + /// Using the `seedColor` as a starting point, a set of tonal palettes are + /// constructed. By default, the tonal palettes are based on the Material 3 + /// Color system and provide all of the [ColorScheme] colors. These colors are + /// designed to work well together and meet contrast requirements for + /// accessibility. /// /// If any of the optional color parameters are non-null they will be /// used in place of the generated colors for that field in the resulting /// color scheme. This allows apps to override specific colors for their /// needs. /// - /// Given the nature of the algorithm, the seedColor may not wind up as + /// Given the nature of the algorithm, the `seedColor` may not wind up as /// one of the ColorScheme colors. /// + /// The `dynamicSchemeVariant` parameter creates different types of + /// [DynamicScheme]s, which are used to generate different styles of [ColorScheme]s. + /// By default, `dynamicSchemeVariant` is set to `tonalSpot`. A [ColorScheme] + /// constructed by `dynamicSchemeVariant.tonalSpot` has pastel palettes and + /// won't be too "colorful" even if the `seedColor` has a high chroma value. + /// If the resulting color scheme is too dark, consider setting `dynamicSchemeVariant` + /// to [DynamicSchemeVariant.fidelity], whose palettes match the seed color. + /// + /// {@tool dartpad} + /// This sample shows how to use [ColorScheme.fromSeed] to create dynamic + /// color schemes with different [DynamicSchemeVariant]s. + /// + /// ** See code in examples/api/lib/material/color_scheme/color_scheme.0.dart ** + /// {@end-tool} + /// /// See also: /// /// * , the @@ -238,6 +301,7 @@ class ColorScheme with Diagnosticable { factory ColorScheme.fromSeed({ required Color seedColor, Brightness brightness = Brightness.light, + DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot, Color? primary, Color? onPrimary, Color? primaryContainer, @@ -300,13 +364,7 @@ class ColorScheme with Diagnosticable { ) Color? surfaceVariant, }) { - final SchemeTonalSpot scheme; - switch (brightness) { - case Brightness.light: - scheme = SchemeTonalSpot(sourceColorHct: Hct.fromInt(seedColor.value), isDark: false, contrastLevel: 0.0); - case Brightness.dark: - scheme = SchemeTonalSpot(sourceColorHct: Hct.fromInt(seedColor.value), isDark: true, contrastLevel: 0.0); - } + final DynamicScheme scheme = _buildDynamicScheme(brightness, seedColor, dynamicSchemeVariant); return ColorScheme( primary: primary ?? Color(MaterialDynamicColors.primary.getArgb(scheme)), @@ -1615,6 +1673,7 @@ class ColorScheme with Diagnosticable { static Future fromImageProvider({ required ImageProvider provider, Brightness brightness = Brightness.light, + DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot, Color? primary, Color? onPrimary, Color? primaryContainer, @@ -1688,13 +1747,7 @@ class ColorScheme with Diagnosticable { final List scoredResults = Score.score(colorToCount, desired: 1); final ui.Color baseColor = Color(scoredResults.first); - final SchemeTonalSpot scheme; - switch (brightness) { - case Brightness.light: - scheme = SchemeTonalSpot(sourceColorHct: Hct.fromInt(baseColor.value), isDark: false, contrastLevel: 0.0); - case Brightness.dark: - scheme = SchemeTonalSpot(sourceColorHct: Hct.fromInt(baseColor.value), isDark: true, contrastLevel: 0.0); - } + final DynamicScheme scheme = _buildDynamicScheme(brightness, baseColor, dynamicSchemeVariant); return ColorScheme( primary: primary ?? Color(MaterialDynamicColors.primary.getArgb(scheme)), @@ -1830,4 +1883,20 @@ class ColorScheme with Diagnosticable { final int b = abgr & onlyBMask; return (abgr & exceptRMask & exceptBMask) | (b << 16) | r; } + + static DynamicScheme _buildDynamicScheme(Brightness brightness, Color seedColor, DynamicSchemeVariant schemeVariant) { + final bool isDark = brightness == Brightness.dark; + final Hct sourceColor = Hct.fromInt(seedColor.value); + return switch (schemeVariant) { + DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.fidelity => SchemeFidelity(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.content => SchemeContent(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.monochrome => SchemeMonochrome(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.neutral => SchemeNeutral(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.vibrant => SchemeVibrant(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.expressive => SchemeExpressive(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.rainbow => SchemeRainbow(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + }; + } } diff --git a/packages/flutter/test/material/color_scheme_test.dart b/packages/flutter/test/material/color_scheme_test.dart index 17eba66e0e7..36df995aac0 100644 --- a/packages/flutter/test/material/color_scheme_test.dart +++ b/packages/flutter/test/material/color_scheme_test.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:material_color_utilities/material_color_utilities.dart'; +import 'package:material_color_utilities/scheme/scheme_fruit_salad.dart'; +import 'package:material_color_utilities/scheme/scheme_rainbow.dart'; import '../image_data.dart'; @@ -684,4 +687,143 @@ void main() { }, skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); + + testWidgets('Color values in ColorScheme.fromSeed with different variants matches values in DynamicScheme', (WidgetTester tester) async { + const Color seedColor = Colors.orange; + final Hct sourceColor = Hct.fromInt(seedColor.value); + for (final DynamicSchemeVariant schemeVariant in DynamicSchemeVariant.values) { + final DynamicScheme dynamicScheme = switch (schemeVariant) { + DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.fidelity => SchemeFidelity(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.content => SchemeContent(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.monochrome => SchemeMonochrome(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.neutral => SchemeNeutral(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.vibrant => SchemeVibrant(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.expressive => SchemeExpressive(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.rainbow => SchemeRainbow(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + }; + final ColorScheme colorScheme = ColorScheme.fromSeed( + seedColor: seedColor, + dynamicSchemeVariant: schemeVariant, + ); + + expect(colorScheme.primary.value, MaterialDynamicColors.primary.getArgb(dynamicScheme)); + expect(colorScheme.onPrimary.value, MaterialDynamicColors.onPrimary.getArgb(dynamicScheme)); + expect(colorScheme.primaryContainer.value, MaterialDynamicColors.primaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.onPrimaryContainer.value, MaterialDynamicColors.onPrimaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.primaryFixed.value, MaterialDynamicColors.primaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.primaryFixedDim.value, MaterialDynamicColors.primaryFixedDim.getArgb(dynamicScheme)); + expect(colorScheme.onPrimaryFixed.value, MaterialDynamicColors.onPrimaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.onPrimaryFixedVariant.value, MaterialDynamicColors.onPrimaryFixedVariant.getArgb(dynamicScheme)); + expect(colorScheme.secondary.value, MaterialDynamicColors.secondary.getArgb(dynamicScheme)); + expect(colorScheme.onSecondary.value, MaterialDynamicColors.onSecondary.getArgb(dynamicScheme)); + expect(colorScheme.secondaryContainer.value, MaterialDynamicColors.secondaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.onSecondaryContainer.value, MaterialDynamicColors.onSecondaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.secondaryFixed.value, MaterialDynamicColors.secondaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.secondaryFixedDim.value, MaterialDynamicColors.secondaryFixedDim.getArgb(dynamicScheme)); + expect(colorScheme.onSecondaryFixed.value, MaterialDynamicColors.onSecondaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.onSecondaryFixedVariant.value, MaterialDynamicColors.onSecondaryFixedVariant.getArgb(dynamicScheme)); + expect(colorScheme.tertiary.value, MaterialDynamicColors.tertiary.getArgb(dynamicScheme)); + expect(colorScheme.onTertiary.value, MaterialDynamicColors.onTertiary.getArgb(dynamicScheme)); + expect(colorScheme.tertiaryContainer.value, MaterialDynamicColors.tertiaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.onTertiaryContainer.value, MaterialDynamicColors.onTertiaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.tertiaryFixed.value, MaterialDynamicColors.tertiaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.tertiaryFixedDim.value, MaterialDynamicColors.tertiaryFixedDim.getArgb(dynamicScheme)); + expect(colorScheme.onTertiaryFixed.value, MaterialDynamicColors.onTertiaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.onTertiaryFixedVariant.value, MaterialDynamicColors.onTertiaryFixedVariant.getArgb(dynamicScheme)); + expect(colorScheme.error.value, MaterialDynamicColors.error.getArgb(dynamicScheme)); + expect(colorScheme.onError.value, MaterialDynamicColors.onError.getArgb(dynamicScheme)); + expect(colorScheme.errorContainer.value, MaterialDynamicColors.errorContainer.getArgb(dynamicScheme)); + expect(colorScheme.onErrorContainer.value, MaterialDynamicColors.onErrorContainer.getArgb(dynamicScheme)); + expect(colorScheme.background.value, MaterialDynamicColors.background.getArgb(dynamicScheme)); + expect(colorScheme.onBackground.value, MaterialDynamicColors.onBackground.getArgb(dynamicScheme)); + expect(colorScheme.surface.value, MaterialDynamicColors.surface.getArgb(dynamicScheme)); + expect(colorScheme.surfaceDim.value, MaterialDynamicColors.surfaceDim.getArgb(dynamicScheme)); + expect(colorScheme.surfaceBright.value, MaterialDynamicColors.surfaceBright.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainerLowest.value, MaterialDynamicColors.surfaceContainerLowest.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainerLow.value, MaterialDynamicColors.surfaceContainerLow.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainer.value, MaterialDynamicColors.surfaceContainer.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainerHigh.value, MaterialDynamicColors.surfaceContainerHigh.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainerHighest.value, MaterialDynamicColors.surfaceContainerHighest.getArgb(dynamicScheme)); + expect(colorScheme.onSurface.value, MaterialDynamicColors.onSurface.getArgb(dynamicScheme)); + expect(colorScheme.surfaceVariant.value, MaterialDynamicColors.surfaceVariant.getArgb(dynamicScheme)); + expect(colorScheme.onSurfaceVariant.value, MaterialDynamicColors.onSurfaceVariant.getArgb(dynamicScheme)); + expect(colorScheme.outline.value, MaterialDynamicColors.outline.getArgb(dynamicScheme)); + expect(colorScheme.outlineVariant.value, MaterialDynamicColors.outlineVariant.getArgb(dynamicScheme)); + expect(colorScheme.shadow.value, MaterialDynamicColors.shadow.getArgb(dynamicScheme)); + expect(colorScheme.scrim.value, MaterialDynamicColors.scrim.getArgb(dynamicScheme)); + expect(colorScheme.inverseSurface.value, MaterialDynamicColors.inverseSurface.getArgb(dynamicScheme)); + expect(colorScheme.onInverseSurface.value, MaterialDynamicColors.inverseOnSurface.getArgb(dynamicScheme)); + expect(colorScheme.inversePrimary.value, MaterialDynamicColors.inversePrimary.getArgb(dynamicScheme)); + } + }); + + testWidgets('ColorScheme.fromSeed with different variants spot checks', (WidgetTester tester) async { + // Default (Variant.tonalSpot). + await _testFilledButtonColor(tester, ColorScheme.fromSeed(seedColor: const Color(0xFF000000)), const Color(0xFF8C4A60)); + await _testFilledButtonColor(tester, ColorScheme.fromSeed(seedColor: const Color(0xFF00FF00)), const Color(0xFF406836)); + await _testFilledButtonColor(tester, ColorScheme.fromSeed(seedColor: const Color(0xFF6559F5)), const Color(0xFF5B5891)); + await _testFilledButtonColor(tester, ColorScheme.fromSeed(seedColor: const Color(0xFFFFFFFF)), const Color(0xFF006874)); + + // Variant.fidelity. + await _testFilledButtonColor( + tester, + ColorScheme.fromSeed( + seedColor: const Color(0xFF000000), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity + ), + const Color(0xFF000000) + ); + await _testFilledButtonColor( + tester, + ColorScheme.fromSeed( + seedColor: const Color(0xFF00FF00), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity + ), + const Color(0xFF026E00) + ); + await _testFilledButtonColor( + tester, + ColorScheme.fromSeed( + seedColor: const Color(0xFF6559F5), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity + ), + const Color(0xFF3F2CD0) + ); + await _testFilledButtonColor( + tester, + ColorScheme.fromSeed( + seedColor: const Color(0xFFFFFFFF), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity + ), + const Color(0xFF5D5F5F) + ); + }); +} + +Future _testFilledButtonColor(WidgetTester tester, ColorScheme scheme, Color expectation) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(Container()); // reset + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: scheme, + ), + home: FilledButton( + key: key, + onPressed: () {}, + child: const SizedBox.square(dimension: 200), + ), + ), + ); + + + final Finder buttonMaterial = find.descendant( + of: find.byType(FilledButton), + matching: find.byType(Material), + ); + final Material material = tester.widget(buttonMaterial); + + expect(material.color, expectation); } -- Gitee From 5da6d1a85e5f98545a023e51f6e994f9106bde03 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:54:58 -0700 Subject: [PATCH 05/13] [CP-stable] Remove `TextEditingController` private member access (#149042) (#149208) Manual request (merge conflicts in the test file) for cherry-picking #149042 to stable ### Issue Link: What is the link to the issue this cherry-pick is addressing? #148692 ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/wiki/Hotfix-Documentation-Best-Practices) for examples Fixes an `EditableText` crash that happens with a custom `TextEditingController` which only `implements` the `TextEditingController` interface. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) Production apps crash if the user gives an `EditableText` a custom `TextEditingControllor` class that `implements TextEditingControllor`. ### Workaround: Is there a workaround for this issue? The crash does not happen if the user `extends TextEditingControllor`, but it's not always desirable / feasible. The class could be already extending a different base class. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? Run the test in the PR, or follow the repro steps in https://github.com/flutter/flutter/issues/148692 --- .../lib/src/widgets/editable_text.dart | 10 ++-- .../test/widgets/editable_text_test.dart | 51 +++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 6d2e749c97e..bfc41d990ee 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -311,7 +311,7 @@ class TextEditingController extends ValueNotifier { /// If the new selection is outside the composing range, the composing range is /// cleared. set selection(TextSelection newSelection) { - if (!_isSelectionWithinTextBounds(newSelection)) { + if (text.length < newSelection.end || text.length < newSelection.start) { throw FlutterError('invalid text selection: $newSelection'); } final TextRange newComposing = _isSelectionWithinComposingRange(newSelection) ? value.composing : TextRange.empty; @@ -345,11 +345,6 @@ class TextEditingController extends ValueNotifier { value = value.copyWith(composing: TextRange.empty); } - /// Check that the [selection] is inside of the bounds of [text]. - bool _isSelectionWithinTextBounds(TextSelection selection) { - return selection.start <= text.length && selection.end <= text.length; - } - /// Check that the [selection] is inside of the composing range. bool _isSelectionWithinComposingRange(TextSelection selection) { return selection.start >= value.composing.start && selection.end <= value.composing.end; @@ -3938,7 +3933,8 @@ class EditableTextState extends State with AutomaticKeepAliveClien // We return early if the selection is not valid. This can happen when the // text of [EditableText] is updated at the same time as the selection is // changed by a gesture event. - if (!widget.controller._isSelectionWithinTextBounds(selection)) { + final String text = widget.controller.value.text; + if (text.length < selection.end || text.length < selection.start) { return; } diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 63a9f404917..f752d192392 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -17264,6 +17264,27 @@ void main() { await tester.pumpAndSettle(); expect(scrollController.offset, 75.0); }); + + testWidgets('Can implement TextEditingController', (WidgetTester tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: EditableText( + autofocus: true, + backgroundCursorColor: Colors.grey, + controller: _TextEditingControllerImpl(), + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + }); } class UnsettableController extends TextEditingController { @@ -17626,6 +17647,36 @@ class _AccentColorTextEditingController extends TextEditingController { } } +class _TextEditingControllerImpl extends ChangeNotifier implements TextEditingController { + final TextEditingController _innerController = TextEditingController(); + + @override + void clear() => _innerController.clear(); + + @override + void clearComposing() => _innerController.clearComposing(); + + @override + TextSelection get selection => _innerController.selection; + @override + set selection(TextSelection newSelection) => _innerController.selection = newSelection; + + @override + String get text => _innerController.text; + @override + set text(String newText) => _innerController.text = newText; + + @override + TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) { + return _innerController.buildTextSpan(context: context, style: style, withComposing: withComposing); + } + + @override + TextEditingValue get value => _innerController.value; + @override + set value(TextEditingValue newValue) => _innerController.value = newValue; +} + class _TestScrollController extends ScrollController { bool get attached => hasListeners; } -- Gitee From 169aff6a55b14c9fa3bb5e96d3c23507487ae1b2 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:38:03 -0700 Subject: [PATCH 06/13] [CP-stable] Add frame number and widget location map service extension (#149345) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? DevTools release 2.36.0: https://dart-review.googlesource.com/c/sdk/+/368762 ### Changelog Description: This change adds a service extension that DevTools uses to support a "Track widget build counts" feature. This feature is first included in DevTools 2.36.0. ### Impact Description: This service extension is required for DevTools 2.36.0, which needs to be [cherry-picked](https://dart-review.googlesource.com/c/sdk/+/368762) into the Dart SDK. This beta release of DevTools missed the code cutoff because we were targeting the date listed on go/dash-team-releases (5/29). Since the beta release went out early, we missed the cut off and need to cherry-pick. Since this DevTools release depended on the changes from https://github.com/flutter/flutter/pull/148702, we also need to cherry pick those changes into the flutter beta channel. ### Workaround: No. This is required for DevTools 2.36.0 to work properly: https://dart-review.googlesource.com/c/sdk/+/368762. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? With this change, a flutter app connecting to DevTools 2.36.0 (already on the Dart SDK main branch and trying to CP into the Dart SDK beta branch) will be able to use the "Track widget build counts" feature in DevTools. --- .../lib/src/widgets/service_extensions.dart | 15 ++++ .../lib/src/widgets/widget_inspector.dart | 70 ++++++++++++++++--- .../foundation/service_extensions_test.dart | 2 +- .../test/widgets/widget_inspector_test.dart | 45 ++++++++++++ 4 files changed, 123 insertions(+), 9 deletions(-) diff --git a/packages/flutter/lib/src/widgets/service_extensions.dart b/packages/flutter/lib/src/widgets/service_extensions.dart index caeb190758a..54971b59c38 100644 --- a/packages/flutter/lib/src/widgets/service_extensions.dart +++ b/packages/flutter/lib/src/widgets/service_extensions.dart @@ -144,7 +144,22 @@ enum WidgetInspectorServiceExtensions { /// extension is registered. trackRebuildDirtyWidgets, + /// Name of service extension that, when called, returns the mapping of + /// widget locations to ids. + /// + /// This service extension is only supported if + /// [WidgetInspectorService._widgetCreationTracked] is true. + /// + /// See also: + /// + /// * [trackRebuildDirtyWidgets], which toggles dispatching events that use + /// these ids to efficiently indicate the locations of widgets. + /// * [WidgetInspectorService.initServiceExtensions], where the service + /// extension is registered. + widgetLocationIdMap, + /// Name of service extension that, when called, determines whether + /// [WidgetInspectorService._trackRepaintWidgets], which determines whether /// a callback is invoked for every [RenderObject] painted each frame. /// /// See also: diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index d578e594cb8..9ebcf222974 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -1120,6 +1120,14 @@ mixin WidgetInspectorService { registerExtension: registerExtension, ); + _registerSignalServiceExtension( + name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name, + callback: () { + return _locationIdMapToJson(); + }, + registerExtension: registerExtension, + ); + _registerBoolServiceExtension( name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name, getter: () async => _trackRepaintWidgets, @@ -2375,9 +2383,11 @@ mixin WidgetInspectorService { bool? _widgetCreationTracked; late Duration _frameStart; + late int _frameNumber; void _onFrameStart(Duration timeStamp) { _frameStart = timeStamp; + _frameNumber = PlatformDispatcher.instance.frameData.frameNumber; SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd, debugLabel: 'WidgetInspector.onFrameStart'); } @@ -2391,7 +2401,13 @@ mixin WidgetInspectorService { } void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) { - postEvent(eventName, stats.exportToJson(_frameStart)); + postEvent( + eventName, + stats.exportToJson( + _frameStart, + frameNumber: _frameNumber, + ), + ); } /// All events dispatched by a [WidgetInspectorService] use this method @@ -2600,7 +2616,7 @@ class _ElementLocationStatsTracker { /// Exports the current counts and then resets the stats to prepare to track /// the next frame of data. - Map exportToJson(Duration startTime) { + Map exportToJson(Duration startTime, {required int frameNumber}) { final List events = List.filled(active.length * 2, 0); int j = 0; for (final _LocationCount stat in active) { @@ -2610,6 +2626,7 @@ class _ElementLocationStatsTracker { final Map json = { 'startTime': startTime.inMicroseconds, + 'frameNumber': frameNumber, 'events': events, }; @@ -3256,12 +3273,21 @@ class _InspectorOverlayLayer extends Layer { final Rect targetRect = MatrixUtils.transformRect( state.selected.transform, state.selected.rect, ); - final Offset target = Offset(targetRect.left, targetRect.center.dy); - const double offsetFromWidget = 9.0; - final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget; - - _paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect); - + if (!targetRect.hasNaN) { + final Offset target = Offset(targetRect.left, targetRect.center.dy); + const double offsetFromWidget = 9.0; + final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget; + + _paintDescription( + canvas, + state.tooltip, + state.textDirection, + target, + verticalOffset, + size, + targetRect, + ); + } // TODO(jacobr): provide an option to perform a debug paint of just the // selected widget. return recorder.endRecording(); @@ -3651,6 +3677,34 @@ int _toLocationId(_Location location) { return id; } +Map _locationIdMapToJson() { + const String idsKey = 'ids'; + const String linesKey = 'lines'; + const String columnsKey = 'columns'; + const String namesKey = 'names'; + + final Map>> fileLocationsMap = + >>{}; + for (final MapEntry<_Location, int> entry in _locationToId.entries) { + final _Location location = entry.key; + final Map> locations = fileLocationsMap.putIfAbsent( + location.file, + () => >{ + idsKey: [], + linesKey: [], + columnsKey: [], + namesKey: [], + }, + ); + + locations[idsKey]!.add(entry.value); + locations[linesKey]!.add(location.line); + locations[columnsKey]!.add(location.column); + locations[namesKey]!.add(location.name); + } + return fileLocationsMap; +} + /// A delegate that configures how a hierarchy of [DiagnosticsNode]s are /// serialized by the Flutter Inspector. @visibleForTesting diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index 14db86f397f..b0c3ba6541e 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -170,7 +170,7 @@ void main() { if (WidgetInspectorService.instance.isWidgetCreationTracked()) { // Some inspector extensions are only exposed if widget creation locations // are tracked. - widgetInspectorExtensionCount += 2; + widgetInspectorExtensionCount += 3; } expect(binding.extensions.keys.where((String name) => name.startsWith('inspector.')), hasLength(widgetInspectorExtensionCount)); diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index b40f8ed905e..32808e30343 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -3765,6 +3765,49 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag. ); + testWidgets('ext.flutter.inspector.widgetLocationIdMap', + (WidgetTester tester) async { + service.rebuildCount = 0; + + await tester.pumpWidget(const ClockDemo()); + + final Element clockDemoElement = find.byType(ClockDemo).evaluate().first; + + service.setSelection(clockDemoElement, 'my-group'); + final Map jsonObject = (await service.testExtension( + WidgetInspectorServiceExtensions.getSelectedWidget.name, + {'objectGroup': 'my-group'}, + ))! as Map; + final Map creationLocation = + jsonObject['creationLocation']! as Map; + final String file = creationLocation['file']! as String; + expect(file, endsWith('widget_inspector_test.dart')); + + final Map locationMapJson = (await service.testExtension( + WidgetInspectorServiceExtensions.widgetLocationIdMap.name, + {}, + ))! as Map; + + final Map widgetTestLocations = + locationMapJson[file]! as Map; + expect(widgetTestLocations, isNotNull); + + final List ids = widgetTestLocations['ids']! as List; + expect(ids.length, greaterThan(0)); + final List lines = + widgetTestLocations['lines']! as List; + expect(lines.length, equals(ids.length)); + final List columns = + widgetTestLocations['columns']! as List; + expect(columns.length, equals(ids.length)); + final List names = + widgetTestLocations['names']! as List; + expect(names.length, equals(ids.length)); + expect(names, contains('ClockDemo')); + expect(names, contains('Directionality')); + expect(names, contains('ClockText')); + }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag. + testWidgets('ext.flutter.inspector.trackRebuildDirtyWidgets', (WidgetTester tester) async { service.rebuildCount = 0; @@ -3951,6 +3994,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(rebuildEvents.length, equals(1)); event = removeLastEvent(rebuildEvents); expect(event['startTime'], isA()); + expect(event['frameNumber'], isA()); data = event['events']! as List; newLocations = event['newLocations']! as Map>; fileLocationsMap = event['locations']! as Map>>; @@ -4080,6 +4124,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(repaintEvents.length, equals(1)); event = removeLastEvent(repaintEvents); expect(event['startTime'], isA()); + expect(event['frameNumber'], isA()); data = event['events']! as List; // No new locations were rebuilt. expect(event, isNot(contains('newLocations'))); -- Gitee From 7b607f7878b14c69c9d03299a5c511b4cf993782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Gawron?= Date: Wed, 5 Jun 2024 22:15:13 +0200 Subject: [PATCH 07/13] [CP] Fix TwoDimensionalViewport's keep alive child not always removed (when no longer should be kept alive) (#149639) Cherry pick request of https://github.com/flutter/flutter/pull/148298 to stable. Fixes problems in 2D viewport when using keep alive widgets (affects users using Material Ink components). --- .../src/widgets/two_dimensional_viewport.dart | 4 ++ .../test/widgets/two_dimensional_utils.dart | 36 +++++++++++ .../two_dimensional_viewport_test.dart | 60 +++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart b/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart index 4bc518129c9..b10a9ca437a 100644 --- a/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart +++ b/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart @@ -1647,6 +1647,10 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA _children.remove(slot); } assert(_debugTrackOrphans(noLongerOrphan: child)); + if (_keepAliveBucket[childParentData.vicinity] == child) { + _keepAliveBucket.remove(childParentData.vicinity); + } + assert(_keepAliveBucket[childParentData.vicinity] != child); dropChild(child); return; } diff --git a/packages/flutter/test/widgets/two_dimensional_utils.dart b/packages/flutter/test/widgets/two_dimensional_utils.dart index dd13a277c39..1321786fbd3 100644 --- a/packages/flutter/test/widgets/two_dimensional_utils.dart +++ b/packages/flutter/test/widgets/two_dimensional_utils.dart @@ -510,3 +510,39 @@ class TestParentDataWidget extends ParentDataWidget { @override Type get debugTypicalAncestorWidgetClass => SimpleBuilderTableViewport; } + +class KeepAliveOnlyWhenHovered extends StatefulWidget { + const KeepAliveOnlyWhenHovered({ required this.child, super.key }); + + final Widget child; + + @override + KeepAliveOnlyWhenHoveredState createState() => KeepAliveOnlyWhenHoveredState(); +} + +class KeepAliveOnlyWhenHoveredState extends State with AutomaticKeepAliveClientMixin { + bool _hovered = false; + + @override + bool get wantKeepAlive => _hovered; + + @override + Widget build(BuildContext context) { + super.build(context); + return MouseRegion( + onEnter: (_) { + setState(() { + _hovered = true; + updateKeepAlive(); + }); + }, + onExit: (_) { + setState(() { + _hovered = false; + updateKeepAlive(); + }); + }, + child: widget.child, + ); + } +} diff --git a/packages/flutter/test/widgets/two_dimensional_viewport_test.dart b/packages/flutter/test/widgets/two_dimensional_viewport_test.dart index a44bf49ce99..a85f384c76a 100644 --- a/packages/flutter/test/widgets/two_dimensional_viewport_test.dart +++ b/packages/flutter/test/widgets/two_dimensional_viewport_test.dart @@ -735,6 +735,66 @@ void main() { ); }); + testWidgets('Ensure KeepAlive widget is not held onto when it no longer should be kept alive offscreen', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/138977 + final UniqueKey checkBoxKey = UniqueKey(); + final Widget originCell = KeepAliveOnlyWhenHovered( + key: checkBoxKey, + child: const SizedBox.square(dimension: 200), + ); + const Widget otherCell = SizedBox.square(dimension: 200, child: Placeholder()); + final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); + final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate( + children: >[ + [originCell, otherCell, otherCell, otherCell, otherCell], + [otherCell, otherCell, otherCell, otherCell, otherCell], + [otherCell, otherCell, otherCell, otherCell, otherCell], + [otherCell, otherCell, otherCell, otherCell, otherCell], + [otherCell, otherCell, otherCell, otherCell, otherCell], + ], + ); + addTearDown(listDelegate.dispose); + + await tester.pumpWidget(simpleListTest( + delegate: listDelegate, + verticalDetails: ScrollableDetails.vertical(controller: verticalController), + )); + await tester.pumpAndSettle(); + expect(find.byKey(checkBoxKey), findsOneWidget); + + // Scroll away, should not be kept alive (disposed). + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view + verticalController.jumpTo(0.0); + await tester.pump(); + expect(find.byKey(checkBoxKey), findsOneWidget); + + // Hover over widget to make it keep alive. + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byKey(checkBoxKey))); + await tester.pump(); + + // Scroll away, should be kept alive still. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(find.byKey(checkBoxKey), findsOneWidget); + + // Move the pointer outside the widget bounds to trigger exit event + // and remove it from keep alive bucket. + await gesture.moveTo(const Offset(300, 300)); + await tester.pump(); + expect(find.byKey(checkBoxKey), findsNothing); + }); + testWidgets('list delegate will not add automatic keep alives', (WidgetTester tester) async { final UniqueKey checkBoxKey = UniqueKey(); final Widget originCell = SizedBox.square( -- Gitee From bf3bb42a8dfed80e3f531b66a3ff59b7402e43ab Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Tue, 2 Jul 2024 03:44:51 +0800 Subject: [PATCH 08/13] [CP] Point kotlin message in `gradle_errors.dart` towards new place (#149881) Cherry pick request of https://github.com/flutter/flutter/pull/145936 to stable Update Flutter Fix log on how to update Kotlin Gradle Plugin that was introduced in Flutter 3.19.x Closes #149856 --- .../lib/src/android/gradle_errors.dart | 11 +++++++++-- .../android/gradle_errors_test.dart | 19 ++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart index 4bc3efaae3c..97e1ce89fde 100644 --- a/packages/flutter_tools/lib/src/android/gradle_errors.dart +++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart @@ -441,10 +441,17 @@ final GradleHandledError incompatibleKotlinVersionHandler = GradleHandledError( final File gradleFile = project.directory .childDirectory('android') .childFile('build.gradle'); + final File settingsFile = project.directory + .childDirectory('android') + .childFile('settings.gradle'); globals.printBox( '${globals.logger.terminal.warningMark} Your project requires a newer version of the Kotlin Gradle plugin.\n' - 'Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then update ${gradleFile.path}:\n' - "ext.kotlin_version = ''", + 'Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then update the \n' + 'version number of the plugin with id "org.jetbrains.kotlin.android" in the plugins block of \n' + '${settingsFile.path}.\n\n' + 'Alternatively (if your project was created before Flutter 3.19), update \n' + '${gradleFile.path}\n' + "ext.kotlin_version = ''", title: _boxTitle, ); return GradleBuildStatus.exit; diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart index 06691596602..ba1fe95b4a9 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart @@ -814,13 +814,18 @@ Execution failed for task ':app:generateDebugFeatureTransitiveDeps'. expect( testLogger.statusText, contains( - '\n' - '┌─ Flutter Fix ──────────────────────────────────────────────────────────────────────────────┐\n' - '│ [!] Your project requires a newer version of the Kotlin Gradle plugin. │\n' - '│ Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then │\n' - '│ update /android/build.gradle: │\n' - "│ ext.kotlin_version = '' │\n" - '└────────────────────────────────────────────────────────────────────────────────────────────┘\n' + '\n' + '┌─ Flutter Fix ────────────────────────────────────────────────────────────────────────────────┐\n' + '│ [!] Your project requires a newer version of the Kotlin Gradle plugin. │\n' + '│ Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then │\n' + '│ update the │\n' + '│ version number of the plugin with id "org.jetbrains.kotlin.android" in the plugins block of │\n' + '│ /android/settings.gradle. │\n' + '│ │\n' + '│ Alternatively (if your project was created before Flutter 3.19), update │\n' + '│ /android/build.gradle │\n' + "│ ext.kotlin_version = '' │\n" + '└──────────────────────────────────────────────────────────────────────────────────────────────┘\n' ) ); }, overrides: { -- Gitee From b31c7bdbc5dff998bdf680eb72044e732bbe5425 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:20:19 -0700 Subject: [PATCH 09/13] [CP-stable][CLI tool] in `flutter test`, consider `--flavor` when validating the cached asset bundle (#150617) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? https://github.com/flutter/flutter/issues/150296 ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples. Fixed bug in `flutter test` where `--flavor` wasn't considered when validating cached [assets](https://docs.flutter.dev/ui/assets/assets-and-images), causing the [flavor-conditional asset bundling](https://docs.flutter.dev/deployment/flavors#conditionally-bundling-assets-based-on-flavor) feature to not work as expected. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) For users of flavor-conditional asset bundling that used `--flavor` in `flutter test`, this could result in tests incorrectly passing or failing depending on which `--flavor` value they used since their last `flutter clean`. ### Workaround: Is there a workaround for this issue? `flutter clean` between `flutter test` runs (if `--flavor` is being used and any tests depend on the flavor-conditional asset bundling feature). ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? Attempt to repro with [these steps](https://github.com/flutter/flutter/issues/150296#issuecomment-2175300780) provided by the original issue reporter. --- .../flutter_tools/lib/src/commands/test.dart | 32 +++++++++-- .../commands.shard/hermetic/test_test.dart | 54 +++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index abaa42f11f3..9c5519a9cf6 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -678,7 +678,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { if (build != 0) { throwToolExit('Error: Failed to build asset bundle'); } - if (_needRebuild(assetBundle.entries)) { + if (_needsRebuild(assetBundle.entries, flavor)) { await writeBundle( globals.fs.directory(globals.fs.path.join('build', 'unit_test_assets')), assetBundle.entries, @@ -690,14 +690,25 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { logger: globals.logger, projectDir: globals.fs.currentDirectory, ); + + final File cachedFlavorFile = globals.fs.file( + globals.fs.path.join('build', 'test_cache', 'flavor.txt'), + ); + if (cachedFlavorFile.existsSync()) { + await cachedFlavorFile.delete(); + } + if (flavor != null) { + cachedFlavorFile.createSync(recursive: true); + cachedFlavorFile.writeAsStringSync(flavor); + } } } - bool _needRebuild(Map entries) { + bool _needsRebuild(Map entries, String? flavor) { // TODO(andrewkolos): This logic might fail in the future if we change the - // schema of the contents of the asset manifest file and the user does not - // perform a `flutter clean` after upgrading. - // See https://github.com/flutter/flutter/issues/128563. + // schema of the contents of the asset manifest file and the user does not + // perform a `flutter clean` after upgrading. + // See https://github.com/flutter/flutter/issues/128563. final File manifest = globals.fs.file(globals.fs.path.join('build', 'unit_test_assets', 'AssetManifest.bin')); if (!manifest.existsSync()) { return true; @@ -718,6 +729,17 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { return true; } } + + final File cachedFlavorFile = globals.fs.file( + globals.fs.path.join('build', 'test_cache', 'flavor.txt'), + ); + final String? cachedFlavor = cachedFlavorFile.existsSync() + ? cachedFlavorFile.readAsStringSync() + : null; + if (cachedFlavor != flavor) { + return true; + } + return false; } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index fa27e70499c..c08115660fe 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/async_guard.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -1170,6 +1171,59 @@ dev_dependencies: DeviceManager: () => _FakeDeviceManager([]), }); + testUsingContext('correctly considers --flavor when validating the cached asset bundle', () async { + final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0); + fs.file('vanilla.txt').writeAsStringSync('vanilla'); + fs.file('flavorless.txt').writeAsStringSync('flavorless'); + fs.file('pubspec.yaml').writeAsStringSync(''' +flutter: + assets: + - path: vanilla.txt + flavors: + - vanilla + - flavorless.txt +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter'''); + final TestCommand testCommand = TestCommand(testRunner: testRunner); + final CommandRunner commandRunner = createTestCommandRunner(testCommand); + + const List buildArgsFlavorless = [ + 'test', + '--no-pub', + ]; + + const List buildArgsVanilla = [ + 'test', + '--no-pub', + '--flavor', + 'vanilla', + ]; + + final File builtVanillaAssetFile = fs.file( + fs.path.join('build', 'unit_test_assets', 'vanilla.txt'), + ); + final File builtFlavorlessAssetFile = fs.file( + fs.path.join('build', 'unit_test_assets', 'flavorless.txt'), + ); + + await commandRunner.run(buildArgsVanilla); + await commandRunner.run(buildArgsFlavorless); + + expect(builtVanillaAssetFile, isNot(exists)); + expect(builtFlavorlessAssetFile, exists); + + await commandRunner.run(buildArgsVanilla); + + expect(builtVanillaAssetFile, exists); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.empty(), + DeviceManager: () => _FakeDeviceManager([]), + }); + testUsingContext("Don't build the asset manifest if --no-test-assets if informed", () async { final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0); -- Gitee From 1e4348610447cf4efa4bc49603ba75d28bb0c36f Mon Sep 17 00:00:00 2001 From: Francisco Nicola Date: Mon, 1 Jul 2024 18:28:19 -0300 Subject: [PATCH 10/13] [CP] Fix Linux numpad shortcuts on web (#150723) Cherry pick request of https://github.com/flutter/flutter/pull/148988 to stable. Fixes an issue on Web+Linux that prevents users from inputting data using the numpad. --- .../default_text_editing_shortcuts.dart | 15 +++- .../default_text_editing_shortcuts_test.dart | 80 ++++++++++++++++++- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart b/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart index 3ffc0d2c83a..28640116abd 100644 --- a/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart +++ b/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart @@ -532,7 +532,20 @@ class DefaultTextEditingShortcuts extends StatelessWidget { Map? _getDisablingShortcut() { if (kIsWeb) { - return _webDisablingTextShortcuts; + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + return { + ..._webDisablingTextShortcuts, + for (final ShortcutActivator activator in _linuxNumpadShortcuts.keys) + activator as SingleActivator: const DoNothingAndStopPropagationTextIntent(), + }; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return _webDisablingTextShortcuts; + } } switch (defaultTargetPlatform) { case TargetPlatform.android: diff --git a/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart b/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart index 96b4c9c5353..3c5b2b971eb 100644 --- a/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart +++ b/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart @@ -487,8 +487,8 @@ void main() { }, variant: macOSOnly); }, skip: kIsWeb); // [intended] specific tests target non-web. - group('Linux does accept numpad shortcuts', () { - testWidgets('when numlock is locked', (WidgetTester tester) async { + group('Linux numpad shortcuts', () { + testWidgets('are triggered when numlock is locked', (WidgetTester tester) async { final FocusNode editable = FocusNode(); addTearDown(editable.dispose); final FocusNode spy = FocusNode(); @@ -577,7 +577,7 @@ void main() { expect((state.lastIntent! as DeleteToNextWordBoundaryIntent).forward, true); }, variant: TargetPlatformVariant.only(TargetPlatform.linux)); - testWidgets('when numlock is unlocked', (WidgetTester tester) async { + testWidgets('are triggered when numlock is unlocked', (WidgetTester tester) async { final FocusNode editable = FocusNode(); addTearDown(editable.dispose); final FocusNode spy = FocusNode(); @@ -664,7 +664,79 @@ void main() { expect(state.lastIntent, isA()); expect((state.lastIntent! as DeleteToNextWordBoundaryIntent).forward, true); }, variant: TargetPlatformVariant.only(TargetPlatform.linux)); - }, skip: kIsWeb); // [intended] specific tests target non-web. + + testWidgets('update the editable text content when triggered on non-web', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + final TextEditingController controller = TextEditingController(text: 'Flutter'); + addTearDown(controller.dispose); + + await tester.pumpWidget(MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: EditableText( + controller: controller, + autofocus: true, + focusNode: focusNode, + style: const TextStyle(fontSize: 10.0), + cursorColor: Colors.blue, + backgroundCursorColor: Colors.grey, + ), + ), + )); + + // Verify that NumLock is unlocked. + expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse); + + await tester.enterText(find.byType(EditableText), 'Flutter'); + expect(controller.selection.end, 7); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad4)); + // Verify the cursor moved to the left (numpad4). + expect(controller.selection.end, 6); + }, + variant: TargetPlatformVariant.only(TargetPlatform.linux), + skip: kIsWeb, // [intended] Non-web test. + ); + + testWidgets('do not update the editable text content when triggered on web', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + final TextEditingController controller = TextEditingController(text: 'Flutter'); + addTearDown(controller.dispose); + + await tester.pumpWidget(MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: EditableText( + controller: controller, + autofocus: true, + focusNode: focusNode, + style: const TextStyle(fontSize: 10.0), + cursorColor: Colors.blue, + backgroundCursorColor: Colors.grey, + ), + ), + )); + + // Verify that NumLock is unlocked. + expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse); + + await tester.enterText(find.byType(EditableText), 'Flutter'); + expect(controller.selection.end, 7); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad4)); + // On web, the editable text would have been updated by the browser. + // In the flutter test environment, the browser logic is not called + // so the editable content is not updated when a shortcut is triggered. + // This is the intended result, this test is checking that numpad shortcuts + // have no effect on the web (their intent is set to DoNothingAndStopPropagationTextIntent). + expect(controller.selection.end, 7); + }, + variant: TargetPlatformVariant.only(TargetPlatform.linux), + skip: !kIsWeb, // [intended] Web only. + ); + }); } class ActionSpy extends StatefulWidget { -- Gitee From 3c3db843fbedce606a5b17344713b820b40d3847 Mon Sep 17 00:00:00 2001 From: flutteractionsbot <154381524+flutteractionsbot@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:36:05 -0700 Subject: [PATCH 11/13] [CP-stable][tool] make `ErrorHandlingFileSystem.deleteIfExists` catch error code 3 (`ERROR_PATH_NOT_FOUND` on Windows) (#150787) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? https://github.com/flutter/flutter/issues/150736 ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples Fixes some CLI tool crashes due to trying to delete a file that no longer exists. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) When running certain commands, such as `flutter run` or `flutter build`, users get a lengthy crash message including the full contents of a FileSystemException. ### Workaround: Is there a workaround for this issue? Attempt to run the command again, or try again after running `flutter clean`. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? N/A. This issue is due to races within the user's file system (a file getting deleted or quarantined after being confirmed to exist but before the tool could delete it. --- .../lib/src/base/error_handling_io.dart | 26 ++++++++++++------- .../base/error_handling_io_test.dart | 26 ++++++++++++++++--- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/error_handling_io.dart b/packages/flutter_tools/lib/src/base/error_handling_io.dart index 468bfbf7057..072bd5da4a0 100644 --- a/packages/flutter_tools/lib/src/base/error_handling_io.dart +++ b/packages/flutter_tools/lib/src/base/error_handling_io.dart @@ -21,9 +21,13 @@ import 'platform.dart'; // ToolExit and a message that is more clear than the FileSystemException by // itself. -/// On windows this is error code 2: ERROR_FILE_NOT_FOUND, and on +/// On Windows this is error code 2: ERROR_FILE_NOT_FOUND, and on /// macOS/Linux it is error code 2/ENOENT: No such file or directory. -const int kSystemCannotFindFile = 2; +const int kSystemCodeCannotFindFile = 2; + +/// On Windows this error is 3: ERROR_PATH_NOT_FOUND, and on +/// macOS/Linux, it is error code 3/ESRCH: No such process. +const int kSystemCodePathNotFound = 3; /// A [FileSystem] that throws a [ToolExit] on certain errors. /// @@ -72,22 +76,26 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem { /// This method should be preferred to checking if it exists and /// then deleting, because it handles the edge case where the file or directory /// is deleted by a different program between the two calls. - static bool deleteIfExists(FileSystemEntity file, {bool recursive = false}) { - if (!file.existsSync()) { + static bool deleteIfExists(FileSystemEntity entity, {bool recursive = false}) { + if (!entity.existsSync()) { return false; } try { - file.deleteSync(recursive: recursive); + entity.deleteSync(recursive: recursive); } on FileSystemException catch (err) { // Certain error codes indicate the file could not be found. It could have // been deleted by a different program while the tool was running. // if it still exists, the file likely exists on a read-only volume. - if (err.osError?.errorCode != kSystemCannotFindFile || _noExitOnFailure) { + // This check will falsely match "3/ESRCH: No such process" on Linux/macOS, + // but this should be fine since this code should never come up here. + final bool codeCorrespondsToPathOrFileNotFound = err.osError?.errorCode == kSystemCodeCannotFindFile || + err.osError?.errorCode == kSystemCodePathNotFound; + if (!codeCorrespondsToPathOrFileNotFound || _noExitOnFailure) { rethrow; } - if (file.existsSync()) { + if (entity.existsSync()) { throwToolExit( - 'The Flutter tool tried to delete the file or directory ${file.path} but was ' + 'The Flutter tool tried to delete the file or directory ${entity.path} but was ' "unable to. This may be due to the file and/or project's location on a read-only " 'volume. Consider relocating the project and trying again', ); @@ -104,7 +112,7 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem { return _runSync(() => directory(delegate.currentDirectory), platform: _platform); } on FileSystemException catch (err) { // Special handling for OS error 2 for current directory only. - if (err.osError?.errorCode == kSystemCannotFindFile) { + if (err.osError?.errorCode == kSystemCodeCannotFindFile) { throwToolExit( 'Unable to read current working directory. This can happen if the directory the ' 'Flutter tool was run from was moved or deleted.' diff --git a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart index ef2752b6047..f6a56a58d6a 100644 --- a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart +++ b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart @@ -98,6 +98,24 @@ void main() { }, throwsFileSystemException()); }); + testWithoutContext('deleteIfExists throws tool exit if the path is not found on Windows', () { + final FileExceptionHandler exceptionHandler = FileExceptionHandler(); + final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem( + delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle), + platform: windowsPlatform, + ); + final File file = fileSystem.file(fileSystem.path.join('directory', 'file')) + ..createSync(recursive: true); + + exceptionHandler.addError( + file, + FileSystemOp.delete, + FileSystemException('', file.path, const OSError('', 2)), + ); + + expect(() => ErrorHandlingFileSystem.deleteIfExists(file), throwsToolExit()); + }); + group('throws ToolExit on Windows', () { const int kDeviceFull = 112; const int kUserMappedSectionOpened = 1224; @@ -571,14 +589,14 @@ void main() { testWithoutContext('When the current working directory disappears', () async { final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem( - delegate: ThrowsOnCurrentDirectoryFileSystem(kSystemCannotFindFile), + delegate: ThrowsOnCurrentDirectoryFileSystem(kSystemCodeCannotFindFile), platform: linuxPlatform, ); expect(() => fileSystem.currentDirectory, throwsToolExit(message: 'Unable to read current working directory')); }); - testWithoutContext('Rethrows os error $kSystemCannotFindFile', () { + testWithoutContext('Rethrows os error $kSystemCodeCannotFindFile', () { final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem( delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle), platform: linuxPlatform, @@ -588,11 +606,11 @@ void main() { exceptionHandler.addError( file, FileSystemOp.read, - FileSystemException('', file.path, const OSError('', kSystemCannotFindFile)), + FileSystemException('', file.path, const OSError('', kSystemCodeCannotFindFile)), ); // Error is not caught by other operations. - expect(() => fileSystem.file('foo').readAsStringSync(), throwsFileSystemException(kSystemCannotFindFile)); + expect(() => fileSystem.file('foo').readAsStringSync(), throwsFileSystemException(kSystemCodeCannotFindFile)); }); }); -- Gitee From ac58c1301d25d5e5f2fa679d221830cb45dd9fad Mon Sep 17 00:00:00 2001 From: Andrew Kolos Date: Mon, 1 Jul 2024 14:38:03 -0700 Subject: [PATCH 12/13] [CP-stable] Upgrade dependency on package:unified_analytics from 5.8.8+1 to 5.8.8+2 (#151046) Resolves https://github.com/flutter/flutter/issues/150137 (the issue is already resolved in master with the use of package:unified_analytics:6.1.2). Patches the fixes from [unified_analytics:5.8.8+2](https://github.com/dart-lang/tools/pull/280) to stable. --- packages/flutter_tools/pubspec.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 528ae91f69d..88b03dc399f 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -54,7 +54,7 @@ dependencies: async: 2.11.0 # this dependence use to parse json5 config file json5: ^0.8.0 - unified_analytics: 5.8.8+1 + unified_analytics: 5.8.8+2 cli_config: 0.2.0 graphs: 2.3.1 @@ -122,5 +122,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# TODO: fix checksum -# PUBSPEC CHECKSUM: 067f +# PUBSPEC CHECKSUM: d480 -- Gitee From 33e4ac18cafa94dc3728c6f4ff5fc9455d91f931 Mon Sep 17 00:00:00 2001 From: gengfei Date: Mon, 2 Dec 2024 14:48:46 +0800 Subject: [PATCH 13/13] [ohos 3.22.3] Upgrade adaptation for Flutter 3.22.3 : fix switch of defaultTargetPlatform --- .../flutter/lib/src/widgets/default_text_editing_shortcuts.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart b/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart index 28640116abd..e9890a1a751 100644 --- a/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart +++ b/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart @@ -544,6 +544,7 @@ class DefaultTextEditingShortcuts extends StatelessWidget { case TargetPlatform.windows: case TargetPlatform.iOS: case TargetPlatform.macOS: + case TargetPlatform.ohos: return _webDisablingTextShortcuts; } } -- Gitee