| // 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. |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'color_scheme.dart'; |
| import 'constants.dart'; |
| import 'debug.dart'; |
| import 'divider.dart'; |
| import 'icon_button.dart'; |
| import 'icons.dart'; |
| import 'ink_well.dart'; |
| import 'list_tile.dart'; |
| import 'material.dart'; |
| import 'material_localizations.dart'; |
| import 'material_state.dart'; |
| import 'popup_menu_theme.dart'; |
| import 'text_theme.dart'; |
| import 'theme.dart'; |
| import 'tooltip.dart'; |
| |
| // Examples can assume: |
| // enum Commands { heroAndScholar, hurricaneCame } |
| // late bool _heroAndScholar; |
| // late dynamic _selection; |
| // late BuildContext context; |
| // void setState(VoidCallback fn) { } |
| // enum Menu { itemOne, itemTwo, itemThree, itemFour } |
| |
| const Duration _kMenuDuration = Duration(milliseconds: 300); |
| const double _kMenuCloseIntervalEnd = 2.0 / 3.0; |
| const double _kMenuHorizontalPadding = 16.0; |
| const double _kMenuDividerHeight = 16.0; |
| const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; |
| const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; |
| const double _kMenuVerticalPadding = 8.0; |
| const double _kMenuWidthStep = 56.0; |
| const double _kMenuScreenPadding = 8.0; |
| |
| /// A base class for entries in a Material Design popup menu. |
| /// |
| /// The popup menu widget uses this interface to interact with the menu items. |
| /// To show a popup menu, use the [showMenu] function. To create a button that |
| /// shows a popup menu, consider using [PopupMenuButton]. |
| /// |
| /// The type `T` is the type of the value(s) the entry represents. All the |
| /// entries in a given menu must represent values with consistent types. |
| /// |
| /// A [PopupMenuEntry] may represent multiple values, for example a row with |
| /// several icons, or a single entry, for example a menu item with an icon (see |
| /// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]). |
| /// |
| /// See also: |
| /// |
| /// * [PopupMenuItem], a popup menu entry for a single value. |
| /// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. |
| /// * [CheckedPopupMenuItem], a popup menu item with a checkmark. |
| /// * [showMenu], a method to dynamically show a popup menu at a given location. |
| /// * [PopupMenuButton], an [IconButton] that automatically shows a menu when |
| /// it is tapped. |
| abstract class PopupMenuEntry<T> extends StatefulWidget { |
| /// Abstract const constructor. This constructor enables subclasses to provide |
| /// const constructors so that they can be used in const expressions. |
| const PopupMenuEntry({ super.key }); |
| |
| /// The amount of vertical space occupied by this entry. |
| /// |
| /// This value is used at the time the [showMenu] method is called, if the |
| /// `initialValue` argument is provided, to determine the position of this |
| /// entry when aligning the selected entry over the given `position`. It is |
| /// otherwise ignored. |
| double get height; |
| |
| /// Whether this entry represents a particular value. |
| /// |
| /// This method is used by [showMenu], when it is called, to align the entry |
| /// representing the `initialValue`, if any, to the given `position`, and then |
| /// later is called on each entry to determine if it should be highlighted (if |
| /// the method returns true, the entry will have its background color set to |
| /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then |
| /// this method is not called. |
| /// |
| /// If the [PopupMenuEntry] represents a single value, this should return true |
| /// if the argument matches that value. If it represents multiple values, it |
| /// should return true if the argument matches any of them. |
| bool represents(T? value); |
| } |
| |
| /// A horizontal divider in a Material Design popup menu. |
| /// |
| /// This widget adapts the [Divider] for use in popup menus. |
| /// |
| /// See also: |
| /// |
| /// * [PopupMenuItem], for the kinds of items that this widget divides. |
| /// * [showMenu], a method to dynamically show a popup menu at a given location. |
| /// * [PopupMenuButton], an [IconButton] that automatically shows a menu when |
| /// it is tapped. |
| class PopupMenuDivider extends PopupMenuEntry<Never> { |
| /// Creates a horizontal divider for a popup menu. |
| /// |
| /// By default, the divider has a height of 16 logical pixels. |
| const PopupMenuDivider({ super.key, this.height = _kMenuDividerHeight }); |
| |
| /// The height of the divider entry. |
| /// |
| /// Defaults to 16 pixels. |
| @override |
| final double height; |
| |
| @override |
| bool represents(void value) => false; |
| |
| @override |
| State<PopupMenuDivider> createState() => _PopupMenuDividerState(); |
| } |
| |
| class _PopupMenuDividerState extends State<PopupMenuDivider> { |
| @override |
| Widget build(BuildContext context) => Divider(height: widget.height); |
| } |
| |
| // This widget only exists to enable _PopupMenuRoute to save the sizes of |
| // each menu item. The sizes are used by _PopupMenuRouteLayout to compute the |
| // y coordinate of the menu's origin so that the center of selected menu |
| // item lines up with the center of its PopupMenuButton. |
| class _MenuItem extends SingleChildRenderObjectWidget { |
| const _MenuItem({ |
| required this.onLayout, |
| required super.child, |
| }); |
| |
| final ValueChanged<Size> onLayout; |
| |
| @override |
| RenderObject createRenderObject(BuildContext context) { |
| return _RenderMenuItem(onLayout); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, covariant _RenderMenuItem renderObject) { |
| renderObject.onLayout = onLayout; |
| } |
| } |
| |
| class _RenderMenuItem extends RenderShiftedBox { |
| _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child); |
| |
| ValueChanged<Size> onLayout; |
| |
| @override |
| Size computeDryLayout(BoxConstraints constraints) { |
| if (child == null) { |
| return Size.zero; |
| } |
| return child!.getDryLayout(constraints); |
| } |
| |
| @override |
| void performLayout() { |
| if (child == null) { |
| size = Size.zero; |
| } else { |
| child!.layout(constraints, parentUsesSize: true); |
| size = constraints.constrain(child!.size); |
| final BoxParentData childParentData = child!.parentData! as BoxParentData; |
| childParentData.offset = Offset.zero; |
| } |
| onLayout(size); |
| } |
| } |
| |
| /// An item in a Material Design popup menu. |
| /// |
| /// To show a popup menu, use the [showMenu] function. To create a button that |
| /// shows a popup menu, consider using [PopupMenuButton]. |
| /// |
| /// To show a checkmark next to a popup menu item, consider using |
| /// [CheckedPopupMenuItem]. |
| /// |
| /// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More |
| /// elaborate menus with icons can use a [ListTile]. By default, a |
| /// [PopupMenuItem] is [kMinInteractiveDimension] pixels high. If you use a widget |
| /// with a different height, it must be specified in the [height] property. |
| /// |
| /// {@tool snippet} |
| /// |
| /// Here, a [Text] widget is used with a popup menu item. The `Menu` type |
| /// is an enum, not shown here. |
| /// |
| /// ```dart |
| /// const PopupMenuItem<Menu>( |
| /// value: Menu.itemOne, |
| /// child: Text('Item 1'), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See the example at [PopupMenuButton] for how this example could be used in a |
| /// complete menu, and see the example at [CheckedPopupMenuItem] for one way to |
| /// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child] |
| /// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem] |
| /// that use a [ListTile] in their [child] slot. |
| /// |
| /// See also: |
| /// |
| /// * [PopupMenuDivider], which can be used to divide items from each other. |
| /// * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark. |
| /// * [showMenu], a method to dynamically show a popup menu at a given location. |
| /// * [PopupMenuButton], an [IconButton] that automatically shows a menu when |
| /// it is tapped. |
| class PopupMenuItem<T> extends PopupMenuEntry<T> { |
| /// Creates an item for a popup menu. |
| /// |
| /// By default, the item is [enabled]. |
| /// |
| /// The `enabled` and `height` arguments must not be null. |
| const PopupMenuItem({ |
| super.key, |
| this.value, |
| this.onTap, |
| this.enabled = true, |
| this.height = kMinInteractiveDimension, |
| this.padding, |
| this.textStyle, |
| this.labelTextStyle, |
| this.mouseCursor, |
| required this.child, |
| }); |
| |
| /// The value that will be returned by [showMenu] if this entry is selected. |
| final T? value; |
| |
| /// Called when the menu item is tapped. |
| final VoidCallback? onTap; |
| |
| /// Whether the user is permitted to select this item. |
| /// |
| /// Defaults to true. If this is false, then the item will not react to |
| /// touches. |
| final bool enabled; |
| |
| /// The minimum height of the menu item. |
| /// |
| /// Defaults to [kMinInteractiveDimension] pixels. |
| @override |
| final double height; |
| |
| /// The padding of the menu item. |
| /// |
| /// The [height] property may interact with the applied padding. For example, |
| /// If a [height] greater than the height of the sum of the padding and [child] |
| /// is provided, then the padding's effect will not be visible. |
| /// |
| /// When null, the horizontal padding defaults to 16.0 on both sides. |
| final EdgeInsets? padding; |
| |
| /// The text style of the popup menu item. |
| /// |
| /// If this property is null, then [PopupMenuThemeData.textStyle] is used. |
| /// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium] |
| /// of [ThemeData.textTheme] is used. |
| final TextStyle? textStyle; |
| |
| /// The label style of the popup menu item. |
| /// |
| /// When [ThemeData.useMaterial3] is true, this styles the text of the popup menu item. |
| /// |
| /// If this property is null, then [PopupMenuThemeData.labelTextStyle] is used. |
| /// If [PopupMenuThemeData.labelTextStyle] is also null, then [TextTheme.labelLarge] |
| /// is used with the [ColorScheme.onSurface] color when popup menu item is enabled and |
| /// the [ColorScheme.onSurface] color with 0.38 opacity when the popup menu item is disabled. |
| final MaterialStateProperty<TextStyle?>? labelTextStyle; |
| |
| /// {@template flutter.material.popupmenu.mouseCursor} |
| /// The cursor for a mouse pointer when it enters or is hovering over the |
| /// widget. |
| /// |
| /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>], |
| /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: |
| /// |
| /// * [MaterialState.hovered]. |
| /// * [MaterialState.focused]. |
| /// * [MaterialState.disabled]. |
| /// {@endtemplate} |
| /// |
| /// If null, then the value of [PopupMenuThemeData.mouseCursor] is used. If |
| /// that is also null, then [MaterialStateMouseCursor.clickable] is used. |
| final MouseCursor? mouseCursor; |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An |
| /// appropriate [DefaultTextStyle] is put in scope for the child. In either |
| /// case, the text should be short enough that it won't wrap. |
| final Widget? child; |
| |
| @override |
| bool represents(T? value) => value == this.value; |
| |
| @override |
| PopupMenuItemState<T, PopupMenuItem<T>> createState() => PopupMenuItemState<T, PopupMenuItem<T>>(); |
| } |
| |
| /// The [State] for [PopupMenuItem] subclasses. |
| /// |
| /// By default this implements the basic styling and layout of Material Design |
| /// popup menu items. |
| /// |
| /// The [buildChild] method can be overridden to adjust exactly what gets placed |
| /// in the menu. By default it returns [PopupMenuItem.child]. |
| /// |
| /// The [handleTap] method can be overridden to adjust exactly what happens when |
| /// the item is tapped. By default, it uses [Navigator.pop] to return the |
| /// [PopupMenuItem.value] from the menu route. |
| /// |
| /// This class takes two type arguments. The second, `W`, is the exact type of |
| /// the [Widget] that is using this [State]. It must be a subclass of |
| /// [PopupMenuItem]. The first, `T`, must match the type argument of that widget |
| /// class, and is the type of values returned from this menu. |
| class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> { |
| /// The menu item contents. |
| /// |
| /// Used by the [build] method. |
| /// |
| /// By default, this returns [PopupMenuItem.child]. Override this to put |
| /// something else in the menu entry. |
| @protected |
| Widget? buildChild() => widget.child; |
| |
| /// The handler for when the user selects the menu item. |
| /// |
| /// Used by the [InkWell] inserted by the [build] method. |
| /// |
| /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from |
| /// the menu route. |
| @protected |
| void handleTap() { |
| // Need to pop the navigator first in case onTap may push new route onto navigator. |
| Navigator.pop<T>(context, widget.value); |
| |
| widget.onTap?.call(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final ThemeData theme = Theme.of(context); |
| final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); |
| final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context); |
| final Set<MaterialState> states = <MaterialState>{ |
| if (!widget.enabled) MaterialState.disabled, |
| }; |
| |
| TextStyle style = theme.useMaterial3 |
| ? (widget.labelTextStyle?.resolve(states) |
| ?? popupMenuTheme.labelTextStyle?.resolve(states)! |
| ?? defaults.labelTextStyle!.resolve(states)!) |
| : (widget.textStyle |
| ?? popupMenuTheme.textStyle |
| ?? defaults.textStyle!); |
| |
| if (!widget.enabled && !theme.useMaterial3) { |
| style = style.copyWith(color: theme.disabledColor); |
| } |
| |
| Widget item = AnimatedDefaultTextStyle( |
| style: style, |
| duration: kThemeChangeDuration, |
| child: Container( |
| alignment: AlignmentDirectional.centerStart, |
| constraints: BoxConstraints(minHeight: widget.height), |
| padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), |
| child: buildChild(), |
| ), |
| ); |
| |
| if (!widget.enabled) { |
| final bool isDark = theme.brightness == Brightness.dark; |
| item = IconTheme.merge( |
| data: IconThemeData(opacity: isDark ? 0.5 : 0.38), |
| child: item, |
| ); |
| } |
| |
| return MergeSemantics( |
| child: Semantics( |
| enabled: widget.enabled, |
| button: true, |
| child: InkWell( |
| onTap: widget.enabled ? handleTap : null, |
| canRequestFocus: widget.enabled, |
| mouseCursor: _EffectiveMouseCursor(widget.mouseCursor, popupMenuTheme.mouseCursor), |
| child: item, |
| ), |
| ), |
| ); |
| } |
| } |
| |
| /// An item with a checkmark in a Material Design popup menu. |
| /// |
| /// To show a popup menu, use the [showMenu] function. To create a button that |
| /// shows a popup menu, consider using [PopupMenuButton]. |
| /// |
| /// A [CheckedPopupMenuItem] is kMinInteractiveDimension pixels high, which |
| /// matches the default minimum height of a [PopupMenuItem]. The horizontal |
| /// layout uses [ListTile]; the checkmark is an [Icons.done] icon, shown in the |
| /// [ListTile.leading] position. |
| /// |
| /// {@tool snippet} |
| /// |
| /// Suppose a `Commands` enum exists that lists the possible commands from a |
| /// particular popup menu, including `Commands.heroAndScholar` and |
| /// `Commands.hurricaneCame`, and further suppose that there is a |
| /// `_heroAndScholar` member field which is a boolean. The example below shows a |
| /// menu with one menu item with a checkmark that can toggle the boolean, and |
| /// one menu item without a checkmark for selecting the second option. (It also |
| /// shows a divider placed between the two menu items.) |
| /// |
| /// ```dart |
| /// PopupMenuButton<Commands>( |
| /// onSelected: (Commands result) { |
| /// switch (result) { |
| /// case Commands.heroAndScholar: |
| /// setState(() { _heroAndScholar = !_heroAndScholar; }); |
| /// case Commands.hurricaneCame: |
| /// // ...handle hurricane option |
| /// break; |
| /// // ...other items handled here |
| /// } |
| /// }, |
| /// itemBuilder: (BuildContext context) => <PopupMenuEntry<Commands>>[ |
| /// CheckedPopupMenuItem<Commands>( |
| /// checked: _heroAndScholar, |
| /// value: Commands.heroAndScholar, |
| /// child: const Text('Hero and scholar'), |
| /// ), |
| /// const PopupMenuDivider(), |
| /// const PopupMenuItem<Commands>( |
| /// value: Commands.hurricaneCame, |
| /// child: ListTile(leading: Icon(null), title: Text('Bring hurricane')), |
| /// ), |
| /// // ...other items listed here |
| /// ], |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// In particular, observe how the second menu item uses a [ListTile] with a |
| /// blank [Icon] in the [ListTile.leading] position to get the same alignment as |
| /// the item with the checkmark. |
| /// |
| /// See also: |
| /// |
| /// * [PopupMenuItem], a popup menu entry for picking a command (as opposed to |
| /// toggling a value). |
| /// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. |
| /// * [showMenu], a method to dynamically show a popup menu at a given location. |
| /// * [PopupMenuButton], an [IconButton] that automatically shows a menu when |
| /// it is tapped. |
| class CheckedPopupMenuItem<T> extends PopupMenuItem<T> { |
| /// Creates a popup menu item with a checkmark. |
| /// |
| /// By default, the menu item is [enabled] but unchecked. To mark the item as |
| /// checked, set [checked] to true. |
| /// |
| /// The `checked` and `enabled` arguments must not be null. |
| const CheckedPopupMenuItem({ |
| super.key, |
| super.value, |
| this.checked = false, |
| super.enabled, |
| super.padding, |
| super.height, |
| super.labelTextStyle, |
| super.mouseCursor, |
| super.child, |
| }); |
| |
| /// Whether to display a checkmark next to the menu item. |
| /// |
| /// Defaults to false. |
| /// |
| /// When true, an [Icons.done] checkmark is displayed. |
| /// |
| /// When this popup menu item is selected, the checkmark will fade in or out |
| /// as appropriate to represent the implied new state. |
| final bool checked; |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for |
| /// the child. The text should be short enough that it won't wrap. |
| /// |
| /// This widget is placed in the [ListTile.title] slot of a [ListTile] whose |
| /// [ListTile.leading] slot is an [Icons.done] icon. |
| @override |
| Widget? get child => super.child; |
| |
| @override |
| PopupMenuItemState<T, CheckedPopupMenuItem<T>> createState() => _CheckedPopupMenuItemState<T>(); |
| } |
| |
| class _CheckedPopupMenuItemState<T> extends PopupMenuItemState<T, CheckedPopupMenuItem<T>> with SingleTickerProviderStateMixin { |
| static const Duration _fadeDuration = Duration(milliseconds: 150); |
| late AnimationController _controller; |
| Animation<double> get _opacity => _controller.view; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _controller = AnimationController(duration: _fadeDuration, vsync: this) |
| ..value = widget.checked ? 1.0 : 0.0 |
| ..addListener(() => setState(() { /* animation changed */ })); |
| } |
| |
| @override |
| void handleTap() { |
| // This fades the checkmark in or out when tapped. |
| if (widget.checked) { |
| _controller.reverse(); |
| } else { |
| _controller.forward(); |
| } |
| super.handleTap(); |
| } |
| |
| @override |
| Widget buildChild() { |
| final ThemeData theme = Theme.of(context); |
| final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); |
| final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context); |
| final Set<MaterialState> states = <MaterialState>{ |
| if (widget.checked) MaterialState.selected, |
| }; |
| final MaterialStateProperty<TextStyle?>? effectiveLabelTextStyle = widget.labelTextStyle |
| ?? popupMenuTheme.labelTextStyle |
| ?? defaults.labelTextStyle; |
| return IgnorePointer( |
| child: ListTile( |
| enabled: widget.enabled, |
| titleTextStyle: effectiveLabelTextStyle?.resolve(states), |
| leading: FadeTransition( |
| opacity: _opacity, |
| child: Icon(_controller.isDismissed ? null : Icons.done), |
| ), |
| title: widget.child, |
| ), |
| ); |
| } |
| } |
| |
| class _PopupMenu<T> extends StatelessWidget { |
| const _PopupMenu({ |
| super.key, |
| required this.route, |
| required this.semanticLabel, |
| this.constraints, |
| required this.clipBehavior, |
| }); |
| |
| final _PopupMenuRoute<T> route; |
| final String? semanticLabel; |
| final BoxConstraints? constraints; |
| final Clip clipBehavior; |
| |
| @override |
| Widget build(BuildContext context) { |
| final double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. |
| final List<Widget> children = <Widget>[]; |
| final ThemeData theme = Theme.of(context); |
| final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); |
| final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context); |
| |
| for (int i = 0; i < route.items.length; i += 1) { |
| final double start = (i + 1) * unit; |
| final double end = clampDouble(start + 1.5 * unit, 0.0, 1.0); |
| final CurvedAnimation opacity = CurvedAnimation( |
| parent: route.animation!, |
| curve: Interval(start, end), |
| ); |
| Widget item = route.items[i]; |
| if (route.initialValue != null && route.items[i].represents(route.initialValue)) { |
| item = ColoredBox( |
| color: Theme.of(context).highlightColor, |
| child: item, |
| ); |
| } |
| children.add( |
| _MenuItem( |
| onLayout: (Size size) { |
| route.itemSizes[i] = size; |
| }, |
| child: FadeTransition( |
| opacity: opacity, |
| child: item, |
| ), |
| ), |
| ); |
| } |
| |
| final CurveTween opacity = CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); |
| final CurveTween width = CurveTween(curve: Interval(0.0, unit)); |
| final CurveTween height = CurveTween(curve: Interval(0.0, unit * route.items.length)); |
| |
| final Widget child = ConstrainedBox( |
| constraints: constraints ?? const BoxConstraints( |
| minWidth: _kMenuMinWidth, |
| maxWidth: _kMenuMaxWidth, |
| ), |
| child: IntrinsicWidth( |
| stepWidth: _kMenuWidthStep, |
| child: Semantics( |
| scopesRoute: true, |
| namesRoute: true, |
| explicitChildNodes: true, |
| label: semanticLabel, |
| child: SingleChildScrollView( |
| padding: const EdgeInsets.symmetric( |
| vertical: _kMenuVerticalPadding, |
| ), |
| child: ListBody(children: children), |
| ), |
| ), |
| ), |
| ); |
| |
| return AnimatedBuilder( |
| animation: route.animation!, |
| builder: (BuildContext context, Widget? child) { |
| return FadeTransition( |
| opacity: opacity.animate(route.animation!), |
| child: Material( |
| shape: route.shape ?? popupMenuTheme.shape ?? defaults.shape, |
| color: route.color ?? popupMenuTheme.color ?? defaults.color, |
| clipBehavior: clipBehavior, |
| type: MaterialType.card, |
| elevation: route.elevation ?? popupMenuTheme.elevation ?? defaults.elevation!, |
| shadowColor: route.shadowColor ?? popupMenuTheme.shadowColor ?? defaults.shadowColor, |
| surfaceTintColor: route.surfaceTintColor ?? popupMenuTheme.surfaceTintColor ?? defaults.surfaceTintColor, |
| child: Align( |
| alignment: AlignmentDirectional.topEnd, |
| widthFactor: width.evaluate(route.animation!), |
| heightFactor: height.evaluate(route.animation!), |
| child: child, |
| ), |
| ), |
| ); |
| }, |
| child: child, |
| ); |
| } |
| } |
| |
| // Positioning of the menu on the screen. |
| class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { |
| _PopupMenuRouteLayout( |
| this.position, |
| this.itemSizes, |
| this.selectedItemIndex, |
| this.textDirection, |
| this.padding, |
| this.avoidBounds, |
| ); |
| |
| // Rectangle of underlying button, relative to the overlay's dimensions. |
| final RelativeRect position; |
| |
| // The sizes of each item are computed when the menu is laid out, and before |
| // the route is laid out. |
| List<Size?> itemSizes; |
| |
| // The index of the selected item, or null if PopupMenuButton.initialValue |
| // was not specified. |
| final int? selectedItemIndex; |
| |
| // Whether to prefer going to the left or to the right. |
| final TextDirection textDirection; |
| |
| // The padding of unsafe area. |
| EdgeInsets padding; |
| |
| // List of rectangles that we should avoid overlapping. Unusable screen area. |
| final Set<Rect> avoidBounds; |
| |
| // We put the child wherever position specifies, so long as it will fit within |
| // the specified parent size padded (inset) by 8. If necessary, we adjust the |
| // child's position so that it fits. |
| |
| @override |
| BoxConstraints getConstraintsForChild(BoxConstraints constraints) { |
| // The menu can be at most the size of the overlay minus 8.0 pixels in each |
| // direction. |
| return BoxConstraints.loose(constraints.biggest).deflate( |
| const EdgeInsets.all(_kMenuScreenPadding) + padding, |
| ); |
| } |
| |
| @override |
| Offset getPositionForChild(Size size, Size childSize) { |
| // size: The size of the overlay. |
| // childSize: The size of the menu, when fully open, as determined by |
| // getConstraintsForChild. |
| |
| final double buttonHeight = size.height - position.top - position.bottom; |
| // Find the ideal vertical position. |
| double y = position.top; |
| if (selectedItemIndex != null) { |
| double selectedItemOffset = _kMenuVerticalPadding; |
| for (int index = 0; index < selectedItemIndex!; index += 1) { |
| selectedItemOffset += itemSizes[index]!.height; |
| } |
| selectedItemOffset += itemSizes[selectedItemIndex!]!.height / 2; |
| y = y + buttonHeight / 2.0 - selectedItemOffset; |
| } |
| |
| // Find the ideal horizontal position. |
| double x; |
| if (position.left > position.right) { |
| // Menu button is closer to the right edge, so grow to the left, aligned to the right edge. |
| x = size.width - position.right - childSize.width; |
| } else if (position.left < position.right) { |
| // Menu button is closer to the left edge, so grow to the right, aligned to the left edge. |
| x = position.left; |
| } else { |
| // Menu button is equidistant from both edges, so grow in reading direction. |
| switch (textDirection) { |
| case TextDirection.rtl: |
| x = size.width - position.right - childSize.width; |
| case TextDirection.ltr: |
| x = position.left; |
| } |
| } |
| final Offset wantedPosition = Offset(x, y); |
| final Offset originCenter = position.toRect(Offset.zero & size).center; |
| final Iterable<Rect> subScreens = DisplayFeatureSubScreen.subScreensInBounds(Offset.zero & size, avoidBounds); |
| final Rect subScreen = _closestScreen(subScreens, originCenter); |
| return _fitInsideScreen(subScreen, childSize, wantedPosition); |
| } |
| |
| Rect _closestScreen(Iterable<Rect> screens, Offset point) { |
| Rect closest = screens.first; |
| for (final Rect screen in screens) { |
| if ((screen.center - point).distance < (closest.center - point).distance) { |
| closest = screen; |
| } |
| } |
| return closest; |
| } |
| |
| Offset _fitInsideScreen(Rect screen, Size childSize, Offset wantedPosition){ |
| double x = wantedPosition.dx; |
| double y = wantedPosition.dy; |
| // Avoid going outside an area defined as the rectangle 8.0 pixels from the |
| // edge of the screen in every direction. |
| if (x < screen.left + _kMenuScreenPadding + padding.left) { |
| x = screen.left + _kMenuScreenPadding + padding.left; |
| } else if (x + childSize.width > screen.right - _kMenuScreenPadding - padding.right) { |
| x = screen.right - childSize.width - _kMenuScreenPadding - padding.right; |
| } |
| if (y < screen.top + _kMenuScreenPadding + padding.top) { |
| y = _kMenuScreenPadding + padding.top; |
| } else if (y + childSize.height > screen.bottom - _kMenuScreenPadding - padding.bottom) { |
| y = screen.bottom - childSize.height - _kMenuScreenPadding - padding.bottom; |
| } |
| |
| return Offset(x,y); |
| } |
| |
| @override |
| bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) { |
| // If called when the old and new itemSizes have been initialized then |
| // we expect them to have the same length because there's no practical |
| // way to change length of the items list once the menu has been shown. |
| assert(itemSizes.length == oldDelegate.itemSizes.length); |
| |
| return position != oldDelegate.position |
| || selectedItemIndex != oldDelegate.selectedItemIndex |
| || textDirection != oldDelegate.textDirection |
| || !listEquals(itemSizes, oldDelegate.itemSizes) |
| || padding != oldDelegate.padding |
| || !setEquals(avoidBounds, oldDelegate.avoidBounds); |
| } |
| } |
| |
| class _PopupMenuRoute<T> extends PopupRoute<T> { |
| _PopupMenuRoute({ |
| required this.position, |
| required this.items, |
| this.initialValue, |
| this.elevation, |
| this.surfaceTintColor, |
| this.shadowColor, |
| required this.barrierLabel, |
| this.semanticLabel, |
| this.shape, |
| this.color, |
| required this.capturedThemes, |
| this.constraints, |
| required this.clipBehavior, |
| super.settings, |
| }) : itemSizes = List<Size?>.filled(items.length, null), |
| // Menus always cycle focus through their items irrespective of the |
| // focus traversal edge behavior set in the Navigator. |
| super(traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop); |
| |
| final RelativeRect position; |
| final List<PopupMenuEntry<T>> items; |
| final List<Size?> itemSizes; |
| final T? initialValue; |
| final double? elevation; |
| final Color? surfaceTintColor; |
| final Color? shadowColor; |
| final String? semanticLabel; |
| final ShapeBorder? shape; |
| final Color? color; |
| final CapturedThemes capturedThemes; |
| final BoxConstraints? constraints; |
| final Clip clipBehavior; |
| |
| @override |
| Animation<double> createAnimation() { |
| return CurvedAnimation( |
| parent: super.createAnimation(), |
| curve: Curves.linear, |
| reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd), |
| ); |
| } |
| |
| @override |
| Duration get transitionDuration => _kMenuDuration; |
| |
| @override |
| bool get barrierDismissible => true; |
| |
| @override |
| Color? get barrierColor => null; |
| |
| @override |
| final String barrierLabel; |
| |
| @override |
| Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| |
| int? selectedItemIndex; |
| if (initialValue != null) { |
| for (int index = 0; selectedItemIndex == null && index < items.length; index += 1) { |
| if (items[index].represents(initialValue)) { |
| selectedItemIndex = index; |
| } |
| } |
| } |
| |
| final Widget menu = _PopupMenu<T>( |
| route: this, |
| semanticLabel: semanticLabel, |
| constraints: constraints, |
| clipBehavior: clipBehavior, |
| ); |
| final MediaQueryData mediaQuery = MediaQuery.of(context); |
| return MediaQuery.removePadding( |
| context: context, |
| removeTop: true, |
| removeBottom: true, |
| removeLeft: true, |
| removeRight: true, |
| child: Builder( |
| builder: (BuildContext context) { |
| return CustomSingleChildLayout( |
| delegate: _PopupMenuRouteLayout( |
| position, |
| itemSizes, |
| selectedItemIndex, |
| Directionality.of(context), |
| mediaQuery.padding, |
| _avoidBounds(mediaQuery), |
| ), |
| child: capturedThemes.wrap(menu), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| Set<Rect> _avoidBounds(MediaQueryData mediaQuery) { |
| return DisplayFeatureSubScreen.avoidBounds(mediaQuery).toSet(); |
| } |
| } |
| |
| /// Show a popup menu that contains the `items` at `position`. |
| /// |
| /// `items` should be non-null and not empty. |
| /// |
| /// If `initialValue` is specified then the first item with a matching value |
| /// will be highlighted and the value of `position` gives the rectangle whose |
| /// vertical center will be aligned with the vertical center of the highlighted |
| /// item (when possible). |
| /// |
| /// If `initialValue` is not specified then the top of the menu will be aligned |
| /// with the top of the `position` rectangle. |
| /// |
| /// In both cases, the menu position will be adjusted if necessary to fit on the |
| /// screen. |
| /// |
| /// Horizontally, the menu is positioned so that it grows in the direction that |
| /// has the most room. For example, if the `position` describes a rectangle on |
| /// the left edge of the screen, then the left edge of the menu is aligned with |
| /// the left edge of the `position`, and the menu grows to the right. If both |
| /// edges of the `position` are equidistant from the opposite edge of the |
| /// screen, then the ambient [Directionality] is used as a tie-breaker, |
| /// preferring to grow in the reading direction. |
| /// |
| /// The positioning of the `initialValue` at the `position` is implemented by |
| /// iterating over the `items` to find the first whose |
| /// [PopupMenuEntry.represents] method returns true for `initialValue`, and then |
| /// summing the values of [PopupMenuEntry.height] for all the preceding widgets |
| /// in the list. |
| /// |
| /// The `elevation` argument specifies the z-coordinate at which to place the |
| /// menu. The elevation defaults to 8, the appropriate elevation for popup |
| /// menus. |
| /// |
| /// The `context` argument is used to look up the [Navigator] and [Theme] for |
| /// the menu. It is only used when the method is called. Its corresponding |
| /// widget can be safely removed from the tree before the popup menu is closed. |
| /// |
| /// The `useRootNavigator` argument is used to determine whether to push the |
| /// menu to the [Navigator] furthest from or nearest to the given `context`. It |
| /// is `false` by default. |
| /// |
| /// The `semanticLabel` argument is used by accessibility frameworks to |
| /// announce screen transitions when the menu is opened and closed. If this |
| /// label is not provided, it will default to |
| /// [MaterialLocalizations.popupMenuLabel]. |
| /// |
| /// The `clipBehavior` argument is used to clip the shape of the menu. Defaults to |
| /// [Clip.none]. |
| /// |
| /// See also: |
| /// |
| /// * [PopupMenuItem], a popup menu entry for a single value. |
| /// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. |
| /// * [CheckedPopupMenuItem], a popup menu item with a checkmark. |
| /// * [PopupMenuButton], which provides an [IconButton] that shows a menu by |
| /// calling this method automatically. |
| /// * [SemanticsConfiguration.namesRoute], for a description of edge triggered |
| /// semantics. |
| Future<T?> showMenu<T>({ |
| required BuildContext context, |
| required RelativeRect position, |
| required List<PopupMenuEntry<T>> items, |
| T? initialValue, |
| double? elevation, |
| Color? shadowColor, |
| Color? surfaceTintColor, |
| String? semanticLabel, |
| ShapeBorder? shape, |
| Color? color, |
| bool useRootNavigator = false, |
| BoxConstraints? constraints, |
| Clip clipBehavior = Clip.none, |
| RouteSettings? routeSettings, |
| }) { |
| assert(items.isNotEmpty); |
| assert(debugCheckHasMaterialLocalizations(context)); |
| |
| switch (Theme.of(context).platform) { |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| break; |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| semanticLabel ??= MaterialLocalizations.of(context).popupMenuLabel; |
| } |
| |
| final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator); |
| return navigator.push(_PopupMenuRoute<T>( |
| position: position, |
| items: items, |
| initialValue: initialValue, |
| elevation: elevation, |
| shadowColor: shadowColor, |
| surfaceTintColor: surfaceTintColor, |
| semanticLabel: semanticLabel, |
| barrierLabel: MaterialLocalizations.of(context).menuDismissLabel, |
| shape: shape, |
| color: color, |
| capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), |
| constraints: constraints, |
| clipBehavior: clipBehavior, |
| settings: routeSettings, |
| )); |
| } |
| |
| /// Signature for the callback invoked when a menu item is selected. The |
| /// argument is the value of the [PopupMenuItem] that caused its menu to be |
| /// dismissed. |
| /// |
| /// Used by [PopupMenuButton.onSelected]. |
| typedef PopupMenuItemSelected<T> = void Function(T value); |
| |
| /// Signature for the callback invoked when a [PopupMenuButton] is dismissed |
| /// without selecting an item. |
| /// |
| /// Used by [PopupMenuButton.onCanceled]. |
| typedef PopupMenuCanceled = void Function(); |
| |
| /// Signature used by [PopupMenuButton] to lazily construct the items shown when |
| /// the button is pressed. |
| /// |
| /// Used by [PopupMenuButton.itemBuilder]. |
| typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(BuildContext context); |
| |
| /// Displays a menu when pressed and calls [onSelected] when the menu is dismissed |
| /// because an item was selected. The value passed to [onSelected] is the value of |
| /// the selected menu item. |
| /// |
| /// One of [child] or [icon] may be provided, but not both. If [icon] is provided, |
| /// then [PopupMenuButton] behaves like an [IconButton]. |
| /// |
| /// If both are null, then a standard overflow icon is created (depending on the |
| /// platform). |
| /// |
| /// /// ## Updating to [MenuAnchor] |
| /// |
| /// There is a Material 3 component, |
| /// [MenuAnchor] that is preferred for applications that are configured |
| /// for Material 3 (see [ThemeData.useMaterial3]). |
| /// The [MenuAnchor] widget's visuals |
| /// are a little bit different, see the Material 3 spec at |
| /// <https://m3.material.io/components/menus/guidelines> for |
| /// more details. |
| /// |
| /// The [MenuAnchor] widget's API is also slightly different. |
| /// [MenuAnchor]'s were built to be lower level interface for |
| /// creating menus that are displayed from an anchor. |
| /// |
| /// There are a few steps you would take to migrate from |
| /// [PopupMenuButton] to [MenuAnchor]: |
| /// |
| /// 1. Instead of using the [PopupMenuButton.itemBuilder] to build |
| /// a list of [PopupMenuEntry]s, you would use the [MenuAnchor.menuChildren] |
| /// which takes a list of [Widget]s. Usually, you would use a list of |
| /// [MenuItemButton]s as shown in the example below. |
| /// |
| /// 2. Instead of using the [PopupMenuButton.onSelected] callback, you would |
| /// set individual callbacks for each of the [MenuItemButton]s using the |
| /// [MenuItemButton.onPressed] property. |
| /// |
| /// 3. To anchor the [MenuAnchor] to a widget, you would use the [MenuAnchor.builder] |
| /// to return the widget of choice - usually a [TextButton] or an [IconButton]. |
| /// |
| /// 4. You may want to style the [MenuItemButton]s, see the [MenuItemButton] |
| /// documentation for details. |
| /// |
| /// Use the sample below for an example of migrating from [PopupMenuButton] to |
| /// [MenuAnchor]. |
| /// |
| /// {@tool dartpad} |
| /// This example shows a menu with three items, selecting between an enum's |
| /// values and setting a `selectedMenu` field based on the selection. |
| /// |
| /// ** See code in examples/api/lib/material/popup_menu/popup_menu.0.dart ** |
| /// {@end-tool} |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to migrate the above to a [MenuAnchor]. |
| /// |
| /// ** See code in examples/api/lib/material/menu_anchor/menu_anchor.2.dart ** |
| /// {@end-tool} |
| /// |
| /// {@tool dartpad} |
| /// This sample shows the creation of a popup menu, as described in: |
| /// https://m3.material.io/components/menus/overview |
| /// |
| /// ** See code in examples/api/lib/material/popup_menu/popup_menu.1.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [PopupMenuItem], a popup menu entry for a single value. |
| /// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. |
| /// * [CheckedPopupMenuItem], a popup menu item with a checkmark. |
| /// * [showMenu], a method to dynamically show a popup menu at a given location. |
| class PopupMenuButton<T> extends StatefulWidget { |
| /// Creates a button that shows a popup menu. |
| /// |
| /// The [itemBuilder] argument must not be null. |
| const PopupMenuButton({ |
| super.key, |
| required this.itemBuilder, |
| this.initialValue, |
| this.onOpened, |
| this.onSelected, |
| this.onCanceled, |
| this.tooltip, |
| this.elevation, |
| this.shadowColor, |
| this.surfaceTintColor, |
| this.padding = const EdgeInsets.all(8.0), |
| this.child, |
| this.splashRadius, |
| this.icon, |
| this.iconSize, |
| this.offset = Offset.zero, |
| this.enabled = true, |
| this.shape, |
| this.color, |
| this.enableFeedback, |
| this.constraints, |
| this.position, |
| this.clipBehavior = Clip.none, |
| }) : assert( |
| !(child != null && icon != null), |
| 'You can only pass [child] or [icon], not both.', |
| ); |
| |
| /// Called when the button is pressed to create the items to show in the menu. |
| final PopupMenuItemBuilder<T> itemBuilder; |
| |
| /// The value of the menu item, if any, that should be highlighted when the menu opens. |
| final T? initialValue; |
| |
| /// Called when the popup menu is shown. |
| final VoidCallback? onOpened; |
| |
| /// Called when the user selects a value from the popup menu created by this button. |
| /// |
| /// If the popup menu is dismissed without selecting a value, [onCanceled] is |
| /// called instead. |
| final PopupMenuItemSelected<T>? onSelected; |
| |
| /// Called when the user dismisses the popup menu without selecting an item. |
| /// |
| /// If the user selects a value, [onSelected] is called instead. |
| final PopupMenuCanceled? onCanceled; |
| |
| /// Text that describes the action that will occur when the button is pressed. |
| /// |
| /// This text is displayed when the user long-presses on the button and is |
| /// used for accessibility. |
| final String? tooltip; |
| |
| /// The z-coordinate at which to place the menu when open. This controls the |
| /// size of the shadow below the menu. |
| /// |
| /// Defaults to 8, the appropriate elevation for popup menus. |
| final double? elevation; |
| |
| /// The color used to paint the shadow below the menu. |
| /// |
| /// If null then the ambient [PopupMenuThemeData.shadowColor] is used. |
| /// If that is null too, then the overall theme's [ThemeData.shadowColor] |
| /// (default black) is used. |
| final Color? shadowColor; |
| |
| /// The color used as an overlay on [color] to indicate elevation. |
| /// |
| /// If null, [PopupMenuThemeData.surfaceTintColor] is used. If that |
| /// is also null, the default value is [ColorScheme.surfaceTint]. |
| /// |
| /// See [Material.surfaceTintColor] for more details on how this |
| /// overlay is applied. |
| final Color? surfaceTintColor; |
| |
| /// Matches IconButton's 8 dps padding by default. In some cases, notably where |
| /// this button appears as the trailing element of a list item, it's useful to be able |
| /// to set the padding to zero. |
| final EdgeInsetsGeometry padding; |
| |
| /// The splash radius. |
| /// |
| /// If null, default splash radius of [InkWell] or [IconButton] is used. |
| final double? splashRadius; |
| |
| /// If provided, [child] is the widget used for this button |
| /// and the button will utilize an [InkWell] for taps. |
| final Widget? child; |
| |
| /// If provided, the [icon] is used for this button |
| /// and the button will behave like an [IconButton]. |
| final Widget? icon; |
| |
| /// The offset is applied relative to the initial position |
| /// set by the [position]. |
| /// |
| /// When not set, the offset defaults to [Offset.zero]. |
| final Offset offset; |
| |
| /// Whether this popup menu button is interactive. |
| /// |
| /// Must be non-null, defaults to `true` |
| /// |
| /// If `true` the button will respond to presses by displaying the menu. |
| /// |
| /// If `false`, the button is styled with the disabled color from the |
| /// current [Theme] and will not respond to presses or show the popup |
| /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called. |
| /// |
| /// This can be useful in situations where the app needs to show the button, |
| /// but doesn't currently have anything to show in the menu. |
| final bool enabled; |
| |
| /// If provided, the shape used for the menu. |
| /// |
| /// If this property is null, then [PopupMenuThemeData.shape] is used. |
| /// If [PopupMenuThemeData.shape] is also null, then the default shape for |
| /// [MaterialType.card] is used. This default shape is a rectangle with |
| /// rounded edges of BorderRadius.circular(2.0). |
| final ShapeBorder? shape; |
| |
| /// If provided, the background color used for the menu. |
| /// |
| /// If this property is null, then [PopupMenuThemeData.color] is used. |
| /// If [PopupMenuThemeData.color] is also null, then |
| /// Theme.of(context).cardColor is used. |
| final Color? color; |
| |
| /// Whether detected gestures should provide acoustic and/or haptic feedback. |
| /// |
| /// For example, on Android a tap will produce a clicking sound and a |
| /// long-press will produce a short vibration, when feedback is enabled. |
| /// |
| /// See also: |
| /// |
| /// * [Feedback] for providing platform-specific feedback to certain actions. |
| final bool? enableFeedback; |
| |
| /// If provided, the size of the [Icon]. |
| /// |
| /// If this property is null, then [IconThemeData.size] is used. |
| /// If [IconThemeData.size] is also null, then |
| /// default size is 24.0 pixels. |
| final double? iconSize; |
| |
| /// Optional size constraints for the menu. |
| /// |
| /// When unspecified, defaults to: |
| /// ```dart |
| /// const BoxConstraints( |
| /// minWidth: 2.0 * 56.0, |
| /// maxWidth: 5.0 * 56.0, |
| /// ) |
| /// ``` |
| /// |
| /// The default constraints ensure that the menu width matches maximum width |
| /// recommended by the Material Design guidelines. |
| /// Specifying this parameter enables creation of menu wider than |
| /// the default maximum width. |
| final BoxConstraints? constraints; |
| |
| /// Whether the popup menu is positioned over or under the popup menu button. |
| /// |
| /// [offset] is used to change the position of the popup menu relative to the |
| /// position set by this parameter. |
| /// |
| /// If this property is `null`, then [PopupMenuThemeData.position] is used. If |
| /// [PopupMenuThemeData.position] is also `null`, then the position defaults |
| /// to [PopupMenuPosition.over] which makes the popup menu appear directly |
| /// over the button that was used to create it. |
| final PopupMenuPosition? position; |
| |
| /// {@macro flutter.material.Material.clipBehavior} |
| /// |
| /// The [clipBehavior] argument is used the clip shape of the menu. |
| /// |
| /// Defaults to [Clip.none], and must not be null. |
| final Clip clipBehavior; |
| |
| @override |
| PopupMenuButtonState<T> createState() => PopupMenuButtonState<T>(); |
| } |
| |
| /// The [State] for a [PopupMenuButton]. |
| /// |
| /// See [showButtonMenu] for a way to programmatically open the popup menu |
| /// of your button state. |
| class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { |
| /// A method to show a popup menu with the items supplied to |
| /// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton]. |
| /// |
| /// By default, it is called when the user taps the button and [PopupMenuButton.enabled] |
| /// is set to `true`. Moreover, you can open the button by calling the method manually. |
| /// |
| /// You would access your [PopupMenuButtonState] using a [GlobalKey] and |
| /// show the menu of the button with `globalKey.currentState.showButtonMenu`. |
| void showButtonMenu() { |
| final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); |
| final RenderBox button = context.findRenderObject()! as RenderBox; |
| final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox; |
| final PopupMenuPosition popupMenuPosition = widget.position ?? popupMenuTheme.position ?? PopupMenuPosition.over; |
| late Offset offset; |
| switch (popupMenuPosition) { |
| case PopupMenuPosition.over: |
| offset = widget.offset; |
| case PopupMenuPosition.under: |
| offset = Offset(0.0, button.size.height) + widget.offset; |
| if (widget.child == null) { |
| // Remove the padding of the icon button. |
| offset -= Offset(0.0, widget.padding.vertical / 2); |
| } |
| } |
| final RelativeRect position = RelativeRect.fromRect( |
| Rect.fromPoints( |
| button.localToGlobal(offset, ancestor: overlay), |
| button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay), |
| ), |
| Offset.zero & overlay.size, |
| ); |
| final List<PopupMenuEntry<T>> items = widget.itemBuilder(context); |
| // Only show the menu if there is something to show |
| if (items.isNotEmpty) { |
| widget.onOpened?.call(); |
| showMenu<T?>( |
| context: context, |
| elevation: widget.elevation ?? popupMenuTheme.elevation, |
| shadowColor: widget.shadowColor ?? popupMenuTheme.shadowColor, |
| surfaceTintColor: widget.surfaceTintColor ?? popupMenuTheme.surfaceTintColor, |
| items: items, |
| initialValue: widget.initialValue, |
| position: position, |
| shape: widget.shape ?? popupMenuTheme.shape, |
| color: widget.color ?? popupMenuTheme.color, |
| constraints: widget.constraints, |
| clipBehavior: widget.clipBehavior, |
| ) |
| .then<void>((T? newValue) { |
| if (!mounted) { |
| return null; |
| } |
| if (newValue == null) { |
| widget.onCanceled?.call(); |
| return null; |
| } |
| widget.onSelected?.call(newValue); |
| }); |
| } |
| } |
| |
| bool get _canRequestFocus { |
| final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional; |
| switch (mode) { |
| case NavigationMode.traditional: |
| return widget.enabled; |
| case NavigationMode.directional: |
| return true; |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final IconThemeData iconTheme = IconTheme.of(context); |
| final bool enableFeedback = widget.enableFeedback |
| ?? PopupMenuTheme.of(context).enableFeedback |
| ?? true; |
| |
| assert(debugCheckHasMaterialLocalizations(context)); |
| |
| if (widget.child != null) { |
| return Tooltip( |
| message: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, |
| child: InkWell( |
| onTap: widget.enabled ? showButtonMenu : null, |
| canRequestFocus: _canRequestFocus, |
| radius: widget.splashRadius, |
| enableFeedback: enableFeedback, |
| child: widget.child, |
| ), |
| ); |
| } |
| |
| return IconButton( |
| icon: widget.icon ?? Icon(Icons.adaptive.more), |
| padding: widget.padding, |
| splashRadius: widget.splashRadius, |
| iconSize: widget.iconSize ?? iconTheme.size, |
| color: widget.color ?? iconTheme.color, |
| tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, |
| onPressed: widget.enabled ? showButtonMenu : null, |
| enableFeedback: enableFeedback, |
| ); |
| } |
| } |
| |
| // This MaterialStateProperty is passed along to the menu item's InkWell which |
| // resolves the property against MaterialState.disabled, MaterialState.hovered, |
| // MaterialState.focused. |
| class _EffectiveMouseCursor extends MaterialStateMouseCursor { |
| const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor); |
| |
| final MouseCursor? widgetCursor; |
| final MaterialStateProperty<MouseCursor?>? themeCursor; |
| |
| @override |
| MouseCursor resolve(Set<MaterialState> states) { |
| return MaterialStateProperty.resolveAs<MouseCursor?>(widgetCursor, states) |
| ?? themeCursor?.resolve(states) |
| ?? MaterialStateMouseCursor.clickable.resolve(states); |
| } |
| |
| @override |
| String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)'; |
| } |
| |
| class _PopupMenuDefaultsM2 extends PopupMenuThemeData { |
| _PopupMenuDefaultsM2(this.context) |
| : super(elevation: 8.0); |
| |
| final BuildContext context; |
| late final ThemeData _theme = Theme.of(context); |
| late final TextTheme _textTheme = _theme.textTheme; |
| |
| @override |
| TextStyle? get textStyle => _textTheme.subtitle1; |
| } |
| |
| // BEGIN GENERATED TOKEN PROPERTIES - PopupMenu |
| |
| // Do not edit by hand. The code between the "BEGIN GENERATED" and |
| // "END GENERATED" comments are generated from data in the Material |
| // Design token database by the script: |
| // dev/tools/gen_defaults/bin/gen_defaults.dart. |
| |
| class _PopupMenuDefaultsM3 extends PopupMenuThemeData { |
| _PopupMenuDefaultsM3(this.context) |
| : super(elevation: 3.0); |
| |
| final BuildContext context; |
| late final ThemeData _theme = Theme.of(context); |
| late final ColorScheme _colors = _theme.colorScheme; |
| late final TextTheme _textTheme = _theme.textTheme; |
| |
| @override MaterialStateProperty<TextStyle?>? get labelTextStyle { |
| return MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| final TextStyle style = _textTheme.labelLarge!; |
| if (states.contains(MaterialState.disabled)) { |
| return style.apply(color: _colors.onSurface.withOpacity(0.38)); |
| } |
| return style.apply(color: _colors.onSurface); |
| }); |
| } |
| |
| @override |
| Color? get color => _colors.surface; |
| |
| @override |
| Color? get shadowColor => _colors.shadow; |
| |
| @override |
| Color? get surfaceTintColor => _colors.surfaceTint; |
| |
| @override |
| ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); |
| } |
| // END GENERATED TOKEN PROPERTIES - PopupMenu |