using System; using System.Collections.Generic; using Unity.UIWidgets.animation; using Unity.UIWidgets.foundation; using Unity.UIWidgets.gestures; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using Unity.UIWidgets.async2; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; using UnityEngine; using Color = Unity.UIWidgets.ui.Color; using Rect = Unity.UIWidgets.ui.Rect; using TextStyle = Unity.UIWidgets.painting.TextStyle; using Brightness = Unity.UIWidgets.ui.Brightness; namespace Unity.UIWidgets.material { public static class PopupMenuUtils { internal static readonly TimeSpan _kMenuDuration = new TimeSpan(0, 0, 0, 0, 300); internal const float _kBaselineOffsetFromBottom = 20.0f; internal const float _kMenuCloseIntervalEnd = 2.0f / 3.0f; internal const float _kMenuHorizontalPadding = 16.0f; internal const float _kMenuItemHeight = 48.0f; internal const float _kMenuDividerHeight = 16.0f; internal const float _kMenuMaxWidth = 5.0f * _kMenuWidthStep; internal const float _kMenuMinWidth = 2.0f * _kMenuWidthStep; internal const float _kMenuVerticalPadding = 8.0f; internal const float _kMenuWidthStep = 56.0f; internal const float _kMenuScreenPadding = 8.0f; public static Future showMenu( BuildContext context, RelativeRect position, List> items, bool captureInheritedThemes = true, bool useRootNavigator = false, T initialValue = default(T), float? elevation = null, string semanticLabel = null, ShapeBorder shape = null, Color color = null ) { D.assert(context != null); D.assert(position != null); D.assert(useRootNavigator != null); D.assert(items != null && items.isNotEmpty()); D.assert(captureInheritedThemes != null); D.assert(MaterialD.debugCheckHasMaterialLocalizations(context)); string label = semanticLabel; switch (Theme.of(context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: label = semanticLabel; break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel; break; } return Navigator.of(context, rootNavigator: useRootNavigator).push( new _PopupMenuRoute( position: position, items: items, initialValue: initialValue, elevation: elevation, semanticLabel: label, theme: Theme.of(context, shadowThemeOnly: true), popupMenuTheme: PopupMenuTheme.of(context), barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, shape: shape, color: color, showMenuContext: context, captureInheritedThemes: captureInheritedThemes )).to(); } } public delegate void PopupMenuItemSelected(T value); public delegate void PopupMenuCanceled(); public delegate List> PopupMenuItemBuilder(BuildContext context); public abstract class PopupMenuEntry : StatefulWidget { protected PopupMenuEntry(Key key = null) : base(key: key) { } public abstract float height { get; } public abstract bool represents(T value); } public class PopupMenuDivider : PopupMenuEntry { public PopupMenuDivider(Key key = null, float height = PopupMenuUtils._kMenuDividerHeight) : base(key: key) { _height = height; } readonly float _height; public override float height { get { return _height; } } public override bool represents(object value) { return false; } public override State createState() { return new _PopupMenuDividerState(); } } class _PopupMenuDividerState : State { public override Widget build(BuildContext context) { return new Divider(height: widget.height); } } public class PopupMenuItem : PopupMenuEntry { public PopupMenuItem( Key key = null, T value = default, bool enabled = true, float height = PopupMenuUtils._kMenuItemHeight, Widget child = null ) : base(key: key) { this.value = value; this.enabled = enabled; _height = height; this.child = child; } public readonly T value; public readonly bool enabled; readonly float _height; public override float height { get { return _height; } } public readonly Widget child; public override bool represents(T value) { return Equals(value, this.value); } public override State createState() { return new PopupMenuItemState>(); } } public class PopupMenuItemState : State where W : PopupMenuItem { protected virtual Widget buildChild() { return widget.child; } protected virtual void handleTap() { Navigator.pop(context, widget.value); } public override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); TextStyle style = theme.textTheme.subhead; if (!widget.enabled) { style = style.copyWith(color: theme.disabledColor); } Widget item = new AnimatedDefaultTextStyle( style: style, duration: Constants.kThemeChangeDuration, child: new Baseline( baseline: widget.height - PopupMenuUtils._kBaselineOffsetFromBottom, baselineType: style.textBaseline, child: buildChild() ) ); if (!widget.enabled) { bool isDark = theme.brightness == Brightness.dark; item = IconTheme.merge( data: new IconThemeData(opacity: isDark ? 0.5f : 0.38f), child: item ); } return new InkWell( onTap: widget.enabled ? handleTap : (GestureTapCallback) null, child: new Container( height: widget.height, padding: EdgeInsets.symmetric(horizontal: PopupMenuUtils._kMenuHorizontalPadding), child: item ) ); } } public class PopupMenuItemSingleTickerProviderState : SingleTickerProviderStateMixin where W : PopupMenuItem { protected virtual Widget buildChild() { return widget.child; } protected virtual void handleTap() { Navigator.pop(context, widget.value); } public override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); TextStyle style = theme.textTheme.subhead; if (!widget.enabled) { style = style.copyWith(color: theme.disabledColor); } Widget item = new AnimatedDefaultTextStyle( style: style, duration: Constants.kThemeChangeDuration, child: new Baseline( baseline: widget.height - PopupMenuUtils._kBaselineOffsetFromBottom, baselineType: style.textBaseline, child: buildChild() ) ); if (!widget.enabled) { bool isDark = theme.brightness == Brightness.dark; item = IconTheme.merge( data: new IconThemeData(opacity: isDark ? 0.5f : 0.38f), child: item ); } return new InkWell( onTap: widget.enabled ? handleTap : (GestureTapCallback) null, child: new Container( height: widget.height, padding: EdgeInsets.symmetric(horizontal: PopupMenuUtils._kMenuHorizontalPadding), child: item ) ); } } class CheckedPopupMenuItem : PopupMenuItem { public CheckedPopupMenuItem( Key key = null, T value = default, bool isChecked = false, bool enabled = true, Widget child = null ) : base( key: key, value: value, enabled: enabled, child: child ) { this.isChecked = isChecked; } public readonly bool isChecked; public override State createState() { return new _CheckedPopupMenuItemState(); } } class _CheckedPopupMenuItemState : PopupMenuItemSingleTickerProviderState> { static readonly TimeSpan _fadeDuration = new TimeSpan(0, 0, 0, 0, 150); AnimationController _controller; Animation _opacity { get { return _controller.view; } } public override void initState() { base.initState(); _controller = new AnimationController(duration: _fadeDuration, vsync: this); _controller.setValue(widget.isChecked ? 1.0f : 0.0f); _controller.addListener(() => setState(() => { /* animation changed */ })); } protected override void handleTap() { if (widget.isChecked) { _controller.reverse(); } else { _controller.forward(); } base.handleTap(); } protected override Widget buildChild() { return new ListTile( enabled: widget.enabled, leading: new FadeTransition( opacity: _opacity, child: new Icon(_controller.isDismissed ? null : Icons.done) ), title: widget.child ); } } class _PopupMenu : StatelessWidget { public _PopupMenu( Key key = null, _PopupMenuRoute route = null ) : base(key: key) { this.route = route; } public readonly _PopupMenuRoute route; public override Widget build(BuildContext context) { float unit = 1.0f / (route.items.Count + 1.5f); List children = new List(); for (int i = 0; i < route.items.Count; i += 1) { float start = (i + 1) * unit; float end = (start + 1.5f * unit).clamp(0.0f, 1.0f); Widget item = route.items[i]; if (route.initialValue != null && route.items[i].represents((T) route.initialValue)) { item = new Container( color: Theme.of(context).highlightColor, child: item ); } children.Add(new FadeTransition( opacity: new CurvedAnimation( parent: route.animation, curve: new Interval(start, end) ), child: item )); } CurveTween opacity = new CurveTween(curve: new Interval(0.0f, 1.0f / 3.0f)); CurveTween width = new CurveTween(curve: new Interval(0.0f, unit)); CurveTween height = new CurveTween(curve: new Interval(0.0f, unit * route.items.Count)); Widget child = new ConstrainedBox( constraints: new BoxConstraints( minWidth: PopupMenuUtils._kMenuMinWidth, maxWidth: PopupMenuUtils._kMenuMaxWidth ), child: new IntrinsicWidth( stepWidth: PopupMenuUtils._kMenuWidthStep, child: new SingleChildScrollView( padding: EdgeInsets.symmetric( vertical: PopupMenuUtils._kMenuVerticalPadding ), child: new ListBody(children: children) ) ) ); return new AnimatedBuilder( animation: route.animation, builder: (_, builderChild) => { return new Opacity( opacity: opacity.evaluate(route.animation), child: new Material( type: MaterialType.card, elevation: (float)route.elevation, child: new Align( alignment: Alignment.topRight, widthFactor: width.evaluate(route.animation), heightFactor: height.evaluate(route.animation), child: builderChild ) ) ); }, child: child ); } } class _PopupMenuRouteLayout : SingleChildLayoutDelegate { public _PopupMenuRouteLayout( RelativeRect position = null, List itemSizes = null, int? selectedItemIndex = null, TextDirection? textDirection = null ) { this.position = position; this.itemSizes = itemSizes; this.selectedItemIndex = selectedItemIndex; this.textDirection = textDirection; } public readonly RelativeRect position; List itemSizes; public readonly int? selectedItemIndex; public readonly TextDirection? textDirection; public override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { return BoxConstraints.loose( constraints.biggest - new Offset(PopupMenuUtils._kMenuScreenPadding * 2.0f, PopupMenuUtils._kMenuScreenPadding * 2.0f) as Size ); } public override Offset getPositionForChild(Size size, Size childSize) { float y = position.top; if (selectedItemIndex != null && itemSizes != null) { float selectedItemOffset = PopupMenuUtils._kMenuVerticalPadding; for (int index = 0; index < selectedItemIndex; index += 1) selectedItemOffset += itemSizes[index].height; selectedItemOffset += itemSizes[(int)selectedItemIndex].height / 2; y = position.top + (size.height - position.top - position.bottom) / 2.0f - selectedItemOffset; } float x = 0f; if (position.left > position.right) { x = size.width - position.right - childSize.width; } else if (position.left < position.right) { x = position.left; } else { D.assert(textDirection != null); switch (textDirection) { case TextDirection.rtl: x = size.width - position.right - childSize.width; break; case TextDirection.ltr: x = position.left; break; } } if (x < PopupMenuUtils._kMenuScreenPadding) x = PopupMenuUtils._kMenuScreenPadding; else if (x + childSize.width > size.width - PopupMenuUtils._kMenuScreenPadding) x = size.width - childSize.width - PopupMenuUtils._kMenuScreenPadding; if (y < PopupMenuUtils._kMenuScreenPadding) y = PopupMenuUtils._kMenuScreenPadding; else if (y + childSize.height > size.height - PopupMenuUtils._kMenuScreenPadding) y = size.height - childSize.height - PopupMenuUtils._kMenuScreenPadding; return new Offset(x, y); } public override bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) { return position != ((_PopupMenuRouteLayout) oldDelegate).position; } } class _PopupMenuRoute : PopupRoute { public _PopupMenuRoute( RelativeRect position = null, List> items = null, T initialValue = default(T), float? elevation = null, ThemeData theme = null, PopupMenuThemeData popupMenuTheme = null, string barrierLabel = null, string semanticLabel = null, ShapeBorder shape = null, Color color = null, BuildContext showMenuContext = null, bool? captureInheritedThemes = null ) { this.position = position; this.items = items; this.initialValue = initialValue; this.elevation = elevation; this.theme = theme; this.barrierLabel = barrierLabel; this.semanticLabel = semanticLabel; this.shape = shape; this.color = color; this.popupMenuTheme = popupMenuTheme; this.showMenuContext = showMenuContext; this.captureInheritedThemes = captureInheritedThemes; itemSizes = new List(items.Count); } public readonly RelativeRect position; public readonly List> items; public readonly List itemSizes; public readonly T initialValue; public readonly float? elevation; public readonly ThemeData theme; public readonly string barrierLabel; public readonly string semanticLabel; public readonly ShapeBorder shape; public readonly Color color; public readonly PopupMenuThemeData popupMenuTheme; public readonly BuildContext showMenuContext; public readonly bool? captureInheritedThemes; public override Animation createAnimation() { return new CurvedAnimation( parent: base.createAnimation(), curve: Curves.linear, reverseCurve: new Interval(0.0f, PopupMenuUtils._kMenuCloseIntervalEnd) ); } public override TimeSpan transitionDuration { get { return PopupMenuUtils._kMenuDuration; } } public override bool barrierDismissible { get { return true; } } public override Color barrierColor { get { return null; } } public override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { int? selectedItemIndex = null; if (initialValue != null) { for (int index = 0; selectedItemIndex == null && index < items.Count; index += 1) { if (items[index].represents(initialValue)) selectedItemIndex = index; } } Widget menu = new _PopupMenu(route: this);//, semanticLabel: semanticLabel); if (captureInheritedThemes.Value) { menu = InheritedTheme.captureAll(showMenuContext, menu); } else { if (theme != null) menu = new Theme(data: theme, child: menu); } return MediaQuery.removePadding( context: context, removeTop: true, removeBottom: true, removeLeft: true, removeRight: true, child: new Builder( builder: (BuildContext context1)=> { return new CustomSingleChildLayout( layoutDelegate: new _PopupMenuRouteLayout( position: position, itemSizes: itemSizes, selectedItemIndex: selectedItemIndex, textDirection:Directionality.of(context1)), child: menu); } ) ); } } public class PopupMenuButton : StatefulWidget { public PopupMenuButton( Key key = null, PopupMenuItemBuilder itemBuilder = null, T initialValue = default, PopupMenuItemSelected onSelected = null, PopupMenuCanceled onCanceled = null, string tooltip = null, float? elevation = null, EdgeInsets padding = null, Widget child = null, Icon icon = null, Offset offset = null, bool enabled = true, ShapeBorder shape = null, Color color = null, bool captureInheritedThemes = true ) : base(key: key) { offset = offset ?? Offset.zero; padding = padding ?? EdgeInsets.all(8.0f); D.assert(itemBuilder != null); D.assert(offset != null); D.assert(captureInheritedThemes != null); D.assert(!(child != null && icon != null)); this.itemBuilder = itemBuilder; this.initialValue = initialValue; this.onSelected = onSelected; this.onCanceled = onCanceled; this.tooltip = tooltip; this.elevation = elevation; this.padding = padding ?? EdgeInsets.all(8.0f); this.child = child; this.icon = icon; this.offset = offset; this.enabled = enabled; this.shape = shape; this.color = color; this.captureInheritedThemes = captureInheritedThemes; } public readonly PopupMenuItemBuilder itemBuilder; public readonly T initialValue; public readonly PopupMenuItemSelected onSelected; public readonly PopupMenuCanceled onCanceled; public readonly string tooltip; public readonly float? elevation; public readonly EdgeInsets padding; public readonly Widget child; public readonly Icon icon; public readonly Offset offset; public readonly bool enabled; public readonly ShapeBorder shape; public readonly Color color; public readonly bool captureInheritedThemes; public override State createState() { return new PopupMenuButtonState(); } } class PopupMenuButtonState : State> { void showButtonMenu() { PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); RenderBox button = (RenderBox) context.findRenderObject(); RenderBox overlay = (RenderBox) Overlay.of(context).context.findRenderObject(); RelativeRect position = RelativeRect.fromRect( Rect.fromPoints( button.localToGlobal(widget.offset, ancestor: overlay), button.localToGlobal(button.size.bottomRight(Offset.zero), ancestor: overlay) ), Offset.zero & overlay.size ); List> items = widget.itemBuilder(context); if (items.isNotEmpty()) { PopupMenuUtils.showMenu( context: context, elevation: widget.elevation ?? popupMenuTheme.elevation, items: items, initialValue: widget.initialValue, position: position, shape: widget.shape ?? popupMenuTheme.shape, color: widget.color ?? popupMenuTheme.color, captureInheritedThemes: widget.captureInheritedThemes ).then((T newValue)=> { if (!mounted) return null; if (newValue == null) { if (widget.onCanceled != null) widget.onCanceled(); return null; } if (widget.onSelected != null) widget.onSelected(newValue); }); } } Icon _getIcon(TargetPlatform platform) { /*switch (platform) { case RuntimePlatform.IPhonePlayer: return new Icon(Icons.more_horiz); default: return new Icon(Icons.more_vert); }*/ D.assert(platform != null); switch (platform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: return new Icon(Icons.more_vert); case TargetPlatform.iOS: case TargetPlatform.macOS: return new Icon(Icons.more_horiz); } return null; } public override Widget build(BuildContext context) { D.assert(MaterialD.debugCheckHasMaterialLocalizations(context)); return widget.child != null ? (Widget) new InkWell( onTap: this.showButtonMenu, child: widget.child ) : new IconButton( icon: widget.icon ?? _getIcon((TargetPlatform)Theme.of(context).platform), padding: widget.padding, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: showButtonMenu ); } } }