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'; |
7 | library; |
8 | |
9 | import 'dart:ui'; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | import 'package:flutter/rendering.dart'; |
13 | import 'package:flutter/scheduler.dart'; |
14 | import 'package:flutter/widgets.dart'; |
15 | |
16 | import 'button_style.dart'; |
17 | import 'color_scheme.dart'; |
18 | import 'colors.dart'; |
19 | import 'constants.dart'; |
20 | import 'debug.dart'; |
21 | import 'divider.dart'; |
22 | import 'icon_button.dart'; |
23 | import 'icons.dart'; |
24 | import 'ink_well.dart'; |
25 | import 'list_tile.dart'; |
26 | import 'list_tile_theme.dart'; |
27 | import 'material.dart'; |
28 | import 'material_localizations.dart'; |
29 | import 'material_state.dart'; |
30 | import 'popup_menu_theme.dart'; |
31 | import 'text_theme.dart'; |
32 | import 'theme.dart'; |
33 | import 'theme_data.dart'; |
34 | import '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 | |
44 | const Duration _kMenuDuration = Duration(milliseconds: 300); |
45 | const double _kMenuCloseIntervalEnd = 2.0 / 3.0; |
46 | const double _kMenuDividerHeight = 16.0; |
47 | const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; |
48 | const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; |
49 | const double _kMenuWidthStep = 56.0; |
50 | const 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. |
73 | abstract 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. |
111 | class 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 | |
170 | class _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. |
188 | class _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 | |
204 | class _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. |
272 | class 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. |
384 | class 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. |
532 | class 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 | |
574 | class _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 | |
636 | class _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 | |
656 | class _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. |
786 | class _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 | |
906 | class _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. |
1070 | typedef 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. |
1140 | Future<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]. |
1213 | typedef 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]. |
1219 | typedef 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]. |
1225 | typedef 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. |
1305 | class 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. |
1570 | class 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. |
1716 | class _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 | |
1733 | class _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 |
1757 | class _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 |
Definitions
- _kMenuDuration
- _kMenuCloseIntervalEnd
- _kMenuDividerHeight
- _kMenuMaxWidth
- _kMenuMinWidth
- _kMenuWidthStep
- _kMenuScreenPadding
- PopupMenuEntry
- PopupMenuEntry
- height
- represents
- PopupMenuDivider
- PopupMenuDivider
- represents
- createState
- _PopupMenuDividerState
- build
- _MenuItem
- _MenuItem
- createRenderObject
- updateRenderObject
- _RenderMenuItem
- _RenderMenuItem
- computeDryLayout
- computeDryBaseline
- performLayout
- PopupMenuItem
- PopupMenuItem
- represents
- createState
- PopupMenuItemState
- buildChild
- handleTap
- build
- CheckedPopupMenuItem
- CheckedPopupMenuItem
- child
- createState
- _CheckedPopupMenuItemState
- _opacity
- initState
- dispose
- handleTap
- buildChild
- _PopupMenu
- _PopupMenu
- createState
- _PopupMenuState
- initState
- didUpdateWidget
- _setOpacities
- dispose
- build
- _PopupMenuRouteLayout
- _PopupMenuRouteLayout
- getConstraintsForChild
- getPositionForChild
- _closestScreen
- _fitInsideScreen
- shouldRelayout
- _PopupMenuRoute
- _PopupMenuRoute
- createAnimation
- scrollTo
- transitionDuration
- barrierDismissible
- barrierColor
- buildPage
- _avoidBounds
- dispose
- showMenu
- PopupMenuButton
- PopupMenuButton
- createState
- PopupMenuButtonState
- _positionBuilder
- showButtonMenu
- _canRequestFocus
- build
- _EffectiveMouseCursor
- _EffectiveMouseCursor
- resolve
- debugDescription
- _PopupMenuDefaultsM2
- _PopupMenuDefaultsM2
- textStyle
- menuPadding
- _PopupMenuDefaultsM3
- _PopupMenuDefaultsM3
- labelTextStyle
- color
- shadowColor
- surfaceTintColor
- shape
Learn more about Flutter for embedded and desktop on industrialflutter.com