MenuAnchor class
A widget used to mark the "anchor" for a set of submenus, defining the rectangle used to position the menu, which can be done either with an explicit location, or with an alignment.
When creating a menu with MenuBar or a SubmenuButton, a MenuAnchor is not needed, since they provide their own internally.
The MenuAnchor is meant to be a slightly lower level interface than MenuBar, used in situations where a MenuBar isn't appropriate, or to construct widgets or screen regions that have submenus.
To programmatically control a MenuAnchor, like opening or closing it, or checking its state,
you can get its associated MenuController. Use MenuController.maybeOf(BuildContext context)
to retrieve the controller for the closest MenuAnchor ancestor of a given BuildContext.
More detailed usage of MenuController is available in its class documentation.
This example shows how to use a MenuAnchor to wrap a button and open a cascading menu from the button. This example also shows how to use onAnimationStatusChanged to track animation status and toggle the menu.
To see it in action, copy and run this code snippet on DartPad.
import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart';
/// Flutter code sample for [MenuAnchor].
void main() => runApp(const MenuApp());
/// An enhanced enum to define the available menus and their shortcuts.
///
/// Using an enum for menu definition is not required, but this illustrates how
/// they could be used for simple menu systems.
enum MenuEntry {
about('About'),
showMessage(
'Show Message',
SingleActivator(LogicalKeyboardKey.keyS, control: true),
),
hideMessage(
'Hide Message',
SingleActivator(LogicalKeyboardKey.keyS, control: true),
),
colorMenu('Color Menu'),
colorRed(
'Red Background',
SingleActivator(LogicalKeyboardKey.keyR, control: true),
),
colorGreen(
'Green Background',
SingleActivator(LogicalKeyboardKey.keyG, control: true),
),
colorBlue(
'Blue Background',
SingleActivator(LogicalKeyboardKey.keyB, control: true),
);
const MenuEntry(this.label, [this.shortcut]);
final String label;
final MenuSerializableShortcut? shortcut;
}
class MyCascadingMenu extends StatefulWidget {
const MyCascadingMenu({super.key, required this.message});
final String message;
@override
State<MyCascadingMenu> createState() => _MyCascadingMenuState();
}
class _MyCascadingMenuState extends State<MyCascadingMenu> {
MenuEntry? _lastSelection;
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
ShortcutRegistryEntry? _shortcutsEntry;
AnimationStatus _animationStatus = .dismissed;
Color get backgroundColor => _backgroundColor;
Color _backgroundColor = Colors.red;
set backgroundColor(Color value) {
if (_backgroundColor != value) {
setState(() {
_backgroundColor = value;
});
}
}
bool get showingMessage => _showingMessage;
bool _showingMessage = false;
set showingMessage(bool value) {
if (_showingMessage != value) {
setState(() {
_showingMessage = value;
});
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Dispose of any previously registered shortcuts, since they are about to
// be replaced.
_shortcutsEntry?.dispose();
// Collect the shortcuts from the different menu selections so that they can
// be registered to apply to the entire app. Menus don't register their
// shortcuts, they only display the shortcut hint text.
final Map<ShortcutActivator, Intent> shortcuts =
<ShortcutActivator, Intent>{
for (final MenuEntry item in MenuEntry.values)
if (item.shortcut != null)
item.shortcut!: VoidCallbackIntent(() => _activate(item)),
};
// Register the shortcuts with the ShortcutRegistry so that they are
// available to the entire application.
_shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts);
}
@override
void dispose() {
_shortcutsEntry?.dispose();
_buttonFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: .start,
children: <Widget>[
MenuAnchor(
animated: true,
onAnimationStatusChanged: (AnimationStatus status) {
// Store the animation status so that it can be used to determine
// whether the menu is opening or closing when the button is
// pressed.
_animationStatus = status;
},
childFocusNode: _buttonFocusNode,
menuChildren: <Widget>[
MenuItemButton(
child: Text(MenuEntry.about.label),
onPressed: () => _activate(MenuEntry.about),
),
if (_showingMessage)
MenuItemButton(
onPressed: () => _activate(MenuEntry.hideMessage),
shortcut: MenuEntry.hideMessage.shortcut,
child: Text(MenuEntry.hideMessage.label),
),
if (!_showingMessage)
MenuItemButton(
onPressed: () => _activate(MenuEntry.showMessage),
shortcut: MenuEntry.showMessage.shortcut,
child: Text(MenuEntry.showMessage.label),
),
SubmenuButton(
animated: true,
menuChildren: <Widget>[
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorRed),
shortcut: MenuEntry.colorRed.shortcut,
child: Text(MenuEntry.colorRed.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorGreen),
shortcut: MenuEntry.colorGreen.shortcut,
child: Text(MenuEntry.colorGreen.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorBlue),
shortcut: MenuEntry.colorBlue.shortcut,
child: Text(MenuEntry.colorBlue.label),
),
],
child: const Text('Background Color'),
),
],
builder:
(BuildContext context, MenuController controller, Widget? child) {
return TextButton(
focusNode: _buttonFocusNode,
onPressed: () {
if (_animationStatus.isForwardOrCompleted) {
controller.close();
} else {
controller.open();
}
},
child: const Text('OPEN MENU'),
);
},
),
Expanded(
child: Container(
alignment: .center,
color: backgroundColor,
child: Column(
mainAxisAlignment: .center,
children: <Widget>[
Padding(
padding: const .all(12.0),
child: Text(
showingMessage ? widget.message : '',
style: Theme.of(context).textTheme.headlineSmall,
),
),
Text(
_lastSelection != null
? 'Last Selected: ${_lastSelection!.label}'
: '',
),
],
),
),
),
],
);
}
void _activate(MenuEntry selection) {
setState(() {
_lastSelection = selection;
});
switch (selection) {
case MenuEntry.about:
showAboutDialog(
context: context,
applicationName: 'MenuBar Sample',
applicationVersion: '1.0.0',
);
case MenuEntry.hideMessage:
case MenuEntry.showMessage:
showingMessage = !showingMessage;
case MenuEntry.colorMenu:
break;
case MenuEntry.colorRed:
backgroundColor = Colors.red;
case MenuEntry.colorGreen:
backgroundColor = Colors.green;
case MenuEntry.colorBlue:
backgroundColor = Colors.blue;
}
}
}
class MenuApp extends StatelessWidget {
const MenuApp({super.key});
static const String kMessage = '"Talk less. Smile more." - A. Burr';
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: SafeArea(child: MyCascadingMenu(message: kMessage)),
),
);
}
}
This example shows how to use a MenuAnchor to create a cascading context menu in a region of the view, positioned where the user clicks the mouse with Ctrl pressed. The anchorTapClosesMenu attribute is set to true so that clicks on the MenuAnchor area will cause the menus to be closed.
To see it in action, copy and run this code snippet on DartPad.
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart';
/// Flutter code sample for [MenuAnchor].
void main() => runApp(const ContextMenuApp());
/// An enhanced enum to define the available menus and their shortcuts.
///
/// Using an enum for menu definition is not required, but this illustrates how
/// they could be used for simple menu systems.
enum MenuEntry {
about('About'),
showMessage(
'Show Message',
SingleActivator(LogicalKeyboardKey.keyS, control: true),
),
hideMessage(
'Hide Message',
SingleActivator(LogicalKeyboardKey.keyS, control: true),
),
colorMenu('Color Menu'),
colorRed(
'Red Background',
SingleActivator(LogicalKeyboardKey.keyR, control: true),
),
colorGreen(
'Green Background',
SingleActivator(LogicalKeyboardKey.keyG, control: true),
),
colorBlue(
'Blue Background',
SingleActivator(LogicalKeyboardKey.keyB, control: true),
);
const MenuEntry(this.label, [this.shortcut]);
final String label;
final MenuSerializableShortcut? shortcut;
}
class MyContextMenu extends StatefulWidget {
const MyContextMenu({super.key, required this.message});
final String message;
@override
State<MyContextMenu> createState() => _MyContextMenuState();
}
class _MyContextMenuState extends State<MyContextMenu> {
MenuEntry? _lastSelection;
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
final MenuController _menuController = MenuController();
ShortcutRegistryEntry? _shortcutsEntry;
bool _menuWasEnabled = false;
Color get backgroundColor => _backgroundColor;
Color _backgroundColor = Colors.red;
set backgroundColor(Color value) {
if (_backgroundColor != value) {
setState(() {
_backgroundColor = value;
});
}
}
bool get showingMessage => _showingMessage;
bool _showingMessage = false;
set showingMessage(bool value) {
if (_showingMessage != value) {
setState(() {
_showingMessage = value;
});
}
}
@override
void initState() {
super.initState();
_disableContextMenu();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Dispose of any previously registered shortcuts, since they are about to
// be replaced.
_shortcutsEntry?.dispose();
// Collect the shortcuts from the different menu selections so that they can
// be registered to apply to the entire app. Menus don't register their
// shortcuts, they only display the shortcut hint text.
final Map<ShortcutActivator, Intent> shortcuts =
<ShortcutActivator, Intent>{
for (final MenuEntry item in MenuEntry.values)
if (item.shortcut != null)
item.shortcut!: VoidCallbackIntent(() => _activate(item)),
};
// Register the shortcuts with the ShortcutRegistry so that they are
// available to the entire application.
_shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts);
}
@override
void dispose() {
_shortcutsEntry?.dispose();
_buttonFocusNode.dispose();
_reenableContextMenu();
super.dispose();
}
Future<void> _disableContextMenu() async {
if (!kIsWeb) {
// Does nothing on non-web platforms.
return;
}
_menuWasEnabled = BrowserContextMenu.enabled;
if (_menuWasEnabled) {
await BrowserContextMenu.disableContextMenu();
}
}
void _reenableContextMenu() {
if (!kIsWeb) {
// Does nothing on non-web platforms.
return;
}
if (_menuWasEnabled && !BrowserContextMenu.enabled) {
BrowserContextMenu.enableContextMenu();
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const .all(50),
child: GestureDetector(
onTapDown: _handleTapDown,
onSecondaryTapDown: _handleSecondaryTapDown,
child: MenuAnchor(
animated: true,
controller: _menuController,
menuChildren: <Widget>[
MenuItemButton(
child: Text(MenuEntry.about.label),
onPressed: () => _activate(MenuEntry.about),
),
if (_showingMessage)
MenuItemButton(
onPressed: () => _activate(MenuEntry.hideMessage),
shortcut: MenuEntry.hideMessage.shortcut,
child: Text(MenuEntry.hideMessage.label),
),
if (!_showingMessage)
MenuItemButton(
onPressed: () => _activate(MenuEntry.showMessage),
shortcut: MenuEntry.showMessage.shortcut,
child: Text(MenuEntry.showMessage.label),
),
SubmenuButton(
animated: true,
menuChildren: <Widget>[
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorRed),
shortcut: MenuEntry.colorRed.shortcut,
child: Text(MenuEntry.colorRed.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorGreen),
shortcut: MenuEntry.colorGreen.shortcut,
child: Text(MenuEntry.colorGreen.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorBlue),
shortcut: MenuEntry.colorBlue.shortcut,
child: Text(MenuEntry.colorBlue.label),
),
],
child: const Text('Background Color'),
),
],
child: Container(
alignment: .center,
color: backgroundColor,
child: Column(
mainAxisAlignment: .center,
children: <Widget>[
const Padding(
padding: .all(8.0),
child: Text(
'Right-click anywhere on the background to show the menu.',
),
),
Padding(
padding: const .all(12.0),
child: Text(
showingMessage ? widget.message : '',
style: Theme.of(context).textTheme.headlineSmall,
),
),
Text(
_lastSelection != null
? 'Last Selected: ${_lastSelection!.label}'
: '',
),
],
),
),
),
),
);
}
void _activate(MenuEntry selection) {
setState(() {
_lastSelection = selection;
});
switch (selection) {
case MenuEntry.about:
showAboutDialog(
context: context,
applicationName: 'MenuBar Sample',
applicationVersion: '1.0.0',
);
case MenuEntry.showMessage:
case MenuEntry.hideMessage:
showingMessage = !showingMessage;
case MenuEntry.colorMenu:
break;
case MenuEntry.colorRed:
backgroundColor = Colors.red;
case MenuEntry.colorGreen:
backgroundColor = Colors.green;
case MenuEntry.colorBlue:
backgroundColor = Colors.blue;
}
}
void _handleSecondaryTapDown(TapDownDetails details) {
_menuController.open(position: details.localPosition);
}
void _handleTapDown(TapDownDetails details) {
if (_menuController.isOpen) {
_menuController.close();
return;
}
switch (defaultTargetPlatform) {
case .android:
case .fuchsia:
case .linux:
case .windows:
// Don't open the menu on these platforms with a Ctrl-tap (or a
// tap).
break;
case .iOS:
case .macOS:
// Only open the menu on these platforms if the control button is down
// when the tap occurs.
if (HardwareKeyboard.instance.logicalKeysPressed.contains(
LogicalKeyboardKey.controlLeft,
) ||
HardwareKeyboard.instance.logicalKeysPressed.contains(
LogicalKeyboardKey.controlRight,
)) {
_menuController.open(position: details.localPosition);
}
}
}
}
class ContextMenuApp extends StatelessWidget {
const ContextMenuApp({super.key});
static const String kMessage = '"Talk less. Smile more." - A. Burr';
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(body: MyContextMenu(message: kMessage)),
);
}
}
This example demonstrates a simplified cascading menu using the MenuAnchor widget.
To see it in action, copy and run this code snippet on DartPad.
import 'package:material_ui/material_ui.dart';
/// Flutter code sample for [SimpleCascadingMenuApp].
void main() => runApp(const SimpleCascadingMenuApp());
/// A Simple Cascading Menu example using the [MenuAnchor] Widget.
class MyCascadingMenu extends StatefulWidget {
const MyCascadingMenu({super.key});
@override
State<MyCascadingMenu> createState() => _MyCascadingMenuState();
}
class _MyCascadingMenuState extends State<MyCascadingMenu> {
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
@override
void dispose() {
_buttonFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MenuAnchor(
childFocusNode: _buttonFocusNode,
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('Revert')),
MenuItemButton(onPressed: () {}, child: const Text('Setting')),
MenuItemButton(onPressed: () {}, child: const Text('Send Feedback')),
],
builder: (_, MenuController controller, Widget? child) {
return IconButton(
focusNode: _buttonFocusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert),
);
},
);
}
}
/// Top Level Application Widget.
class SimpleCascadingMenuApp extends StatelessWidget {
const SimpleCascadingMenuApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('MenuAnchor Simple Example'),
actions: const <Widget>[MyCascadingMenu()],
),
),
);
}
}
The MenuStyle.visualDensity setting only affects horizontal padding, and it will never make it negative. Vertical padding is not affected at all.
- Inheritance
-
- Object
- DiagnosticableTree
- Widget
- StatefulWidget
- MenuAnchor
Constructors
-
MenuAnchor({Key? key, MenuController? controller, FocusNode? childFocusNode, MenuStyle? style, Offset? alignmentOffset = Offset.zero, EdgeInsetsGeometry? reservedPadding, LayerLink? layerLink, Clip clipBehavior = Clip.hardEdge, @Deprecated('Use consumeOutsideTap instead. ' 'This feature was deprecated after v3.16.0-8.0.pre.') bool anchorTapClosesMenu = false, bool consumeOutsideTap = false, VoidCallback? onOpen, VoidCallback? onClose, bool crossAxisUnconstrained = true, bool useRootOverlay = false, bool animated = false, ValueChanged<
AnimationStatus> ? onAnimationStatusChanged, MenuAnchorChildBuilder? builder, Widget? child}) -
Creates a const MenuAnchor.
const
Properties
- alignmentOffset → Offset?
-
The offset of the menu relative to the alignment origin determined by
MenuStyle.alignment on the style attribute and the ambient
Directionality.
final
- anchorTapClosesMenu → bool
-
Whether the menus will be closed if the anchor area is tapped.
final
- animated → bool
-
Whether this widget should open or close a submenu with an animation.
final
- builder → MenuAnchorChildBuilder?
-
The widget that this MenuAnchor surrounds.
final
- child → Widget?
-
The optional child to be passed to the builder.
final
- childFocusNode → FocusNode?
-
The childFocusNode attribute is the optional FocusNode also associated
to the child or builder widget that opens the menu.
final
- clipBehavior → Clip
-
The content will be clipped (or not) according to this option.
final
- consumeOutsideTap → bool
-
Whether or not a tap event that closes the menu will be permitted to
continue on to the gesture arena.
final
- controller → MenuController?
-
An optional controller that allows opening and closing of the menu from
other widgets.
final
- crossAxisUnconstrained → bool
-
Determine if the menu panel can be wrapped by a UnconstrainedBox which allows
the panel to render at its "natural" size.
final
- hashCode → int
-
The hash code for this object.
no setterinherited
- key → Key?
-
Controls how one widget replaces another widget in the tree.
finalinherited
- layerLink → LayerLink?
-
An optional LayerLink to attach the menu to the widget that this
MenuAnchor surrounds.
final
-
A list of children containing the menu items that are the contents of the
menu surrounded by this MenuAnchor.
final
-
onAnimationStatusChanged
→ ValueChanged<
AnimationStatus> ? -
An optional callback that is invoked when the AnimationStatus of the
menu changes during open and close animations.
final
- onClose → VoidCallback?
-
A callback that is invoked when the menu finishes closing.
final
- onOpen → VoidCallback?
-
A callback that is invoked when the menu begins opening.
final
- reservedPadding → EdgeInsetsGeometry?
-
The padding between the edge of the safe area and the menu panel.
final
- runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
- style → MenuStyle?
-
The MenuStyle that defines the visual attributes of the menu bar.
final
- useRootOverlay → bool
-
Whether the menu panel should be rendered in the root Overlay.
final
Methods
-
createElement(
) → StatefulElement -
Creates a StatefulElement to manage this widget's location in the tree.
inherited
-
createState(
) → State< MenuAnchor> -
Creates the mutable state for this widget at a given location in the tree.
override
-
debugDescribeChildren(
) → List< DiagnosticsNode> -
Returns a list of DiagnosticsNode objects describing this node's
children.
override
-
debugFillProperties(
DiagnosticPropertiesBuilder properties) → void -
Add additional properties associated with the node.
override
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
toDiagnosticsNode(
{String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode -
Returns a debug representation of the object that is used by debugging
tools and by DiagnosticsNode.toStringDeep.
inherited
-
toString(
{DiagnosticLevel minLevel = DiagnosticLevel.info}) → String -
A string representation of this object.
inherited
-
toStringDeep(
{String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) → String -
Returns a string representation of this node and its descendants.
inherited
-
toStringShallow(
{String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String -
Returns a one-line detailed description of the object.
inherited
-
toStringShort(
) → String -
A short, textual description of this widget.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited