1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'menu_anchor.dart';
6/// @docImport 'text_button.dart';
7library;
8
9import 'dart:ui';
10
11import 'package:flutter/foundation.dart';
12import 'package:flutter/rendering.dart';
13import 'package:flutter/scheduler.dart';
14import 'package:flutter/widgets.dart';
15
16import 'button_style.dart';
17import 'color_scheme.dart';
18import 'colors.dart';
19import 'constants.dart';
20import 'debug.dart';
21import 'divider.dart';
22import 'icon_button.dart';
23import 'icons.dart';
24import 'ink_well.dart';
25import 'list_tile.dart';
26import 'list_tile_theme.dart';
27import 'material.dart';
28import 'material_localizations.dart';
29import 'material_state.dart';
30import 'popup_menu_theme.dart';
31import 'text_theme.dart';
32import 'theme.dart';
33import 'theme_data.dart';
34import 'tooltip.dart';
35
36// Examples can assume:
37// enum Commands { heroAndScholar, hurricaneCame }
38// late bool _heroAndScholar;
39// late dynamic _selection;
40// late BuildContext context;
41// void setState(VoidCallback fn) { }
42// enum Menu { itemOne, itemTwo, itemThree, itemFour }
43
44const Duration _kMenuDuration = Duration(milliseconds: 300);
45const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
46const double _kMenuDividerHeight = 16.0;
47const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
48const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
49const double _kMenuWidthStep = 56.0;
50const double _kMenuScreenPadding = 8.0;
51
52/// A base class for entries in a Material Design popup menu.
53///
54/// The popup menu widget uses this interface to interact with the menu items.
55/// To show a popup menu, use the [showMenu] function. To create a button that
56/// shows a popup menu, consider using [PopupMenuButton].
57///
58/// The type `T` is the type of the value(s) the entry represents. All the
59/// entries in a given menu must represent values with consistent types.
60///
61/// A [PopupMenuEntry] may represent multiple values, for example a row with
62/// several icons, or a single entry, for example a menu item with an icon (see
63/// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]).
64///
65/// See also:
66///
67/// * [PopupMenuItem], a popup menu entry for a single value.
68/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
69/// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
70/// * [showMenu], a method to dynamically show a popup menu at a given location.
71/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
72/// it is tapped.
73abstract class PopupMenuEntry<T> extends StatefulWidget {
74 /// Abstract const constructor. This constructor enables subclasses to provide
75 /// const constructors so that they can be used in const expressions.
76 const PopupMenuEntry({super.key});
77
78 /// The amount of vertical space occupied by this entry.
79 ///
80 /// This value is used at the time the [showMenu] method is called, if the
81 /// `initialValue` argument is provided, to determine the position of this
82 /// entry when aligning the selected entry over the given `position`. It is
83 /// otherwise ignored.
84 double get height;
85
86 /// Whether this entry represents a particular value.
87 ///
88 /// This method is used by [showMenu], when it is called, to align the entry
89 /// representing the `initialValue`, if any, to the given `position`, and then
90 /// later is called on each entry to determine if it should be highlighted (if
91 /// the method returns true, the entry will have its background color set to
92 /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then
93 /// this method is not called.
94 ///
95 /// If the [PopupMenuEntry] represents a single value, this should return true
96 /// if the argument matches that value. If it represents multiple values, it
97 /// should return true if the argument matches any of them.
98 bool represents(T? value);
99}
100
101/// A horizontal divider in a Material Design popup menu.
102///
103/// This widget adapts the [Divider] for use in popup menus.
104///
105/// See also:
106///
107/// * [PopupMenuItem], for the kinds of items that this widget divides.
108/// * [showMenu], a method to dynamically show a popup menu at a given location.
109/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
110/// it is tapped.
111class PopupMenuDivider extends PopupMenuEntry<Never> {
112 /// Creates a horizontal divider for a popup menu.
113 ///
114 /// By default, the divider has a height of 16 logical pixels.
115 const PopupMenuDivider({
116 super.key,
117 this.height = _kMenuDividerHeight,
118 this.thickness,
119 this.indent,
120 this.endIndent,
121 this.radius,
122 this.color,
123 });
124
125 /// The height of the divider entry.
126 ///
127 /// Defaults to 16 pixels.
128 @override
129 final double height;
130
131 /// The thickness of the line drawn within the [PopupMenuDivider].
132 ///
133 /// {@macro flutter.material.Divider.thickness}
134 final double? thickness;
135
136 /// The amount of empty space to the leading edge of the [PopupMenuDivider].
137 ///
138 /// {@macro flutter.material.Divider.indent}
139 final double? indent;
140
141 /// The amount of empty space to the trailing edge of the [PopupMenuDivider].
142 ///
143 /// {@macro flutter.material.Divider.endIndent}
144 final double? endIndent;
145
146 /// The amount of radius for the border of the [PopupMenuDivider].
147 ///
148 /// {@macro flutter.material.Divider.radius}
149 final BorderRadiusGeometry? radius;
150
151 /// {@macro flutter.material.Divider.color}
152 ///
153 /// {@tool snippet}
154 ///
155 /// ```dart
156 /// const PopupMenuDivider(
157 /// color: Colors.deepOrange,
158 /// )
159 /// ```
160 /// {@end-tool}
161 final Color? color;
162
163 @override
164 bool represents(void value) => false;
165
166 @override
167 State<PopupMenuDivider> createState() => _PopupMenuDividerState();
168}
169
170class _PopupMenuDividerState extends State<PopupMenuDivider> {
171 @override
172 Widget build(BuildContext context) {
173 return Divider(
174 height: widget.height,
175 thickness: widget.thickness,
176 indent: widget.indent,
177 color: widget.color,
178 endIndent: widget.endIndent,
179 radius: widget.radius,
180 );
181 }
182}
183
184// This widget only exists to enable _PopupMenuRoute to save the sizes of
185// each menu item. The sizes are used by _PopupMenuRouteLayout to compute the
186// y coordinate of the menu's origin so that the center of selected menu
187// item lines up with the center of its PopupMenuButton.
188class _MenuItem extends SingleChildRenderObjectWidget {
189 const _MenuItem({required this.onLayout, required super.child});
190
191 final ValueChanged<Size> onLayout;
192
193 @override
194 RenderObject createRenderObject(BuildContext context) {
195 return _RenderMenuItem(onLayout);
196 }
197
198 @override
199 void updateRenderObject(BuildContext context, covariant _RenderMenuItem renderObject) {
200 renderObject.onLayout = onLayout;
201 }
202}
203
204class _RenderMenuItem extends RenderShiftedBox {
205 _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);
206
207 ValueChanged<Size> onLayout;
208
209 @override
210 Size computeDryLayout(BoxConstraints constraints) {
211 return child?.getDryLayout(constraints) ?? Size.zero;
212 }
213
214 @override
215 double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
216 return child?.getDryBaseline(constraints, baseline);
217 }
218
219 @override
220 void performLayout() {
221 if (child == null) {
222 size = Size.zero;
223 } else {
224 child!.layout(constraints, parentUsesSize: true);
225 size = constraints.constrain(child!.size);
226 final BoxParentData childParentData = child!.parentData! as BoxParentData;
227 childParentData.offset = Offset.zero;
228 }
229 onLayout(size);
230 }
231}
232
233/// An item in a Material Design popup menu.
234///
235/// To show a popup menu, use the [showMenu] function. To create a button that
236/// shows a popup menu, consider using [PopupMenuButton].
237///
238/// To show a checkmark next to a popup menu item, consider using
239/// [CheckedPopupMenuItem].
240///
241/// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More
242/// elaborate menus with icons can use a [ListTile]. By default, a
243/// [PopupMenuItem] is [kMinInteractiveDimension] pixels high. If you use a widget
244/// with a different height, it must be specified in the [height] property.
245///
246/// {@tool snippet}
247///
248/// Here, a [Text] widget is used with a popup menu item. The `Menu` type
249/// is an enum, not shown here.
250///
251/// ```dart
252/// const PopupMenuItem<Menu>(
253/// value: Menu.itemOne,
254/// child: Text('Item 1'),
255/// )
256/// ```
257/// {@end-tool}
258///
259/// See the example at [PopupMenuButton] for how this example could be used in a
260/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to
261/// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child]
262/// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem]
263/// that use a [ListTile] in their [child] slot.
264///
265/// See also:
266///
267/// * [PopupMenuDivider], which can be used to divide items from each other.
268/// * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark.
269/// * [showMenu], a method to dynamically show a popup menu at a given location.
270/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
271/// it is tapped.
272class PopupMenuItem<T> extends PopupMenuEntry<T> {
273 /// Creates an item for a popup menu.
274 ///
275 /// By default, the item is [enabled].
276 const PopupMenuItem({
277 super.key,
278 this.value,
279 this.onTap,
280 this.enabled = true,
281 this.height = kMinInteractiveDimension,
282 this.padding,
283 this.textStyle,
284 this.labelTextStyle,
285 this.mouseCursor,
286 required this.child,
287 });
288
289 /// The value that will be returned by [showMenu] if this entry is selected.
290 final T? value;
291
292 /// Called when the menu item is tapped.
293 final VoidCallback? onTap;
294
295 /// Whether the user is permitted to select this item.
296 ///
297 /// Defaults to true. If this is false, then the item will not react to
298 /// touches.
299 final bool enabled;
300
301 /// The minimum height of the menu item.
302 ///
303 /// Defaults to [kMinInteractiveDimension] pixels.
304 @override
305 final double height;
306
307 /// The padding of the menu item.
308 ///
309 /// The [height] property may interact with the applied padding. For example,
310 /// If a [height] greater than the height of the sum of the padding and [child]
311 /// is provided, then the padding's effect will not be visible.
312 ///
313 /// If this is null and [ThemeData.useMaterial3] is true, the horizontal padding
314 /// defaults to 12.0 on both sides.
315 ///
316 /// If this is null and [ThemeData.useMaterial3] is false, the horizontal padding
317 /// defaults to 16.0 on both sides.
318 final EdgeInsets? padding;
319
320 /// The text style of the popup menu item.
321 ///
322 /// If this property is null, then [PopupMenuThemeData.textStyle] is used.
323 /// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium]
324 /// of [ThemeData.textTheme] is used.
325 final TextStyle? textStyle;
326
327 /// The label style of the popup menu item.
328 ///
329 /// When [ThemeData.useMaterial3] is true, this styles the text of the popup menu item.
330 ///
331 /// If this property is null, then [PopupMenuThemeData.labelTextStyle] is used.
332 /// If [PopupMenuThemeData.labelTextStyle] is also null, then [TextTheme.labelLarge]
333 /// is used with the [ColorScheme.onSurface] color when popup menu item is enabled and
334 /// the [ColorScheme.onSurface] color with 0.38 opacity when the popup menu item is disabled.
335 final MaterialStateProperty<TextStyle?>? labelTextStyle;
336
337 /// {@template flutter.material.popupmenu.mouseCursor}
338 /// The cursor for a mouse pointer when it enters or is hovering over the
339 /// widget.
340 ///
341 /// If [mouseCursor] is a [WidgetStateMouseCursor],
342 /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
343 ///
344 /// * [WidgetState.hovered].
345 /// * [WidgetState.focused].
346 /// * [WidgetState.disabled].
347 /// {@endtemplate}
348 ///
349 /// If null, then the value of [PopupMenuThemeData.mouseCursor] is used. If
350 /// that is also null, then [WidgetStateMouseCursor.clickable] is used.
351 final MouseCursor? mouseCursor;
352
353 /// The widget below this widget in the tree.
354 ///
355 /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
356 /// appropriate [DefaultTextStyle] is put in scope for the child. In either
357 /// case, the text should be short enough that it won't wrap.
358 final Widget? child;
359
360 @override
361 bool represents(T? value) => value == this.value;
362
363 @override
364 PopupMenuItemState<T, PopupMenuItem<T>> createState() =>
365 PopupMenuItemState<T, PopupMenuItem<T>>();
366}
367
368/// The [State] for [PopupMenuItem] subclasses.
369///
370/// By default this implements the basic styling and layout of Material Design
371/// popup menu items.
372///
373/// The [buildChild] method can be overridden to adjust exactly what gets placed
374/// in the menu. By default it returns [PopupMenuItem.child].
375///
376/// The [handleTap] method can be overridden to adjust exactly what happens when
377/// the item is tapped. By default, it uses [Navigator.pop] to return the
378/// [PopupMenuItem.value] from the menu route.
379///
380/// This class takes two type arguments. The second, `W`, is the exact type of
381/// the [Widget] that is using this [State]. It must be a subclass of
382/// [PopupMenuItem]. The first, `T`, must match the type argument of that widget
383/// class, and is the type of values returned from this menu.
384class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
385 /// The menu item contents.
386 ///
387 /// Used by the [build] method.
388 ///
389 /// By default, this returns [PopupMenuItem.child]. Override this to put
390 /// something else in the menu entry.
391 @protected
392 Widget? buildChild() => widget.child;
393
394 /// The handler for when the user selects the menu item.
395 ///
396 /// Used by the [InkWell] inserted by the [build] method.
397 ///
398 /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from
399 /// the menu route.
400 @protected
401 void handleTap() {
402 // Need to pop the navigator first in case onTap may push new route onto navigator.
403 Navigator.pop<T>(context, widget.value);
404
405 widget.onTap?.call();
406 }
407
408 @protected
409 @override
410 Widget build(BuildContext context) {
411 final ThemeData theme = Theme.of(context);
412 final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
413 final PopupMenuThemeData defaults =
414 theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context);
415 final Set<MaterialState> states = <MaterialState>{if (!widget.enabled) MaterialState.disabled};
416
417 TextStyle style =
418 theme.useMaterial3
419 ? (widget.labelTextStyle?.resolve(states) ??
420 popupMenuTheme.labelTextStyle?.resolve(states)! ??
421 defaults.labelTextStyle!.resolve(states)!)
422 : (widget.textStyle ?? popupMenuTheme.textStyle ?? defaults.textStyle!);
423
424 if (!widget.enabled && !theme.useMaterial3) {
425 style = style.copyWith(color: theme.disabledColor);
426 }
427 final EdgeInsetsGeometry padding =
428 widget.padding ??
429 (theme.useMaterial3
430 ? _PopupMenuDefaultsM3.menuItemPadding
431 : _PopupMenuDefaultsM2.menuItemPadding);
432
433 Widget item = AnimatedDefaultTextStyle(
434 style: style,
435 duration: kThemeChangeDuration,
436 child: ConstrainedBox(
437 constraints: BoxConstraints(minHeight: widget.height),
438 child: Padding(
439 key: const Key('menu item padding'),
440 padding: padding,
441 child: Align(alignment: AlignmentDirectional.centerStart, child: buildChild()),
442 ),
443 ),
444 );
445
446 if (!widget.enabled) {
447 final bool isDark = theme.brightness == Brightness.dark;
448 item = IconTheme.merge(data: IconThemeData(opacity: isDark ? 0.5 : 0.38), child: item);
449 }
450
451 return MergeSemantics(
452 child: Semantics(
453 role: SemanticsRole.menuItem,
454 enabled: widget.enabled,
455 button: true,
456 child: InkWell(
457 onTap: widget.enabled ? handleTap : null,
458 canRequestFocus: widget.enabled,
459 mouseCursor: _EffectiveMouseCursor(widget.mouseCursor, popupMenuTheme.mouseCursor),
460 child: ListTileTheme.merge(
461 contentPadding: EdgeInsets.zero,
462 titleTextStyle: style,
463 child: item,
464 ),
465 ),
466 ),
467 );
468 }
469}
470
471/// An item with a checkmark in a Material Design popup menu.
472///
473/// To show a popup menu, use the [showMenu] function. To create a button that
474/// shows a popup menu, consider using [PopupMenuButton].
475///
476/// A [CheckedPopupMenuItem] is kMinInteractiveDimension pixels high, which
477/// matches the default minimum height of a [PopupMenuItem]. The horizontal
478/// layout uses [ListTile]; the checkmark is an [Icons.done] icon, shown in the
479/// [ListTile.leading] position.
480///
481/// {@tool snippet}
482///
483/// Suppose a `Commands` enum exists that lists the possible commands from a
484/// particular popup menu, including `Commands.heroAndScholar` and
485/// `Commands.hurricaneCame`, and further suppose that there is a
486/// `_heroAndScholar` member field which is a boolean. The example below shows a
487/// menu with one menu item with a checkmark that can toggle the boolean, and
488/// one menu item without a checkmark for selecting the second option. (It also
489/// shows a divider placed between the two menu items.)
490///
491/// ```dart
492/// PopupMenuButton<Commands>(
493/// onSelected: (Commands result) {
494/// switch (result) {
495/// case Commands.heroAndScholar:
496/// setState(() { _heroAndScholar = !_heroAndScholar; });
497/// case Commands.hurricaneCame:
498/// // ...handle hurricane option
499/// break;
500/// // ...other items handled here
501/// }
502/// },
503/// itemBuilder: (BuildContext context) => <PopupMenuEntry<Commands>>[
504/// CheckedPopupMenuItem<Commands>(
505/// checked: _heroAndScholar,
506/// value: Commands.heroAndScholar,
507/// child: const Text('Hero and scholar'),
508/// ),
509/// const PopupMenuDivider(),
510/// const PopupMenuItem<Commands>(
511/// value: Commands.hurricaneCame,
512/// child: ListTile(leading: Icon(null), title: Text('Bring hurricane')),
513/// ),
514/// // ...other items listed here
515/// ],
516/// )
517/// ```
518/// {@end-tool}
519///
520/// In particular, observe how the second menu item uses a [ListTile] with a
521/// blank [Icon] in the [ListTile.leading] position to get the same alignment as
522/// the item with the checkmark.
523///
524/// See also:
525///
526/// * [PopupMenuItem], a popup menu entry for picking a command (as opposed to
527/// toggling a value).
528/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
529/// * [showMenu], a method to dynamically show a popup menu at a given location.
530/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
531/// it is tapped.
532class CheckedPopupMenuItem<T> extends PopupMenuItem<T> {
533 /// Creates a popup menu item with a checkmark.
534 ///
535 /// By default, the menu item is [enabled] but unchecked. To mark the item as
536 /// checked, set [checked] to true.
537 const CheckedPopupMenuItem({
538 super.key,
539 super.value,
540 this.checked = false,
541 super.enabled,
542 super.padding,
543 super.height,
544 super.labelTextStyle,
545 super.mouseCursor,
546 super.child,
547 super.onTap,
548 });
549
550 /// Whether to display a checkmark next to the menu item.
551 ///
552 /// Defaults to false.
553 ///
554 /// When true, an [Icons.done] checkmark is displayed.
555 ///
556 /// When this popup menu item is selected, the checkmark will fade in or out
557 /// as appropriate to represent the implied new state.
558 final bool checked;
559
560 /// The widget below this widget in the tree.
561 ///
562 /// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for
563 /// the child. The text should be short enough that it won't wrap.
564 ///
565 /// This widget is placed in the [ListTile.title] slot of a [ListTile] whose
566 /// [ListTile.leading] slot is an [Icons.done] icon.
567 @override
568 Widget? get child => super.child;
569
570 @override
571 PopupMenuItemState<T, CheckedPopupMenuItem<T>> createState() => _CheckedPopupMenuItemState<T>();
572}
573
574class _CheckedPopupMenuItemState<T> extends PopupMenuItemState<T, CheckedPopupMenuItem<T>>
575 with SingleTickerProviderStateMixin {
576 static const Duration _fadeDuration = Duration(milliseconds: 150);
577 late AnimationController _controller;
578 Animation<double> get _opacity => _controller.view;
579
580 @override
581 void initState() {
582 super.initState();
583 _controller =
584 AnimationController(duration: _fadeDuration, vsync: this)
585 ..value = widget.checked ? 1.0 : 0.0
586 ..addListener(
587 () => setState(() {
588 /* animation changed */
589 }),
590 );
591 }
592
593 @override
594 void dispose() {
595 _controller.dispose();
596 super.dispose();
597 }
598
599 @override
600 void handleTap() {
601 // This fades the checkmark in or out when tapped.
602 if (widget.checked) {
603 _controller.reverse();
604 } else {
605 _controller.forward();
606 }
607 super.handleTap();
608 }
609
610 @override
611 Widget buildChild() {
612 final ThemeData theme = Theme.of(context);
613 final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
614 final PopupMenuThemeData defaults =
615 theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context);
616 final Set<MaterialState> states = <MaterialState>{if (widget.checked) MaterialState.selected};
617 final MaterialStateProperty<TextStyle?>? effectiveLabelTextStyle =
618 widget.labelTextStyle ?? popupMenuTheme.labelTextStyle ?? defaults.labelTextStyle;
619 return IgnorePointer(
620 child: ListTileTheme.merge(
621 contentPadding: EdgeInsets.zero,
622 child: ListTile(
623 enabled: widget.enabled,
624 titleTextStyle: effectiveLabelTextStyle?.resolve(states),
625 leading: FadeTransition(
626 opacity: _opacity,
627 child: Icon(_controller.isDismissed ? null : Icons.done),
628 ),
629 title: widget.child,
630 ),
631 ),
632 );
633 }
634}
635
636class _PopupMenu<T> extends StatefulWidget {
637 const _PopupMenu({
638 super.key,
639 required this.itemKeys,
640 required this.route,
641 required this.semanticLabel,
642 this.constraints,
643 required this.clipBehavior,
644 });
645
646 final List<GlobalKey> itemKeys;
647 final _PopupMenuRoute<T> route;
648 final String? semanticLabel;
649 final BoxConstraints? constraints;
650 final Clip clipBehavior;
651
652 @override
653 State<_PopupMenu<T>> createState() => _PopupMenuState<T>();
654}
655
656class _PopupMenuState<T> extends State<_PopupMenu<T>> {
657 List<CurvedAnimation> _opacities = const <CurvedAnimation>[];
658
659 @override
660 void initState() {
661 super.initState();
662 _setOpacities();
663 }
664
665 @override
666 void didUpdateWidget(covariant _PopupMenu<T> oldWidget) {
667 super.didUpdateWidget(oldWidget);
668 if (oldWidget.route.items.length != widget.route.items.length ||
669 oldWidget.route.animation != widget.route.animation) {
670 _setOpacities();
671 }
672 }
673
674 void _setOpacities() {
675 for (final CurvedAnimation opacity in _opacities) {
676 opacity.dispose();
677 }
678 final List<CurvedAnimation> newOpacities = <CurvedAnimation>[];
679 final double unit =
680 1.0 /
681 (widget.route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
682 for (int i = 0; i < widget.route.items.length; i += 1) {
683 final double start = (i + 1) * unit;
684 final double end = clampDouble(start + 1.5 * unit, 0.0, 1.0);
685 final CurvedAnimation opacity = CurvedAnimation(
686 parent: widget.route.animation!,
687 curve: Interval(start, end),
688 );
689 newOpacities.add(opacity);
690 }
691 _opacities = newOpacities;
692 }
693
694 @override
695 void dispose() {
696 for (final CurvedAnimation opacity in _opacities) {
697 opacity.dispose();
698 }
699 super.dispose();
700 }
701
702 @override
703 Widget build(BuildContext context) {
704 final double unit =
705 1.0 /
706 (widget.route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
707 final List<Widget> children = <Widget>[];
708 final ThemeData theme = Theme.of(context);
709 final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
710 final PopupMenuThemeData defaults =
711 theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context);
712
713 for (int i = 0; i < widget.route.items.length; i += 1) {
714 final CurvedAnimation opacity = _opacities[i];
715 Widget item = widget.route.items[i];
716 if (widget.route.initialValue != null &&
717 widget.route.items[i].represents(widget.route.initialValue)) {
718 item = ColoredBox(color: Theme.of(context).highlightColor, child: item);
719 }
720 children.add(
721 _MenuItem(
722 onLayout: (Size size) {
723 widget.route.itemSizes[i] = size;
724 },
725 child: FadeTransition(key: widget.itemKeys[i], opacity: opacity, child: item),
726 ),
727 );
728 }
729
730 final CurveTween opacity = CurveTween(curve: const Interval(0.0, 1.0 / 3.0));
731 final CurveTween width = CurveTween(curve: Interval(0.0, unit));
732 final CurveTween height = CurveTween(curve: Interval(0.0, unit * widget.route.items.length));
733
734 final Widget child = ConstrainedBox(
735 constraints:
736 widget.constraints ??
737 const BoxConstraints(minWidth: _kMenuMinWidth, maxWidth: _kMenuMaxWidth),
738 child: IntrinsicWidth(
739 stepWidth: _kMenuWidthStep,
740 child: Semantics(
741 role: SemanticsRole.menu,
742 scopesRoute: true,
743 namesRoute: true,
744 explicitChildNodes: true,
745 label: widget.semanticLabel,
746 child: SingleChildScrollView(
747 padding: widget.route.menuPadding ?? popupMenuTheme.menuPadding ?? defaults.menuPadding,
748 child: ListBody(children: children),
749 ),
750 ),
751 ),
752 );
753
754 return AnimatedBuilder(
755 animation: widget.route.animation!,
756 builder: (BuildContext context, Widget? child) {
757 return FadeTransition(
758 opacity: opacity.animate(widget.route.animation!),
759 child: Material(
760 shape: widget.route.shape ?? popupMenuTheme.shape ?? defaults.shape,
761 color: widget.route.color ?? popupMenuTheme.color ?? defaults.color,
762 clipBehavior: widget.clipBehavior,
763 type: MaterialType.card,
764 elevation: widget.route.elevation ?? popupMenuTheme.elevation ?? defaults.elevation!,
765 shadowColor:
766 widget.route.shadowColor ?? popupMenuTheme.shadowColor ?? defaults.shadowColor,
767 surfaceTintColor:
768 widget.route.surfaceTintColor ??
769 popupMenuTheme.surfaceTintColor ??
770 defaults.surfaceTintColor,
771 child: Align(
772 alignment: AlignmentDirectional.topEnd,
773 widthFactor: width.evaluate(widget.route.animation!),
774 heightFactor: height.evaluate(widget.route.animation!),
775 child: child,
776 ),
777 ),
778 );
779 },
780 child: child,
781 );
782 }
783}
784
785// Positioning of the menu on the screen.
786class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
787 _PopupMenuRouteLayout(
788 this.position,
789 this.itemSizes,
790 this.selectedItemIndex,
791 this.textDirection,
792 this.padding,
793 this.avoidBounds,
794 );
795
796 // Rectangle of underlying button, relative to the overlay's dimensions.
797 final RelativeRect position;
798
799 // The sizes of each item are computed when the menu is laid out, and before
800 // the route is laid out.
801 List<Size?> itemSizes;
802
803 // The index of the selected item, or null if PopupMenuButton.initialValue
804 // was not specified.
805 final int? selectedItemIndex;
806
807 // Whether to prefer going to the left or to the right.
808 final TextDirection textDirection;
809
810 // The padding of unsafe area.
811 EdgeInsets padding;
812
813 // List of rectangles that we should avoid overlapping. Unusable screen area.
814 final Set<Rect> avoidBounds;
815
816 // We put the child wherever position specifies, so long as it will fit within
817 // the specified parent size padded (inset) by 8. If necessary, we adjust the
818 // child's position so that it fits.
819
820 @override
821 BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
822 // The menu can be at most the size of the overlay minus 8.0 pixels in each
823 // direction.
824 return BoxConstraints.loose(
825 constraints.biggest,
826 ).deflate(const EdgeInsets.all(_kMenuScreenPadding) + padding);
827 }
828
829 @override
830 Offset getPositionForChild(Size size, Size childSize) {
831 final double y = position.top;
832
833 // Find the ideal horizontal position.
834 // size: The size of the overlay.
835 // childSize: The size of the menu, when fully open, as determined by
836 // getConstraintsForChild.
837 double x;
838 if (position.left > position.right) {
839 // Menu button is closer to the right edge, so grow to the left, aligned to the right edge.
840 x = size.width - position.right - childSize.width;
841 } else if (position.left < position.right) {
842 // Menu button is closer to the left edge, so grow to the right, aligned to the left edge.
843 x = position.left;
844 } else {
845 // Menu button is equidistant from both edges, so grow in reading direction.
846 x = switch (textDirection) {
847 TextDirection.rtl => size.width - position.right - childSize.width,
848 TextDirection.ltr => position.left,
849 };
850 }
851 final Offset wantedPosition = Offset(x, y);
852 final Offset originCenter = position.toRect(Offset.zero & size).center;
853 final Iterable<Rect> subScreens = DisplayFeatureSubScreen.subScreensInBounds(
854 Offset.zero & size,
855 avoidBounds,
856 );
857 final Rect subScreen = _closestScreen(subScreens, originCenter);
858 return _fitInsideScreen(subScreen, childSize, wantedPosition);
859 }
860
861 Rect _closestScreen(Iterable<Rect> screens, Offset point) {
862 Rect closest = screens.first;
863 for (final Rect screen in screens) {
864 if ((screen.center - point).distance < (closest.center - point).distance) {
865 closest = screen;
866 }
867 }
868 return closest;
869 }
870
871 Offset _fitInsideScreen(Rect screen, Size childSize, Offset wantedPosition) {
872 double x = wantedPosition.dx;
873 double y = wantedPosition.dy;
874 // Avoid going outside an area defined as the rectangle 8.0 pixels from the
875 // edge of the screen in every direction.
876 if (x < screen.left + _kMenuScreenPadding + padding.left) {
877 x = screen.left + _kMenuScreenPadding + padding.left;
878 } else if (x + childSize.width > screen.right - _kMenuScreenPadding - padding.right) {
879 x = screen.right - childSize.width - _kMenuScreenPadding - padding.right;
880 }
881 if (y < screen.top + _kMenuScreenPadding + padding.top) {
882 y = _kMenuScreenPadding + padding.top;
883 } else if (y + childSize.height > screen.bottom - _kMenuScreenPadding - padding.bottom) {
884 y = screen.bottom - childSize.height - _kMenuScreenPadding - padding.bottom;
885 }
886
887 return Offset(x, y);
888 }
889
890 @override
891 bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) {
892 // If called when the old and new itemSizes have been initialized then
893 // we expect them to have the same length because there's no practical
894 // way to change length of the items list once the menu has been shown.
895 assert(itemSizes.length == oldDelegate.itemSizes.length);
896
897 return position != oldDelegate.position ||
898 selectedItemIndex != oldDelegate.selectedItemIndex ||
899 textDirection != oldDelegate.textDirection ||
900 !listEquals(itemSizes, oldDelegate.itemSizes) ||
901 padding != oldDelegate.padding ||
902 !setEquals(avoidBounds, oldDelegate.avoidBounds);
903 }
904}
905
906class _PopupMenuRoute<T> extends PopupRoute<T> {
907 _PopupMenuRoute({
908 this.position,
909 this.positionBuilder,
910 required this.items,
911 required this.itemKeys,
912 this.initialValue,
913 this.elevation,
914 this.surfaceTintColor,
915 this.shadowColor,
916 required this.barrierLabel,
917 this.semanticLabel,
918 this.shape,
919 this.menuPadding,
920 this.color,
921 required this.capturedThemes,
922 this.constraints,
923 required this.clipBehavior,
924 super.settings,
925 super.requestFocus,
926 this.popUpAnimationStyle,
927 }) : assert(
928 (position != null) != (positionBuilder != null),
929 'Either position or positionBuilder must be provided.',
930 ),
931 itemSizes = List<Size?>.filled(items.length, null),
932 // Menus always cycle focus through their items irrespective of the
933 // focus traversal edge behavior set in the Navigator.
934 super(traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop);
935
936 final RelativeRect? position;
937 final PopupMenuPositionBuilder? positionBuilder;
938 final List<PopupMenuEntry<T>> items;
939 final List<GlobalKey> itemKeys;
940 final List<Size?> itemSizes;
941 final T? initialValue;
942 final double? elevation;
943 final Color? surfaceTintColor;
944 final Color? shadowColor;
945 final String? semanticLabel;
946 final ShapeBorder? shape;
947 final EdgeInsetsGeometry? menuPadding;
948 final Color? color;
949 final CapturedThemes capturedThemes;
950 final BoxConstraints? constraints;
951 final Clip clipBehavior;
952 final AnimationStyle? popUpAnimationStyle;
953
954 CurvedAnimation? _animation;
955
956 @override
957 Animation<double> createAnimation() {
958 if (popUpAnimationStyle != AnimationStyle.noAnimation) {
959 return _animation ??= CurvedAnimation(
960 parent: super.createAnimation(),
961 curve: popUpAnimationStyle?.curve ?? Curves.linear,
962 reverseCurve:
963 popUpAnimationStyle?.reverseCurve ?? const Interval(0.0, _kMenuCloseIntervalEnd),
964 );
965 }
966 return super.createAnimation();
967 }
968
969 void scrollTo(int selectedItemIndex) {
970 SchedulerBinding.instance.addPostFrameCallback((_) {
971 if (itemKeys[selectedItemIndex].currentContext != null) {
972 Scrollable.ensureVisible(itemKeys[selectedItemIndex].currentContext!);
973 }
974 });
975 }
976
977 @override
978 Duration get transitionDuration => popUpAnimationStyle?.duration ?? _kMenuDuration;
979
980 @override
981 bool get barrierDismissible => true;
982
983 @override
984 Color? get barrierColor => null;
985
986 @override
987 final String barrierLabel;
988
989 @override
990 Widget buildPage(
991 BuildContext context,
992 Animation<double> animation,
993 Animation<double> secondaryAnimation,
994 ) {
995 int? selectedItemIndex;
996 if (initialValue != null) {
997 for (int index = 0; selectedItemIndex == null && index < items.length; index += 1) {
998 if (items[index].represents(initialValue)) {
999 selectedItemIndex = index;
1000 }
1001 }
1002 }
1003 if (selectedItemIndex != null) {
1004 scrollTo(selectedItemIndex);
1005 }
1006
1007 final Widget menu = _PopupMenu<T>(
1008 route: this,
1009 itemKeys: itemKeys,
1010 semanticLabel: semanticLabel,
1011 constraints: constraints,
1012 clipBehavior: clipBehavior,
1013 );
1014 final MediaQueryData mediaQuery = MediaQuery.of(context);
1015 return MediaQuery.removePadding(
1016 context: context,
1017 removeTop: true,
1018 removeBottom: true,
1019 removeLeft: true,
1020 removeRight: true,
1021 child: LayoutBuilder(
1022 builder: (BuildContext context, BoxConstraints constraints) {
1023 return CustomSingleChildLayout(
1024 delegate: _PopupMenuRouteLayout(
1025 positionBuilder?.call(context, constraints) ?? position!,
1026 itemSizes,
1027 selectedItemIndex,
1028 Directionality.of(context),
1029 mediaQuery.padding,
1030 _avoidBounds(mediaQuery),
1031 ),
1032 child: capturedThemes.wrap(menu),
1033 );
1034 },
1035 ),
1036 );
1037 }
1038
1039 Set<Rect> _avoidBounds(MediaQueryData mediaQuery) {
1040 return DisplayFeatureSubScreen.avoidBounds(mediaQuery).toSet();
1041 }
1042
1043 @override
1044 void dispose() {
1045 _animation?.dispose();
1046 super.dispose();
1047 }
1048}
1049
1050/// A builder that creates a [RelativeRect] to position a popup menu.
1051/// Both [BuildContext] and [BoxConstraints] are from the [PopupRoute] that
1052/// displays this menu.
1053///
1054/// The returned [RelativeRect] determines the position of the popup menu relative
1055/// to the bounds of the [Navigator]'s overlay. The menu dimensions are not yet
1056/// known when this callback is invoked, as they depend on the items and other
1057/// properties of the menu.
1058///
1059/// The coordinate system used by the [RelativeRect] has its origin at the top
1060/// left of the [Navigator]'s overlay. Positive y coordinates are down (below the
1061/// origin), and positive x coordinates are to the right of the origin.
1062///
1063/// See also:
1064///
1065/// * [RelativeRect.fromLTRB], which creates a [RelativeRect] from left, top,
1066/// right, and bottom coordinates.
1067/// * [RelativeRect.fromRect], which creates a [RelativeRect] from two [Rect]s,
1068/// one representing the size of the popup menu and one representing the size
1069/// of the overlay.
1070typedef PopupMenuPositionBuilder =
1071 RelativeRect Function(BuildContext context, BoxConstraints constraints);
1072
1073/// Shows a popup menu that contains the `items` at `position`.
1074///
1075/// The `items` parameter must not be empty.
1076///
1077/// Only one of [position] or [positionBuilder] should be provided. Providing both
1078/// throws an assertion error. The [positionBuilder] is called at the time the
1079/// menu is shown to compute its position and every time the layout is updated,
1080/// which is useful when the position needs
1081/// to be determined at runtime based on the current layout.
1082///
1083/// If `initialValue` is specified then the first item with a matching value
1084/// will be highlighted and the value of `position` gives the rectangle whose
1085/// vertical center will be aligned with the vertical center of the highlighted
1086/// item (when possible).
1087///
1088/// If `initialValue` is not specified then the top of the menu will be aligned
1089/// with the top of the `position` rectangle.
1090///
1091/// In both cases, the menu position will be adjusted if necessary to fit on the
1092/// screen.
1093///
1094/// Horizontally, the menu is positioned so that it grows in the direction that
1095/// has the most room. For example, if the `position` describes a rectangle on
1096/// the left edge of the screen, then the left edge of the menu is aligned with
1097/// the left edge of the `position`, and the menu grows to the right. If both
1098/// edges of the `position` are equidistant from the opposite edge of the
1099/// screen, then the ambient [Directionality] is used as a tie-breaker,
1100/// preferring to grow in the reading direction.
1101///
1102/// The positioning of the `initialValue` at the `position` is implemented by
1103/// iterating over the `items` to find the first whose
1104/// [PopupMenuEntry.represents] method returns true for `initialValue`, and then
1105/// summing the values of [PopupMenuEntry.height] for all the preceding widgets
1106/// in the list.
1107///
1108/// The `elevation` argument specifies the z-coordinate at which to place the
1109/// menu. The elevation defaults to 8, the appropriate elevation for popup
1110/// menus.
1111///
1112/// The `context` argument is used to look up the [Navigator] and [Theme] for
1113/// the menu. It is only used when the method is called. Its corresponding
1114/// widget can be safely removed from the tree before the popup menu is closed.
1115///
1116/// The `useRootNavigator` argument is used to determine whether to push the
1117/// menu to the [Navigator] furthest from or nearest to the given `context`. It
1118/// is `false` by default.
1119///
1120/// The `semanticLabel` argument is used by accessibility frameworks to
1121/// announce screen transitions when the menu is opened and closed. If this
1122/// label is not provided, it will default to
1123/// [MaterialLocalizations.popupMenuLabel].
1124///
1125/// The `clipBehavior` argument is used to clip the shape of the menu. Defaults to
1126/// [Clip.none].
1127///
1128/// The `requestFocus` argument specifies whether the menu should request focus
1129/// when it appears. If it is null, [Navigator.requestFocus] is used instead.
1130///
1131/// See also:
1132///
1133/// * [PopupMenuItem], a popup menu entry for a single value.
1134/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
1135/// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
1136/// * [PopupMenuButton], which provides an [IconButton] that shows a menu by
1137/// calling this method automatically.
1138/// * [SemanticsConfiguration.namesRoute], for a description of edge triggered
1139/// semantics.
1140Future<T?> showMenu<T>({
1141 required BuildContext context,
1142 RelativeRect? position,
1143 PopupMenuPositionBuilder? positionBuilder,
1144 required List<PopupMenuEntry<T>> items,
1145 T? initialValue,
1146 double? elevation,
1147 Color? shadowColor,
1148 Color? surfaceTintColor,
1149 String? semanticLabel,
1150 ShapeBorder? shape,
1151 EdgeInsetsGeometry? menuPadding,
1152 Color? color,
1153 bool useRootNavigator = false,
1154 BoxConstraints? constraints,
1155 Clip clipBehavior = Clip.none,
1156 RouteSettings? routeSettings,
1157 AnimationStyle? popUpAnimationStyle,
1158 bool? requestFocus,
1159}) {
1160 assert(items.isNotEmpty);
1161 assert(debugCheckHasMaterialLocalizations(context));
1162 assert(
1163 (position != null) != (positionBuilder != null),
1164 'Either position or positionBuilder must be provided.',
1165 );
1166
1167 switch (Theme.of(context).platform) {
1168 case TargetPlatform.iOS:
1169 case TargetPlatform.macOS:
1170 break;
1171 case TargetPlatform.android:
1172 case TargetPlatform.fuchsia:
1173 case TargetPlatform.linux:
1174 case TargetPlatform.windows:
1175 semanticLabel ??= MaterialLocalizations.of(context).popupMenuLabel;
1176 }
1177
1178 final List<GlobalKey> menuItemKeys = List<GlobalKey>.generate(
1179 items.length,
1180 (int index) => GlobalKey(),
1181 );
1182 final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
1183 return navigator.push(
1184 _PopupMenuRoute<T>(
1185 position: position,
1186 positionBuilder: positionBuilder,
1187 items: items,
1188 itemKeys: menuItemKeys,
1189 initialValue: initialValue,
1190 elevation: elevation,
1191 shadowColor: shadowColor,
1192 surfaceTintColor: surfaceTintColor,
1193 semanticLabel: semanticLabel,
1194 barrierLabel: MaterialLocalizations.of(context).menuDismissLabel,
1195 shape: shape,
1196 menuPadding: menuPadding,
1197 color: color,
1198 capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
1199 constraints: constraints,
1200 clipBehavior: clipBehavior,
1201 settings: routeSettings,
1202 popUpAnimationStyle: popUpAnimationStyle,
1203 requestFocus: requestFocus,
1204 ),
1205 );
1206}
1207
1208/// Signature for the callback invoked when a menu item is selected. The
1209/// argument is the value of the [PopupMenuItem] that caused its menu to be
1210/// dismissed.
1211///
1212/// Used by [PopupMenuButton.onSelected].
1213typedef PopupMenuItemSelected<T> = void Function(T value);
1214
1215/// Signature for the callback invoked when a [PopupMenuButton] is dismissed
1216/// without selecting an item.
1217///
1218/// Used by [PopupMenuButton.onCanceled].
1219typedef PopupMenuCanceled = void Function();
1220
1221/// Signature used by [PopupMenuButton] to lazily construct the items shown when
1222/// the button is pressed.
1223///
1224/// Used by [PopupMenuButton.itemBuilder].
1225typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(BuildContext context);
1226
1227/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed
1228/// because an item was selected. The value passed to [onSelected] is the value of
1229/// the selected menu item.
1230///
1231/// One of [child] or [icon] may be provided, but not both. If [icon] is provided,
1232/// then [PopupMenuButton] behaves like an [IconButton].
1233///
1234/// If both are null, then a standard overflow icon is created (depending on the
1235/// platform).
1236///
1237/// ## Updating to [MenuAnchor]
1238///
1239/// There is a Material 3 component,
1240/// [MenuAnchor] that is preferred for applications that are configured
1241/// for Material 3 (see [ThemeData.useMaterial3]).
1242/// The [MenuAnchor] widget's visuals
1243/// are a little bit different, see the Material 3 spec at
1244/// <https://m3.material.io/components/menus/guidelines> for
1245/// more details.
1246///
1247/// The [MenuAnchor] widget's API is also slightly different.
1248/// [MenuAnchor]'s were built to be lower level interface for
1249/// creating menus that are displayed from an anchor.
1250///
1251/// There are a few steps you would take to migrate from
1252/// [PopupMenuButton] to [MenuAnchor]:
1253///
1254/// 1. Instead of using the [PopupMenuButton.itemBuilder] to build
1255/// a list of [PopupMenuEntry]s, you would use the [MenuAnchor.menuChildren]
1256/// which takes a list of [Widget]s. Usually, you would use a list of
1257/// [MenuItemButton]s as shown in the example below.
1258///
1259/// 2. Instead of using the [PopupMenuButton.onSelected] callback, you would
1260/// set individual callbacks for each of the [MenuItemButton]s using the
1261/// [MenuItemButton.onPressed] property.
1262///
1263/// 3. To anchor the [MenuAnchor] to a widget, you would use the [MenuAnchor.builder]
1264/// to return the widget of choice - usually a [TextButton] or an [IconButton].
1265///
1266/// 4. You may want to style the [MenuItemButton]s, see the [MenuItemButton]
1267/// documentation for details.
1268///
1269/// Use the sample below for an example of migrating from [PopupMenuButton] to
1270/// [MenuAnchor].
1271///
1272/// {@tool dartpad}
1273/// This example shows a menu with three items, selecting between an enum's
1274/// values and setting a `selectedMenu` field based on the selection.
1275///
1276/// ** See code in examples/api/lib/material/popup_menu/popup_menu.0.dart **
1277/// {@end-tool}
1278///
1279/// {@tool dartpad}
1280/// This example shows how to migrate the above to a [MenuAnchor].
1281///
1282/// ** See code in examples/api/lib/material/menu_anchor/menu_anchor.2.dart **
1283/// {@end-tool}
1284///
1285/// {@tool dartpad}
1286/// This sample shows the creation of a popup menu, as described in:
1287/// https://m3.material.io/components/menus/overview
1288///
1289/// ** See code in examples/api/lib/material/popup_menu/popup_menu.1.dart **
1290/// {@end-tool}
1291///
1292/// {@tool dartpad}
1293/// This sample showcases how to override the [PopupMenuButton] animation
1294/// curves and duration using [AnimationStyle].
1295///
1296/// ** See code in examples/api/lib/material/popup_menu/popup_menu.2.dart **
1297/// {@end-tool}
1298///
1299/// See also:
1300///
1301/// * [PopupMenuItem], a popup menu entry for a single value.
1302/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
1303/// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
1304/// * [showMenu], a method to dynamically show a popup menu at a given location.
1305class PopupMenuButton<T> extends StatefulWidget {
1306 /// Creates a button that shows a popup menu.
1307 const PopupMenuButton({
1308 super.key,
1309 required this.itemBuilder,
1310 this.initialValue,
1311 this.onOpened,
1312 this.onSelected,
1313 this.onCanceled,
1314 this.tooltip,
1315 this.elevation,
1316 this.shadowColor,
1317 this.surfaceTintColor,
1318 this.padding = const EdgeInsets.all(8.0),
1319 this.menuPadding,
1320 this.child,
1321 this.borderRadius,
1322 this.splashRadius,
1323 this.icon,
1324 this.iconSize,
1325 this.offset = Offset.zero,
1326 this.enabled = true,
1327 this.shape,
1328 this.color,
1329 this.iconColor,
1330 this.enableFeedback,
1331 this.constraints,
1332 this.position,
1333 this.clipBehavior = Clip.none,
1334 this.useRootNavigator = false,
1335 this.popUpAnimationStyle,
1336 this.routeSettings,
1337 this.style,
1338 this.requestFocus,
1339 }) : assert(!(child != null && icon != null), 'You can only pass [child] or [icon], not both.');
1340
1341 /// Called when the button is pressed to create the items to show in the menu.
1342 final PopupMenuItemBuilder<T> itemBuilder;
1343
1344 /// The value of the menu item, if any, that should be highlighted when the menu opens.
1345 final T? initialValue;
1346
1347 /// Called when the popup menu is shown.
1348 final VoidCallback? onOpened;
1349
1350 /// Called when the user selects a value from the popup menu created by this button.
1351 ///
1352 /// If the popup menu is dismissed without selecting a value, [onCanceled] is
1353 /// called instead.
1354 final PopupMenuItemSelected<T>? onSelected;
1355
1356 /// Called when the user dismisses the popup menu without selecting an item.
1357 ///
1358 /// If the user selects a value, [onSelected] is called instead.
1359 final PopupMenuCanceled? onCanceled;
1360
1361 /// Text that describes the action that will occur when the button is pressed.
1362 ///
1363 /// This text is displayed when the user long-presses on the button and is
1364 /// used for accessibility.
1365 final String? tooltip;
1366
1367 /// The z-coordinate at which to place the menu when open. This controls the
1368 /// size of the shadow below the menu.
1369 ///
1370 /// Defaults to 8, the appropriate elevation for popup menus.
1371 final double? elevation;
1372
1373 /// The color used to paint the shadow below the menu.
1374 ///
1375 /// If null then the ambient [PopupMenuThemeData.shadowColor] is used.
1376 /// If that is null too, then the overall theme's [ThemeData.shadowColor]
1377 /// (default black) is used.
1378 final Color? shadowColor;
1379
1380 /// The color used as an overlay on [color] to indicate elevation.
1381 ///
1382 /// This is not recommended for use. [Material 3 spec](https://m3.material.io/styles/color/the-color-system/color-roles)
1383 /// introduced a set of tone-based surfaces and surface containers in its [ColorScheme],
1384 /// which provide more flexibility. The intention is to eventually remove surface tint color from
1385 /// the framework.
1386 ///
1387 /// If null, [PopupMenuThemeData.surfaceTintColor] is used. If that
1388 /// is also null, the default value is [Colors.transparent].
1389 ///
1390 /// See [Material.surfaceTintColor] for more details on how this
1391 /// overlay is applied.
1392 final Color? surfaceTintColor;
1393
1394 /// Matches IconButton's 8 dps padding by default. In some cases, notably where
1395 /// this button appears as the trailing element of a list item, it's useful to be able
1396 /// to set the padding to zero.
1397 final EdgeInsetsGeometry padding;
1398
1399 /// If provided, menu padding is used for empty space around the outside
1400 /// of the popup menu.
1401 ///
1402 /// If this property is null, then [PopupMenuThemeData.menuPadding] is used.
1403 /// If [PopupMenuThemeData.menuPadding] is also null, then vertical padding
1404 /// of 8 pixels is used.
1405 final EdgeInsetsGeometry? menuPadding;
1406
1407 /// The splash radius.
1408 ///
1409 /// If null, default splash radius of [InkWell] or [IconButton] is used.
1410 final double? splashRadius;
1411
1412 /// If provided, [child] is the widget used for this button
1413 /// and the button will utilize an [InkWell] for taps.
1414 final Widget? child;
1415
1416 /// The border radius for the [InkWell] that wraps the [child].
1417 ///
1418 /// Defaults to null, which indicates no border radius should be applied.
1419 final BorderRadius? borderRadius;
1420
1421 /// If provided, the [icon] is used for this button
1422 /// and the button will behave like an [IconButton].
1423 final Widget? icon;
1424
1425 /// The offset is applied relative to the initial position
1426 /// set by the [position].
1427 ///
1428 /// When not set, the offset defaults to [Offset.zero].
1429 final Offset offset;
1430
1431 /// Whether this popup menu button is interactive.
1432 ///
1433 /// Defaults to true.
1434 ///
1435 /// If true, the button will respond to presses by displaying the menu.
1436 ///
1437 /// If false, the button is styled with the disabled color from the
1438 /// current [Theme] and will not respond to presses or show the popup
1439 /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called.
1440 ///
1441 /// This can be useful in situations where the app needs to show the button,
1442 /// but doesn't currently have anything to show in the menu.
1443 final bool enabled;
1444
1445 /// If provided, the shape used for the menu.
1446 ///
1447 /// If this property is null, then [PopupMenuThemeData.shape] is used.
1448 /// If [PopupMenuThemeData.shape] is also null, then the default shape for
1449 /// [MaterialType.card] is used. This default shape is a rectangle with
1450 /// rounded edges of BorderRadius.circular(2.0).
1451 final ShapeBorder? shape;
1452
1453 /// If provided, the background color used for the menu.
1454 ///
1455 /// If this property is null, then [PopupMenuThemeData.color] is used.
1456 /// If [PopupMenuThemeData.color] is also null, then
1457 /// [ThemeData.cardColor] is used in Material 2. In Material3, defaults to
1458 /// [ColorScheme.surfaceContainer].
1459 final Color? color;
1460
1461 /// If provided, this color is used for the button icon.
1462 ///
1463 /// If this property is null, then [PopupMenuThemeData.iconColor] is used.
1464 /// If [PopupMenuThemeData.iconColor] is also null then defaults to
1465 /// [IconThemeData.color].
1466 final Color? iconColor;
1467
1468 /// Whether detected gestures should provide acoustic and/or haptic feedback.
1469 ///
1470 /// For example, on Android a tap will produce a clicking sound and a
1471 /// long-press will produce a short vibration, when feedback is enabled.
1472 ///
1473 /// See also:
1474 ///
1475 /// * [Feedback] for providing platform-specific feedback to certain actions.
1476 final bool? enableFeedback;
1477
1478 /// If provided, the size of the [Icon].
1479 ///
1480 /// If this property is null, then [IconThemeData.size] is used.
1481 /// If [IconThemeData.size] is also null, then
1482 /// default size is 24.0 pixels.
1483 final double? iconSize;
1484
1485 /// Optional size constraints for the menu.
1486 ///
1487 /// When unspecified, defaults to:
1488 /// ```dart
1489 /// const BoxConstraints(
1490 /// minWidth: 2.0 * 56.0,
1491 /// maxWidth: 5.0 * 56.0,
1492 /// )
1493 /// ```
1494 ///
1495 /// The default constraints ensure that the menu width matches maximum width
1496 /// recommended by the Material Design guidelines.
1497 /// Specifying this parameter enables creation of menu wider than
1498 /// the default maximum width.
1499 final BoxConstraints? constraints;
1500
1501 /// Whether the popup menu is positioned over or under the popup menu button.
1502 ///
1503 /// [offset] is used to change the position of the popup menu relative to the
1504 /// position set by this parameter.
1505 ///
1506 /// If this property is `null`, then [PopupMenuThemeData.position] is used. If
1507 /// [PopupMenuThemeData.position] is also `null`, then the position defaults
1508 /// to [PopupMenuPosition.over] which makes the popup menu appear directly
1509 /// over the button that was used to create it.
1510 final PopupMenuPosition? position;
1511
1512 /// {@macro flutter.material.Material.clipBehavior}
1513 ///
1514 /// The [clipBehavior] argument is used the clip shape of the menu.
1515 ///
1516 /// Defaults to [Clip.none].
1517 final Clip clipBehavior;
1518
1519 /// Used to determine whether to push the menu to the [Navigator] furthest
1520 /// from or nearest to the given `context`.
1521 ///
1522 /// Defaults to false.
1523 final bool useRootNavigator;
1524
1525 /// Used to override the default animation curves and durations of the popup
1526 /// menu's open and close transitions.
1527 ///
1528 /// If [AnimationStyle.curve] is provided, it will be used to override
1529 /// the default popup animation curve. Otherwise, defaults to [Curves.linear].
1530 ///
1531 /// If [AnimationStyle.reverseCurve] is provided, it will be used to
1532 /// override the default popup animation reverse curve. Otherwise, defaults to
1533 /// `Interval(0.0, 2.0 / 3.0)`.
1534 ///
1535 /// If [AnimationStyle.duration] is provided, it will be used to override
1536 /// the default popup animation duration. Otherwise, defaults to 300ms.
1537 ///
1538 /// To disable the theme animation, use [AnimationStyle.noAnimation].
1539 ///
1540 /// If this is null, then the default animation will be used.
1541 final AnimationStyle? popUpAnimationStyle;
1542
1543 /// Optional route settings for the menu.
1544 ///
1545 /// See [RouteSettings] for details.
1546 final RouteSettings? routeSettings;
1547
1548 /// Customizes this icon button's appearance.
1549 ///
1550 /// The [style] is only used for Material 3 [IconButton]s. If [ThemeData.useMaterial3]
1551 /// is set to true, [style] is preferred for icon button customization, and any
1552 /// parameters defined in [style] will override the same parameters in [IconButton].
1553 ///
1554 /// Null by default.
1555 final ButtonStyle? style;
1556
1557 /// Whether to request focus when the menu appears.
1558 ///
1559 /// If null, [Navigator.requestFocus] will be used instead.
1560 final bool? requestFocus;
1561
1562 @override
1563 PopupMenuButtonState<T> createState() => PopupMenuButtonState<T>();
1564}
1565
1566/// The [State] for a [PopupMenuButton].
1567///
1568/// See [showButtonMenu] for a way to programmatically open the popup menu
1569/// of your button state.
1570class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
1571 bool _isMenuExpanded = false;
1572 RelativeRect _positionBuilder(BuildContext _, BoxConstraints constraints) {
1573 final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
1574 final RenderBox button = context.findRenderObject()! as RenderBox;
1575 final RenderBox overlay =
1576 Navigator.of(
1577 context,
1578 rootNavigator: widget.useRootNavigator,
1579 ).overlay!.context.findRenderObject()!
1580 as RenderBox;
1581 final PopupMenuPosition popupMenuPosition =
1582 widget.position ?? popupMenuTheme.position ?? PopupMenuPosition.over;
1583 late Offset offset;
1584 switch (popupMenuPosition) {
1585 case PopupMenuPosition.over:
1586 offset = widget.offset;
1587 case PopupMenuPosition.under:
1588 offset = Offset(0.0, button.size.height) + widget.offset;
1589 if (widget.child == null) {
1590 // Remove the padding of the icon button.
1591 offset -= Offset(0.0, widget.padding.vertical / 2);
1592 }
1593 }
1594 final RelativeRect position = RelativeRect.fromRect(
1595 Rect.fromPoints(
1596 button.localToGlobal(offset, ancestor: overlay),
1597 button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
1598 ),
1599 Offset.zero & overlay.size,
1600 );
1601
1602 return position;
1603 }
1604
1605 /// A method to show a popup menu with the items supplied to
1606 /// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton].
1607 ///
1608 /// By default, it is called when the user taps the button and [PopupMenuButton.enabled]
1609 /// is set to `true`. Moreover, you can open the button by calling the method manually.
1610 ///
1611 /// You would access your [PopupMenuButtonState] using a [GlobalKey] and
1612 /// show the menu of the button with `globalKey.currentState.showButtonMenu`.
1613 void showButtonMenu() {
1614 final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
1615 final List<PopupMenuEntry<T>> items = widget.itemBuilder(context);
1616 // Only show the menu if there is something to show
1617 if (items.isNotEmpty) {
1618 widget.onOpened?.call();
1619 _isMenuExpanded = true;
1620 showMenu<T?>(
1621 context: context,
1622 elevation: widget.elevation ?? popupMenuTheme.elevation,
1623 shadowColor: widget.shadowColor ?? popupMenuTheme.shadowColor,
1624 surfaceTintColor: widget.surfaceTintColor ?? popupMenuTheme.surfaceTintColor,
1625 items: items,
1626 initialValue: widget.initialValue,
1627 positionBuilder: _positionBuilder,
1628 shape: widget.shape ?? popupMenuTheme.shape,
1629 menuPadding: widget.menuPadding ?? popupMenuTheme.menuPadding,
1630 color: widget.color ?? popupMenuTheme.color,
1631 constraints: widget.constraints,
1632 clipBehavior: widget.clipBehavior,
1633 useRootNavigator: widget.useRootNavigator,
1634 popUpAnimationStyle: widget.popUpAnimationStyle,
1635 routeSettings: widget.routeSettings,
1636 requestFocus: widget.requestFocus,
1637 ).then<void>((T? newValue) {
1638 if (!mounted) {
1639 return null;
1640 }
1641 if (newValue == null) {
1642 widget.onCanceled?.call();
1643 return null;
1644 }
1645 widget.onSelected?.call(newValue);
1646 _isMenuExpanded = false;
1647 });
1648 }
1649 }
1650
1651 bool get _canRequestFocus {
1652 final NavigationMode mode =
1653 MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
1654 return switch (mode) {
1655 NavigationMode.traditional => widget.enabled,
1656 NavigationMode.directional => true,
1657 };
1658 }
1659
1660 @protected
1661 @override
1662 Widget build(BuildContext context) {
1663 final IconThemeData iconTheme = IconTheme.of(context);
1664 final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
1665 final bool enableFeedback =
1666 widget.enableFeedback ?? PopupMenuTheme.of(context).enableFeedback ?? true;
1667
1668 assert(debugCheckHasMaterialLocalizations(context));
1669
1670 if (widget.child != null) {
1671 final Widget child = Tooltip(
1672 message: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
1673 child: InkWell(
1674 borderRadius: widget.borderRadius,
1675 onTap: widget.enabled ? showButtonMenu : null,
1676 canRequestFocus: _canRequestFocus,
1677 radius: widget.splashRadius,
1678 enableFeedback: enableFeedback,
1679 child: widget.child,
1680 ),
1681 );
1682 final MaterialTapTargetSize tapTargetSize =
1683 widget.style?.tapTargetSize ?? MaterialTapTargetSize.shrinkWrap;
1684 if (tapTargetSize == MaterialTapTargetSize.padded) {
1685 return ConstrainedBox(
1686 constraints: const BoxConstraints(
1687 minWidth: kMinInteractiveDimension,
1688 minHeight: kMinInteractiveDimension,
1689 ),
1690 child: child,
1691 );
1692 }
1693 return Semantics(expanded: _isMenuExpanded, child: child);
1694 }
1695
1696 return Semantics(
1697 child: IconButton(
1698 key: StandardComponentType.moreButton.key,
1699 icon: Semantics(expanded: _isMenuExpanded, child: widget.icon ?? Icon(Icons.adaptive.more)),
1700 padding: widget.padding,
1701 splashRadius: widget.splashRadius,
1702 iconSize: widget.iconSize ?? popupMenuTheme.iconSize ?? iconTheme.size,
1703 color: widget.iconColor ?? popupMenuTheme.iconColor ?? iconTheme.color,
1704 tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
1705 onPressed: widget.enabled ? showButtonMenu : null,
1706 enableFeedback: enableFeedback,
1707 style: widget.style,
1708 ),
1709 );
1710 }
1711}
1712
1713// This MaterialStateProperty is passed along to the menu item's InkWell which
1714// resolves the property against MaterialState.disabled, MaterialState.hovered,
1715// MaterialState.focused.
1716class _EffectiveMouseCursor extends MaterialStateMouseCursor {
1717 const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor);
1718
1719 final MouseCursor? widgetCursor;
1720 final MaterialStateProperty<MouseCursor?>? themeCursor;
1721
1722 @override
1723 MouseCursor resolve(Set<MaterialState> states) {
1724 return MaterialStateProperty.resolveAs<MouseCursor?>(widgetCursor, states) ??
1725 themeCursor?.resolve(states) ??
1726 MaterialStateMouseCursor.clickable.resolve(states);
1727 }
1728
1729 @override
1730 String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)';
1731}
1732
1733class _PopupMenuDefaultsM2 extends PopupMenuThemeData {
1734 _PopupMenuDefaultsM2(this.context) : super(elevation: 8.0);
1735
1736 final BuildContext context;
1737 late final ThemeData _theme = Theme.of(context);
1738 late final TextTheme _textTheme = _theme.textTheme;
1739
1740 @override
1741 TextStyle? get textStyle => _textTheme.titleMedium;
1742
1743 @override
1744 EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0);
1745
1746 static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 16.0);
1747}
1748
1749// BEGIN GENERATED TOKEN PROPERTIES - PopupMenu
1750
1751// Do not edit by hand. The code between the "BEGIN GENERATED" and
1752// "END GENERATED" comments are generated from data in the Material
1753// Design token database by the script:
1754// dev/tools/gen_defaults/bin/gen_defaults.dart.
1755
1756// dart format off
1757class _PopupMenuDefaultsM3 extends PopupMenuThemeData {
1758 _PopupMenuDefaultsM3(this.context)
1759 : super(elevation: 3.0);
1760
1761 final BuildContext context;
1762 late final ThemeData _theme = Theme.of(context);
1763 late final ColorScheme _colors = _theme.colorScheme;
1764 late final TextTheme _textTheme = _theme.textTheme;
1765
1766 @override MaterialStateProperty<TextStyle?>? get labelTextStyle {
1767 return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
1768 // TODO(quncheng): Update this hard-coded value to use the latest tokens.
1769 final TextStyle style = _textTheme.labelLarge!;
1770 if (states.contains(MaterialState.disabled)) {
1771 return style.apply(color: _colors.onSurface.withOpacity(0.38));
1772 }
1773 return style.apply(color: _colors.onSurface);
1774 });
1775 }
1776
1777 @override
1778 Color? get color => _colors.surfaceContainer;
1779
1780 @override
1781 Color? get shadowColor => _colors.shadow;
1782
1783 @override
1784 Color? get surfaceTintColor => Colors.transparent;
1785
1786 @override
1787 ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
1788
1789 // TODO(bleroux): This is taken from https://m3.material.io/components/menus/specs
1790 // Update this when the token is available.
1791 @override
1792 EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0);
1793
1794 // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs
1795 // Update this when the token is available.
1796 static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 12.0);
1797}// dart format on
1798
1799// END GENERATED TOKEN PROPERTIES - PopupMenu
1800

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com