1435 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1435 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| // 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/material.dart';
 | |
| import 'package:flutter_hbb/common.dart';
 | |
| import 'package:flutter_hbb/desktop/widgets/menu_button.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 Duration _kMenuDuration = Duration(milliseconds: 0);
 | |
| 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 _kMenuMaxWidth = double.infinity;
 | |
| // const double _kMenuVerticalPadding = 8.0;
 | |
| const double _kMenuVerticalPadding = 8.0;
 | |
| const double _kMenuWidthStep = 0.0;
 | |
| //const double _kMenuScreenPadding = 8.0;
 | |
| const double _kMenuScreenPadding = 0.0;
 | |
| const double _kDefaultIconSize = 24.0;
 | |
| 
 | |
| /// Used to configure how the [PopupMenuButton] positions its popup menu.
 | |
| enum PopupMenuPosition {
 | |
|   /// Menu is positioned over the anchor.
 | |
|   over,
 | |
| 
 | |
|   /// Menu is positioned under the anchor.
 | |
|   under,
 | |
| 
 | |
|   // Only support right side (TextDirection.ltr) for now
 | |
|   /// Menu is positioned over side the anchor
 | |
|   overSide,
 | |
| 
 | |
|   // Only support right side (TextDirection.ltr) for now
 | |
|   /// Menu is positioned under side the anchor
 | |
|   underSide,
 | |
| }
 | |
| 
 | |
| /// 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({Key? key}) : super(key: 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({Key? key, this.height = _kMenuDividerHeight})
 | |
|       : super(key: key);
 | |
| 
 | |
|   /// 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({
 | |
|     Key? key,
 | |
|     required this.onLayout,
 | |
|     required Widget? child,
 | |
