diff --git a/experimental/context_menus/README.md b/experimental/context_menus/README.md index 66a8014d9..89ec986c5 100644 --- a/experimental/context_menus/README.md +++ b/experimental/context_menus/README.md @@ -27,6 +27,10 @@ Shows how to create a context menu with cascading submenus using ### [Custom buttons](https://github.com/flutter/samples/blob/main/experimental/context_menus/lib/custom_buttons_page.dart) Shows how to customize the default buttons in the existing context menus. +### [Custom menu](https://github.com/flutter/samples/blob/main/experimental/context_menus/lib/custom_menu_page.dart) +Shows how to use any custom widgets as the menu itself, including the option to +keep the default buttons. + ### [Default values](https://github.com/flutter/samples/blob/main/experimental/context_menus/lib/default_values_page.dart) Demonstrates how the [contextMenuBuilder](https://master-api.flutter.dev/flutter/material/TextField/contextMenuBuilder.html) diff --git a/experimental/context_menus/lib/custom_menu_page.dart b/experimental/context_menus/lib/custom_menu_page.dart new file mode 100644 index 000000000..fa792f186 --- /dev/null +++ b/experimental/context_menus/lib/custom_menu_page.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'constants.dart'; +import 'platform_selector.dart'; + +class CustomMenuPage extends StatelessWidget { + CustomMenuPage({ + Key? key, + required this.onChangedPlatform, + }) : super(key: key); + + static const String route = 'custom-menu'; + static const String title = 'Custom Menu'; + static const String subtitle = + 'A custom menu built from scratch, but using the default buttons.'; + + final PlatformCallback onChangedPlatform; + + final TextEditingController _controller = TextEditingController( + text: 'Show the menu to see a custom menu with the default buttons.', + ); + + static const String url = '$kCodeUrl/custom_menu_page.dart'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(CustomMenuPage.title), + actions: [ + PlatformSelector( + onChangedPlatform: onChangedPlatform, + ), + IconButton( + icon: const Icon(Icons.code), + onPressed: () async { + if (!await launchUrl(Uri.parse(url))) { + throw 'Could not launch $url'; + } + }, + ), + ], + ), + body: Center( + child: SizedBox( + width: 300.0, + child: TextField( + controller: _controller, + maxLines: 4, + minLines: 2, + contextMenuBuilder: + (BuildContext context, EditableTextState editableTextState) { + return _MyContextMenu( + anchor: editableTextState.contextMenuAnchors.primaryAnchor, + children: AdaptiveTextSelectionToolbar.getAdaptiveButtons( + context, + editableTextState.contextMenuButtonItems, + ).toList(), + ); + }, + ), + ), + ), + ); + } +} + +class _MyContextMenu extends StatelessWidget { + const _MyContextMenu({ + required this.anchor, + required this.children, + }); + + final Offset anchor; + final List children; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: anchor.dy, + left: anchor.dx, + child: Container( + width: 200.0, + height: 200.0, + color: Colors.amberAccent, + child: Column( + children: children, + ), + ), + ), + ], + ); + } +} diff --git a/experimental/context_menus/lib/main.dart b/experimental/context_menus/lib/main.dart index 535884b01..0a5ff6a47 100644 --- a/experimental/context_menus/lib/main.dart +++ b/experimental/context_menus/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'anywhere_page.dart'; import 'cascading_menu_page.dart'; import 'custom_buttons_page.dart'; +import 'custom_menu_page.dart'; import 'default_values_page.dart'; import 'email_button_page.dart'; import 'field_types_page.dart'; @@ -51,6 +52,8 @@ class _MyAppState extends State { AnywherePage(onChangedPlatform: onChangedPlatform), CustomButtonsPage.route: (context) => CustomButtonsPage(onChangedPlatform: onChangedPlatform), + CustomMenuPage.route: (context) => + CustomMenuPage(onChangedPlatform: onChangedPlatform), ReorderedButtonsPage.route: (context) => ReorderedButtonsPage(onChangedPlatform: onChangedPlatform), EmailButtonPage.route: (context) => @@ -115,6 +118,11 @@ class MyHomePage extends StatelessWidget { title: CustomButtonsPage.title, subtitle: CustomButtonsPage.subtitle, ), + _MyListItem( + route: CustomMenuPage.route, + title: CustomMenuPage.title, + subtitle: CustomMenuPage.subtitle, + ), _MyListItem( route: EmailButtonPage.route, title: EmailButtonPage.title, diff --git a/experimental/context_menus/test/cascading_menu_page_test.dart b/experimental/context_menus/test/cascading_menu_page_test.dart index a3eba5ddf..0bf8ed2ff 100644 --- a/experimental/context_menus/test/cascading_menu_page_test.dart +++ b/experimental/context_menus/test/cascading_menu_page_test.dart @@ -14,7 +14,7 @@ void main() { await tester.dragUntilVisible( find.text(CascadingMenuPage.title), find.byType(ListView), - const Offset(0.0, -300.0), + const Offset(0.0, -250.0), ); await tester.tap(find.text(CascadingMenuPage.title)); await tester.pumpAndSettle(); diff --git a/experimental/context_menus/test/custom_menu_page_test.dart b/experimental/context_menus/test/custom_menu_page_test.dart new file mode 100644 index 000000000..2c30cbd7e --- /dev/null +++ b/experimental/context_menus/test/custom_menu_page_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:context_menus/main.dart'; +import 'package:context_menus/custom_menu_page.dart'; + +void main() { + testWidgets('Shows default buttons in a custom context menu', + (WidgetTester tester) async { + await tester.pumpWidget(const MyApp()); + + // Navigate to the CustomMenuPage example. + await tester.dragUntilVisible( + find.text(CustomMenuPage.title), + find.byType(ListView), + const Offset(0.0, -200.0), + ); + await tester.tap(find.text(CustomMenuPage.title)); + await tester.pumpAndSettle(); + + // Right click on the text field to show the context menu. + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(EditableText)), + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + + // A custom context menu is shown, and the buttons are the default ones. + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing); + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + expect( + find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2)); + break; + case TargetPlatform.macOS: + expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), + findsNWidgets(2)); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + expect(find.byType(TextSelectionToolbarTextButton), findsNWidgets(1)); + break; + case TargetPlatform.linux: + case TargetPlatform.windows: + expect( + find.byType(DesktopTextSelectionToolbarButton), findsNWidgets(1)); + break; + } + }); +}