diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 21aae04dd65f..1623836863db 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -82d96ef98a33f3f35dabf7795e701f8a4d2d4bec +fb03253e32ce6aba92872ed9c1224e999ec6abcb diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version index ad241bc2bef7..cab742975b85 100644 --- a/.ci/flutter_stable.version +++ b/.ci/flutter_stable.version @@ -1 +1 @@ -2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa +db50e20168db8fee486b9abf32fc912de3bc5b6a diff --git a/.github/workflows/release_from_branches.yml b/.github/workflows/release_from_branches.yml index a235a2576ef3..5286cc18e041 100644 --- a/.github/workflows/release_from_branches.yml +++ b/.github/workflows/release_from_branches.yml @@ -5,7 +5,7 @@ on: - 'release-go_router' jobs: release: - uses: ./.github/workflows/resuable_release.yml + uses: ./.github/workflows/reusable_release.yml with: is-batch-release: true branch-name: '${{ github.ref_name }}' diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index a49a588717fc..000000000000 --- a/CODEOWNERS +++ /dev/null @@ -1,127 +0,0 @@ -# Below is a list of Flutter team members who are suggested reviewers -# for contributions to packages in this repository. -# -# These names are just suggestions. It is fine to have your changes -# reviewed by someone else. - -packages/animations/** @hannah-hyj -packages/camera/** @bparrishMines -packages/cross_file/** @stuartmorgan-g -packages/cupertino_ui/** @dkwingsmt -packages/extension_google_sign_in_as_googleapis_auth/** @stuartmorgan-g -packages/file_selector/** @stuartmorgan-g -packages/flutter_lints/** @chunhtai -packages/flutter_template_images/** @stuartmorgan-g -packages/go_router/** @chunhtai -packages/go_router_builder/** @chunhtai -packages/google_adsense/** @sokoloff06 @ditman -packages/google_identity_services_web/** @mdebbar -packages/google_fonts/** @Piinks -packages/google_maps_flutter/** @stuartmorgan-g -packages/google_sign_in/** @stuartmorgan-g -packages/image_picker/** @tarrinneal -packages/interactive_media_ads/** @bparrishMines -packages/in_app_purchase/** @bparrishMines -packages/local_auth/** @stuartmorgan-g -packages/material_ui/** @qunccccccc -packages/metrics_center/** @bkonyi -packages/multicast_dns/** @vashworth -packages/path_provider/** @stuartmorgan-g -packages/pigeon/** @tarrinneal -packages/platform/** @stuartmorgan-g -packages/plugin_platform_interface/** @stuartmorgan-g -packages/pointer_interceptor/** @ditman -packages/quick_actions/** @bparrishMines -packages/rfw/** @Hixie -packages/shared_preferences/** @tarrinneal -packages/standard_message_codec/** @stuartmorgan-g -packages/two_dimensional_scrollables/** @Piinks -packages/url_launcher/** @stuartmorgan-g -packages/vector_graphics/** @jtmcdole -packages/vector_graphics_codec/** @jtmcdole -packages/vector_graphics_compiler/** @jtmcdole -packages/video_player/** @tarrinneal -packages/web_benchmarks/** @yjbanov -packages/webview_flutter/** @bparrishMines -packages/xdg_directories/** @stuartmorgan-g -third_party/packages/cupertino_icons/** @victorsanni -third_party/packages/cupertino_icons/test/goldens/** @LongCatIsLooong -third_party/packages/flutter_svg/** @domesticmouse -third_party/packages/flutter_svg_test/** @domesticmouse -third_party/packages/mustache_template/** @bkonyi @parlough -third_party/packages/path_parsing/** @domesticmouse - -# Plugin platform implementation rules. These should stay last, since the last -# matching entry takes precedence. - -# - Web -packages/camera/camera_web/** @mdebbar -packages/file_selector/file_selector_web/** @mdebbar -packages/google_maps_flutter/google_maps_flutter_web/** @mdebbar -packages/google_sign_in/google_sign_in_web/** @mdebbar -packages/image_picker/image_picker_for_web/** @mdebbar -packages/pointer_interceptor/pointer_interceptor_web/** @mdebbar -packages/shared_preferences/shared_preferences_web/** @mdebbar -packages/url_launcher/url_launcher_web/** @mdebbar -packages/video_player/video_player_web/** @mdebbar -packages/webview_flutter/webview_flutter_web/** @mdebbar - -# - Android -packages/camera/camera_android/** @camsim99 -packages/camera/camera_android_camerax/** @camsim99 -packages/espresso/** @jesswrd -packages/file_selector/file_selector_android/** @mboetger -packages/flutter_plugin_android_lifecycle/** @reidbaker -packages/google_maps_flutter/google_maps_flutter_android/** @reidbaker -packages/google_sign_in/google_sign_in_android/** @reidbaker -packages/image_picker/image_picker_android/** @gmackall -packages/in_app_purchase/in_app_purchase_android/** @gmackall -packages/local_auth/local_auth_android/** @mboetger -packages/path_provider/path_provider_android/** @camsim99 -packages/quick_actions/quick_actions_android/** @jesswrd -packages/shared_preferences/shared_preferences_android/** @jesswrd -packages/url_launcher/url_launcher_android/** @gmackall -packages/video_player/video_player_android/** @mboetger -# Owned by ecosystem team for now during the wrapper evaluation. -packages/webview_flutter/webview_flutter_android/** @bparrishMines - -# - Darwin -packages/camera/camera_avfoundation/** @hellohuanlin @louisehsu -packages/file_selector/file_selector_ios/** @okorohelijah @vashworth -packages/file_selector/file_selector_macos/** @okorohelijah @vashworth -packages/google_maps_flutter/google_maps_flutter_ios/** @vashworth @LongCatIsLooong -packages/google_maps_flutter/google_maps_flutter_ios_sdk9/** @vashworth @LongCatIsLooong -packages/google_maps_flutter/google_maps_flutter_ios_sdk10/** @vashworth @LongCatIsLooong -packages/google_sign_in/google_sign_in_ios/** @LongCatIsLooong @okorohelijah -packages/image_picker/image_picker_ios/** @okorohelijah @vashworth -packages/image_picker/image_picker_macos/** @okorohelijah @vashworth -packages/in_app_purchase/in_app_purchase_storekit/** @louisehsu @LongCatIsLooong -packages/local_auth/local_auth_darwin/** @louisehsu @okorohelijah -packages/path_provider/path_provider_foundation/** @LongCatIsLooong @vashworth -packages/pointer_interceptor/pointer_interceptor_ios/** @louisehsu @hellohuanlin -packages/quick_actions/quick_actions_ios/** @louisehsu @LongCatIsLooong -packages/shared_preferences/shared_preferences_foundation/** @tarrinneal -packages/url_launcher/url_launcher_ios/** @vashworth @LongCatIsLooong -packages/url_launcher/url_launcher_macos/** @vashworth @LongCatIsLooong -packages/video_player/video_player_avfoundation/** @hellohuanlin @louisehsu -packages/webview_flutter/webview_flutter_wkwebview/** @LongCatIsLooong @hellohuanlin - -# - Linux -packages/file_selector/file_selector_linux/** @robert-ancell @stuartmorgan-g -packages/image_picker/image_picker_linux/** @robert-ancell @stuartmorgan-g -packages/path_provider/path_provider_linux/** @robert-ancell @stuartmorgan-g -packages/shared_preferences/shared_preferences_linux/** @robert-ancell @stuartmorgan-g -packages/url_launcher/url_launcher_linux/** @robert-ancell @stuartmorgan-g - -# - Windows -packages/camera/camera_windows/** @stuartmorgan-g -packages/file_selector/file_selector_windows/** @stuartmorgan-g -packages/image_picker/image_picker_windows/** @stuartmorgan-g -packages/local_auth/local_auth_windows/** @stuartmorgan-g -packages/path_provider/path_provider_windows/** @stuartmorgan-g -packages/shared_preferences/shared_preferences_windows/** @stuartmorgan-g -packages/url_launcher/url_launcher_windows/** @stuartmorgan-g - -# - DevTools extensions -# @adsonpleal is the actual maintainer of shared_preferences_tool but is not yet a committer, so can't be listed as the owner. -packages/shared_preferences/shared_preferences_tool/** @tarrinneal diff --git a/SUGGESTED_REVIEWERS.md b/SUGGESTED_REVIEWERS.md new file mode 100644 index 000000000000..ad9a954e7683 --- /dev/null +++ b/SUGGESTED_REVIEWERS.md @@ -0,0 +1,187 @@ +Below is a list of Flutter team members who are suggested reviewers +for contributions to packages in this repository. + +These names are just suggestions. It is fine to have your changes +reviewed by someone else. + +`animations`: + - @hannah-hyj + +`camera`: + - **Cross-platform**: @bparrishMines + - **Android**: @camsim99 + - **iOS**: @hellohuanlin, @louisehsu + - **Web**: @mdebbar + - **Windows**: @stuartmorgan-g + +`cross_file`: + - @stuartmorgan-g + +`cupertino_icons`: + - @victorsanni + +`cupertino_ui`: + - @dkwingsmt + +`espresso`: + - @jesswrd + +`extension_google_sign_in_as_googleapis_auth`: + - @stuartmorgan-g + +`file_selector`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @mboetger + - **iOS**: @okorohelijah, @vashworth + - **Linux**: @robert-ancell, @stuartmorgan-g + - **macOS**: @okorohelijah, @vashworth + - **Web**: @mdebbar + - **Windows**: @stuartmorgan-g + +`flutter_lints`: + - @chunhtai + +`flutter_plugin_android_lifecycle`: + - @reidbaker + +`flutter_svg, flutter_svg_test`: + - @domesticmouse + +`flutter_template_images`: + - @stuartmorgan-g + +`go_router / go_router_builder`: + - @chunhtai + +`google_adsense`: + - @sokoloff06, @ditman + +`google_identity_services_web`: + - @mdebbar + +`google_fonts`: + - @Piinks + +`google_maps_flutter`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @reidbaker + - **iOS**: @vashworth, @LongCatIsLooong + - **Web**: @mdebbar + +`google_sign_in`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @reidbaker + - **iOS**: @LongCatIsLooong, @okorohelijah + - **Web**: @mdebbar + +`image_picker`: + - **Cross-platform**: @tarrinneal + - **Android**: @gmackall + - **iOS**: @okorohelijah, @vashworth + - **Linux**: @robert-ancell, @stuartmorgan-g + - **macOS**: @okorohelijah, @vashworth + - **Web**: @mdebbar + - **Windows**: @stuartmorgan-g + +`interactive_media_ads`: + - @bparrishMines + +`in_app_purchase`: + - **Cross-platform**: @bparrishMines + - **Android**: @gmackall + - **iOS**: @louisehsu, @LongCatIsLooong + +`local_auth`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @mboetger + - **iOS/macOS**: @louisehsu, @okorohelijah + - **Windows**: @stuartmorgan-g + +`material_ui`: + - @qunccccccc + +`metrics_center`: + - @bkonyi + +`multicast_dns`: + - @vashworth + +`mustache_template`: + - @bkonyi, @parlough + +`path_parsing`: + - @domesticmouse + +`path_provider`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @camsim99 + - **iOS/macOS**: @LongCatIsLooong, @vashworth + - **Linux**: @robert-ancell, @stuartmorgan-g + - **Windows**: @stuartmorgan-g + +`pigeon`: + - @tarrinneal + +`platform`: + - @stuartmorgan-g + +`plugin_platform_interface`: + - @stuartmorgan-g + +`pointer_interceptor`: + - **Cross-platform**: @ditman + - **iOS**: @louisehsu, @hellohuanlin + - **Web**: @mdebbar + +`quick_actions`: + - **Cross-platform**: @bparrishMines + - **Android**: @jesswrd + - **iOS**: @louisehsu, @LongCatIsLooong + +`rfw`: + - @Hixie + +`shared_preferences`: + - **Cross-platform**: @tarrinneal + - **Android**: @jesswrd + - **iOS/macOS**: @tarrinneal + - **Linux**: @robert-ancell, @stuartmorgan-g + - **Windows**: @stuartmorgan-g + - **Web**: @mdebbar + - **Devtools**: @adsonpleal + +`standard_message_codec`: + - @stuartmorgan-g + +`two_dimensional_scrollables`: + - @Piinks + +`url_launcher`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @gmackall + - **iOS**: @vashworth, @LongCatIsLooong + - **Linux**: @robert-ancell, @stuartmorgan-g + - **macOS**: @vashworth, @LongCatIsLooong + - **Windows**: @stuartmorgan-g + - **Web**: @mdebbar + +`vector_graphics, vector_graphics_codec, vector_graphics_compiler`: + - @jtmcdole + +`video_player`: + - **Cross-platform**: @tarrinneal + - **Android**: @mboetger + - **iOS/macOS**: @hellohuanlin, @louisehsu + - **Web**: @mdebbar + +`web_benchmarks`: + - @yjbanov + +`webview_flutter`: + - **Cross-platform**: @bparrishMines + - **Android**: @bparrishMines + - **iOS/macOS**: @bparrishMines, @LongCatIsLooong, @hellohuanlin + - **Web**: @mdebbar + +`xdg_directories`: + - @stuartmorgan-g diff --git a/packages/animations/CHANGELOG.md b/packages/animations/CHANGELOG.md index 6d7110f9b8ec..0779bea39e8a 100644 --- a/packages/animations/CHANGELOG.md +++ b/packages/animations/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.1.2 * Updates minimum supported SDK version to Flutter 3.35/Dart 3.9. +* Adds an example of using `OpenContainer` ## 2.1.1 diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart index 10dd2b2f5e70..4683c3597642 100644 --- a/packages/animations/lib/src/open_container.dart +++ b/packages/animations/lib/src/open_container.dart @@ -66,7 +66,46 @@ typedef ClosedCallback = void Function(S data); /// `T` refers to the type of data returned by the route when the container /// is closed. This value can be accessed in the `onClosed` function. /// -// TODO(goderbauer): Add example animations and sample code. +/// The following example shows an [OpenContainer] that transforms a blue +/// container widget into a full screen page using the Material container +/// transform animation. When the user taps the closed widget, the container +/// expands and morphs into the destination page defined in [openBuilder], +/// while the original widget from [closedBuilder] fades out during the +/// transition. +/// +/// ```dart +/// OpenContainer( +/// transitionDuration: const Duration(milliseconds: 500), +/// transitionType: ContainerTransitionType.fadeThrough, +/// openBuilder: (context, action) { +/// return Scaffold( +/// appBar: AppBar(title: const Text('Details Page')), +/// body: const Center( +/// child: Text( +/// 'This page opened with Container Transform animation', +/// style: TextStyle(fontSize: 18), +/// textAlign: TextAlign.center, +/// ), +/// ), +/// ); +/// }, +/// closedBuilder: (context, action) { +/// return Container( +/// width: 200, +/// height: 120, +/// alignment: Alignment.center, +/// decoration: BoxDecoration( +/// color: Colors.blue, +/// borderRadius: BorderRadius.circular(16), +/// ), +/// child: const Text( +/// 'Open Details', +/// style: TextStyle(color: Colors.white, fontSize: 18), +/// ), +/// ); +/// }, +/// ), +/// ``` /// /// See also: /// diff --git a/packages/animations/pubspec.yaml b/packages/animations/pubspec.yaml index 012218b3db2b..0670649c6e95 100644 --- a/packages/animations/pubspec.yaml +++ b/packages/animations/pubspec.yaml @@ -2,7 +2,7 @@ name: animations description: Fancy pre-built animations that can easily be integrated into any Flutter application. repository: https://github.com/flutter/packages/tree/main/packages/animations issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+animations%22 -version: 2.1.1 +version: 2.1.2 environment: sdk: ^3.9.0 diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index f1d95f90c9fc..20c576b4e4dc 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 17.2.0 + +- Adds `encoder`, `decoder` and `compare` parameters to `TypedQueryParameter` annotation for custom encoding, decoding and comparison of query parameters in `TypedGoRoute` constructors. + ## 17.1.0 - Adds `TypedQueryParameter` annotation to override parameter names in `TypedGoRoute` constructors. diff --git a/packages/go_router/pending_changelogs/support_custom_types.yaml b/packages/go_router/pending_changelogs/support_custom_types.yaml deleted file mode 100644 index 40240bfcae31..000000000000 --- a/packages/go_router/pending_changelogs/support_custom_types.yaml +++ /dev/null @@ -1,3 +0,0 @@ -changelog: | - - Adds `encoder`, `decoder` and `compare` parameters to `TypedQueryParameter` annotation for custom encoding, decoding and comparison of query parameters in `TypedGoRoute` constructors. -version: minor diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 55ad56b1058c..a397f17cf850 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 17.1.0 +version: 17.2.0 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index f9e9da0393ab..498ba7174dcb 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.1 + +* Adds support for analyzer 11 and 12. + ## 4.2.0 - Adds supports for `TypedQueryParameter` annotation. diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 9985c37166a4..536b9c94b813 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 4.2.0 +version: 4.2.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 @@ -11,7 +11,7 @@ environment: flutter: ">=3.35.0" dependencies: - analyzer: ">=8.2.0 <10.0.0" + analyzer: ">=8.2.0 <13.0.0" async: ^2.8.0 # TODO(piinks): Pin version once new stable rolls. build: ">=3.0.0 <5.0.0" diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index d14e07c1b4b4..368abc59a80f 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,11 @@ +## 26.3.3 + +* Updates `analyzer` dependency to support versions 10 through 12. + +## 26.3.2 + +* Updates `analyzer` dependency to support version 10. + ## 26.3.1 * Fixes dartdoc comments that accidentally used HTML. diff --git a/packages/pigeon/lib/src/generator_tools.dart b/packages/pigeon/lib/src/generator_tools.dart index 2890bb571542..604691d46d3e 100644 --- a/packages/pigeon/lib/src/generator_tools.dart +++ b/packages/pigeon/lib/src/generator_tools.dart @@ -15,7 +15,7 @@ import 'generator.dart'; /// The current version of pigeon. /// /// This must match the version in pubspec.yaml. -const String pigeonVersion = '26.3.1'; +const String pigeonVersion = '26.3.3'; /// Default plugin package name. const String defaultPluginPackageName = 'dev.flutter.pigeon'; diff --git a/packages/pigeon/lib/src/pigeon_lib_internal.dart b/packages/pigeon/lib/src/pigeon_lib_internal.dart index 75bbb96a293e..617a901f37cc 100644 --- a/packages/pigeon/lib/src/pigeon_lib_internal.dart +++ b/packages/pigeon/lib/src/pigeon_lib_internal.dart @@ -1579,7 +1579,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { _errors.add( Error( message: - 'API "${node.name.lexeme}" can only have one API annotation but contains: ${node.metadata}', + 'API "${node.namePart.typeName.lexeme}" can only have one API annotation but contains: ${node.metadata}', lineNumber: calculateLineNumber(source, node.offset), ), ); @@ -1606,7 +1606,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { } _currentApi = AstHostApi( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, methods: [], dartHostTestHandler: dartHostTestHandler, documentationComments: _documentationCommentsParser( @@ -1615,7 +1615,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { ); } else if (_hasMetadata(node.metadata, 'FlutterApi')) { _currentApi = AstFlutterApi( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, methods: [], documentationComments: _documentationCommentsParser( node.documentationComment?.tokens, @@ -1642,7 +1642,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { _errors.add( Error( message: - 'ProxyApis should either set the super class in the annotation OR use extends: ("${node.name.lexeme}").', + 'ProxyApis should either set the super class in the annotation OR use extends: ("${node.namePart.typeName.lexeme}").', lineNumber: calculateLineNumber(source, node.offset), ), ); @@ -1713,7 +1713,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { } _currentApi = AstProxyApi( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, methods: [], constructors: [], fields: [], @@ -1760,7 +1760,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { ); } _currentApi = AstEventChannelApi( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, methods: [], swiftOptions: swiftOptions, kotlinOptions: kotlinOptions, @@ -1771,7 +1771,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { } } else { _currentClass = Class( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, fields: [], superClassName: node.implementsClause?.interfaces.first.name.toString() ?? @@ -1922,10 +1922,14 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { // resolved return type, via // `node.declaredFragment!.element.returnType`. String erroneousDeclaration = node.name.lexeme; - final dart_ast.AstNode? enclosingDeclaration = node.parent; + dart_ast.AstNode? enclosingDeclaration = node.parent; + while (enclosingDeclaration != null && + enclosingDeclaration is! dart_ast.ClassDeclaration) { + enclosingDeclaration = enclosingDeclaration.parent; + } if (enclosingDeclaration is dart_ast.ClassDeclaration) { erroneousDeclaration = - '${enclosingDeclaration.name}.$erroneousDeclaration'; + '${enclosingDeclaration.namePart.typeName}.$erroneousDeclaration'; } _errors.add( Error( @@ -1982,8 +1986,8 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { Object? visitEnumDeclaration(dart_ast.EnumDeclaration node) { _enums.add( Enum( - name: node.name.lexeme, - members: node.constants + name: node.namePart.typeName.lexeme, + members: node.body.constants .map( (dart_ast.EnumConstantDeclaration e) => EnumMember( name: e.name.lexeme, diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 0fc8a2cdd64c..637fb3f6fe6b 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -2,13 +2,13 @@ name: pigeon description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. repository: https://github.com/flutter/packages/tree/main/packages/pigeon issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22 -version: 26.3.1 # This must match the version in lib/src/generator_tools.dart +version: 26.3.3 # This must match the version in lib/src/generator_tools.dart environment: sdk: ^3.9.0 dependencies: - analyzer: ">=8.0.0 <10.0.0" + analyzer: ">=10.0.0 <13.0.0" args: ^2.5.0 code_builder: ^4.10.0 collection: ^1.15.0 @@ -27,3 +27,4 @@ topics: - interop - platform-channels - plugin-development + diff --git a/packages/quick_actions/quick_actions_ios/CHANGELOG.md b/packages/quick_actions/quick_actions_ios/CHANGELOG.md index 506580fd211c..4f1cbc7cf8eb 100644 --- a/packages/quick_actions/quick_actions_ios/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.2.4 + +* Adds support for UIScene lifecycle. +* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. + ## 1.2.3 * Updates to Pigeon 26. diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift b/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift index 6fa401143d5a..b5bc5f9682ba 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift @@ -6,14 +6,17 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) super.application(application, didFinishLaunchingWithOptions: launchOptions) // For UI integration tests. See https://github.com/flutter/plugins/pull/3811. return false } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist index fd3b62987824..7fba3010b0d8 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift index 98766194b3bc..ad2f043af105 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift @@ -223,4 +223,108 @@ struct QuickActionsPluginTests { plugin.applicationDidBecomeActive(UIApplication.shared) } } + + // MARK: - Scene lifecycle tests + + @Test func windowScenePerformActionForShortcutItem() async { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + await confirmation("shortcut should be handled via windowScene") { confirmed in + flutterApi.launchActionCallback = { aString in + #expect(aString == item.type) + confirmed() + } + + let windowScene = UIApplication.shared.connectedScenes.first as! UIWindowScene + var completionSuccess: Bool? + let actionResult = plugin.windowScene( + windowScene, + performActionFor: item + ) { success in + completionSuccess = success + } + + #expect(actionResult, "windowScene performActionFor must return true.") + #expect(completionSuccess == true) + } + } + + @Test func sceneWillConnectToWithoutShortcut() { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let connectResult = plugin.scene( + UIApplication.shared.connectedScenes.first!, + willConnectTo: UIApplication.shared.connectedScenes.first!.session, + options: nil) + #expect( + !connectResult, + "scene willConnectTo must return false if not launched from shortcut.") + } + + @Test func sceneDidBecomeActiveLaunchWithoutShortcut() async { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let connectResult = plugin.scene( + UIApplication.shared.connectedScenes.first!, + willConnectTo: UIApplication.shared.connectedScenes.first!.session, + options: nil) + #expect(!connectResult) + + await confirmation("launchAction should not be called", expectedCount: 0) { confirmed in + flutterApi.launchActionCallback = { _ in + confirmed() + } + plugin.sceneDidBecomeActive(UIApplication.shared.connectedScenes.first!) + } + } + + @Test func sceneDidBecomeActiveLaunchWithShortcut() async { + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + await confirmation("shortcut should be handled when scene becomes active") { confirmed in + flutterApi.launchActionCallback = { aString in + #expect(aString == item.type) + confirmed() + } + + let connectResult = plugin.handleSceneWillConnectTo(shortcutItem: item) + #expect(connectResult, "scene willConnectTo must return true when shortcut is provided.") + + plugin.sceneDidBecomeActive(UIApplication.shared.connectedScenes.first!) + } + } } diff --git a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml index 8917f495bf01..46a29f8dc9ee 100644 --- a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the quick_actions plugin. publish_to: none environment: - sdk: ^3.9.0 - flutter: ">=3.35.0" + sdk: ^3.10.0 + flutter: ">=3.38.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift index 3f2d91518a11..0ff0980c81d5 100644 --- a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift +++ b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift @@ -3,8 +3,11 @@ // found in the LICENSE file. import Flutter +import UIKit -public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsApi { +public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsApi, + FlutterSceneLifeCycleDelegate +{ public static func register(with registrar: FlutterPluginRegistrar) { let messenger = registrar.messenger() @@ -12,6 +15,7 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsA let instance = QuickActionsPlugin(flutterApi: flutterApi) IOSQuickActionsApiSetup.setUp(binaryMessenger: messenger, api: instance) registrar.addApplicationDelegate(instance) + registrar.addSceneDelegate(instance) } private let shortcutItemProvider: ShortcutItemProviding @@ -72,6 +76,46 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsA } } + // MARK: - FlutterSceneLifeCycleDelegate + + public func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions? + ) -> Bool { + return handleSceneWillConnectTo(shortcutItem: connectionOptions?.shortcutItem) + } + + func handleSceneWillConnectTo(shortcutItem: UIApplicationShortcutItem?) -> Bool { + if let shortcutItem { + // Keep hold of the shortcut type and handle it in the + // `sceneDidBecomeActive:` method once the Dart MethodChannel + // is initialized. + launchingShortcutType = shortcutItem.type + return true + } + return false + } + + public func sceneDidBecomeActive(_ scene: UIScene) { + if let shortcutType = launchingShortcutType { + handleShortcut(shortcutType) + launchingShortcutType = nil + } + } + + public func windowScene( + _ windowScene: UIWindowScene, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void + ) -> Bool { + handleShortcut(shortcutItem.type) + completionHandler(true) + return true + } + + // MARK: - Shortcut handling + func handleShortcut(_ shortcut: String) { flutterApi.launchAction(action: shortcut) { _ in // noop diff --git a/packages/quick_actions/quick_actions_ios/pubspec.yaml b/packages/quick_actions/quick_actions_ios/pubspec.yaml index 478edffea6ea..e1f48024ccf0 100644 --- a/packages/quick_actions/quick_actions_ios/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: quick_actions_ios description: An implementation for the iOS platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/packages/tree/main/packages/quick_actions/quick_actions_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.2.3 +version: 1.2.4 environment: - sdk: ^3.9.0 - flutter: ">=3.35.0" + sdk: ^3.10.0 + flutter: ">=3.38.0" flutter: plugin: diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index c9332dcb13ac..2f031898ab46 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.4.1 + +* Adds warnings for TableView pinned rows and columns that exceed the viewport dimensions. + +## 0.4.0 + +* Added `alignment` property to `TableView` and `TreeView` to align content within the viewport when it is smaller than the viewport extent. + ## 0.3.9 * Fixes TableSpan borders being flipped when one or both axis directions are reversed. diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index e0dd64e9e0b0..b09a7898e852 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -116,6 +116,7 @@ class TableView extends TwoDimensionalScrollView { super.dragStartBehavior, super.keyboardDismissBehavior, super.clipBehavior, + this.alignment = Alignment.topLeft, }); /// Creates a [TableView] of widgets that are created on demand. @@ -155,6 +156,7 @@ class TableView extends TwoDimensionalScrollView { required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, required TableViewCellBuilder cellBuilder, + this.alignment = Alignment.topLeft, }) : assert(pinnedRowCount >= 0), assert(rowCount == null || rowCount >= 0), assert(rowCount == null || rowCount >= pinnedRowCount), @@ -199,6 +201,7 @@ class TableView extends TwoDimensionalScrollView { required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, List> cells = const >[], + this.alignment = Alignment.topLeft, }) : assert(pinnedRowCount >= 0), assert(pinnedColumnCount >= 0), super( @@ -211,6 +214,11 @@ class TableView extends TwoDimensionalScrollView { ), ); + /// The alignment of the table within the viewport when there is extra space. + /// + /// Defaults to [Alignment.topLeft]. + final AlignmentGeometry alignment; + @override TableViewport buildViewport( BuildContext context, @@ -226,6 +234,7 @@ class TableView extends TwoDimensionalScrollView { mainAxis: mainAxis, cacheExtent: cacheExtent, clipBehavior: clipBehavior, + alignment: alignment, ); } } @@ -245,8 +254,12 @@ class TableViewport extends TwoDimensionalViewport { required super.mainAxis, super.cacheExtent, super.clipBehavior, + this.alignment = Alignment.topLeft, }); + /// The alignment of the table within the viewport when there is extra space. + final AlignmentGeometry alignment; + @override RenderTwoDimensionalViewport createRenderObject(BuildContext context) { return RenderTableViewport( @@ -259,6 +272,8 @@ class TableViewport extends TwoDimensionalViewport { clipBehavior: clipBehavior, delegate: delegate as TableCellDelegateMixin, childManager: context as TwoDimensionalChildManager, + alignment: alignment, + textDirection: Directionality.maybeOf(context), ); } @@ -275,7 +290,9 @@ class TableViewport extends TwoDimensionalViewport { ..mainAxis = mainAxis ..cacheExtent = cacheExtent ..clipBehavior = clipBehavior - ..delegate = delegate as TableCellDelegateMixin; + ..delegate = delegate as TableCellDelegateMixin + ..alignment = alignment + ..textDirection = Directionality.maybeOf(context); } } @@ -299,7 +316,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { required super.childManager, super.cacheExtent, super.clipBehavior, - }); + AlignmentGeometry alignment = Alignment.topLeft, + TextDirection? textDirection, + }) : _alignment = alignment, + _textDirection = textDirection; @override TableCellDelegateMixin get delegate => @@ -309,6 +329,31 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { super.delegate = value; } + /// The alignment of the table within the viewport when there is extra space. + AlignmentGeometry get alignment => _alignment; + AlignmentGeometry _alignment; + set alignment(AlignmentGeometry value) { + if (_alignment == value) { + return; + } + _alignment = value; + markNeedsLayout(); + } + + /// The text direction with which to resolve [alignment]. + TextDirection? get textDirection => _textDirection; + TextDirection? _textDirection; + set textDirection(TextDirection? value) { + if (_textDirection == value) { + return; + } + _textDirection = value; + markNeedsLayout(); + } + + double _hAlignmentOffset = 0.0; + double _vAlignmentOffset = 0.0; + // Skipped vicinities for the current frame based on merged cells. // This prevents multiple build calls for the same cell that spans multiple // vicinities. @@ -384,13 +429,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); } - // TODO(Piinks): Pinned rows/cols do not account for what is visible on the - // screen. Ostensibly, we would not want to have pinned rows/columns that - // extend beyond the viewport, we would never see them as they would never - // scroll into view. So this currently implementation is fairly assuming - // we will never have rows/cols that are outside of the viewport. We should - // maybe add an assertion for this during layout. - // https://github.com/flutter/flutter/issues/136833 int? get _lastPinnedRow => delegate.pinnedRowCount > 0 ? delegate.pinnedRowCount - 1 : null; int? get _lastPinnedColumn => @@ -403,6 +441,49 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ? _columnMetrics[_lastPinnedColumn]!.trailingOffset : 0.0; + void _debugCheckPinnedExtent() { + assert(() { + if (_pinnedColumnsExtent > viewportDimension.width) { + debugPrint( + 'TableView has pinned columns with a total width of ' + '$_pinnedColumnsExtent, which exceeds the viewport width of ' + '${viewportDimension.width}. This will prevent unpinned columns ' + 'from being visible.', + ); + } else if (_pinnedColumnsExtent == viewportDimension.width) { + final bool hasUnpinnedColumns = + delegate.columnCount == null || + delegate.columnCount! > delegate.pinnedColumnCount; + if (hasUnpinnedColumns) { + debugPrint( + 'TableView has pinned columns that fully consume the viewport width. ' + 'Unpinned columns will not be visible.', + ); + } + } + + if (_pinnedRowsExtent > viewportDimension.height) { + debugPrint( + 'TableView has pinned rows with a total height of ' + '$_pinnedRowsExtent, which exceeds the viewport height of ' + '${viewportDimension.height}. This will prevent unpinned rows ' + 'from being visible.', + ); + } else if (_pinnedRowsExtent == viewportDimension.height) { + final bool hasUnpinnedRows = + delegate.rowCount == null || + delegate.rowCount! > delegate.pinnedRowCount; + if (hasUnpinnedRows) { + debugPrint( + 'TableView has pinned rows that fully consume the viewport height. ' + 'Unpinned rows will not be visible.', + ); + } + } + return true; + }()); + } + @override TableViewParentData parentDataOf(RenderBox child) => super.parentDataOf(child) as TableViewParentData; @@ -847,11 +928,39 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _updateColumnMetrics(); _updateRowMetrics(); _updateScrollBounds(); + _debugCheckPinnedExtent(); } else { // Updates the visible cells based on cached table metrics. _updateFirstAndLastVisibleCell(); } + final Alignment resolvedAlignment = alignment.resolve(textDirection); + _hAlignmentOffset = 0.0; + if (!_columnsAreInfinite && _columnMetrics.isNotEmpty) { + final double totalWidth = + _pinnedColumnsExtent + + _columnMetrics[delegate.columnCount! - 1]!.trailingOffset; + if (totalWidth < viewportDimension.width) { + _hAlignmentOffset = + (viewportDimension.width - totalWidth) * + (resolvedAlignment.x + 1.0) / + 2.0; + } + } + + _vAlignmentOffset = 0.0; + if (!_rowsAreInfinite && _rowMetrics.isNotEmpty) { + final double totalHeight = + _pinnedRowsExtent + + _rowMetrics[delegate.rowCount! - 1]!.trailingOffset; + if (totalHeight < viewportDimension.height) { + _vAlignmentOffset = + (viewportDimension.height - totalHeight) * + (resolvedAlignment.y + 1.0) / + 2.0; + } + } + if (_firstNonPinnedCell == null && _lastPinnedRow == null && _lastPinnedColumn == null) { @@ -862,19 +971,21 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final double? offsetIntoColumn = _firstNonPinnedColumn != null ? horizontalOffset.pixels - _columnMetrics[_firstNonPinnedColumn]!.leadingOffset - - _pinnedColumnsExtent + _pinnedColumnsExtent - + _hAlignmentOffset : null; final double? offsetIntoRow = _firstNonPinnedRow != null ? verticalOffset.pixels - _rowMetrics[_firstNonPinnedRow]!.leadingOffset - - _pinnedRowsExtent + _pinnedRowsExtent - + _vAlignmentOffset : null; if (_lastPinnedRow != null && _lastPinnedColumn != null) { // Layout cells that are contained in both pinned rows and columns _layoutCells( start: TableVicinity.zero, end: TableVicinity(column: _lastPinnedColumn!, row: _lastPinnedRow!), - offset: Offset.zero, + offset: Offset(-_hAlignmentOffset, -_vAlignmentOffset), ); } @@ -886,7 +997,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _layoutCells( start: TableVicinity(column: _firstNonPinnedColumn!, row: 0), end: TableVicinity(column: _lastNonPinnedColumn!, row: _lastPinnedRow!), - offset: Offset(offsetIntoColumn!, 0), + offset: Offset(offsetIntoColumn!, -_vAlignmentOffset), ); } if (_lastPinnedColumn != null && _firstNonPinnedRow != null) { @@ -897,7 +1008,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _layoutCells( start: TableVicinity(column: 0, row: _firstNonPinnedRow!), end: TableVicinity(column: _lastPinnedColumn!, row: _lastNonPinnedRow!), - offset: Offset(0, offsetIntoRow!), + offset: Offset(-_hAlignmentOffset, offsetIntoRow!), ); } if (_firstNonPinnedCell != null) { @@ -1176,6 +1287,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // follows row or column major ordering. Here is slightly different // as we break the cells up into 4 main paint passes to clip for overlap. + final bool reversedH = axisDirectionIsReversed(horizontalAxisDirection); + final bool reversedV = axisDirectionIsReversed(verticalAxisDirection); + if (_firstNonPinnedCell != null) { // Paint all visible un-pinned cells assert(_lastNonPinnedCell != null); @@ -1183,12 +1297,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { needsCompositing, offset, Rect.fromLTWH( - axisDirectionIsReversed(horizontalAxisDirection) - ? 0.0 - : _pinnedColumnsExtent, - axisDirectionIsReversed(verticalAxisDirection) - ? 0.0 - : _pinnedRowsExtent, + (reversedH ? 0.0 : _pinnedColumnsExtent) + + (reversedH ? -_hAlignmentOffset : _hAlignmentOffset), + (reversedV ? 0.0 : _pinnedRowsExtent) + + (reversedV ? -_vAlignmentOffset : _vAlignmentOffset), viewportDimension.width - _pinnedColumnsExtent, viewportDimension.height - _pinnedRowsExtent, ), @@ -1214,12 +1326,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { needsCompositing, offset, Rect.fromLTWH( - axisDirectionIsReversed(horizontalAxisDirection) - ? viewportDimension.width - _pinnedColumnsExtent - : 0.0, - axisDirectionIsReversed(verticalAxisDirection) - ? 0.0 - : _pinnedRowsExtent, + reversedH + ? viewportDimension.width - + _pinnedColumnsExtent - + _hAlignmentOffset + : _hAlignmentOffset, + (reversedV ? 0.0 : _pinnedRowsExtent) + + (reversedV ? -_vAlignmentOffset : _vAlignmentOffset), _pinnedColumnsExtent, viewportDimension.height - _pinnedRowsExtent, ), @@ -1248,12 +1361,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { needsCompositing, offset, Rect.fromLTWH( - axisDirectionIsReversed(horizontalAxisDirection) - ? 0.0 - : _pinnedColumnsExtent, - axisDirectionIsReversed(verticalAxisDirection) - ? viewportDimension.height - _pinnedRowsExtent - : 0.0, + (reversedH ? 0.0 : _pinnedColumnsExtent) + + (reversedH ? -_hAlignmentOffset : _hAlignmentOffset), + reversedV + ? viewportDimension.height - _pinnedRowsExtent - _vAlignmentOffset + : _vAlignmentOffset, viewportDimension.width - _pinnedColumnsExtent, _pinnedRowsExtent, ), diff --git a/packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart b/packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart index f10c147a50db..10eb1ac3646a 100644 --- a/packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart +++ b/packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart @@ -38,9 +38,13 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { required super.childManager, super.cacheExtent, super.clipBehavior, + AlignmentGeometry alignment = Alignment.topLeft, + TextDirection? textDirection, }) : _activeAnimations = activeAnimations, _rowDepths = rowDepths, _indentation = indentation, + _alignment = alignment, + _textDirection = textDirection, assert(indentation >= 0), assert( verticalAxisDirection == AxisDirection.down && @@ -56,6 +60,30 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { super.delegate = value; } + /// The alignment of the tree within the viewport when there is extra space. + AlignmentGeometry get alignment => _alignment; + AlignmentGeometry _alignment; + set alignment(AlignmentGeometry value) { + if (_alignment == value) { + return; + } + _alignment = value; + markNeedsLayout(); + } + + /// The text direction with which to resolve [alignment]. + TextDirection? get textDirection => _textDirection; + TextDirection? _textDirection; + set textDirection(TextDirection? value) { + if (_textDirection == value) { + return; + } + _textDirection = value; + markNeedsLayout(); + } + + double _vAlignmentOffset = 0.0; + /// The currently active [TreeViewNode] animations. /// /// Since the index of animating nodes can change at any time, the unique key @@ -348,6 +376,19 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { _updateFirstAndLastVisibleRow(); } + final Alignment resolvedAlignment = alignment.resolve(textDirection); + _vAlignmentOffset = 0.0; + if (_rowMetrics.isNotEmpty) { + final double totalHeight = + _rowMetrics[_rowMetrics.length - 1]!.trailingOffset; + if (totalHeight < viewportDimension.height) { + _vAlignmentOffset = + (viewportDimension.height - totalHeight) * + (resolvedAlignment.y + 1.0) / + 2.0; + } + } + if (_firstRow == null) { assert(_lastRow == null); return; @@ -356,7 +397,9 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { _Span rowSpan; double rowOffset = - -verticalOffset.pixels + _rowMetrics[_firstRow!]!.leadingOffset; + -verticalOffset.pixels + + _rowMetrics[_firstRow!]!.leadingOffset + + _vAlignmentOffset; for (int row = _firstRow!; row <= _lastRow!; row++) { rowSpan = _rowMetrics[row]!; final double rowHeight = rowSpan.extent; @@ -489,11 +532,11 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { final double trailingOffset = _rowMetrics[segment.trailingIndex]!.trailingOffset; final rect = Rect.fromPoints( - Offset(0.0, leadingOffset - verticalOffset.pixels), + Offset(0.0, leadingOffset - verticalOffset.pixels + _vAlignmentOffset), Offset( viewportDimension.width, math.min( - trailingOffset - verticalOffset.pixels, + trailingOffset - verticalOffset.pixels + _vAlignmentOffset, viewportDimension.height, ), ), diff --git a/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart b/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart index 3c5fa67bf134..6819c9424018 100644 --- a/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart +++ b/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart @@ -322,6 +322,7 @@ class TreeView extends StatefulWidget { this.clipBehavior = Clip.hardEdge, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, + this.alignment = Alignment.topLeft, }) : assert( verticalDetails.direction == AxisDirection.down && horizontalDetails.direction == AxisDirection.right, @@ -496,6 +497,14 @@ class TreeView extends StatefulWidget { /// Defaults to true. final bool addRepaintBoundaries; + /// The alignment of the tree within the viewport when there is extra space. + /// + /// Currently, [TreeView] only supports the vertical component of [alignment] + /// for aligning the tree within the viewport. + /// + /// Defaults to [Alignment.topLeft]. + final AlignmentGeometry alignment; + /// The default [AnimationStyle] used for node expand and collapse animations, /// when one has not been provided in [toggleAnimationStyle]. // ignore: prefer_const_constructors @@ -758,6 +767,7 @@ class _TreeViewState extends State> }, addAutomaticKeepAlives: widget.addAutomaticKeepAlives, indentation: widget.indentation.value, + alignment: widget.alignment, ); } @@ -984,6 +994,7 @@ class _TreeView extends TwoDimensionalScrollView { required this.activeAnimations, required this.rowDepths, required this.indentation, + required this.alignment, required int rowCount, bool addAutomaticKeepAlives = true, }) : assert(verticalDetails.direction == AxisDirection.down), @@ -1000,6 +1011,7 @@ class _TreeView extends TwoDimensionalScrollView { final Map activeAnimations; final Map rowDepths; final double indentation; + final AlignmentGeometry alignment; @override TreeViewport buildViewport( @@ -1018,6 +1030,7 @@ class _TreeView extends TwoDimensionalScrollView { activeAnimations: activeAnimations, rowDepths: rowDepths, indentation: indentation, + alignment: alignment, ); } } @@ -1039,6 +1052,7 @@ class TreeViewport extends TwoDimensionalViewport { required this.activeAnimations, required this.rowDepths, required this.indentation, + this.alignment = Alignment.topLeft, }) : assert( verticalAxisDirection == AxisDirection.down && horizontalAxisDirection == AxisDirection.right, @@ -1063,6 +1077,9 @@ class TreeViewport extends TwoDimensionalViewport { /// for more options to customize the indented space. final double indentation; + /// The alignment of the tree within the viewport when there is extra space. + final AlignmentGeometry alignment; + @override RenderTreeViewport createRenderObject(BuildContext context) { return RenderTreeViewport( @@ -1077,6 +1094,8 @@ class TreeViewport extends TwoDimensionalViewport { clipBehavior: clipBehavior, delegate: delegate as TreeRowDelegateMixin, childManager: context as TwoDimensionalChildManager, + alignment: alignment, + textDirection: Directionality.maybeOf(context), ); } @@ -1095,6 +1114,8 @@ class TreeViewport extends TwoDimensionalViewport { ..verticalAxisDirection = verticalAxisDirection ..cacheExtent = cacheExtent ..clipBehavior = clipBehavior - ..delegate = delegate as TreeRowDelegateMixin; + ..delegate = delegate as TreeRowDelegateMixin + ..alignment = alignment + ..textDirection = Directionality.maybeOf(context); } } diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 9d16a900d995..8e6d131bc8e0 100644 --- a/packages/two_dimensional_scrollables/pubspec.yaml +++ b/packages/two_dimensional_scrollables/pubspec.yaml @@ -1,6 +1,6 @@ name: two_dimensional_scrollables description: Widgets that scroll using the two dimensional scrolling foundation. -version: 0.3.9 +version: 0.4.1 repository: https://github.com/flutter/packages/tree/main/packages/two_dimensional_scrollables issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+two_dimensional_scrollables%22+ diff --git a/packages/two_dimensional_scrollables/test/table_view/alignment_test.dart b/packages/two_dimensional_scrollables/test/table_view/alignment_test.dart new file mode 100644 index 000000000000..cba1e1fbc872 --- /dev/null +++ b/packages/two_dimensional_scrollables/test/table_view/alignment_test.dart @@ -0,0 +1,608 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; + +void main() { + group('TableView alignment', () { + testWidgets('Default alignment - topLeft', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Default is Alignment.topLeft (0, 0) + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect(tester.getTopLeft(cell00) - tableTopLeft, Offset.zero); + }); + + testWidgets('Horizontal alignment - center', (WidgetTester tester) async { + const viewportWidth = 600.0; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: viewportWidth, + height: 400, + child: TableView.builder( + columnCount: 3, + rowCount: 1, + alignment: Alignment.topCenter, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table is 300 wide, viewport is 600 wide. Centered means 150 offset. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(150.0, 0.0), + ); + + final Finder cell20 = find.byKey(const ValueKey('cell 2:0')); + expect( + tester.getTopLeft(cell20) - tableTopLeft, + const Offset(350.0, 0.0), + ); + }); + + testWidgets('Horizontal alignment - end', (WidgetTester tester) async { + const viewportWidth = 600.0; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: viewportWidth, + height: 400, + child: TableView.builder( + columnCount: 3, + rowCount: 1, + alignment: Alignment.topRight, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table is 300 wide, viewport is 600 wide. End means 300 offset. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(300.0, 0.0), + ); + }); + + testWidgets('Vertical alignment - center', (WidgetTester tester) async { + const viewportHeight = 600.0; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: viewportHeight, + child: TableView.builder( + columnCount: 1, + rowCount: 2, + alignment: Alignment.centerLeft, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table is 200 high, viewport is 600 high. Centered means 200 offset. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 200.0), + ); + + final Finder cell01 = find.byKey(const ValueKey('cell 0:1')); + expect( + tester.getTopLeft(cell01) - tableTopLeft, + const Offset(0.0, 300.0), + ); + }); + + testWidgets('Combined alignment', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: Alignment.center, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table is 100x100, viewport is 600x600. Centered means 250, 250 offset. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(250.0, 250.0), + ); + }); + + testWidgets('Alignment with pinned columns', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 400, + child: TableView.builder( + columnCount: 3, + rowCount: 1, + pinnedColumnCount: 1, + alignment: Alignment.topCenter, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Total width 300 (1 pinned, 2 unpinned). Viewport 600. Offset 150. + // Pinned column 0 should be at 150. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(150.0, 0.0), + ); + + // Unpinned column 1 should be at 250. + final Finder cell10 = find.byKey(const ValueKey('cell 1:0')); + expect( + tester.getTopLeft(cell10) - tableTopLeft, + const Offset(250.0, 0.0), + ); + }); + + testWidgets('Alignment with pinned rows', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 3, + pinnedRowCount: 1, + alignment: Alignment.centerLeft, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Total height 300 (1 pinned, 2 unpinned). Viewport 600. Offset 150. + // Pinned row 0 should be at 150. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 150.0), + ); + + // Unpinned row 1 should be at 250. + final Finder cell01 = find.byKey(const ValueKey('cell 0:1')); + expect( + tester.getTopLeft(cell01) - tableTopLeft, + const Offset(0.0, 250.0), + ); + }); + + testWidgets('Alignment with reversed horizontal axis', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 400, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: Alignment.topCenter, + horizontalDetails: const ScrollableDetails.horizontal( + reverse: true, + ), + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Reversed horizontal. Start is on the right (600). + // Center should still be at 250. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(250.0, 0.0), + ); + }); + + testWidgets('Alignment with reversed vertical axis', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: Alignment.centerLeft, + verticalDetails: const ScrollableDetails.vertical( + reverse: true, + ), + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Reversed vertical. Center should still be at 250. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 250.0), + ); + }); + + testWidgets('Alignment with both axes reversed', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: Alignment.center, + horizontalDetails: const ScrollableDetails.horizontal( + reverse: true, + ), + verticalDetails: const ScrollableDetails.vertical( + reverse: true, + ), + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Both reversed. Center should still be at (250, 250). + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(250.0, 250.0), + ); + }); + + testWidgets('AlignmentDirectional with RTL', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.rtl, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 400, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: AlignmentDirectional.centerEnd, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // RTL + centerEnd means alignment to the left. + // Table is 100 wide, viewport 600. centerEnd in RTL resolved to left (x = -1). + // Wait, centerEnd in RTL is actually Alignment(-1.0, 0.0) which is left. + // centerEnd in LTR is Alignment(1.0, 0.0) which is right. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 150.0), // Center Y is 150 (400-100)/2 + ); + }); + + testWidgets('Overflow alignment behaves like start in overflow axis', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 200, + height: 400, + child: TableView.builder( + columnCount: 3, // 300 wide + rowCount: 1, + alignment: Alignment.center, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table (300) > Viewport (200). Horizontal alignment should be ignored (start). + // Viewport (400) > Table (100) Row. Vertical alignment should be center (150). + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 150.0), + ); + }); + }); +} diff --git a/packages/two_dimensional_scrollables/test/table_view/pinned_extent_warning_test.dart b/packages/two_dimensional_scrollables/test/table_view/pinned_extent_warning_test.dart new file mode 100644 index 000000000000..af0d6625b6e8 --- /dev/null +++ b/packages/two_dimensional_scrollables/test/table_view/pinned_extent_warning_test.dart @@ -0,0 +1,241 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; + +void main() { + group('TableView pinned extent warnings', () { + testWidgets('Warns when pinned columns exceed viewport width', ( + WidgetTester tester, + ) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 400, + child: TableView.builder( + columnCount: 5, + rowCount: 5, + pinnedColumnCount: 3, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned columns extent = 300 (3 * 100), viewport width = 200. + // A warning is expected because the pinned columns are wider than the + // viewport, meaning even the pinned content cannot be fully displayed. + expect( + log, + contains( + matches( + r'TableView has pinned columns with a total width of 300(\.0)?, which exceeds the viewport width of 200(\.0)?', + ), + ), + ); + debugPrint = oldDebugPrint; + }); + + testWidgets('Warns when pinned rows exceed viewport height', ( + WidgetTester tester, + ) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 400, + height: 200, + child: TableView.builder( + columnCount: 5, + rowCount: 5, + pinnedRowCount: 3, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned rows extent = 300 (3 * 100), viewport height = 200. + // A warning is expected because the pinned rows are taller than the + // viewport, meaning even the pinned content cannot be fully displayed. + expect( + log, + contains( + matches( + r'TableView has pinned rows with a total height of 300(\.0)?, which exceeds the viewport height of 200(\.0)?', + ), + ), + ); + debugPrint = oldDebugPrint; + }); + + testWidgets( + 'Warns when pinned columns fully consume viewport width and there are unpinned columns', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 400, + child: TableView.builder( + columnCount: 3, + rowCount: 5, + pinnedColumnCount: 2, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned columns extent = 200 (2 * 100), viewport width = 200. + // There is 1 unpinned column (columnCount: 3, pinnedColumnCount: 2). + // Since the pinned columns take up the entire viewport width, the + // unpinned column will never be visible during scrolling. + expect( + log, + contains( + 'TableView has pinned columns that fully consume the viewport width. Unpinned columns will not be visible.', + ), + ); + debugPrint = oldDebugPrint; + }, + ); + + testWidgets( + 'Warns when pinned rows fully consume viewport height and there are unpinned rows', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 400, + height: 200, + child: TableView.builder( + columnCount: 5, + rowCount: 3, + pinnedRowCount: 2, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned rows extent = 200 (2 * 100), viewport height = 200. + // There is 1 unpinned row (rowCount: 3, pinnedRowCount: 2). + // Since the pinned rows take up the entire viewport height, the + // unpinned row will never be visible during scrolling. + expect( + log, + contains( + 'TableView has pinned rows that fully consume the viewport height. Unpinned rows will not be visible.', + ), + ); + debugPrint = oldDebugPrint; + }, + ); + + testWidgets( + 'Does not warn when all columns are pinned even if they consume viewport', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 400, + child: TableView.builder( + columnCount: 2, + rowCount: 5, + pinnedColumnCount: 2, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned columns extent = 200 (2 * 100), viewport width = 200. + // Although the pinned columns fully consume the viewport width, + // ALL columns are pinned (columnCount: 2, pinnedColumnCount: 2). + // Since there are no unpinned columns, no warning is issued about + // unpinned columns being hidden. + expect( + log, + isNot(contains(contains('Unpinned columns will not be visible'))), + ); + debugPrint = oldDebugPrint; + }, + ); + }); +} diff --git a/packages/two_dimensional_scrollables/test/table_view/table_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_test.dart index 50ec9d4640db..a59a3301a778 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -4175,6 +4175,105 @@ void main() { ); }, ); + + testWidgets( + 'Table does not crash when focusing outside of the table while focused text field is not in the view', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/137112 + final verticalController = ScrollController(); + final horizontalController = ScrollController(); + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Column( + children: [ + const TextField(key: Key('outside_textfield')), + Expanded( + child: TableView.builder( + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + ), + cellBuilder: + (BuildContext context, TableVicinity vicinity) { + return TableViewCell( + child: Center( + child: TextField( + key: Key( + 'cell_${vicinity.row}_${vicinity.column}', + ), + ), + ), + ); + }, + columnCount: 20, + columnBuilder: (int index) { + return const TableSpan( + foregroundDecoration: TableSpanDecoration( + border: TableSpanBorder(trailing: BorderSide()), + ), + extent: FixedTableSpanExtent(100), + ); + }, + rowCount: 40, + rowBuilder: (int index) { + return TableSpan( + backgroundDecoration: TableSpanDecoration( + color: index.isEven ? Colors.purple[100] : null, + border: const TableSpanBorder( + trailing: BorderSide(width: 3), + ), + ), + extent: const FixedTableSpanExtent(50), + ); + }, + ), + ), + ], + ), + ), + ), + ); + + // 1. Select a TextField in the table. + // Use the vicinity from the original crash report. + const vicinity = TableVicinity(row: 5, column: 6); + final Finder cellTextField = find.byKey( + Key('cell_${vicinity.row}_${vicinity.column}'), + ); + // Bring it into view. + verticalController.jumpTo(250); + horizontalController.jumpTo(600); + await tester.pumpAndSettle(); + + await tester.tap(cellTextField); + await tester.pumpAndSettle(); + expect(FocusManager.instance.primaryFocus, isNotNull); + + // 2. Scroll until it disappears from the view, without unfocusing it. + verticalController.jumpTo(verticalController.offset + 1000); + await tester.pumpAndSettle(); + + // 3. Select another TextField outside of the table. + final Finder outsideTextField = find.byKey( + const Key('outside_textfield'), + ); + await tester.tap(outsideTextField); + await tester.pumpAndSettle(); + + // 4. Scroll back and ensure the table does not crash. + verticalController.jumpTo(verticalController.offset - 1000); + await tester.pumpAndSettle(); + expect(cellTextField, findsOneWidget); + }, + ); } class _NullBuildContext implements BuildContext, TwoDimensionalChildManager { diff --git a/packages/two_dimensional_scrollables/test/tree_view/alignment_test.dart b/packages/two_dimensional_scrollables/test/tree_view/alignment_test.dart new file mode 100644 index 000000000000..9783e253309e --- /dev/null +++ b/packages/two_dimensional_scrollables/test/tree_view/alignment_test.dart @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; + +void main() { + group('TreeView alignment', () { + testWidgets('Default alignment - topLeft', (WidgetTester tester) async { + final tree = >[TreeViewNode('Root')]; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: 400, + child: TreeView( + tree: tree, + treeNodeBuilder: (context, node, toggleAnimationStyle) => + SizedBox( + key: const ValueKey('Root'), + height: 100, + child: Text(node.content), + ), + treeRowBuilder: (node) => + const TreeRow(extent: FixedTreeRowExtent(100)), + ), + ), + ), + ), + ), + ); + + final Offset treeTopLeft = tester.getTopLeft( + find.byType(TreeView), + ); + // Default is Alignment.topLeft (0, 0) + final Finder root = find.byKey(const ValueKey('Root')); + expect(tester.getTopLeft(root) - treeTopLeft, Offset.zero); + }); + + testWidgets('Vertical alignment - center', (WidgetTester tester) async { + const viewportHeight = 600.0; + final tree = >[TreeViewNode('Root')]; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: viewportHeight, + child: TreeView( + tree: tree, + alignment: Alignment.center, + treeNodeBuilder: (context, node, toggleAnimationStyle) => + SizedBox( + key: const ValueKey('Root'), + height: 100, + child: Text(node.content), + ), + treeRowBuilder: (node) => + const TreeRow(extent: FixedTreeRowExtent(100)), + ), + ), + ), + ), + ), + ); + + final Offset treeTopLeft = tester.getTopLeft( + find.byType(TreeView), + ); + // Tree is 100 high, viewport is 600 high. Centered means 250 offset. + final Finder root = find.byKey(const ValueKey('Root')); + expect(tester.getTopLeft(root) - treeTopLeft, const Offset(0.0, 250.0)); + }); + }); +} diff --git a/script/tool/lib/src/common/file_filters.dart b/script/tool/lib/src/common/file_filters.dart index 7a1bafd06a5c..90be24da8d03 100644 --- a/script/tool/lib/src/common/file_filters.dart +++ b/script/tool/lib/src/common/file_filters.dart @@ -9,10 +9,10 @@ bool isRepoLevelNonCodeImpactingFile(String path) { return [ 'AUTHORS', - 'CODEOWNERS', 'CONTRIBUTING.md', 'LICENSE', 'README.md', + 'SUGGESTED_REVIEWERS.md', 'AGENTS.md', // This deliberate lists specific files rather than excluding the whole // .github directory since it's better to have false negatives than to diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index c423d760ea85..1c213fd2d0f3 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -16,7 +16,7 @@ const int _exitBadTableEntry = 3; const int _exitUnknownPackageEntry = 4; /// A command to verify repository-level metadata about packages, such as -/// repo README and CODEOWNERS entries. +/// repo README and auto-label entries. class RepoPackageInfoCheckCommand extends PackageLoopingCommand { /// Creates Dependabot check command instance. RepoPackageInfoCheckCommand(super.packagesDir, {super.gitDir}); @@ -27,9 +27,6 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { final Map> _readmeTableEntries = >{}; - /// Packages with entries in CODEOWNERS. - final List _ownedPackages = []; - /// Packages with entries in labeler.yml. final List _autoLabeledPackages = []; @@ -82,25 +79,6 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { } } - // Extract all of the CODEOWNERS package entries. - final packageOwnershipPattern = RegExp( - r'^((?:third_party/)?packages/(?:[^/]*/)?([^/]*))/\*\*', - ); - for (final String line - in _repoRoot.childFile('CODEOWNERS').readAsLinesSync()) { - final RegExpMatch? match = packageOwnershipPattern.firstMatch(line); - if (match == null) { - continue; - } - final String path = match.group(1)!; - final String name = match.group(2)!; - if (!_repoRoot.childDirectory(path).existsSync()) { - printError('Unknown directory "$path" in CODEOWNERS'); - throw ToolExit(_exitUnknownPackageEntry); - } - _ownedPackages.add(name); - } - // Extract all of the lebeler.yml package entries. // Validate the match rules rather than the label itself, as the labels // don't always correspond 1:1 to packages and package names. @@ -126,16 +104,6 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { final String packageName = package.directory.basename; final errors = []; - // All packages should have an owner. - // Platform interface packages are considered to be owned by the app-facing - // package owner. - if (!(_ownedPackages.contains(packageName) || - package.isPlatformInterface && - _ownedPackages.contains(package.directory.parent.basename))) { - printError('${indentation}Missing CODEOWNERS entry.'); - errors.add('Missing CODEOWNERS entry'); - } - // All packages should have an auto-applied label. For plugins, only the // group needs a rule, so check the app-facing package. if (!(package.isFederated && !package.isAppFacing) && diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 0509f8d25988..ef2a609579ac 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -758,7 +758,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), @@ -1064,7 +1064,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md packages/package_a/lib/foo.dart ''', @@ -1669,7 +1669,7 @@ packages/package_a/$file .gemini/config.yaml AGENTS.md README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md packages/package_a/lib/foo.dart ''', diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index b2f66c5dbd6f..2b8a8c8952fd 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -1133,7 +1133,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), diff --git a/script/tool/test/common/file_filters_test.dart b/script/tool/test/common/file_filters_test.dart index 0143fb3dde30..dd06515509f2 100644 --- a/script/tool/test/common/file_filters_test.dart +++ b/script/tool/test/common/file_filters_test.dart @@ -9,10 +9,10 @@ void main() { group('isRepoLevelNonCodeImpactingFile', () { test('returns true for known non-code files', () { expect(isRepoLevelNonCodeImpactingFile('AUTHORS'), isTrue); - expect(isRepoLevelNonCodeImpactingFile('CODEOWNERS'), isTrue); expect(isRepoLevelNonCodeImpactingFile('CONTRIBUTING.md'), isTrue); expect(isRepoLevelNonCodeImpactingFile('LICENSE'), isTrue); expect(isRepoLevelNonCodeImpactingFile('README.md'), isTrue); + expect(isRepoLevelNonCodeImpactingFile('SUGGESTED_REVIEWERS.md'), isTrue); expect(isRepoLevelNonCodeImpactingFile('AGENTS.md'), isTrue); expect( isRepoLevelNonCodeImpactingFile('.github/PULL_REQUEST_TEMPLATE.md'), diff --git a/script/tool/test/dart_test_command_test.dart b/script/tool/test/dart_test_command_test.dart index 1a16a3703b3f..a6cac49ac0e7 100644 --- a/script/tool/test/dart_test_command_test.dart +++ b/script/tool/test/dart_test_command_test.dart @@ -881,7 +881,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index f4aebdb569ac..6d4e44ca748a 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -1801,7 +1801,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md .gitignore packages/package_a/CHANGELOG.md ''', diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index e469e1f2c645..2fd731ef8ad2 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -1068,7 +1068,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 6c51ed7f2fd1..1a34ebf5acb5 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -492,7 +492,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index 341e0b2892c2..820c45ca6ce0 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -44,22 +44,6 @@ void main() { '''; } - void writeCodeOwners(List ownedPackages) { - final List subpaths = ownedPackages - .map( - (RepositoryPackage p) => p.isFederated - ? [ - p.directory.parent.basename, - p.directory.basename, - ].join('/') - : p.directory.basename, - ) - .toList(); - root.childFile('CODEOWNERS').writeAsStringSync(''' -${subpaths.map((String subpath) => 'packages/$subpath/** @someone').join('\n')} -'''); - } - String readmeTableEntry(String packageName) { final String encodedTag = Uri.encodeComponent('p: $packageName'); return '| [$packageName](./packages/$packageName/) | ' @@ -100,7 +84,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); final List output = await runCapturingPrint(runner, [ 'repo-package-info-check', @@ -130,7 +113,6 @@ ${readmeTableEntry(pluginName)} '''); writeAutoLabelerYaml([packages.first]); writeAutoLabelerYaml([packages.first]); - writeCodeOwners(packages); // 4 packages * 2 checks (git, gh) = 8 calls. // Default mocks in setUp cover 1 call each. We need 3 more each. @@ -162,7 +144,6 @@ ${readmeTableHeader()} ${readmeTableEntry('another_package')} '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -193,7 +174,6 @@ ${readmeTableHeader()} ${readmeTableEntry('another_package')} '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -237,7 +217,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -282,7 +261,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -329,7 +307,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -376,7 +353,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -423,7 +399,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -470,7 +445,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -496,53 +470,15 @@ $entry ); }); - test('fails for missing CODEOWNER', () async { - const packageName = 'a_package'; - final packages = [ - createFakePackage('a_package', packagesDir), - ]; - - root.childFile('README.md').writeAsStringSync(''' -${readmeTableHeader()} -${readmeTableEntry(packageName)} -'''); - writeAutoLabelerYaml(packages); - writeCodeOwners([]); - - Error? commandError; - final List output = await runCapturingPrint( - runner, - ['repo-package-info-check'], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing CODEOWNERS entry.'), - contains( - 'a_package:\n' - ' Missing CODEOWNERS entry', - ), - ]), - ); - }); - test('fails for missing auto-labeler entry', () async { const packageName = 'a_package'; - final packages = [ - createFakePackage('a_package', packagesDir), - ]; + createFakePackage('a_package', packagesDir); root.childFile('README.md').writeAsStringSync(''' ${readmeTableHeader()} ${readmeTableEntry(packageName)} '''); writeAutoLabelerYaml([]); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -578,7 +514,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); package.ciConfigFile.writeAsStringSync(''' release: @@ -603,7 +538,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); final List output = await runCapturingPrint(runner, [ 'repo-package-info-check', @@ -626,7 +560,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); package.ciConfigFile.writeAsStringSync(''' something: true '''); @@ -660,7 +593,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); package.ciConfigFile.writeAsStringSync(''' release: batch: 1 @@ -699,7 +631,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); return package; }