|   }) : super(key: key, child: 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({
 | |
|     Key? key,
 | |
|     this.value,
 | |
|     this.onTap,
 | |
|     this.enabled = true,
 | |
|     this.height = kMinInteractiveDimension,
 | |
|     this.padding,
 | |
|     this.textStyle,
 | |
|     this.mouseCursor,
 | |
|     required this.child,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   /// 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.
 | |
|   ///
 | |
|   /// Note that [height] 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.subtitle1]
 | |
|   /// of [ThemeData.textTheme] is used.
 | |
|   final TextStyle? textStyle;
 | |
| 
 | |
|   /// {@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() {
 | |
|     widget.onTap?.call();
 | |
| 
 | |
|     Navigator.pop<T>(context, widget.value);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final ThemeData theme = Theme.of(context);
 | |
|     final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
 | |
|     TextStyle style = widget.textStyle ??
 | |
|         popupMenuTheme.textStyle ??
 | |
|         theme.textTheme.subtitle1!;
 | |
| 
 | |
|     if (!widget.enabled) 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,
 | |
|         // ),
 | |
|         child: TextButton(
 | |
|           onPressed: widget.enabled ? handleTap : null,
 | |
|           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; });
 | |
| ///         break;
 | |
| ///       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({
 | |
|     Key? key,
 | |
|     T? value,
 | |
|     this.checked = false,
 | |
|     bool enabled = true,
 | |
|     EdgeInsets? padding,
 | |
|     double height = kMinInteractiveDimension,
 | |
|     Widget? child,
 | |
|   }) : super(
 | |
|           key: key,
 | |
|           value: value,
 | |
|           enabled: enabled,
 | |
|           padding: padding,
 | |
|           height: height,
 | |
|           child: 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() {
 | |
|     return ListTile(
 | |
|       enabled: widget.enabled,
 | |
|       leading: FadeTransition(
 | |
|         opacity: _opacity,
 | |
|         child: Icon(_controller.isDismissed ? null : Icons.done),
 | |
|       ),
 | |
|       title: widget.child,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _PopupMenu<T> extends StatelessWidget {
 | |
|   const _PopupMenu({
 | |
|     Key? key,
 | |
|     required this.route,
 | |
|     required this.semanticLabel,
 | |
|     this.constraints,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   final _PopupMenuRoute<T> route;
 | |
|   final String? semanticLabel;
 | |
|   final BoxConstraints? constraints;
 | |
| 
 | |
|   @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 PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
 | |
| 
 | |
|     for (int i = 0; i < route.items.length; i += 1) {
 | |
|       final double start = (i + 1) * unit;
 | |
|       final double end = (start + 1.5 * unit).clamp(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 = Container(
 | |
|           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,
 | |
|             ),
 | |
|             controller: ScrollController(),
 | |
|             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,
 | |
|             color: route.color ?? popupMenuTheme.color,
 | |
|             type: MaterialType.card,
 | |
|             elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0,
 | |
|             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;
 | |
|         break;
 | |
|       case TextDirection.ltr:
 | |
|         x = position.left;
 | |
|         break;
 | |
|     }
 | |
|     //}
 | |
|     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.menuWrapper,
 | |
|     this.initialValue,
 | |
|     this.elevation,
 | |
|     required this.barrierLabel,
 | |
|     this.semanticLabel,
 | |
|     this.shape,
 | |
|     this.color,
 | |
|     required this.capturedThemes,
 | |
|     this.constraints,
 | |
|   }) : itemSizes = List<Size?>.filled(items.length, null);
 | |
| 
 | |
|   final RelativeRect position;
 | |
|   final List<PopupMenuEntry<T>> items;
 | |
|   final MenuWrapper? menuWrapper;
 | |
|   final List<Size?> itemSizes;
 | |
|   final T? initialValue;
 | |
|   final double? elevation;
 | |
|   final String? semanticLabel;
 | |
|   final ShapeBorder? shape;
 | |
|   final Color? color;
 | |
|   final CapturedThemes capturedThemes;
 | |
|   final BoxConstraints? constraints;
 | |
| 
 | |
|   @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;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     Widget menu = _PopupMenu<T>(
 | |
|       route: this,
 | |
|       semanticLabel: semanticLabel,
 | |
|       constraints: constraints,
 | |
|     );
 | |
|     if (this.menuWrapper != null) {
 | |
|       menu = this.menuWrapper!(menu);
 | |
|     }
 | |
|     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();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class PopupMenu<T> extends StatelessWidget {
 | |
|   PopupMenu({
 | |
|     Key? key,
 | |
|     required this.items,
 | |
|     this.initialValue,
 | |
|     this.semanticLabel,
 | |
|     this.constraints,
 | |
|   })  : itemSizes = List<Size?>.filled(items.length, null),
 | |
|         super(key: key);
 | |
| 
 | |
|   final List<PopupMenuEntry<T>> items;
 | |
|   final List<Size?> itemSizes;
 | |
|   final T? initialValue;
 | |
|   final String? semanticLabel;
 | |
|   final BoxConstraints? constraints;
 | |
| 
 | |
|   Widget _buildMenu(BuildContext context) {
 | |
|     final List<Widget> children = <Widget>[];
 | |
|     for (int i = 0; i < items.length; i += 1) {
 | |
|       Widget item = items[i];
 | |
|       if (initialValue != null && items[i].represents(initialValue)) {
 | |
|         item = Container(
 | |
|           color: Theme.of(context).highlightColor,
 | |
|           child: item,
 | |
|         );
 | |
|       }
 | |
|       children.add(
 | |
|         _MenuItem(
 | |
|           onLayout: (Size size) {
 | |
|             itemSizes[i] = size;
 | |
|           },
 | |
|           child: item,
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     final 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,
 | |
|             ),
 | |
|             controller: ScrollController(),
 | |
|             child: ListBody(children: children),
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
| 
 | |
|     final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
 | |
|     return Material(
 | |
|       shape: popupMenuTheme.shape,
 | |
|       color: popupMenuTheme.color,
 | |
|       type: MaterialType.card,
 | |
|       elevation: popupMenuTheme.elevation ?? 8.0,
 | |
|       child: child,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     int? selectedItemIndex;
 | |
|     if (initialValue != null) {
 | |
|       for (int index = 0;
 | |
|           selectedItemIndex == null && index < items.length;
 | |
|           index += 1) {
 | |
|         if (items[index].represents(initialValue)) selectedItemIndex = index;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return MediaQuery.removePadding(
 | |
|       context: context,
 | |
|       removeTop: true,
 | |
|       removeBottom: true,
 | |
|       removeLeft: true,
 | |
|       removeRight: true,
 | |
|       child: Builder(
 | |
|         builder: (BuildContext context) {
 | |
|           return InheritedTheme.capture(from: context, to: context)
 | |
|               .wrap(_buildMenu(context));
 | |
|         },
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| /// 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].
 | |
| ///
 | |
| /// 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,
 | |
|   MenuWrapper? menuWrapper,
 | |
|   T? initialValue,
 | |
|   double? elevation,
 | |
|   String? semanticLabel,
 | |
|   ShapeBorder? shape,
 | |
|   Color? color,
 | |
|   bool useRootNavigator = false,
 | |
|   BoxConstraints? constraints,
 | |
| }) {
 | |
|   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,
 | |
|     menuWrapper: menuWrapper,
 | |
|     initialValue: initialValue,
 | |
|     elevation: elevation,
 | |
|     semanticLabel: semanticLabel,
 | |
|     barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
 | |
|     shape: shape,
 | |
|     color: color,
 | |
|     capturedThemes:
 | |
|         InheritedTheme.capture(from: context, to: navigator.context),
 | |
|     constraints: constraints,
 | |
|   ));
 | |
| }
 | |
| 
 | |
| /// 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);
 | |
| 
 | |
| typedef MenuWrapper = Widget Function(Widget child);
 | |
| 
 | |
| /// 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).
 | |
| ///
 | |
| /// {@tool dartpad}
 | |
| /// This example shows a menu with four items, selecting between an enum's
 | |
| /// values and setting a `_selectedMenu` field based on the selection
 | |
| ///
 | |
| /// ** See code in examples/api/lib/material/popupmenu/popupmenu.0.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({
 | |
|     Key? key,
 | |
|     required this.itemBuilder,
 | |
|     this.menuWrapper,
 | |
|     this.initialValue,
 | |
|     this.onHover,
 | |
|     this.onSelected,
 | |
|     this.onCanceled,
 | |
|     this.tooltip,
 | |
|     this.elevation,
 | |
|     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 = PopupMenuPosition.over,
 | |
|   })  : assert(
 | |
|           !(child != null && icon != null),
 | |
|           'You can only pass [child] or [icon], not both.',
 | |
|         ),
 | |
|         super(key: key);
 | |
| 
 | |
|   /// Called when the button is pressed to create the items to show in the menu.
 | |
|   final PopupMenuItemBuilder<T> itemBuilder;
 | |
| 
 | |
|   /// Menu wrapper.
 | |
|   final MenuWrapper? menuWrapper;
 | |
| 
 | |
|   /// The value of the menu item, if any, that should be highlighted when the menu opens.
 | |
|   final T? initialValue;
 | |
| 
 | |
|   /// Called when the user hovers this button.
 | |
|   final ValueChanged<bool>? onHover;
 | |
| 
 | |
|   /// 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;
 | |
| 
 | |
|   /// 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.
 | |
|   ///
 | |
|   /// When not set, 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;
 | |
| 
 | |
|   @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 Offset offset;
 | |
|     switch (widget.position) {
 | |
|       case PopupMenuPosition.over:
 | |
|         offset = widget.offset;
 | |
|         break;
 | |
|       case PopupMenuPosition.under:
 | |
|         offset =
 | |
|             Offset(0.0, button.size.height - (widget.padding.vertical / 2)) +
 | |
|                 widget.offset;
 | |
|         break;
 | |
|       case PopupMenuPosition.overSide:
 | |
|         offset =
 | |
|             Offset(button.size.width - (widget.padding.horizontal / 2), 0.0) +
 | |
|                 widget.offset;
 | |
|         break;
 | |
|       case PopupMenuPosition.underSide:
 | |
|         offset = Offset(button.size.width - (widget.padding.horizontal / 2),
 | |
|                 button.size.height - (widget.padding.vertical / 2)) +
 | |
|             widget.offset;
 | |
|         break;
 | |
|     }
 | |
|     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) {
 | |
|       showMenu<T?>(
 | |
|         context: context,
 | |
|         elevation: widget.elevation ?? popupMenuTheme.elevation,
 | |
|         items: items,
 | |
|         menuWrapper: widget.menuWrapper,
 | |
|         initialValue: widget.initialValue,
 | |
|         position: position,
 | |
|         shape: widget.shape ?? popupMenuTheme.shape,
 | |
|         color: widget.color ?? popupMenuTheme.color,
 | |
|         constraints: widget.constraints,
 | |
|       ).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.maybeOf(context)?.navigationMode ??
 | |
|         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,
 | |
|           onHover: widget.onHover,
 | |
|           canRequestFocus: _canRequestFocus,
 | |
|           enableFeedback: enableFeedback,
 | |
|           child: widget.child,
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return MenuButton(
 | |
|       child: widget.icon ?? Icon(Icons.adaptive.more),
 | |
|       tooltip:
 | |
|           widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
 | |
|       onPressed: widget.enabled ? showButtonMenu : null,
 | |
|       enableFeedback: enableFeedback,
 | |
|       color: MyTheme.button,
 | |
|       hoverColor: MyTheme.accent,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| // This MaterialStateProperty is passed along to the menu item's InkWell which
 | |
| // resolves the property against MaterialState.disabled, MaterialState.hovered,
 | |
| // MaterialState.focused.
 | |
| // ignore: unused_element
 | |
| 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)';
 | |
| }
 |