mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
Custom context menu example (#1463)
* flutter create * Copied in from 'contextual-menu-examples' repo * Works again with the latest updates to the branch * Clean up anywhere example * Clean up other examples * Add to CI * Updated with release version of context menus, and added platform switcher * Generated files * Home icon on your original platform * Remove web_dashboard from ci, not sure why that change was there... * Add context_menu to beta channel, but commented out, for the future * +x permissions on the master script, which I may have accidentally changed before? * Actual bash comment * dart format * Natural default platform' * A working test, for the email page * Import order fix * Test some pages * More tests * dart format
This commit is contained in:
committed by
GitHub
parent
070ce7303a
commit
41571eae07
101
experimental/context_menus/lib/anywhere_page.dart
Normal file
101
experimental/context_menus/lib/anywhere_page.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
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({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
static const String route = 'anywhere';
|
||||
static const String title = 'Context Menu Anywhere Example';
|
||||
static const String subtitle = 'A ContextMenu 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: (BuildContext context, Offset primaryAnchor,
|
||||
[Offset? secondaryAnchor]) {
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: TextSelectionToolbarAnchors(
|
||||
primaryAnchor: primaryAnchor,
|
||||
secondaryAnchor: secondaryAnchor,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
2
experimental/context_menus/lib/constants.dart
Normal file
2
experimental/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
experimental/context_menus/lib/context_menu_region.dart
Normal file
97
experimental/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: (BuildContext 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
86
experimental/context_menus/lib/custom_buttons_page.dart
Normal file
86
experimental/context_menus/lib/custom_buttons_page.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
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({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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:
|
||||
(BuildContext context, EditableTextState 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((ContextMenuButtonItem 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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
111
experimental/context_menus/lib/default_values_page.dart
Normal file
111
experimental/context_menus/lib/default_values_page.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class DefaultValuesPage extends StatelessWidget {
|
||||
DefaultValuesPage({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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: (BuildContext 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: (BuildContext context,
|
||||
EditableTextState 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,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
experimental/context_menus/lib/email_button_page.dart
Normal file
95
experimental/context_menus/lib/email_button_page.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
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({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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: (BuildContext 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: (BuildContext context,
|
||||
EditableTextState 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
135
experimental/context_menus/lib/field_types_page.dart
Normal file
135
experimental/context_menus/lib/field_types_page.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
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({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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: (BuildContext context,
|
||||
EditableTextState editableTextState) {
|
||||
return AdaptiveTextSelectionToolbar.editableText(
|
||||
editableTextState: editableTextState,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
CupertinoTextField(
|
||||
maxLines: 3,
|
||||
controller: _cupertinoControllerForced,
|
||||
contextMenuBuilder: (BuildContext context,
|
||||
EditableTextState 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: (BuildContext context,
|
||||
EditableTextState editableTextState) {
|
||||
return AdaptiveTextSelectionToolbar.editableText(
|
||||
editableTextState: editableTextState,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
161
experimental/context_menus/lib/full_page.dart
Normal file
161
experimental/context_menus/lib/full_page.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
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({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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: (BuildContext 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: (BuildContext context, Offset 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: (BuildContext context, Offset 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: (BuildContext context,
|
||||
EditableTextState 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((ContextMenuButtonItem 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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
experimental/context_menus/lib/global_selection_page.dart
Normal file
81
experimental/context_menus/lib/global_selection_page.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class GlobalSelectionPage extends StatelessWidget {
|
||||
GlobalSelectionPage({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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:
|
||||
(BuildContext context, SelectableRegionState 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
experimental/context_menus/lib/image_page.dart
Normal file
83
experimental/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({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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: (BuildContext 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: (BuildContext context, Offset 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
experimental/context_menus/lib/is_valid_email.dart
Normal file
10
experimental/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);
|
||||
}
|
||||
180
experimental/context_menus/lib/main.dart
Normal file
180
experimental/context_menus/lib/main.dart
Normal file
@@ -0,0 +1,180 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'anywhere_page.dart';
|
||||
import 'custom_buttons_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
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Flutter Context Menu Examples',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
platform: defaultTargetPlatform,
|
||||
),
|
||||
initialRoute: '/',
|
||||
routes: <String, Widget Function(BuildContext)>{
|
||||
'/': (BuildContext context) =>
|
||||
MyHomePage(onChangedPlatform: onChangedPlatform),
|
||||
AnywherePage.route: (BuildContext context) =>
|
||||
AnywherePage(onChangedPlatform: onChangedPlatform),
|
||||
CustomButtonsPage.route: (BuildContext context) =>
|
||||
CustomButtonsPage(onChangedPlatform: onChangedPlatform),
|
||||
ReorderedButtonsPage.route: (BuildContext context) =>
|
||||
ReorderedButtonsPage(onChangedPlatform: onChangedPlatform),
|
||||
EmailButtonPage.route: (BuildContext context) =>
|
||||
EmailButtonPage(onChangedPlatform: onChangedPlatform),
|
||||
ImagePage.route: (BuildContext context) =>
|
||||
ImagePage(onChangedPlatform: onChangedPlatform),
|
||||
FieldTypesPage.route: (BuildContext context) =>
|
||||
FieldTypesPage(onChangedPlatform: onChangedPlatform),
|
||||
FullPage.route: (BuildContext context) =>
|
||||
FullPage(onChangedPlatform: onChangedPlatform),
|
||||
ModifiedActionPage.route: (BuildContext context) =>
|
||||
ModifiedActionPage(onChangedPlatform: onChangedPlatform),
|
||||
GlobalSelectionPage.route: (BuildContext context) =>
|
||||
GlobalSelectionPage(onChangedPlatform: onChangedPlatform),
|
||||
DefaultValuesPage.route: (BuildContext context) =>
|
||||
DefaultValuesPage(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: 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: 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
experimental/context_menus/lib/modified_action_page.dart
Normal file
98
experimental/context_menus/lib/modified_action_page.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
import 'platform_selector.dart';
|
||||
|
||||
class ModifiedActionPage extends StatelessWidget {
|
||||
ModifiedActionPage({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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: (BuildContext 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: (BuildContext context,
|
||||
EditableTextState editableTextState) {
|
||||
final List<ContextMenuButtonItem> buttonItems =
|
||||
editableTextState.contextMenuButtonItems;
|
||||
// Modify the copy buttonItem to show a dialog after copying.
|
||||
final int copyButtonIndex = buttonItems.indexWhere(
|
||||
(ContextMenuButtonItem 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
experimental/context_menus/lib/platform_selector.dart
Normal file
61
experimental/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: 160.0,
|
||||
child: DropdownButton<TargetPlatform>(
|
||||
value: defaultTargetPlatform,
|
||||
icon: const Icon(Icons.arrow_downward),
|
||||
elevation: 16,
|
||||
onChanged: (TargetPlatform? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onChangedPlatform(value);
|
||||
setState(() {});
|
||||
},
|
||||
items: TargetPlatform.values.map((TargetPlatform platform) {
|
||||
return DropdownMenuItem<TargetPlatform>(
|
||||
value: platform,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (platform == originaPlatform)
|
||||
const Icon(
|
||||
Icons.home,
|
||||
color: Color(0xff616161),
|
||||
),
|
||||
Text(_platformToString(platform)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
97
experimental/context_menus/lib/reordered_buttons_page.dart
Normal file
97
experimental/context_menus/lib/reordered_buttons_page.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
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({
|
||||
Key? key,
|
||||
required this.onChangedPlatform,
|
||||
}) : super(key: key);
|
||||
|
||||
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: (BuildContext context,
|
||||
EditableTextState 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