LCOV - code coverage report
Current view: top level - menu - mongol_popup_menu.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 208 219 95.0 %
Date: 2021-07-30 09:13:58 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2014 The Flutter Authors.
       2             : // Copyright 2021 Suragch.
       3             : // All rights reserved.
       4             : // Use of this source code is governed by a BSD-style license that can be
       5             : // found in the LICENSE file.
       6             : 
       7             : import 'package:flutter/foundation.dart';
       8             : import 'package:flutter/material.dart'
       9             :     show
      10             :         VerticalDivider,
      11             :         ThemeData,
      12             :         Theme,
      13             :         PopupMenuThemeData,
      14             :         PopupMenuTheme,
      15             :         Brightness,
      16             :         MaterialStateProperty,
      17             :         MaterialStateMouseCursor,
      18             :         MaterialState,
      19             :         Material,
      20             :         MaterialType,
      21             :         MaterialLocalizations,
      22             :         IconButton,
      23             :         Icons,
      24             :         InkWell,
      25             :         kMinInteractiveDimension,
      26             :         kThemeChangeDuration;
      27             : import 'package:flutter/rendering.dart';
      28             : import 'package:flutter/widgets.dart';
      29             : 
      30             : import 'mongol_icon_button.dart';
      31             : import 'mongol_instrinsic_height.dart';
      32             : import 'mongol_tooltip.dart';
      33             : 
      34             : // Examples can assume:
      35             : // enum Commands { heroAndScholar, hurricaneCame }
      36             : // late bool _heroAndScholar;
      37             : // late dynamic _selection;
      38             : // late BuildContext context;
      39             : // void setState(VoidCallback fn) { }
      40             : 
      41             : const Duration _kMenuDuration = Duration(milliseconds: 300);
      42             : const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
      43             : const double _kMenuHorizontalPadding = 8.0;
      44             : const double _kMenuDividerWidth = 16.0;
      45             : const double _kMenuMaxHeight = 5.0 * _kMenuHeightStep;
      46             : const double _kMenuMinHeight = 2.0 * _kMenuHeightStep;
      47             : const double _kMenuVerticalPadding = 16.0;
      48             : const double _kMenuHeightStep = 56.0;
      49             : const double _kMenuScreenPadding = 8.0;
      50             : 
      51             : /// A base class for entries in a material design popup menu.
      52             : ///
      53             : /// The popup menu widget uses this interface to interact with the menu items.
      54             : /// To show a popup menu, use the [showMenu] function. To create a button that
      55             : /// shows a popup menu, consider using [MongolPopupMenuButton].
      56             : ///
      57             : /// The type `T` is the type of the value(s) the entry represents. All the
      58             : /// entries in a given menu must represent values with consistent types.
      59             : ///
      60             : /// A [MongolPopupMenuEntry] may represent multiple values, for example a column
      61             : /// with several icons, or a single entry, for example a menu item with an icon
      62             : /// (see [MongolPopupMenuItem]), or no value at all (for example,
      63             : /// [MongolPopupMenuDivider]).
      64             : ///
      65             : /// See also:
      66             : ///
      67             : ///  * [MongolPopupMenuItem], a popup menu entry for a single value.
      68             : ///  * [MongolPopupMenuDivider], a popup menu entry that is just a vertical line.
      69             : ///  * [showMenu], a method to dynamically show a popup menu at a given location.
      70             : ///  * [MongolPopupMenuButton], an [IconButton] that automatically shows a menu
      71             : ///    when it is tapped.
      72             : abstract class MongolPopupMenuEntry<T> extends StatefulWidget {
      73             :   /// Abstract const constructor. This constructor enables subclasses to provide
      74             :   /// const constructors so that they can be used in const expressions.
      75          10 :   const MongolPopupMenuEntry({Key? key}) : super(key: key);
      76             : 
      77             :   /// The amount of horizontal space occupied by this entry.
      78             :   ///
      79             :   /// This value is used at the time the [showMenu] method is called, if the
      80             :   /// `initialValue` argument is provided, to determine the position of this
      81             :   /// entry when aligning the selected entry over the given `position`. It is
      82             :   /// otherwise ignored.
      83             :   double get width;
      84             : 
      85             :   /// Whether this entry represents a particular value.
      86             :   ///
      87             :   /// This method is used by [showMenu], when it is called, to align the entry
      88             :   /// representing the `initialValue`, if any, to the given `position`, and then
      89             :   /// later is called on each entry to determine if it should be highlighted (if
      90             :   /// the method returns true, the entry will have its background color set to
      91             :   /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then
      92             :   /// this method is not called.
      93             :   ///
      94             :   /// If the [MongolPopupMenuEntry] represents a single value, this should
      95             :   /// return true if the argument matches that value. If it represents multiple
      96             :   /// values, it should return true if the argument matches any of them.
      97             :   bool represents(T? value);
      98             : }
      99             : 
     100             : /// A vertical divider in a material design popup menu.
     101             : ///
     102             : /// This widget adapts the [Divider] for use in popup menus.
     103             : ///
     104             : /// See also:
     105             : ///
     106             : ///  * [MongolPopupMenuItem], for the kinds of items that this widget divides.
     107             : ///  * [showMenu], a method to dynamically show a popup menu at a given location.
     108             : ///  * [MongolPopupMenuButton], an [IconButton] that automatically shows a menu
     109             : ///    when it is tapped.
     110             : class MongolPopupMenuDivider extends MongolPopupMenuEntry<Never> {
     111             :   /// Creates a vertical divider for a popup menu.
     112             :   ///
     113             :   /// By default, the divider has a width of 16 logical pixels.
     114           0 :   const MongolPopupMenuDivider({Key? key, this.width = _kMenuDividerWidth})
     115           0 :       : super(key: key);
     116             : 
     117             :   /// The width of the divider entry.
     118             :   ///
     119             :   /// Defaults to 16 pixels.
     120             :   @override
     121             :   final double width;
     122             : 
     123           0 :   @override
     124             :   bool represents(void value) => false;
     125             : 
     126           0 :   @override
     127           0 :   _MongolPopupMenuDividerState createState() => _MongolPopupMenuDividerState();
     128             : }
     129             : 
     130             : class _MongolPopupMenuDividerState extends State<MongolPopupMenuDivider> {
     131           0 :   @override
     132           0 :   Widget build(BuildContext context) => VerticalDivider(width: widget.width);
     133             : }
     134             : 
     135             : // This widget only exists to enable _PopupMenuRoute to save the sizes of
     136             : // each menu item. The sizes are used by _PopupMenuRouteLayout to compute the
     137             : // x coordinate of the menu's origin so that the center of selected menu
     138             : // item lines up with the center of its MongolPopupMenuButton.
     139             : class _MenuItem extends SingleChildRenderObjectWidget {
     140           1 :   const _MenuItem({
     141             :     Key? key,
     142             :     required this.onLayout,
     143             :     required Widget? child,
     144           1 :   }) : super(key: key, child: child);
     145             : 
     146             :   final ValueChanged<Size> onLayout;
     147             : 
     148           1 :   @override
     149             :   RenderObject createRenderObject(BuildContext context) {
     150           2 :     return _RenderMenuItem(onLayout);
     151             :   }
     152             : 
     153           1 :   @override
     154             :   void updateRenderObject(
     155             :       BuildContext context, covariant _RenderMenuItem renderObject) {
     156           2 :     renderObject.onLayout = onLayout;
     157             :   }
     158             : }
     159             : 
     160             : class _RenderMenuItem extends RenderShiftedBox {
     161           2 :   _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);
     162             : 
     163             :   ValueChanged<Size> onLayout;
     164             : 
     165           1 :   @override
     166             :   Size computeDryLayout(BoxConstraints constraints) {
     167           1 :     if (child == null) {
     168             :       return Size.zero;
     169             :     }
     170           2 :     return child!.getDryLayout(constraints);
     171             :   }
     172             : 
     173           1 :   @override
     174             :   void performLayout() {
     175           1 :     if (child == null) {
     176           0 :       size = Size.zero;
     177             :     } else {
     178           3 :       child!.layout(constraints, parentUsesSize: true);
     179           5 :       size = constraints.constrain(child!.size);
     180           2 :       final BoxParentData childParentData = child!.parentData! as BoxParentData;
     181           1 :       childParentData.offset = Offset.zero;
     182             :     }
     183           3 :     onLayout(size);
     184             :   }
     185             : }
     186             : 
     187             : /// An item in a Mongol material design popup menu.
     188             : ///
     189             : /// To show a popup menu, use the [showMenu] function. To create a button that
     190             : /// shows a popup menu, consider using [MongolPopupMenuButton].
     191             : ///
     192             : /// Typically the [child] of a [MongolPopupMenuItem] is a [MongolText] widget.
     193             : /// More elaborate menus with icons can use a [MongolListTile]. By default, a
     194             : /// [MongolPopupMenuItem] is [kMinInteractiveDimension] pixels
     195             : /// wide. If you use a widget with a different width, it must be specified in
     196             : /// the [width] property.
     197             : ///
     198             : /// {@tool snippet}
     199             : ///
     200             : /// Here, a [MongolText] widget is used with a popup menu item. The
     201             : /// `WhyFarther` type is an enum, not shown here.
     202             : ///
     203             : /// ```dart
     204             : /// const MongolPopupMenuItem<WhyFarther>(
     205             : ///   value: WhyFarther.harder,
     206             : ///   child: MongolText('Working a lot harder'),
     207             : /// )
     208             : /// ```
     209             : /// {@end-tool}
     210             : ///
     211             : /// See the example at [MongolPopupMenuButton] for how this example could be
     212             : /// used in a complete menu.
     213             : ///
     214             : /// See also:
     215             : ///
     216             : ///  * [MongolPopupMenuDivider], which can be used to divide items from each other.
     217             : ///  * [showMenu], a method to dynamically show a popup menu at a given location.
     218             : ///  * [MongolPopupMenuButton], an [IconButton] that automatically shows a menu when
     219             : ///    it is tapped.
     220             : class MongolPopupMenuItem<T> extends MongolPopupMenuEntry<T> {
     221             :   /// Creates an item for a popup menu.
     222             :   ///
     223             :   /// By default, the item is [enabled].
     224             :   ///
     225             :   /// The `enabled` and `width` arguments must not be null.
     226           2 :   const MongolPopupMenuItem({
     227             :     Key? key,
     228             :     this.value,
     229             :     this.onTap,
     230             :     this.enabled = true,
     231             :     this.width = kMinInteractiveDimension,
     232             :     this.padding,
     233             :     this.textStyle,
     234             :     this.mouseCursor,
     235             :     required this.child,
     236           1 :   }) : super(key: key);
     237             : 
     238             :   /// The value that will be returned by [showMenu] if this entry is selected.
     239             :   final T? value;
     240             : 
     241             :   /// Called when the menu item is tapped.
     242             :   final VoidCallback? onTap;
     243             : 
     244             :   /// Whether the user is permitted to select this item.
     245             :   ///
     246             :   /// Defaults to true. If this is false, then the item will not react to
     247             :   /// touches.
     248             :   final bool enabled;
     249             : 
     250             :   /// The minimum width of the menu item.
     251             :   ///
     252             :   /// Defaults to [kMinInteractiveDimension] pixels.
     253             :   @override
     254             :   final double width;
     255             : 
     256             :   /// The padding of the menu item.
     257             :   ///
     258             :   /// Note that [width] may interact with the applied padding. For example,
     259             :   /// If a [width] greater than the width of the sum of the padding and [child]
     260             :   /// is provided, then the padding's effect will not be visible.
     261             :   ///
     262             :   /// When null, the vertical padding defaults to 16.0 on both sides.
     263             :   final EdgeInsets? padding;
     264             : 
     265             :   /// The text style of the popup menu item.
     266             :   ///
     267             :   /// If this property is null, then [PopupMenuThemeData.textStyle] is used.
     268             :   /// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1]
     269             :   /// of [ThemeData.textTheme] is used.
     270             :   final TextStyle? textStyle;
     271             : 
     272             :   /// The cursor for a mouse pointer when it enters or is hovering over the
     273             :   /// widget.
     274             :   ///
     275             :   /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
     276             :   /// [MaterialStateProperty.resolve] is used for the following [MaterialState]:
     277             :   ///
     278             :   ///  * [MaterialState.disabled].
     279             :   ///
     280             :   /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
     281             :   final MouseCursor? mouseCursor;
     282             : 
     283             :   /// The widget below this widget in the tree.
     284             :   ///
     285             :   /// Typically a single-line [MongolListTile] (for menus with icons) or a
     286             :   /// [MongolText]. An appropriate [DefaultTextStyle] is put in scope for the
     287             :   /// child. In either case, the text should be short enough that it won't wrap.
     288             :   final Widget? child;
     289             : 
     290           1 :   @override
     291           2 :   bool represents(T? value) => value == this.value;
     292             : 
     293           1 :   @override
     294             :   MongolPopupMenuItemState<T, MongolPopupMenuItem<T>> createState() =>
     295           1 :       MongolPopupMenuItemState<T, MongolPopupMenuItem<T>>();
     296             : }
     297             : 
     298             : /// The [State] for [MongolPopupMenuItem] subclasses.
     299             : ///
     300             : /// By default this implements the basic styling and layout of Material Design
     301             : /// popup menu items.
     302             : ///
     303             : /// The [buildChild] method can be overridden to adjust exactly what gets placed
     304             : /// in the menu. By default it returns [MongolPopupMenuItem.child].
     305             : ///
     306             : /// The [handleTap] method can be overridden to adjust exactly what happens when
     307             : /// the item is tapped. By default, it uses [Navigator.pop] to return the
     308             : /// [MongolPopupMenuItem.value] from the menu route.
     309             : ///
     310             : /// This class takes two type arguments. The second, `W`, is the exact type of
     311             : /// the [Widget] that is using this [State]. It must be a subclass of
     312             : /// [MongolPopupMenuItem]. The first, `T`, must match the type argument of that widget
     313             : /// class, and is the type of values returned from this menu.
     314             : class MongolPopupMenuItemState<T, W extends MongolPopupMenuItem<T>>
     315             :     extends State<W> {
     316             :   /// The menu item contents.
     317             :   ///
     318             :   /// Used by the [build] method.
     319             :   ///
     320             :   /// By default, this returns [MongolPopupMenuItem.child]. Override this to put
     321             :   /// something else in the menu entry.
     322           1 :   @protected
     323           2 :   Widget? buildChild() => widget.child;
     324             : 
     325             :   /// The handler for when the user selects the menu item.
     326             :   ///
     327             :   /// Used by the [InkWell] inserted by the [build] method.
     328             :   ///
     329             :   /// By default, uses [Navigator.pop] to return the [MongolPopupMenuItem.value] from
     330             :   /// the menu route.
     331           1 :   @protected
     332             :   void handleTap() {
     333           3 :     widget.onTap?.call();
     334             : 
     335           4 :     Navigator.pop<T>(context, widget.value);
     336             :   }
     337             : 
     338           1 :   @override
     339             :   Widget build(BuildContext context) {
     340           1 :     final ThemeData theme = Theme.of(context);
     341           1 :     final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
     342           2 :     TextStyle style = widget.textStyle ??
     343           1 :         popupMenuTheme.textStyle ??
     344           2 :         theme.textTheme.subtitle1!;
     345             : 
     346           4 :     if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
     347             : 
     348           1 :     Widget item = AnimatedDefaultTextStyle(
     349             :       style: style,
     350             :       duration: kThemeChangeDuration,
     351           1 :       child: Container(
     352             :         alignment: Alignment.topCenter,
     353           3 :         constraints: BoxConstraints(minWidth: widget.width),
     354           2 :         padding: widget.padding ??
     355             :             const EdgeInsets.symmetric(vertical: _kMenuVerticalPadding),
     356           1 :         child: buildChild(),
     357             :       ),
     358             :     );
     359             : 
     360           2 :     if (!widget.enabled) {
     361           2 :       final bool isDark = theme.brightness == Brightness.dark;
     362           1 :       item = IconTheme.merge(
     363           1 :         data: IconThemeData(opacity: isDark ? 0.5 : 0.38),
     364             :         child: item,
     365             :       );
     366             :     }
     367             :     final MouseCursor effectiveMouseCursor =
     368           1 :         MaterialStateProperty.resolveAs<MouseCursor>(
     369           2 :       widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
     370             :       <MaterialState>{
     371           3 :         if (!widget.enabled) MaterialState.disabled,
     372             :       },
     373             :     );
     374             : 
     375           1 :     return MergeSemantics(
     376           1 :       child: Semantics(
     377           2 :         enabled: widget.enabled,
     378             :         button: true,
     379           1 :         child: InkWell(
     380           3 :           onTap: widget.enabled ? handleTap : null,
     381           2 :           canRequestFocus: widget.enabled,
     382             :           mouseCursor: effectiveMouseCursor,
     383             :           child: item,
     384             :         ),
     385             :       ),
     386             :     );
     387             :   }
     388             : }
     389             : 
     390             : class _PopupMenu<T> extends StatelessWidget {
     391           1 :   const _PopupMenu({
     392             :     Key? key,
     393             :     required this.route,
     394             :     required this.semanticLabel,
     395           1 :   }) : super(key: key);
     396             : 
     397             :   final _PopupMenuRoute<T> route;
     398             :   final String? semanticLabel;
     399             : 
     400           1 :   @override
     401             :   Widget build(BuildContext context) {
     402           1 :     final double unit = 1.0 /
     403           4 :         (route.items.length +
     404             :             1.5); // 1.0 for the height and 0.5 for the last item's fade.
     405           1 :     final List<Widget> children = <Widget>[];
     406           1 :     final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
     407             : 
     408           5 :     for (int i = 0; i < route.items.length; i += 1) {
     409           2 :       final double start = (i + 1) * unit;
     410           3 :       final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
     411           1 :       final CurvedAnimation opacity = CurvedAnimation(
     412           2 :         parent: route.animation!,
     413           1 :         curve: Interval(start, end),
     414             :       );
     415           3 :       Widget item = route.items[i] as Widget;
     416           2 :       if (route.initialValue != null &&
     417           3 :           (route.items[i] as MongolPopupMenuItem)
     418           3 :               .represents(route.initialValue)) {
     419           1 :         item = Container(
     420           2 :           color: Theme.of(context).highlightColor,
     421             :           child: item,
     422             :         );
     423             :       }
     424           1 :       children.add(
     425           1 :         _MenuItem(
     426           1 :           onLayout: (Size size) {
     427           3 :             route.itemSizes[i] = size;
     428             :           },
     429           1 :           child: FadeTransition(
     430             :             opacity: opacity,
     431             :             child: item,
     432             :           ),
     433             :         ),
     434             :       );
     435             :     }
     436             : 
     437             :     final CurveTween opacity =
     438           1 :         CurveTween(curve: const Interval(0.0, 1.0 / 3.0));
     439           2 :     final CurveTween height = CurveTween(curve: Interval(0.0, unit));
     440             :     final CurveTween width =
     441           6 :         CurveTween(curve: Interval(0.0, unit * route.items.length));
     442             : 
     443           1 :     final Widget child = ConstrainedBox(
     444             :       constraints: const BoxConstraints(
     445             :         minHeight: _kMenuMinHeight,
     446             :         maxHeight: _kMenuMaxHeight,
     447             :       ),
     448           1 :       child: MongolIntrinsicHeight(
     449             :         stepHeight: _kMenuHeightStep,
     450           1 :         child: Semantics(
     451             :           scopesRoute: true,
     452             :           namesRoute: true,
     453             :           explicitChildNodes: true,
     454           1 :           label: semanticLabel,
     455           1 :           child: SingleChildScrollView(
     456             :             scrollDirection: Axis.horizontal,
     457             :             padding: const EdgeInsets.symmetric(
     458             :               horizontal: _kMenuHorizontalPadding,
     459             :             ),
     460           1 :             child: ListBody(
     461             :               mainAxis: Axis.horizontal,
     462             :               children: children,
     463             :             ),
     464             :           ),
     465             :         ),
     466             :       ),
     467             :     );
     468             : 
     469           1 :     return AnimatedBuilder(
     470           2 :       animation: route.animation!,
     471           1 :       builder: (BuildContext context, Widget? child) {
     472           1 :         return Opacity(
     473           3 :           opacity: opacity.evaluate(route.animation!),
     474           1 :           child: Material(
     475           3 :             shape: route.shape ?? popupMenuTheme.shape,
     476           3 :             color: route.color ?? popupMenuTheme.color,
     477             :             type: MaterialType.card,
     478           3 :             elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0,
     479           1 :             child: Align(
     480             :               alignment: Alignment.topRight,
     481           3 :               widthFactor: width.evaluate(route.animation!),
     482           3 :               heightFactor: height.evaluate(route.animation!),
     483             :               child: child,
     484             :             ),
     485             :           ),
     486             :         );
     487             :       },
     488             :       child: child,
     489             :     );
     490             :   }
     491             : }
     492             : 
     493             : // Positioning of the menu on the screen.
     494             : class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
     495           1 :   _PopupMenuRouteLayout(
     496             :     this.position,
     497             :     this.itemSizes,
     498             :     this.selectedItemIndex,
     499             :     this.padding,
     500             :   );
     501             : 
     502             :   // Rectangle of underlying button, relative to the overlay's dimensions.
     503             :   final RelativeRect position;
     504             : 
     505             :   // The sizes of each item are computed when the menu is laid out, and before
     506             :   // the route is laid out.
     507             :   List<Size?> itemSizes;
     508             : 
     509             :   // The index of the selected item, or null if MongolPopupMenuButton.initialValue
     510             :   // was not specified.
     511             :   final int? selectedItemIndex;
     512             : 
     513             :   // The padding of unsafe area.
     514             :   EdgeInsets padding;
     515             : 
     516             :   // We put the child wherever position specifies, so long as it will fit within
     517             :   // the specified parent size padded (inset) by 8. If necessary, we adjust the
     518             :   // child's position so that it fits.
     519             : 
     520           1 :   @override
     521             :   BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
     522             :     // The menu can be at most the size of the overlay minus 8.0 pixels in each
     523             :     // direction.
     524           3 :     return BoxConstraints.loose(constraints.biggest).deflate(
     525           2 :       const EdgeInsets.all(_kMenuScreenPadding) + padding,
     526             :     );
     527             :   }
     528             : 
     529           1 :   @override
     530             :   Offset getPositionForChild(Size size, Size childSize) {
     531             :     // size: The size of the overlay.
     532             :     // childSize: The size of the menu, when fully open, as determined by
     533             :     // getConstraintsForChild.
     534             : 
     535           7 :     final double buttonWidth = size.width - position.left - position.right;
     536             :     // Find the ideal horizontal position.
     537           2 :     double x = position.left;
     538           2 :     if (selectedItemIndex != null && itemSizes != null) {
     539             :       double selectedItemOffset = _kMenuHorizontalPadding;
     540           3 :       for (int index = 0; index < selectedItemIndex!; index += 1) {
     541           4 :         selectedItemOffset += itemSizes[index]!.width;
     542             :       }
     543           6 :       selectedItemOffset += itemSizes[selectedItemIndex!]!.width / 2;
     544           3 :       x = x + buttonWidth / 2.0 - selectedItemOffset;
     545             :     }
     546             : 
     547             :     // Find the ideal vertical position.
     548             :     double y;
     549           5 :     if (position.top > position.bottom) {
     550             :       // Menu button is closer to the top edge, so grow to the bottom, aligned to the bottom edge.
     551           0 :       y = size.height - position.bottom - childSize.height;
     552             :     } else {
     553             :       // Menu button is closer to the top edge or is equidistant from both edges, so grow down.
     554           2 :       y = position.top;
     555             :     }
     556             : 
     557             :     // Avoid going outside an area defined as the rectangle 8.0 pixels from the
     558             :     // edge of the screen in every direction.
     559           4 :     if (y < _kMenuScreenPadding + padding.top) {
     560           3 :       y = _kMenuScreenPadding + padding.top;
     561           3 :     } else if (y + childSize.height >
     562           5 :         size.height - _kMenuScreenPadding - padding.bottom) {
     563           0 :       y = size.height - childSize.height - _kMenuScreenPadding - padding.bottom;
     564             :     }
     565           4 :     if (x < _kMenuScreenPadding + padding.left) {
     566           3 :       x = _kMenuScreenPadding + padding.left;
     567           3 :     } else if (x + childSize.width >
     568           5 :         size.width - _kMenuScreenPadding - padding.right) {
     569           7 :       x = size.width - padding.right - _kMenuScreenPadding - childSize.width;
     570             :     }
     571             : 
     572           1 :     return Offset(x, y);
     573             :   }
     574             : 
     575           1 :   @override
     576             :   bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) {
     577             :     // If called when the old and new itemSizes have been initialized then
     578             :     // we expect them to have the same length because there's no practical
     579             :     // way to change length of the items list once the menu has been shown.
     580           5 :     assert(itemSizes.length == oldDelegate.itemSizes.length);
     581             : 
     582           3 :     return position != oldDelegate.position ||
     583           3 :         selectedItemIndex != oldDelegate.selectedItemIndex ||
     584           3 :         !listEquals(itemSizes, oldDelegate.itemSizes) ||
     585           3 :         padding != oldDelegate.padding;
     586             :   }
     587             : }
     588             : 
     589             : class _PopupMenuRoute<T> extends PopupRoute<T> {
     590           1 :   _PopupMenuRoute({
     591             :     required this.position,
     592             :     required this.items,
     593             :     this.initialValue,
     594             :     this.elevation,
     595             :     required this.barrierLabel,
     596             :     this.semanticLabel,
     597             :     this.shape,
     598             :     this.color,
     599             :     required this.capturedThemes,
     600           2 :   }) : itemSizes = List<Size?>.filled(items.length, null);
     601             : 
     602             :   final RelativeRect position;
     603             :   final List<MongolPopupMenuEntry<T>> items;
     604             :   final List<Size?> itemSizes;
     605             :   final T? initialValue;
     606             :   final double? elevation;
     607             :   final String? semanticLabel;
     608             :   final ShapeBorder? shape;
     609             :   final Color? color;
     610             :   final CapturedThemes capturedThemes;
     611             : 
     612           1 :   @override
     613             :   Animation<double> createAnimation() {
     614           1 :     return CurvedAnimation(
     615           1 :       parent: super.createAnimation(),
     616             :       curve: Curves.linear,
     617             :       reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd),
     618             :     );
     619             :   }
     620             : 
     621           1 :   @override
     622             :   Duration get transitionDuration => _kMenuDuration;
     623             : 
     624           1 :   @override
     625             :   bool get barrierDismissible => true;
     626             : 
     627           1 :   @override
     628             :   Color? get barrierColor => null;
     629             : 
     630             :   @override
     631             :   final String barrierLabel;
     632             : 
     633           1 :   @override
     634             :   Widget buildPage(BuildContext context, Animation<double> animation,
     635             :       Animation<double> secondaryAnimation) {
     636             :     int? selectedItemIndex;
     637           1 :     if (initialValue != null) {
     638             :       for (int index = 0;
     639           3 :           selectedItemIndex == null && index < items.length;
     640           1 :           index += 1) {
     641           4 :         if (items[index].represents(initialValue)) selectedItemIndex = index;
     642             :       }
     643             :     }
     644             : 
     645             :     final Widget menu =
     646           2 :         _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
     647           1 :     final MediaQueryData mediaQuery = MediaQuery.of(context);
     648           1 :     return MediaQuery.removePadding(
     649             :       context: context,
     650             :       removeTop: true,
     651             :       removeBottom: true,
     652             :       removeLeft: true,
     653             :       removeRight: true,
     654           1 :       child: Builder(
     655           1 :         builder: (BuildContext context) {
     656           1 :           return CustomSingleChildLayout(
     657           1 :             delegate: _PopupMenuRouteLayout(
     658           1 :               position,
     659           1 :               itemSizes,
     660             :               selectedItemIndex,
     661           1 :               mediaQuery.padding,
     662             :             ),
     663           2 :             child: capturedThemes.wrap(menu),
     664             :           );
     665             :         },
     666             :       ),
     667             :     );
     668             :   }
     669             : }
     670             : 
     671             : /// Show a popup menu that contains the `items` at `position`.
     672             : ///
     673             : /// `items` should be non-null and not empty.
     674             : ///
     675             : /// If `initialValue` is specified then the first item with a matching value
     676             : /// will be highlighted and the value of `position` gives the rectangle whose
     677             : /// horizontal center will be aligned with the horizontal center of the highlighted
     678             : /// item (when possible).
     679             : ///
     680             : /// If `initialValue` is not specified then the right side of the menu will be aligned
     681             : /// with the right side of the `position` rectangle.
     682             : ///
     683             : /// In both cases, the menu position will be adjusted if necessary to fit on the
     684             : /// screen.
     685             : ///
     686             : /// Vertically, the menu is positioned so that it grows in the direction that
     687             : /// has the most room. For example, if the `position` describes a rectangle on
     688             : /// the top edge of the screen, then the top edge of the menu is aligned with
     689             : /// the top edge of the `position`, and the menu grows to the bottom. If both
     690             : /// edges of the `position` are equidistant from the opposite edge of the
     691             : /// screen, then it grows down.
     692             : ///
     693             : /// The positioning of the `initialValue` at the `position` is implemented by
     694             : /// iterating over the `items` to find the first whose
     695             : /// [MongolPopupMenuEntry.represents] method returns true for `initialValue`, and then
     696             : /// summing the values of [MongolPopupMenuEntry.width] for all the preceding widgets
     697             : /// in the list.
     698             : ///
     699             : /// The `elevation` argument specifies the z-coordinate at which to place the
     700             : /// menu. The elevation defaults to 8, the appropriate elevation for popup
     701             : /// menus.
     702             : ///
     703             : /// The `context` argument is used to look up the [Navigator] and [Theme] for
     704             : /// the menu. It is only used when the method is called. Its corresponding
     705             : /// widget can be safely removed from the tree before the popup menu is closed.
     706             : ///
     707             : /// The `useRootNavigator` argument is used to determine whether to push the
     708             : /// menu to the [Navigator] furthest from or nearest to the given `context`. It
     709             : /// is `false` by default.
     710             : ///
     711             : /// The `semanticLabel` argument is used by accessibility frameworks to
     712             : /// announce screen transitions when the menu is opened and closed. If this
     713             : /// label is not provided, it will default to
     714             : /// [MaterialLocalizations.popupMenuLabel].
     715             : ///
     716             : /// See also:
     717             : ///
     718             : ///  * [MongolPopupMenuItem], a popup menu entry for a single value.
     719             : ///  * [MongolPopupMenuDivider], a popup menu entry that is just a vertical line.
     720             : ///  * [MongolPopupMenuButton], which provides an [IconButton] that shows a menu by
     721             : ///    calling this method automatically.
     722             : ///  * [SemanticsConfiguration.namesRoute], for a description of edge triggered
     723             : ///    semantics.
     724           1 : Future<T?> showMenu<T>({
     725             :   required BuildContext context,
     726             :   required RelativeRect position,
     727             :   required List<MongolPopupMenuEntry<T>> items,
     728             :   T? initialValue,
     729             :   double? elevation,
     730             :   String? semanticLabel,
     731             :   ShapeBorder? shape,
     732             :   Color? color,
     733             :   bool useRootNavigator = false,
     734             : }) {
     735           1 :   assert(items.isNotEmpty);
     736             : 
     737           2 :   semanticLabel ??= MaterialLocalizations.of(context).popupMenuLabel;
     738             : 
     739             :   final NavigatorState navigator =
     740           1 :       Navigator.of(context, rootNavigator: useRootNavigator);
     741           2 :   return navigator.push(_PopupMenuRoute<T>(
     742             :     position: position,
     743             :     items: items,
     744             :     initialValue: initialValue,
     745             :     elevation: elevation,
     746             :     semanticLabel: semanticLabel,
     747           2 :     barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
     748             :     shape: shape,
     749             :     color: color,
     750             :     capturedThemes:
     751           2 :         InheritedTheme.capture(from: context, to: navigator.context),
     752             :   ));
     753             : }
     754             : 
     755             : /// Signature for the callback invoked when a menu item is selected. The
     756             : /// argument is the value of the [MongolPopupMenuItem] that caused its menu to be
     757             : /// dismissed.
     758             : ///
     759             : /// Used by [MongolPopupMenuButton.onSelected].
     760             : typedef MongolPopupMenuItemSelected<T> = void Function(T value);
     761             : 
     762             : /// Signature for the callback invoked when a [MongolPopupMenuButton] is dismissed
     763             : /// without selecting an item.
     764             : ///
     765             : /// Used by [MongolPopupMenuButton.onCanceled].
     766             : typedef MongolPopupMenuCanceled = void Function();
     767             : 
     768             : /// Signature used by [MongolPopupMenuButton] to lazily construct the items shown when
     769             : /// the button is pressed.
     770             : ///
     771             : /// Used by [MongolPopupMenuButton.itemBuilder].
     772             : typedef MongolPopupMenuItemBuilder<T> = List<MongolPopupMenuEntry<T>> Function(
     773             :     BuildContext context);
     774             : 
     775             : /// Displays a menu when pressed and calls [onSelected] when the menu is dismissed
     776             : /// because an item was selected. The value passed to [onSelected] is the value of
     777             : /// the selected menu item.
     778             : ///
     779             : /// One of [child] or [icon] may be provided, but not both. If [icon] is provided,
     780             : /// then [MongolPopupMenuButton] behaves like an [IconButton].
     781             : ///
     782             : /// If both are null, then a standard overflow icon is created (depending on the
     783             : /// platform).
     784             : ///
     785             : /// {@tool snippet}
     786             : ///
     787             : /// This example shows a menu with four items, selecting between an enum's
     788             : /// values and setting a `_selection` field based on the selection.
     789             : ///
     790             : /// ```dart
     791             : /// // This is the type used by the popup menu below.
     792             : /// enum WhyFarther { harder, smarter, selfStarter, tradingCharter }
     793             : ///
     794             : /// // This menu button widget updates a _selection field (of type WhyFarther,
     795             : /// // not shown here).
     796             : /// MongolPopupMenuButton<WhyFarther>(
     797             : ///   onSelected: (WhyFarther result) { setState(() { _selection = result; }); },
     798             : ///   itemBuilder: (BuildContext context) => <MongolPopupMenuEntry<WhyFarther>>[
     799             : ///     const MongolPopupMenuItem<WhyFarther>(
     800             : ///       value: WhyFarther.harder,
     801             : ///       child: MongolText('Working a lot harder'),
     802             : ///     ),
     803             : ///     const MongolPopupMenuItem<WhyFarther>(
     804             : ///       value: WhyFarther.smarter,
     805             : ///       child: MongolText('Being a lot smarter'),
     806             : ///     ),
     807             : ///     const MongolPopupMenuItem<WhyFarther>(
     808             : ///       value: WhyFarther.selfStarter,
     809             : ///       child: MongolText('Being a self-starter'),
     810             : ///     ),
     811             : ///     const MongolPopupMenuItem<WhyFarther>(
     812             : ///       value: WhyFarther.tradingCharter,
     813             : ///       child: MongolText('Placed in charge of trading charter'),
     814             : ///     ),
     815             : ///   ],
     816             : /// )
     817             : /// ```
     818             : /// {@end-tool}
     819             : ///
     820             : /// See also:
     821             : ///
     822             : ///  * [MongolPopupMenuItem], a popup menu entry for a single value.
     823             : ///  * [MongolPopupMenuDivider], a popup menu entry that is just a vertical line.
     824             : ///  * [showMenu], a method to dynamically show a popup menu at a given location.
     825             : class MongolPopupMenuButton<T> extends StatefulWidget {
     826             :   /// Creates a button that shows a popup menu.
     827             :   ///
     828             :   /// The [itemBuilder] argument must not be null.
     829           1 :   const MongolPopupMenuButton({
     830             :     Key? key,
     831             :     required this.itemBuilder,
     832             :     this.initialValue,
     833             :     this.onSelected,
     834             :     this.onCanceled,
     835             :     this.tooltip,
     836             :     this.elevation,
     837             :     this.padding = const EdgeInsets.all(8.0),
     838             :     this.child,
     839             :     this.icon,
     840             :     this.iconSize,
     841             :     this.offset = Offset.zero,
     842             :     this.enabled = true,
     843             :     this.shape,
     844             :     this.color,
     845             :     this.enableFeedback,
     846           0 :   })  : assert(itemBuilder != null),
     847             :         assert(
     848           1 :           !(child != null && icon != null),
     849             :           'You can only pass [child] or [icon], not both.',
     850             :         ),
     851           1 :         super(key: key);
     852             : 
     853             :   /// Called when the button is pressed to create the items to show in the menu.
     854             :   final MongolPopupMenuItemBuilder<T> itemBuilder;
     855             : 
     856             :   /// The value of the menu item, if any, that should be highlighted when the menu opens.
     857             :   final T? initialValue;
     858             : 
     859             :   /// Called when the user selects a value from the popup menu created by this button.
     860             :   ///
     861             :   /// If the popup menu is dismissed without selecting a value, [onCanceled] is
     862             :   /// called instead.
     863             :   final MongolPopupMenuItemSelected<T>? onSelected;
     864             : 
     865             :   /// Called when the user dismisses the popup menu without selecting an item.
     866             :   ///
     867             :   /// If the user selects a value, [onSelected] is called instead.
     868             :   final MongolPopupMenuCanceled? onCanceled;
     869             : 
     870             :   /// Text that describes the action that will occur when the button is pressed.
     871             :   ///
     872             :   /// This text is displayed when the user long-presses on the button and is
     873             :   /// used for accessibility.
     874             :   final String? tooltip;
     875             : 
     876             :   /// The z-coordinate at which to place the menu when open. This controls the
     877             :   /// size of the shadow below the menu.
     878             :   ///
     879             :   /// Defaults to 8, the appropriate elevation for popup menus.
     880             :   final double? elevation;
     881             : 
     882             :   /// Matches IconButton's 8 dps padding by default. In some cases, notably where
     883             :   /// this button appears as the trailing element of a list item, it's useful to be able
     884             :   /// to set the padding to zero.
     885             :   final EdgeInsetsGeometry padding;
     886             : 
     887             :   /// If provided, [child] is the widget used for this button
     888             :   /// and the button will utilize an [InkWell] for taps.
     889             :   final Widget? child;
     890             : 
     891             :   /// If provided, the [icon] is used for this button
     892             :   /// and the button will behave like an [IconButton].
     893             :   final Widget? icon;
     894             : 
     895             :   /// The offset applied to the Popup Menu Button.
     896             :   ///
     897             :   /// When not set, the Popup Menu Button will be positioned directly next to
     898             :   /// the button that was used to create it.
     899             :   final Offset offset;
     900             : 
     901             :   /// Whether this popup menu button is interactive.
     902             :   ///
     903             :   /// Must be non-null, defaults to `true`
     904             :   ///
     905             :   /// If `true` the button will respond to presses by displaying the menu.
     906             :   ///
     907             :   /// If `false`, the button is styled with the disabled color from the
     908             :   /// current [Theme] and will not respond to presses or show the popup
     909             :   /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called.
     910             :   ///
     911             :   /// This can be useful in situations where the app needs to show the button,
     912             :   /// but doesn't currently have anything to show in the menu.
     913             :   final bool enabled;
     914             : 
     915             :   /// If provided, the shape used for the menu.
     916             :   ///
     917             :   /// If this property is null, then [PopupMenuThemeData.shape] is used.
     918             :   /// If [PopupMenuThemeData.shape] is also null, then the default shape for
     919             :   /// [MaterialType.card] is used. This default shape is a rectangle with
     920             :   /// rounded edges of BorderRadius.circular(2.0).
     921             :   final ShapeBorder? shape;
     922             : 
     923             :   /// If provided, the background color used for the menu.
     924             :   ///
     925             :   /// If this property is null, then [PopupMenuThemeData.color] is used.
     926             :   /// If [PopupMenuThemeData.color] is also null, then
     927             :   /// Theme.of(context).cardColor is used.
     928             :   final Color? color;
     929             : 
     930             :   /// Whether detected gestures should provide acoustic and/or haptic feedback.
     931             :   ///
     932             :   /// For example, on Android a tap will produce a clicking sound and a
     933             :   /// long-press will produce a short vibration, when feedback is enabled.
     934             :   ///
     935             :   /// See also:
     936             :   ///
     937             :   ///  * [Feedback] for providing platform-specific feedback to certain actions.
     938             :   final bool? enableFeedback;
     939             : 
     940             :   /// If provided, the size of the [Icon].
     941             :   ///
     942             :   /// If this property is null, the default size is 24.0 pixels.
     943             :   final double? iconSize;
     944             : 
     945           1 :   @override
     946             :   MongolPopupMenuButtonState<T> createState() =>
     947           1 :       MongolPopupMenuButtonState<T>();
     948             : }
     949             : 
     950             : /// The [State] for a [MongolPopupMenuButton].
     951             : ///
     952             : /// See [showButtonMenu] for a way to programmatically open the popup menu
     953             : /// of your button state.
     954             : class MongolPopupMenuButtonState<T> extends State<MongolPopupMenuButton<T>> {
     955             :   /// A method to show a popup menu with the items supplied to
     956             :   /// [MongolPopupMenuButton.itemBuilder] at the position of your [MongolPopupMenuButton].
     957             :   ///
     958             :   /// By default, it is called when the user taps the button and [MongolPopupMenuButton.enabled]
     959             :   /// is set to `true`. Moreover, you can open the button by calling the method manually.
     960             :   ///
     961             :   /// You would access your [MongolPopupMenuButtonState] using a [GlobalKey] and
     962             :   /// show the menu of the button with `globalKey.currentState.showButtonMenu`.
     963           1 :   void showButtonMenu() {
     964           2 :     final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
     965           2 :     final RenderBox button = context.findRenderObject()! as RenderBox;
     966             :     final RenderBox overlay =
     967           5 :         Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
     968           1 :     final RelativeRect position = RelativeRect.fromRect(
     969           1 :       Rect.fromPoints(
     970           3 :         button.localToGlobal(widget.offset, ancestor: overlay),
     971           1 :         button.localToGlobal(
     972           5 :             button.size.bottomRight(Offset.zero) + widget.offset,
     973             :             ancestor: overlay),
     974             :       ),
     975           2 :       Offset.zero & overlay.size,
     976             :     );
     977           4 :     final List<MongolPopupMenuEntry<T>> items = widget.itemBuilder(context);
     978             :     // Only show the menu if there is something to show
     979           1 :     if (items.isNotEmpty) {
     980           1 :       showMenu<T?>(
     981           1 :         context: context,
     982           3 :         elevation: widget.elevation ?? popupMenuTheme.elevation,
     983             :         items: items,
     984           2 :         initialValue: widget.initialValue,
     985             :         position: position,
     986           3 :         shape: widget.shape ?? popupMenuTheme.shape,
     987           3 :         color: widget.color ?? popupMenuTheme.color,
     988           2 :       ).then<void>((T? newValue) {
     989           1 :         if (!mounted) return null;
     990             :         if (newValue == null) {
     991           3 :           widget.onCanceled?.call();
     992             :           return null;
     993             :         }
     994           3 :         widget.onSelected?.call(newValue);
     995             :       });
     996             :     }
     997             :   }
     998             : 
     999           1 :   bool get _canRequestFocus {
    1000           3 :     final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ??
    1001             :         NavigationMode.traditional;
    1002             :     switch (mode) {
    1003           1 :       case NavigationMode.traditional:
    1004           2 :         return widget.enabled;
    1005           1 :       case NavigationMode.directional:
    1006             :         return true;
    1007             :     }
    1008             :   }
    1009             : 
    1010           1 :   @override
    1011             :   Widget build(BuildContext context) {
    1012           2 :     final bool enableFeedback = widget.enableFeedback ??
    1013           2 :         PopupMenuTheme.of(context).enableFeedback ??
    1014             :         true;
    1015             : 
    1016           2 :     if (widget.child != null) {
    1017           1 :       return MongolTooltip(
    1018             :         message:
    1019           4 :             widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
    1020           1 :         child: InkWell(
    1021           3 :           onTap: widget.enabled ? showButtonMenu : null,
    1022           1 :           canRequestFocus: _canRequestFocus,
    1023           2 :           child: widget.child,
    1024             :           enableFeedback: enableFeedback,
    1025             :         ),
    1026             :       );
    1027             :     }
    1028             : 
    1029           1 :     return MongolIconButton(
    1030           5 :       icon: widget.icon ?? Icon(Icons.adaptive.more),
    1031           2 :       padding: widget.padding,
    1032           2 :       iconSize: widget.iconSize ?? 24.0,
    1033             :       mongolTooltip:
    1034           4 :           widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
    1035           3 :       onPressed: widget.enabled ? showButtonMenu : null,
    1036             :       enableFeedback: enableFeedback,
    1037             :     );
    1038             :   }
    1039             : }

Generated by: LCOV version 1.15