[flutter_releases] Flutter Stable 2.2.2 Framework Cherrypicks (#84364)
* [flutter_tools] throw a tool exit if pub cannot be run (#83293)
* Re-add the removed MediaQuery.removePadding of PopupMenuButton (#82986)
* import pkg:intl when DateFormat or NumberFormat is used (#83122)
Co-authored-by: Jonah Williams <jonahwilliams@google.com>
Co-authored-by: xubaolin <xubaolin@oppo.com>
Co-authored-by: Michael Goderbauer <goderbauer@google.com>
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 1414ce1..8e7ff4c 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-0fdb562ac8068ce3dda6b69aca3f355f4d1d2718
+91c9fc8fe011352879e3bb6660966eafc0847233
diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart
index cf7feb6..a54f207 100644
--- a/packages/flutter/lib/src/material/popup_menu.dart
+++ b/packages/flutter/lib/src/material/popup_menu.dart
@@ -618,8 +618,7 @@
this.itemSizes,
this.selectedItemIndex,
this.textDirection,
- this.topPadding,
- this.bottomPadding,
+ this.padding,
);
// Rectangle of underlying button, relative to the overlay's dimensions.
@@ -636,11 +635,8 @@
// Whether to prefer going to the left or to the right.
final TextDirection textDirection;
- // Top padding of unsafe area.
- final double topPadding;
-
- // Bottom padding of unsafe area.
- final double bottomPadding;
+ // The padding of unsafe area.
+ EdgeInsets padding;
// We put the child wherever position specifies, so long as it will fit within
// the specified parent size padded (inset) by 8. If necessary, we adjust the
@@ -651,7 +647,8 @@
// The menu can be at most the size of the overlay minus 8.0 pixels in each
// direction.
return BoxConstraints.loose(constraints.biggest).deflate(
- const EdgeInsets.all(_kMenuScreenPadding) + EdgeInsets.only(top: topPadding, bottom: bottomPadding));
+ const EdgeInsets.all(_kMenuScreenPadding) + padding,
+ );
}
@override
@@ -694,14 +691,15 @@
// Avoid going outside an area defined as the rectangle 8.0 pixels from the
// edge of the screen in every direction.
- if (x < _kMenuScreenPadding)
- x = _kMenuScreenPadding;
- else if (x + childSize.width > size.width - _kMenuScreenPadding)
- x = size.width - childSize.width - _kMenuScreenPadding;
- if (y < _kMenuScreenPadding + topPadding)
- y = _kMenuScreenPadding + topPadding;
- else if (y + childSize.height > size.height - _kMenuScreenPadding - bottomPadding)
- y = size.height - bottomPadding - _kMenuScreenPadding - childSize.height ;
+ if (x < _kMenuScreenPadding + padding.left)
+ x = _kMenuScreenPadding + padding.left;
+ else if (x + childSize.width > size.width - _kMenuScreenPadding - padding.right)
+ x = size.width - childSize.width - _kMenuScreenPadding - padding.right ;
+ if (y < _kMenuScreenPadding + padding.top)
+ y = _kMenuScreenPadding + padding.top;
+ else if (y + childSize.height > size.height - _kMenuScreenPadding - padding.bottom)
+ y = size.height - padding.bottom - _kMenuScreenPadding - childSize.height ;
+
return Offset(x, y);
}
@@ -716,8 +714,7 @@
|| selectedItemIndex != oldDelegate.selectedItemIndex
|| textDirection != oldDelegate.textDirection
|| !listEquals(itemSizes, oldDelegate.itemSizes)
- || topPadding != oldDelegate.topPadding
- || bottomPadding != oldDelegate.bottomPadding;
+ || padding != oldDelegate.padding;
}
}
@@ -777,22 +774,27 @@
}
final Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
-
- return Builder(
- builder: (BuildContext context) {
- final MediaQueryData mediaQuery = MediaQuery.of(context);
- return CustomSingleChildLayout(
- delegate: _PopupMenuRouteLayout(
- position,
- itemSizes,
- selectedItemIndex,
- Directionality.of(context),
- mediaQuery.padding.top,
- mediaQuery.padding.bottom,
- ),
- child: capturedThemes.wrap(menu),
- );
- },
+ final MediaQueryData mediaQuery = MediaQuery.of(context);
+ return MediaQuery.removePadding(
+ context: context,
+ removeTop: true,
+ removeBottom: true,
+ removeLeft: true,
+ removeRight: true,
+ child: Builder(
+ builder: (BuildContext context) {
+ return CustomSingleChildLayout(
+ delegate: _PopupMenuRouteLayout(
+ position,
+ itemSizes,
+ selectedItemIndex,
+ Directionality.of(context),
+ mediaQuery.padding,
+ ),
+ child: capturedThemes.wrap(menu),
+ );
+ },
+ ),
);
}
}
diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart
index 19349d7..10b44a0 100644
--- a/packages/flutter/test/material/popup_menu_test.dart
+++ b/packages/flutter/test/material/popup_menu_test.dart
@@ -2058,6 +2058,79 @@
expect(popupMenu, Offset(button.dx - 8.0, button.dy + 8.0));
});
+ // Regression test for https://github.com/flutter/flutter/issues/82874
+ testWidgets('PopupMenu position test when have unsafe area - left/right padding', (WidgetTester tester) async {
+ final GlobalKey buttonKey = GlobalKey();
+ const EdgeInsets padding = EdgeInsets.only(left: 300.0, top: 32.0, right: 310.0, bottom: 64.0);
+ EdgeInsets? mediaQueryPadding;
+
+ Widget buildFrame(double width, double height) {
+ return MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ data: const MediaQueryData(
+ padding: padding,
+ ),
+ child: child!,
+ );
+ },
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('PopupMenu Test'),
+ actions: <Widget>[PopupMenuButton<int>(
+ child: SizedBox(
+ key: buttonKey,
+ height: height,
+ width: width,
+ child: const ColoredBox(
+ color: Colors.pink,
+ ),
+ ),
+ itemBuilder: (BuildContext context) {
+ return <PopupMenuEntry<int>>[
+ PopupMenuItem<int>(
+ value: 1,
+ child: Builder(
+ builder: (BuildContext context) {
+ mediaQueryPadding = MediaQuery.of(context).padding;
+ return Text('-1-' * 500); // A long long text string.
+ },
+ ),
+ ),
+ const PopupMenuItem<int>(value: 2, child: Text('-2-')),
+ ];
+ },
+ )],
+ ),
+ body: const SizedBox.shrink(),
+ ),
+ );
+ }
+
+ await tester.pumpWidget(buildFrame(20.0, 20.0));
+
+ await tester.tap(find.byKey(buttonKey));
+ await tester.pumpAndSettle();
+
+ final Offset button = tester.getTopRight(find.byKey(buttonKey));
+ expect(button, Offset(800.0 - padding.right, padding.top)); // The topPadding is 32.0.
+
+ final Offset popupMenuTopRight = tester.getTopRight(find.byType(SingleChildScrollView));
+
+ // The menu should be positioned directly next to the top of the button.
+ // The 8.0 pixels is [_kMenuScreenPadding].
+ expect(popupMenuTopRight, Offset(800.0 - padding.right - 8.0, padding.top + 8.0));
+
+ final Offset popupMenuTopLeft = tester.getTopLeft(find.byType(SingleChildScrollView));
+ expect(popupMenuTopLeft, Offset(padding.left + 8.0, padding.top + 8.0));
+
+ final Offset popupMenuBottomLeft = tester.getBottomLeft(find.byType(SingleChildScrollView));
+ expect(popupMenuBottomLeft, Offset(padding.left + 8.0, 600.0 - padding.bottom - 8.0));
+
+ // The `MediaQueryData.padding` should be removed.
+ expect(mediaQueryPadding, EdgeInsets.zero);
+ });
+
group('feedback', () {
late FeedbackTester feedback;
diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart
index f48658e..f9d358a 100644
--- a/packages/flutter_tools/lib/src/dart/pub.dart
+++ b/packages/flutter_tools/lib/src/dart/pub.dart
@@ -151,7 +151,8 @@
_processUtils = ProcessUtils(
logger: logger,
processManager: processManager,
- );
+ ),
+ _processManager = processManager;
final FileSystem _fileSystem;
final Logger _logger;
@@ -159,6 +160,7 @@
final Platform _platform;
final BotDetector _botDetector;
final Usage _usage;
+ final ProcessManager _processManager;
@override
Future<void> get({
@@ -393,11 +395,15 @@
'cache',
'dart-sdk',
'bin',
- if (_platform.isWindows)
- 'pub.bat'
- else
- 'pub'
+ 'pub',
]);
+ if (!_processManager.canRun(sdkPath)) {
+ throwToolExit(
+ 'Your Flutter SDK download may be corrupt or missing permissions to run. '
+ 'Try re-downloading the Flutter SDK into a directory that has read/write '
+ 'permissions for the current user.'
+ );
+ }
return <String>[sdkPath, ...arguments];
}
diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
index aeaec77..092c1b0 100644
--- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
+++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
@@ -1029,7 +1029,7 @@
.replaceAll('@(class)', '$className${locale.camelCase()}')
.replaceAll('@(localeName)', locale.toString())
.replaceAll('@(methods)', methods.join('\n\n'))
- .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '');
+ .replaceAll('@(requiresIntlImport)', _requiresIntlImport() ? "import 'package:intl/intl.dart' as intl;" : '');
}
String _generateSubclass(
@@ -1170,12 +1170,12 @@
.replaceAll('@(messageClassImports)', sortedClassImports.join('\n'))
.replaceAll('@(delegateClass)', delegateClass)
.replaceAll('@(requiresFoundationImport)', _useDeferredLoading ? '' : "import 'package:flutter/foundation.dart';")
- .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '')
+ .replaceAll('@(requiresIntlImport)', _requiresIntlImport() ? "import 'package:intl/intl.dart' as intl;" : '')
.replaceAll('@(canBeNullable)', _usesNullableGetter ? '?' : '')
.replaceAll('@(needsNullCheck)', _usesNullableGetter ? '' : '!');
}
- bool _containsPluralMessage() => _allMessages.any((Message message) => message.isPlural);
+ bool _requiresIntlImport() => _allMessages.any((Message message) => message.isPlural || message.placeholdersRequireFormatting);
void writeOutputFiles(Logger logger, { bool isFromYaml = false }) {
// First, generate the string contents of all necessary files.
diff --git a/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart b/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart
index 3fe2d05..f1c9051 100644
--- a/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart
+++ b/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart
@@ -26,6 +26,31 @@
Cache.flutterRoot = '';
});
+ testWithoutContext('Throws a tool exit if pub cannot be run', () async {
+ final FakeProcessManager processManager = FakeProcessManager.any();
+ final BufferLogger logger = BufferLogger.test();
+ final MemoryFileSystem fileSystem = MemoryFileSystem.test();
+ processManager.excludedExecutables.add('bin/cache/dart-sdk/bin/pub');
+
+ fileSystem.file('pubspec.yaml').createSync();
+
+ final Pub pub = Pub(
+ fileSystem: fileSystem,
+ logger: logger,
+ processManager: processManager,
+ usage: TestUsage(),
+ platform: FakePlatform(
+ environment: const <String, String>{},
+ ),
+ botDetector: const BotDetectorAlwaysNo(),
+ );
+
+ await expectLater(() => pub.get(
+ context: PubContext.pubGet,
+ checkUpToDate: true,
+ ), throwsToolExit(message: 'Your Flutter SDK download may be corrupt or missing permissions to run'));
+ });
+
testWithoutContext('checkUpToDate skips pub get if the package config is newer than the pubspec '
'and the current framework version is the same as the last version', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[]);
@@ -717,5 +742,8 @@
}
@override
+ bool canRun(dynamic executable, {String workingDirectory}) => true;
+
+ @override
dynamic noSuchMethod(Invocation invocation) => null;
}
diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
index 28c8f33..9efa893 100644
--- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
+++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
@@ -1568,6 +1568,42 @@
});
group('DateTime tests', () {
+ testUsingContext('imports package:intl', () {
+ const String singleDateMessageArbFileString = '''
+{
+ "@@locale": "en",
+ "springBegins": "Spring begins on {springStartDate}",
+ "@springBegins": {
+ "description": "The first day of spring",
+ "placeholders": {
+ "springStartDate": {
+ "type": "DateTime",
+ "format": "yMd"
+ }
+ }
+ }
+}''';
+ fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+ ..childFile(defaultTemplateArbFileName).writeAsStringSync(singleDateMessageArbFileString);
+
+ LocalizationsGenerator(
+ fs,
+ )
+ ..initialize(
+ inputPathString: defaultL10nPathString,
+ outputPathString: defaultL10nPathString,
+ templateArbFileName: defaultTemplateArbFileName,
+ outputFileString: defaultOutputFileString,
+ classNameString: defaultClassNameString)
+ ..loadResources()
+ ..writeOutputFiles(BufferLogger.test());
+
+ final String localizationsFile = fs.file(
+ fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
+ ).readAsStringSync();
+ expect(localizationsFile, contains(intlImportDartCode));
+ });
+
testUsingContext('throws an exception when improperly formatted date is passed in', () {
const String singleDateMessageArbFileString = '''
{
@@ -1644,6 +1680,45 @@
fail('Improper date formatting should throw an exception');
});
+ });
+
+ group('NumberFormat tests', () {
+ testUsingContext('imports package:intl', () {
+ const String singleDateMessageArbFileString = '''
+{
+ "courseCompletion": "You have completed {progress} of the course.",
+ "@courseCompletion": {
+ "description": "The amount of progress the student has made in their class.",
+ "placeholders": {
+ "progress": {
+ "type": "double",
+ "format": "percentPattern"
+ }
+ }
+ }
+}''';
+ fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
+ ..createSync(recursive: true)
+ ..childFile(defaultTemplateArbFileName).writeAsStringSync(
+ singleDateMessageArbFileString);
+
+ LocalizationsGenerator(
+ fs,
+ )
+ ..initialize(
+ inputPathString: defaultL10nPathString,
+ outputPathString: defaultL10nPathString,
+ templateArbFileName: defaultTemplateArbFileName,
+ outputFileString: defaultOutputFileString,
+ classNameString: defaultClassNameString)
+ ..loadResources()
+ ..writeOutputFiles(BufferLogger.test());
+
+ final String localizationsFile = fs.file(
+ fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
+ ).readAsStringSync();
+ expect(localizationsFile, contains(intlImportDartCode));
+ });
testUsingContext('throws an exception when improperly formatted number is passed in', () {
const String singleDateMessageArbFileString = '''