mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Move context menus out of experimental (#2134)
Now that the context menu code is on stable, move the sample out of experimental. Fixes https://github.com/flutter/samples/issues/2110 ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. --------- Co-authored-by: Brett Morgan <brett.morgan@gmail.com>
This commit is contained in:
committed by
GitHub
parent
9b17f50671
commit
6c8d54a82b
100
context_menus/lib/anywhere_page.dart
Normal file
100
context_menus/lib/anywhere_page.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'context_menu_region.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class AnywherePage extends StatelessWidget {
|
||||
AnywherePage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'anywhere';
|
||||
static const String title = 'Context Menu Anywhere Example';
|
||||
static const String subtitle = 'A context menu outside of a text field';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _materialController = TextEditingController(
|
||||
text: 'TextField shows the default menu still.',
|
||||
);
|
||||
final TextEditingController _cupertinoController = TextEditingController(
|
||||
text: 'CupertinoTextField shows the default menu still.',
|
||||
);
|
||||
final TextEditingController _editableController = TextEditingController(
|
||||
text: 'EditableText has no default menu, so it shows the custom one.',
|
||||
);
|
||||
|
||||
static const String url = '$kCodeUrl/anywhere_page.dart';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(AnywherePage.title),
|
||||
actions: <Widget>[
|
||||
PlatformSelector(
|
||||
onChangedPlatform: onChangedPlatform,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.code),
|
||||
onPressed: () async {
|
||||
if (!await launchUrl(Uri.parse(url))) {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ContextMenuRegion(
|
||||
contextMenuBuilder: (context, primaryAnchor, [secondaryAnchor]) {
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: TextSelectionToolbarAnchors(
|
||||
primaryAnchor: primaryAnchor,
|
||||
secondaryAnchor: secondaryAnchor as Offset?,
|
||||
),
|
||||
buttonItems: <ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: 'Back',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 64.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(height: 20.0),
|
||||
const Text(
|
||||
'Right click anywhere outside of a field to show a custom menu.',
|
||||
),
|
||||
Container(height: 140.0),
|
||||
CupertinoTextField(controller: _cupertinoController),
|
||||
Container(height: 40.0),
|
||||
TextField(controller: _materialController),
|
||||
Container(height: 40.0),
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: EditableText(
|
||||
controller: _editableController,
|
||||
focusNode: FocusNode(),
|
||||
style: Typography.material2021().black.displayMedium!,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
299
context_menus/lib/cascading_menu_page.dart
Normal file
299
context_menus/lib/cascading_menu_page.dart
Normal file
@@ -0,0 +1,299 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'context_menu_region.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
// This example is based on the MenuBar docs example:
|
||||
// https://master-api.flutter.dev/flutter/material/MenuBar-class.html
|
||||
|
||||
class CascadingMenuPage extends StatelessWidget {
|
||||
const CascadingMenuPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'cascading';
|
||||
static const String title = 'Cascading Menu Example';
|
||||
static const String subtitle = 'A context menu with nested submenus.';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
static const String url = '$kCodeUrl/anywhere_page.dart';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(CascadingMenuPage.title),
|
||||
actions: <Widget>[
|
||||
PlatformSelector(
|
||||
onChangedPlatform: onChangedPlatform,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.code),
|
||||
onPressed: () async {
|
||||
if (!await launchUrl(Uri.parse(url))) {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: const _MyContextMenuRegion(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MyContextMenuRegion extends StatefulWidget {
|
||||
const _MyContextMenuRegion();
|
||||
|
||||
@override
|
||||
State<_MyContextMenuRegion> createState() => _MyContextMenuRegionState();
|
||||
}
|
||||
|
||||
class _MyContextMenuRegionState extends State<_MyContextMenuRegion> {
|
||||
String? _lastSelection;
|
||||
|
||||
Color get backgroundColor => _backgroundColor;
|
||||
Color _backgroundColor = Colors.red;
|
||||
set backgroundColor(Color value) {
|
||||
if (_backgroundColor != value) {
|
||||
setState(() {
|
||||
_backgroundColor = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool get showingMessage => _showMessage;
|
||||
bool _showMessage = true;
|
||||
set showingMessage(bool value) {
|
||||
if (_showMessage != value) {
|
||||
setState(() {
|
||||
_showMessage = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContextMenuRegion(
|
||||
contextMenuBuilder: (context, primaryAnchor, [secondaryAnchor]) {
|
||||
return _MyCascadingContextMenu(
|
||||
anchor: primaryAnchor,
|
||||
showingMessage: _showMessage,
|
||||
onToggleMessageVisibility: () {
|
||||
setState(() {
|
||||
_showMessage = !_showMessage;
|
||||
});
|
||||
},
|
||||
onChangeBackgroundColor: (color) {
|
||||
setState(() {
|
||||
_backgroundColor = color;
|
||||
});
|
||||
},
|
||||
onChangeSelection: (selection) {
|
||||
setState(() {
|
||||
_lastSelection = selection;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
color: backgroundColor,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
showingMessage
|
||||
? 'Right click or long press anywhere to show the cascading menu.'
|
||||
: '',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_lastSelection != null ? 'Last Selected: $_lastSelection' : ''),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A class for consolidating the definition of menu entries.
|
||||
///
|
||||
/// This sort of class is not required, but illustrates one way that defining
|
||||
/// menus could be done.
|
||||
class MenuEntry {
|
||||
const MenuEntry(
|
||||
{required this.label, this.shortcut, this.onPressed, this.menuChildren})
|
||||
: assert(menuChildren == null || onPressed == null,
|
||||
'onPressed is ignored if menuChildren are provided');
|
||||
final String label;
|
||||
|
||||
final MenuSerializableShortcut? shortcut;
|
||||
final VoidCallback? onPressed;
|
||||
final List<MenuEntry>? menuChildren;
|
||||
|
||||
static List<Widget> build(List<MenuEntry> selections) {
|
||||
Widget buildSelection(MenuEntry selection) {
|
||||
if (selection.menuChildren != null) {
|
||||
return SubmenuButton(
|
||||
menuChildren: MenuEntry.build(selection.menuChildren!),
|
||||
child: Text(selection.label),
|
||||
);
|
||||
}
|
||||
return MenuItemButton(
|
||||
shortcut: selection.shortcut,
|
||||
onPressed: selection.onPressed,
|
||||
child: Text(selection.label),
|
||||
);
|
||||
}
|
||||
|
||||
return selections.map<Widget>(buildSelection).toList();
|
||||
}
|
||||
|
||||
static Map<MenuSerializableShortcut, Intent> shortcuts(
|
||||
List<MenuEntry> selections) {
|
||||
final Map<MenuSerializableShortcut, Intent> result =
|
||||
<MenuSerializableShortcut, Intent>{};
|
||||
for (final MenuEntry selection in selections) {
|
||||
if (selection.menuChildren != null) {
|
||||
result.addAll(MenuEntry.shortcuts(selection.menuChildren!));
|
||||
} else {
|
||||
if (selection.shortcut != null && selection.onPressed != null) {
|
||||
result[selection.shortcut!] =
|
||||
VoidCallbackIntent(selection.onPressed!);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
typedef _StringCallback = void Function(String string);
|
||||
typedef _ColorCallback = void Function(Color color);
|
||||
|
||||
class _MyCascadingContextMenu extends StatefulWidget {
|
||||
const _MyCascadingContextMenu({
|
||||
required this.anchor,
|
||||
required this.onToggleMessageVisibility,
|
||||
required this.onChangeBackgroundColor,
|
||||
required this.onChangeSelection,
|
||||
required this.showingMessage,
|
||||
});
|
||||
|
||||
final Offset anchor;
|
||||
final VoidCallback onToggleMessageVisibility;
|
||||
final _ColorCallback onChangeBackgroundColor;
|
||||
final _StringCallback onChangeSelection;
|
||||
final bool showingMessage;
|
||||
|
||||
@override
|
||||
State<_MyCascadingContextMenu> createState() =>
|
||||
_MyCascadingContextMenuState();
|
||||
}
|
||||
|
||||
class _MyCascadingContextMenuState extends State<_MyCascadingContextMenu> {
|
||||
ShortcutRegistryEntry? _shortcutsEntry;
|
||||
|
||||
List<MenuEntry> get _menus {
|
||||
final List<MenuEntry> result = <MenuEntry>[
|
||||
MenuEntry(
|
||||
label: 'About',
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'MenuBar Sample',
|
||||
applicationVersion: '1.0.0',
|
||||
);
|
||||
widget.onChangeSelection('About');
|
||||
},
|
||||
),
|
||||
MenuEntry(
|
||||
label: widget.showingMessage ? 'Hide' : 'Show',
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
widget.onChangeSelection(
|
||||
widget.showingMessage ? 'Hide Message' : 'Show Message');
|
||||
widget.onToggleMessageVisibility();
|
||||
},
|
||||
shortcut: const SingleActivator(LogicalKeyboardKey.keyS, control: true),
|
||||
),
|
||||
// Hides the message, but is only enabled if the message isn't
|
||||
// already hidden.
|
||||
MenuEntry(
|
||||
label: 'Reset',
|
||||
onPressed: widget.showingMessage
|
||||
? () {
|
||||
ContextMenuController.removeAny();
|
||||
widget.onChangeSelection('Reset');
|
||||
widget.onToggleMessageVisibility();
|
||||
}
|
||||
: null,
|
||||
shortcut: const SingleActivator(LogicalKeyboardKey.escape),
|
||||
),
|
||||
MenuEntry(
|
||||
label: 'Color',
|
||||
menuChildren: <MenuEntry>[
|
||||
MenuEntry(
|
||||
label: 'Red',
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
widget.onChangeSelection('Red Background');
|
||||
widget.onChangeBackgroundColor(Colors.red);
|
||||
},
|
||||
shortcut:
|
||||
const SingleActivator(LogicalKeyboardKey.keyR, control: true),
|
||||
),
|
||||
MenuEntry(
|
||||
label: 'Green',
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
widget.onChangeSelection('Green Background');
|
||||
widget.onChangeBackgroundColor(Colors.green);
|
||||
},
|
||||
shortcut:
|
||||
const SingleActivator(LogicalKeyboardKey.keyG, control: true),
|
||||
),
|
||||
MenuEntry(
|
||||
label: 'Blue',
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
widget.onChangeSelection('Blue Background');
|
||||
widget.onChangeBackgroundColor(Colors.blue);
|
||||
},
|
||||
shortcut:
|
||||
const SingleActivator(LogicalKeyboardKey.keyB, control: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
// (Re-)register the shortcuts with the ShortcutRegistry so that they are
|
||||
// available to the entire application, and update them if they've changed.
|
||||
_shortcutsEntry?.dispose();
|
||||
_shortcutsEntry =
|
||||
ShortcutRegistry.of(context).addAll(MenuEntry.shortcuts(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shortcutsEntry?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopTextSelectionToolbar(
|
||||
anchor: widget.anchor,
|
||||
children: MenuEntry.build(_menus),
|
||||
);
|
||||
}
|
||||
}
|
||||
2
context_menus/lib/constants.dart
Normal file
2
context_menus/lib/constants.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
const String kCodeUrl =
|
||||
'https://github.com/flutter/samples/blob/experimental/context_menus/lib';
|
||||
97
context_menus/lib/context_menu_region.dart
Normal file
97
context_menus/lib/context_menu_region.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ContextMenuBuilder = Widget Function(
|
||||
BuildContext context, Offset offset);
|
||||
|
||||
/// Shows and hides the context menu based on user gestures.
|
||||
///
|
||||
/// By default, shows the menu on right clicks and long presses.
|
||||
class ContextMenuRegion extends StatefulWidget {
|
||||
/// Creates an instance of [ContextMenuRegion].
|
||||
const ContextMenuRegion({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.contextMenuBuilder,
|
||||
});
|
||||
|
||||
/// Builds the context menu.
|
||||
final ContextMenuBuilder contextMenuBuilder;
|
||||
|
||||
/// The child widget that will be listened to for gestures.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<ContextMenuRegion> createState() => _ContextMenuRegionState();
|
||||
}
|
||||
|
||||
class _ContextMenuRegionState extends State<ContextMenuRegion> {
|
||||
Offset? _longPressOffset;
|
||||
|
||||
final ContextMenuController _contextMenuController = ContextMenuController();
|
||||
|
||||
static bool get _longPressEnabled {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
return true;
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _onSecondaryTapUp(TapUpDetails details) {
|
||||
_show(details.globalPosition);
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (!_contextMenuController.isShown) {
|
||||
return;
|
||||
}
|
||||
_hide();
|
||||
}
|
||||
|
||||
void _onLongPressStart(LongPressStartDetails details) {
|
||||
_longPressOffset = details.globalPosition;
|
||||
}
|
||||
|
||||
void _onLongPress() {
|
||||
assert(_longPressOffset != null);
|
||||
_show(_longPressOffset!);
|
||||
_longPressOffset = null;
|
||||
}
|
||||
|
||||
void _show(Offset position) {
|
||||
_contextMenuController.show(
|
||||
context: context,
|
||||
contextMenuBuilder: (context) {
|
||||
return widget.contextMenuBuilder(context, position);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _hide() {
|
||||
_contextMenuController.remove();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hide();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onSecondaryTapUp: _onSecondaryTapUp,
|
||||
onTap: _onTap,
|
||||
onLongPress: _longPressEnabled ? _onLongPress : null,
|
||||
onLongPressStart: _longPressEnabled ? _onLongPressStart : null,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
85
context_menus/lib/custom_buttons_page.dart
Normal file
85
context_menus/lib/custom_buttons_page.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class CustomButtonsPage extends StatelessWidget {
|
||||
CustomButtonsPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'custom-buttons';
|
||||
static const String title = 'Custom Buttons';
|
||||
static const String subtitle =
|
||||
'The usual buttons, but with a custom appearance.';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _controller = TextEditingController(
|
||||
text:
|
||||
'Show the menu to see the usual default buttons, but with a custom appearance.',
|
||||
);
|
||||
|
||||
static const String url = '$kCodeUrl/custom_buttons_page.dart';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(CustomButtonsPage.title),
|
||||
actions: <Widget>[
|
||||
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: (context, editableTextState) {
|
||||
return AdaptiveTextSelectionToolbar(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
// Build the default buttons, but make them look custom.
|
||||
// Note that in a real project you may want to build
|
||||
// different buttons depending on the platform.
|
||||
children:
|
||||
editableTextState.contextMenuButtonItems.map((buttonItem) {
|
||||
return CupertinoButton(
|
||||
borderRadius: null,
|
||||
color: const Color(0xffaaaa00),
|
||||
disabledColor: const Color(0xffaaaaff),
|
||||
onPressed: buttonItem.onPressed,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
pressedOpacity: 0.7,
|
||||
child: SizedBox(
|
||||
width: 200.0,
|
||||
child: Text(
|
||||
CupertinoTextSelectionToolbarButton.getButtonLabel(
|
||||
context, buttonItem),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
96
context_menus/lib/custom_menu_page.dart
Normal file
96
context_menus/lib/custom_menu_page.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class CustomMenuPage extends StatelessWidget {
|
||||
CustomMenuPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
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: <Widget>[
|
||||
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: (context, 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<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
top: anchor.dy,
|
||||
left: anchor.dx,
|
||||
child: Container(
|
||||
width: 200.0,
|
||||
height: 200.0,
|
||||
color: Colors.amberAccent,
|
||||
child: Column(
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
110
context_menus/lib/default_values_page.dart
Normal file
110
context_menus/lib/default_values_page.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class DefaultValuesPage extends StatelessWidget {
|
||||
DefaultValuesPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'default-values';
|
||||
static const String title = 'Default API Values Example';
|
||||
static const String subtitle =
|
||||
'Shows what happens when you pass various things into contextMenuBuilder.';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _controllerNone = TextEditingController(
|
||||
text: "When contextMenuBuilder isn't given anything at all.",
|
||||
);
|
||||
|
||||
final TextEditingController _controllerNull = TextEditingController(
|
||||
text: "When contextMenuBuilder is explicitly given null.",
|
||||
);
|
||||
|
||||
final TextEditingController _controllerCustom = TextEditingController(
|
||||
text: "When something custom is passed to contextMenuBuilder.",
|
||||
);
|
||||
|
||||
static const String url = '$kCodeUrl/default_values_page.dart';
|
||||
|
||||
DialogRoute _showDialog(BuildContext context, String message) {
|
||||
return DialogRoute<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(title: Text(message)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(DefaultValuesPage.title),
|
||||
actions: <Widget>[
|
||||
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: 400.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'This example simply shows what happens when contextMenuBuilder is given null, a custom value, or omitted altogether.',
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40.0,
|
||||
),
|
||||
TextField(
|
||||
maxLines: 2,
|
||||
minLines: 2,
|
||||
controller: _controllerNone,
|
||||
),
|
||||
TextField(
|
||||
maxLines: 2,
|
||||
minLines: 2,
|
||||
controller: _controllerNull,
|
||||
contextMenuBuilder: null,
|
||||
),
|
||||
TextField(
|
||||
maxLines: 2,
|
||||
minLines: 2,
|
||||
controller: _controllerCustom,
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
buttonItems: <ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
label: 'Custom button',
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.of(context).push(_showDialog(
|
||||
context, 'You clicked the custom button.'));
|
||||
},
|
||||
),
|
||||
...editableTextState.contextMenuButtonItems,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
context_menus/lib/email_button_page.dart
Normal file
94
context_menus/lib/email_button_page.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'is_valid_email.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class EmailButtonPage extends StatelessWidget {
|
||||
EmailButtonPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'email-button';
|
||||
static const String title = 'Email Button';
|
||||
static const String subtitle = 'A selection-aware email button';
|
||||
static const String url = '$kCodeUrl/email_button_page.dart';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _controller = TextEditingController(
|
||||
text: 'Select the email address and open the menu: me@example.com',
|
||||
);
|
||||
|
||||
DialogRoute _showDialog(BuildContext context) {
|
||||
return DialogRoute<void>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const AlertDialog(title: Text('You clicked send email!')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(EmailButtonPage.title),
|
||||
actions: <Widget>[
|
||||
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: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 20.0),
|
||||
const Text(
|
||||
'This example shows how to add a special button to the context menu depending on the current selection.',
|
||||
),
|
||||
const SizedBox(height: 40.0),
|
||||
TextField(
|
||||
maxLines: 2,
|
||||
controller: _controller,
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
final TextEditingValue value =
|
||||
editableTextState.textEditingValue;
|
||||
final List<ContextMenuButtonItem> buttonItems =
|
||||
editableTextState.contextMenuButtonItems;
|
||||
if (isValidEmail(value.selection.textInside(value.text))) {
|
||||
buttonItems.insert(
|
||||
0,
|
||||
ContextMenuButtonItem(
|
||||
label: 'Send email',
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.of(context).push(_showDialog(context));
|
||||
},
|
||||
));
|
||||
}
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
buttonItems: buttonItems,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
132
context_menus/lib/field_types_page.dart
Normal file
132
context_menus/lib/field_types_page.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class FieldTypesPage extends StatelessWidget {
|
||||
FieldTypesPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'field-types';
|
||||
static const String title = 'The Context Menu in Different Field Types';
|
||||
static const String subtitle =
|
||||
'How contextual menus work in TextField, CupertinoTextField, and EditableText';
|
||||
static const String url = '$kCodeUrl/field_types_page.dart';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _controller = TextEditingController(
|
||||
text:
|
||||
"Material text field shows the menu for any platform by default. You'll see the correct menu for your platform here.",
|
||||
);
|
||||
|
||||
final TextEditingController _cupertinoController = TextEditingController(
|
||||
text:
|
||||
"CupertinoTextField can't show Material menus by default. On non-Apple platforms, you'll still see a Cupertino menu here.",
|
||||
);
|
||||
|
||||
final TextEditingController _cupertinoControllerFixed = TextEditingController(
|
||||
text:
|
||||
"But CupertinoTextField can be made to adaptively show any menu. You'll see the correct menu for your platform here.",
|
||||
);
|
||||
|
||||
final TextEditingController _cupertinoControllerForced =
|
||||
TextEditingController(
|
||||
text: 'Or forced to always show a specific menu (Material desktop menu).',
|
||||
);
|
||||
|
||||
final TextEditingController _editableController = TextEditingController(
|
||||
text:
|
||||
"EditableText doesn't show any selection menu by itself, even when contextMenuBuilder is passed.",
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(FieldTypesPage.title),
|
||||
actions: <Widget>[
|
||||
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: 400.0,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 20.0),
|
||||
TextField(
|
||||
maxLines: 3,
|
||||
controller: _controller,
|
||||
),
|
||||
const SizedBox(height: 60.0),
|
||||
CupertinoTextField(
|
||||
maxLines: 3,
|
||||
controller: _cupertinoController,
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
CupertinoTextField(
|
||||
maxLines: 3,
|
||||
controller: _cupertinoControllerFixed,
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
return AdaptiveTextSelectionToolbar.editableText(
|
||||
editableTextState: editableTextState,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
CupertinoTextField(
|
||||
maxLines: 3,
|
||||
controller: _cupertinoControllerForced,
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
return DesktopTextSelectionToolbar(
|
||||
anchor: editableTextState.contextMenuAnchors.primaryAnchor,
|
||||
children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
|
||||
context,
|
||||
editableTextState.contextMenuButtonItems,
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 60.0),
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: EditableText(
|
||||
maxLines: 3,
|
||||
controller: _editableController,
|
||||
focusNode: FocusNode(),
|
||||
style: Typography.material2021().black.displayMedium!,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.white,
|
||||
// contextMenuBuilder doesn't do anything here!
|
||||
// EditableText has no built-in gesture detection for
|
||||
// selection. A wrapper would have to implement
|
||||
// TextSelectionGestureDetectorBuilderDelegate, etc.
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
return AdaptiveTextSelectionToolbar.editableText(
|
||||
editableTextState: editableTextState,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
159
context_menus/lib/full_page.dart
Normal file
159
context_menus/lib/full_page.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'context_menu_region.dart';
|
||||
import 'is_valid_email.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class FullPage extends StatelessWidget {
|
||||
FullPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'full';
|
||||
static const String title = 'Combined Example';
|
||||
static const String subtitle =
|
||||
'Combining several different types of custom menus.';
|
||||
static const String url = '$kCodeUrl/full_page.dart';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _controller = TextEditingController(
|
||||
text: 'Custom menus everywhere. me@example.com',
|
||||
);
|
||||
|
||||
DialogRoute _showDialog(BuildContext context, String message) {
|
||||
return DialogRoute<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(title: Text(message)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(FullPage.title),
|
||||
actions: <Widget>[
|
||||
PlatformSelector(
|
||||
onChangedPlatform: onChangedPlatform,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.code),
|
||||
onPressed: () async {
|
||||
if (!await launchUrl(Uri.parse(url))) {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ContextMenuRegion(
|
||||
contextMenuBuilder: (context, offset) {
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: TextSelectionToolbarAnchors(
|
||||
primaryAnchor: offset,
|
||||
),
|
||||
buttonItems: <ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: 'Back',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 400.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'This example simply shows how many of the previous examples can be combined in a single app.',
|
||||
),
|
||||
const SizedBox(
|
||||
height: 60.0,
|
||||
),
|
||||
ContextMenuRegion(
|
||||
contextMenuBuilder: (context, offset) {
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: TextSelectionToolbarAnchors(
|
||||
primaryAnchor: offset,
|
||||
),
|
||||
buttonItems: <ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.of(context).push(_showDialog(
|
||||
context, 'Image saved! (not really though)'));
|
||||
},
|
||||
label: 'Save',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: 200.0,
|
||||
height: 200.0,
|
||||
child: FlutterLogo(),
|
||||
),
|
||||
),
|
||||
Container(height: 20.0),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
final TextEditingValue value =
|
||||
editableTextState.textEditingValue;
|
||||
final List<ContextMenuButtonItem> buttonItems =
|
||||
editableTextState.contextMenuButtonItems;
|
||||
if (isValidEmail(value.selection.textInside(value.text))) {
|
||||
buttonItems.insert(
|
||||
0,
|
||||
ContextMenuButtonItem(
|
||||
label: 'Send email',
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.of(context).push(_showDialog(
|
||||
context, 'You clicked send email'));
|
||||
},
|
||||
));
|
||||
}
|
||||
return AdaptiveTextSelectionToolbar(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
// Build the default buttons, but make them look crazy.
|
||||
// Note that in a real project you may want to build
|
||||
// different buttons depending on the platform.
|
||||
children: buttonItems.map((buttonItem) {
|
||||
return CupertinoButton(
|
||||
borderRadius: null,
|
||||
color: const Color(0xffaaaa00),
|
||||
disabledColor: const Color(0xffaaaaff),
|
||||
onPressed: buttonItem.onPressed,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
pressedOpacity: 0.7,
|
||||
child: SizedBox(
|
||||
width: 200.0,
|
||||
child: Text(
|
||||
CupertinoTextSelectionToolbarButton
|
||||
.getButtonLabel(context, buttonItem),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
80
context_menus/lib/global_selection_page.dart
Normal file
80
context_menus/lib/global_selection_page.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class GlobalSelectionPage extends StatelessWidget {
|
||||
GlobalSelectionPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'global-selection';
|
||||
static const String title = 'Global Selection Example';
|
||||
static const String subtitle = 'Context menus in and out of global selection';
|
||||
static const String url = '$kCodeUrl/global_selection_page.dart';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _controller = TextEditingController(
|
||||
text: 'TextFields still show their specific context menu.',
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectionArea(
|
||||
contextMenuBuilder: (context, selectableRegionState) {
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: selectableRegionState.contextMenuAnchors,
|
||||
buttonItems: <ContextMenuButtonItem>[
|
||||
...selectableRegionState.contextMenuButtonItems,
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: 'Back',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(GlobalSelectionPage.title),
|
||||
actions: <Widget>[
|
||||
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: 400.0,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 20.0),
|
||||
const Text(
|
||||
'This entire page is wrapped in a SelectionArea with a custom context menu. Clicking on any of the plain text, including the AppBar title, will show the custom menu.',
|
||||
),
|
||||
const SizedBox(height: 40.0),
|
||||
TextField(controller: _controller),
|
||||
const SizedBox(height: 40.0),
|
||||
const SelectableText(
|
||||
'SelectableText also shows its own separate context menu.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
83
context_menus/lib/image_page.dart
Normal file
83
context_menus/lib/image_page.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'context_menu_region.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class ImagePage extends StatelessWidget {
|
||||
const ImagePage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'image';
|
||||
static const String title = 'ContextMenu on an Image';
|
||||
static const String subtitle =
|
||||
'A ContextMenu the displays on an Image widget';
|
||||
static const String url = '$kCodeUrl/image_page.dart';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
DialogRoute _showDialog(BuildContext context) {
|
||||
return DialogRoute<void>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const AlertDialog(title: Text('Image saved! (not really though)')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(ImagePage.title),
|
||||
actions: <Widget>[
|
||||
PlatformSelector(
|
||||
onChangedPlatform: onChangedPlatform,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.code),
|
||||
onPressed: () async {
|
||||
if (!await launchUrl(Uri.parse(url))) {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ContextMenuRegion(
|
||||
contextMenuBuilder: (context, offset) {
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: TextSelectionToolbarAnchors(
|
||||
primaryAnchor: offset,
|
||||
),
|
||||
buttonItems: <ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.of(context).push(_showDialog(context));
|
||||
},
|
||||
label: 'Save',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: 200.0,
|
||||
height: 200.0,
|
||||
child: FlutterLogo(),
|
||||
),
|
||||
),
|
||||
Container(height: 20.0),
|
||||
const Text(
|
||||
'Right click or long press on the image to see a special menu.',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
context_menus/lib/is_valid_email.dart
Normal file
10
context_menus/lib/is_valid_email.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
/// Returns true if the given String is a valid email address.
|
||||
bool isValidEmail(String text) {
|
||||
return RegExp(
|
||||
r'(?<name>[a-zA-Z0-9]+)'
|
||||
r'@'
|
||||
r'(?<domain>[a-zA-Z0-9]+)'
|
||||
r'\.'
|
||||
r'(?<topLevelDomain>[a-zA-Z0-9]+)',
|
||||
).hasMatch(text);
|
||||
}
|
||||
215
context_menus/lib/main.dart
Normal file
215
context_menus/lib/main.dart
Normal file
@@ -0,0 +1,215 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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';
|
||||
import 'full_page.dart';
|
||||
import 'global_selection_page.dart';
|
||||
import 'image_page.dart';
|
||||
import 'modified_action_page.dart';
|
||||
import 'platform_selector.dart';
|
||||
import 'reordered_buttons_page.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
void onChangedPlatform(TargetPlatform platform) {
|
||||
setState(() {
|
||||
debugDefaultTargetPlatformOverride = platform;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// On web, disable the browser's context menu everywhere so that the custom
|
||||
// Flutter-rendered context menu shows.
|
||||
if (kIsWeb) {
|
||||
BrowserContextMenu.disableContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (kIsWeb) {
|
||||
BrowserContextMenu.enableContextMenu();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Flutter Context Menu Examples',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
platform: defaultTargetPlatform,
|
||||
useMaterial3: true,
|
||||
),
|
||||
initialRoute: '/',
|
||||
routes: <String, Widget Function(BuildContext)>{
|
||||
'/': (context) => MyHomePage(onChangedPlatform: onChangedPlatform),
|
||||
AnywherePage.route: (context) =>
|
||||
AnywherePage(onChangedPlatform: onChangedPlatform),
|
||||
CustomButtonsPage.route: (context) =>
|
||||
CustomButtonsPage(onChangedPlatform: onChangedPlatform),
|
||||
CustomMenuPage.route: (context) =>
|
||||
CustomMenuPage(onChangedPlatform: onChangedPlatform),
|
||||
ReorderedButtonsPage.route: (context) =>
|
||||
ReorderedButtonsPage(onChangedPlatform: onChangedPlatform),
|
||||
EmailButtonPage.route: (context) =>
|
||||
EmailButtonPage(onChangedPlatform: onChangedPlatform),
|
||||
ImagePage.route: (context) =>
|
||||
ImagePage(onChangedPlatform: onChangedPlatform),
|
||||
FieldTypesPage.route: (context) =>
|
||||
FieldTypesPage(onChangedPlatform: onChangedPlatform),
|
||||
FullPage.route: (context) =>
|
||||
FullPage(onChangedPlatform: onChangedPlatform),
|
||||
ModifiedActionPage.route: (context) =>
|
||||
ModifiedActionPage(onChangedPlatform: onChangedPlatform),
|
||||
GlobalSelectionPage.route: (context) =>
|
||||
GlobalSelectionPage(onChangedPlatform: onChangedPlatform),
|
||||
DefaultValuesPage.route: (context) =>
|
||||
DefaultValuesPage(onChangedPlatform: onChangedPlatform),
|
||||
CascadingMenuPage.route: (context) =>
|
||||
CascadingMenuPage(onChangedPlatform: onChangedPlatform),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatelessWidget {
|
||||
const MyHomePage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Context Menu Demos'),
|
||||
actions: <Widget>[
|
||||
PlatformSelector(
|
||||
onChangedPlatform: onChangedPlatform,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: const <Widget>[
|
||||
_MyListItem(
|
||||
route: AnywherePage.route,
|
||||
title: AnywherePage.title,
|
||||
subtitle: AnywherePage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: GlobalSelectionPage.route,
|
||||
title: GlobalSelectionPage.title,
|
||||
subtitle: GlobalSelectionPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: ImagePage.route,
|
||||
title: ImagePage.title,
|
||||
subtitle: ImagePage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: CustomButtonsPage.route,
|
||||
title: CustomButtonsPage.title,
|
||||
subtitle: CustomButtonsPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: CustomMenuPage.route,
|
||||
title: CustomMenuPage.title,
|
||||
subtitle: CustomMenuPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: EmailButtonPage.route,
|
||||
title: EmailButtonPage.title,
|
||||
subtitle: EmailButtonPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: ReorderedButtonsPage.route,
|
||||
title: ReorderedButtonsPage.title,
|
||||
subtitle: ReorderedButtonsPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: ModifiedActionPage.route,
|
||||
title: ModifiedActionPage.title,
|
||||
subtitle: ModifiedActionPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: FieldTypesPage.route,
|
||||
title: FieldTypesPage.title,
|
||||
subtitle: FieldTypesPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: DefaultValuesPage.route,
|
||||
title: DefaultValuesPage.title,
|
||||
subtitle: DefaultValuesPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: CascadingMenuPage.route,
|
||||
title: CascadingMenuPage.title,
|
||||
subtitle: CascadingMenuPage.subtitle,
|
||||
),
|
||||
_MyListItem(
|
||||
route: FullPage.route,
|
||||
title: FullPage.title,
|
||||
subtitle: FullPage.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MyListItem extends StatelessWidget {
|
||||
const _MyListItem({
|
||||
required this.route,
|
||||
required this.subtitle,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final String route;
|
||||
final String subtitle;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(route);
|
||||
},
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(12.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: ListTile(
|
||||
title: Text(title),
|
||||
subtitle: Text(subtitle),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
97
context_menus/lib/modified_action_page.dart
Normal file
97
context_menus/lib/modified_action_page.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class ModifiedActionPage extends StatelessWidget {
|
||||
ModifiedActionPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'modified-action';
|
||||
static const String title = 'Modified Action';
|
||||
static const String subtitle =
|
||||
'The copy button copies but also shows a menu.';
|
||||
static const String url = '$kCodeUrl/modified_action_page.dart';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _controller = TextEditingController(
|
||||
text: 'Try using the copy button.',
|
||||
);
|
||||
|
||||
DialogRoute _showDialog(BuildContext context) {
|
||||
return DialogRoute<void>(
|
||||
context: context,
|
||||
builder: (context) => const AlertDialog(
|
||||
title: Text('Copied, but also showed this dialog.')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(ModifiedActionPage.title),
|
||||
actions: <Widget>[
|
||||
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: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'This example shows adding to the behavior of a default button.',
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30.0,
|
||||
),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
final List<ContextMenuButtonItem> buttonItems =
|
||||
editableTextState.contextMenuButtonItems;
|
||||
// Modify the copy buttonItem to show a dialog after copying.
|
||||
final int copyButtonIndex = buttonItems.indexWhere(
|
||||
(buttonItem) {
|
||||
return buttonItem.type == ContextMenuButtonType.copy;
|
||||
},
|
||||
);
|
||||
if (copyButtonIndex >= 0) {
|
||||
final ContextMenuButtonItem copyButtonItem =
|
||||
buttonItems[copyButtonIndex];
|
||||
buttonItems[copyButtonIndex] = copyButtonItem.copyWith(
|
||||
onPressed: () {
|
||||
copyButtonItem.onPressed!();
|
||||
Navigator.of(context).push(_showDialog(context));
|
||||
},
|
||||
);
|
||||
}
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
buttonItems: buttonItems,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
context_menus/lib/platform_selector.dart
Normal file
61
context_menus/lib/platform_selector.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef PlatformCallback = void Function(TargetPlatform platform);
|
||||
|
||||
class PlatformSelector extends StatefulWidget {
|
||||
const PlatformSelector({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
@override
|
||||
State<PlatformSelector> createState() => _PlatformSelectorState();
|
||||
}
|
||||
|
||||
class _PlatformSelectorState extends State<PlatformSelector> {
|
||||
static const int targetPlatformStringLength = 15; // 'TargetPlatform.'.length
|
||||
|
||||
static String _platformToString(TargetPlatform platform) {
|
||||
return platform.toString().substring(targetPlatformStringLength);
|
||||
}
|
||||
|
||||
final TargetPlatform originaPlatform = defaultTargetPlatform;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 170.0,
|
||||
child: DropdownButton<TargetPlatform>(
|
||||
value: defaultTargetPlatform,
|
||||
icon: const Icon(Icons.arrow_downward),
|
||||
elevation: 16,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onChangedPlatform(value);
|
||||
setState(() {});
|
||||
},
|
||||
items: TargetPlatform.values.map((platform) {
|
||||
return DropdownMenuItem<TargetPlatform>(
|
||||
value: platform,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (platform == originaPlatform)
|
||||
const Icon(
|
||||
Icons.home,
|
||||
color: Color(0xff616161),
|
||||
),
|
||||
Text(_platformToString(platform)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
96
context_menus/lib/reordered_buttons_page.dart
Normal file
96
context_menus/lib/reordered_buttons_page.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class ReorderedButtonsPage extends StatelessWidget {
|
||||
ReorderedButtonsPage({
|
||||
super.key,
|
||||
required this.onChangedPlatform,
|
||||
});
|
||||
|
||||
static const String route = 'reordered-buttons';
|
||||
static const String title = 'Reordered Buttons';
|
||||
static const String subtitle = 'The usual buttons, but in a different order.';
|
||||
static const String url = '$kCodeUrl/reordered_buttons_page.dart';
|
||||
|
||||
final PlatformCallback onChangedPlatform;
|
||||
|
||||
final TextEditingController _controllerNormal = TextEditingController(
|
||||
text: 'This button has the default buttons for reference.',
|
||||
);
|
||||
|
||||
final TextEditingController _controllerReordered = TextEditingController(
|
||||
text: 'This field has reordered buttons.',
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(ReorderedButtonsPage.title),
|
||||
actions: <Widget>[
|
||||
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: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
maxLines: 2,
|
||||
controller: _controllerNormal,
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
TextField(
|
||||
controller: _controllerReordered,
|
||||
maxLines: 2,
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
// Reorder the button datas by type.
|
||||
final HashMap<ContextMenuButtonType, ContextMenuButtonItem>
|
||||
buttonItemsMap =
|
||||
HashMap<ContextMenuButtonType, ContextMenuButtonItem>();
|
||||
for (ContextMenuButtonItem buttonItem
|
||||
in editableTextState.contextMenuButtonItems) {
|
||||
buttonItemsMap[buttonItem.type] = buttonItem;
|
||||
}
|
||||
final List<ContextMenuButtonItem> reorderedButtonItems =
|
||||
<ContextMenuButtonItem>[
|
||||
if (buttonItemsMap
|
||||
.containsKey(ContextMenuButtonType.selectAll))
|
||||
buttonItemsMap[ContextMenuButtonType.selectAll]!,
|
||||
if (buttonItemsMap.containsKey(ContextMenuButtonType.paste))
|
||||
buttonItemsMap[ContextMenuButtonType.paste]!,
|
||||
if (buttonItemsMap.containsKey(ContextMenuButtonType.copy))
|
||||
buttonItemsMap[ContextMenuButtonType.copy]!,
|
||||
if (buttonItemsMap.containsKey(ContextMenuButtonType.cut))
|
||||
buttonItemsMap[ContextMenuButtonType.cut]!,
|
||||
];
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
buttonItems: reorderedButtonItems,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user