forked from flutter/flutter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Can show context menus anywhere in the app, not just on text. * Unifies all desktop/mobile context menus to go through one class (ContextMenuController). * All context menus are now just plain widgets that can be fully customized. * Existing default context menus can be customized and reused.
- Loading branch information
Showing
58 changed files
with
5,063 additions
and
915 deletions.
There are no files selected for viewing
159 changes: 159 additions & 0 deletions
159
examples/api/lib/material/context_menu/context_menu_controller.0.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
// Copyright 2014 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
// This sample demonstrates allowing a context menu to be shown in a widget | ||
// subtree in response to user gestures. | ||
|
||
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter/material.dart'; | ||
|
||
void main() => runApp(const MyApp()); | ||
|
||
/// A builder that includes an Offset to draw the context menu at. | ||
typedef ContextMenuBuilder = Widget Function(BuildContext context, Offset offset); | ||
|
||
class MyApp extends StatelessWidget { | ||
const MyApp({super.key}); | ||
|
||
void _showDialog (BuildContext context) { | ||
Navigator.of(context).push( | ||
DialogRoute<void>( | ||
context: context, | ||
builder: (BuildContext context) => | ||
const AlertDialog(title: Text('You clicked print!')), | ||
), | ||
); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
home: Scaffold( | ||
appBar: AppBar( | ||
title: const Text('Context menu outside of text'), | ||
), | ||
body: _ContextMenuRegion( | ||
contextMenuBuilder: (BuildContext context, Offset offset) { | ||
// The custom context menu will look like the default context menu | ||
// on the current platform with a single 'Print' button. | ||
return AdaptiveTextSelectionToolbar.buttonItems( | ||
anchors: TextSelectionToolbarAnchors( | ||
primaryAnchor: offset, | ||
), | ||
buttonItems: <ContextMenuButtonItem>[ | ||
ContextMenuButtonItem( | ||
onPressed: () { | ||
ContextMenuController.removeAny(); | ||
_showDialog(context); | ||
}, | ||
label: 'Print', | ||
), | ||
], | ||
); | ||
}, | ||
// In this case this wraps a big open space in a GestureDetector in | ||
// order to show the context menu, but it could also wrap a single | ||
// wiget like an Image to give it a context menu. | ||
child: ListView( | ||
children: <Widget>[ | ||
Container(height: 20.0), | ||
const Text('Right click or long press anywhere (not just on this text!) to show the custom menu.'), | ||
], | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} | ||
|
||
/// 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({ | ||
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, | ||
); | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Copyright 2014 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
// This example demonstrates showing the default buttons, but customizing their | ||
// appearance. | ||
|
||
import 'package:flutter/cupertino.dart'; | ||
import 'package:flutter/material.dart'; | ||
|
||
void main() => runApp(MyApp()); | ||
|
||
class MyApp extends StatelessWidget { | ||
MyApp({super.key}); | ||
|
||
final TextEditingController _controller = TextEditingController( | ||
text: 'Right click or long press to see the menu with custom buttons.', | ||
); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
home: Scaffold( | ||
appBar: AppBar( | ||
title: const Text('Custom button appearance'), | ||
), | ||
body: Center( | ||
child: Column( | ||
children: <Widget>[ | ||
const SizedBox(height: 20.0), | ||
TextField( | ||
controller: _controller, | ||
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { | ||
return AdaptiveTextSelectionToolbar( | ||
anchors: editableTextState.contextMenuAnchors, | ||
// Build the default buttons, but make them look custom. | ||
// 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(), | ||
); | ||
}, | ||
), | ||
], | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
// Copyright 2014 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
// This example demonstrates showing a custom context menu only when some | ||
// narrowly defined text is selected. | ||
|
||
import 'package:flutter/material.dart'; | ||
|
||
void main() => runApp(MyApp()); | ||
|
||
const String emailAddress = 'me@example.com'; | ||
const String text = 'Select the email address and open the menu: $emailAddress'; | ||
|
||
class MyApp extends StatelessWidget { | ||
MyApp({super.key}); | ||
|
||
final TextEditingController _controller = TextEditingController( | ||
text: text, | ||
); | ||
|
||
void _showDialog (BuildContext context) { | ||
Navigator.of(context).push( | ||
DialogRoute<void>( | ||
context: context, | ||
builder: (BuildContext context) => | ||
const AlertDialog(title: Text('You clicked send email!')), | ||
), | ||
); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
home: Scaffold( | ||
appBar: AppBar( | ||
title: const Text('Custom button for emails'), | ||
), | ||
body: Center( | ||
child: Column( | ||
children: <Widget>[ | ||
Container(height: 20.0), | ||
TextField( | ||
controller: _controller, | ||
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { | ||
final List<ContextMenuButtonItem> buttonItems = | ||
editableTextState.contextMenuButtonItems; | ||
// Here we add an "Email" button to the default TextField | ||
// context menu for the current platform, but only if an email | ||
// address is currently selected. | ||
final TextEditingValue value = _controller.value; | ||
if (_isValidEmail(value.selection.textInside(value.text))) { | ||
buttonItems.insert(0, ContextMenuButtonItem( | ||
label: 'Send email', | ||
onPressed: () { | ||
ContextMenuController.removeAny(); | ||
_showDialog(context); | ||
}, | ||
)); | ||
} | ||
return AdaptiveTextSelectionToolbar.buttonItems( | ||
anchors: editableTextState.contextMenuAnchors, | ||
buttonItems: buttonItems, | ||
); | ||
}, | ||
), | ||
], | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} | ||
|
||
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); | ||
} |
Oops, something went wrong.