popup_menu.dart 39.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/widgets.dart';
10

11
import 'constants.dart';
12
import 'debug.dart';
Hans Muller's avatar
Hans Muller committed
13
import 'divider.dart';
Hans Muller's avatar
Hans Muller committed
14
import 'icon_button.dart';
15
import 'icons.dart';
16
import 'ink_well.dart';
17
import 'list_tile.dart';
18
import 'material.dart';
19
import 'material_localizations.dart';
20
import 'popup_menu_theme.dart';
21
import 'theme.dart';
22
import 'tooltip.dart';
23

24 25 26
// Examples can assume:
// enum Commands { heroAndScholar, hurricaneCame }
// dynamic _heroAndScholar;
27 28 29
// dynamic _selection;
// BuildContext context;
// void setState(VoidCallback fn) { }
30

31
const Duration _kMenuDuration = Duration(milliseconds: 300);
32
const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
33
const double _kMenuHorizontalPadding = 16.0;
Ian Hickson's avatar
Ian Hickson committed
34
const double _kMenuDividerHeight = 16.0;
35 36
const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
37
const double _kMenuVerticalPadding = 8.0;
38
const double _kMenuWidthStep = 56.0;
Hans Muller's avatar
Hans Muller committed
39
const double _kMenuScreenPadding = 8.0;
40

41 42 43 44 45 46
/// A base class for entries in a material design popup menu.
///
/// The popup menu widget uses this interface to interact with the menu items.
/// To show a popup menu, use the [showMenu] function. To create a button that
/// shows a popup menu, consider using [PopupMenuButton].
///
Ian Hickson's avatar
Ian Hickson committed
47 48 49 50 51 52
/// The type `T` is the type of the value(s) the entry represents. All the
/// entries in a given menu must represent values with consistent types.
///
/// A [PopupMenuEntry] may represent multiple values, for example a row with
/// several icons, or a single entry, for example a menu item with an icon (see
/// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]).
53 54 55
///
/// See also:
///
Ian Hickson's avatar
Ian Hickson committed
56 57 58 59 60 61
///  * [PopupMenuItem], a popup menu entry for a single value.
///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
///  * [CheckedPopupMenuItem], a popup menu item with a checkmark.
///  * [showMenu], a method to dynamically show a popup menu at a given location.
///  * [PopupMenuButton], an [IconButton] that automatically shows a menu when
///    it is tapped.
62
abstract class PopupMenuEntry<T> extends StatefulWidget {
63 64 65
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const PopupMenuEntry({ Key key }) : super(key: key);
Hans Muller's avatar
Hans Muller committed
66

67 68
  /// The amount of vertical space occupied by this entry.
  ///
Ian Hickson's avatar
Ian Hickson committed
69 70 71 72
  /// This value is used at the time the [showMenu] method is called, if the
  /// `initialValue` argument is provided, to determine the position of this
  /// entry when aligning the selected entry over the given `position`. It is
  /// otherwise ignored.
Hans Muller's avatar
Hans Muller committed
73
  double get height;
74

Ian Hickson's avatar
Ian Hickson committed
75 76 77 78 79 80 81 82 83 84 85 86 87
  /// Whether this entry represents a particular value.
  ///
  /// This method is used by [showMenu], when it is called, to align the entry
  /// representing the `initialValue`, if any, to the given `position`, and then
  /// later is called on each entry to determine if it should be highlighted (if
  /// the method returns true, the entry will have its background color set to
  /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then
  /// this method is not called.
  ///
  /// If the [PopupMenuEntry] represents a single value, this should return true
  /// if the argument matches that value. If it represents multiple values, it
  /// should return true if the argument matches any of them.
  bool represents(T value);
Hans Muller's avatar
Hans Muller committed
88 89
}

90 91
/// A horizontal divider in a material design popup menu.
///
Ian Hickson's avatar
Ian Hickson committed
92
/// This widget adapts the [Divider] for use in popup menus.
93 94 95
///
/// See also:
///
Ian Hickson's avatar
Ian Hickson committed
96 97 98 99
///  * [PopupMenuItem], for the kinds of items that this widget divides.
///  * [showMenu], a method to dynamically show a popup menu at a given location.
///  * [PopupMenuButton], an [IconButton] that automatically shows a menu when
///    it is tapped.
100
// ignore: prefer_void_to_null, https://github.com/dart-lang/sdk/issues/34416
Ian Hickson's avatar
Ian Hickson committed
101
class PopupMenuDivider extends PopupMenuEntry<Null> {
102 103
  /// Creates a horizontal divider for a popup menu.
  ///
Ian Hickson's avatar
Ian Hickson committed
104
  /// By default, the divider has a height of 16 logical pixels.
105
  const PopupMenuDivider({ Key key, this.height = _kMenuDividerHeight }) : super(key: key);
Hans Muller's avatar
Hans Muller committed
106

Ian Hickson's avatar
Ian Hickson committed
107 108 109
  /// The height of the divider entry.
  ///
  /// Defaults to 16 pixels.
110
  @override
Hans Muller's avatar
Hans Muller committed
111 112
  final double height;

113
  @override
114
  bool represents(void value) => false;
115

116
  @override
117
  _PopupMenuDividerState createState() => _PopupMenuDividerState();
118 119 120
}

class _PopupMenuDividerState extends State<PopupMenuDivider> {
121
  @override
122
  Widget build(BuildContext context) => Divider(height: widget.height);
Hans Muller's avatar
Hans Muller committed
123 124
}

125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
// This widget only exists to enable _PopupMenuRoute to save the sizes of
// each menu item. The sizes are used by _PopupMenuRouteLayout to compute the
// y coordinate of the menu's origin so that the center of selected menu
// item lines up with the center of its PopupMenuButton.
class _MenuItem extends SingleChildRenderObjectWidget {
  const _MenuItem({
    Key key,
    @required this.onLayout,
    Widget child,
  }) : assert(onLayout != null), super(key: key, child: child);

  final ValueChanged<Size> onLayout;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderMenuItem(onLayout);
  }

  @override
  void updateRenderObject(BuildContext context, covariant _RenderMenuItem renderObject) {
    renderObject.onLayout = onLayout;
  }
}

class _RenderMenuItem extends RenderShiftedBox {
  _RenderMenuItem(this.onLayout, [RenderBox child]) : assert(onLayout != null), super(child);

  ValueChanged<Size> onLayout;

  @override
  void performLayout() {
    if (child == null) {
      size = Size.zero;
    } else {
      child.layout(constraints, parentUsesSize: true);
      size = constraints.constrain(child.size);
    }
162
    final BoxParentData childParentData = child.parentData as BoxParentData;
163 164 165 166 167
    childParentData.offset = Offset.zero;
    onLayout(size);
  }
}

168 169 170 171 172 173 174 175
/// An item in a material design popup menu.
///
/// To show a popup menu, use the [showMenu] function. To create a button that
/// shows a popup menu, consider using [PopupMenuButton].
///
/// To show a checkmark next to a popup menu item, consider using
/// [CheckedPopupMenuItem].
///
Ian Hickson's avatar
Ian Hickson committed
176 177
/// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More
/// elaborate menus with icons can use a [ListTile]. By default, a
178 179
/// [PopupMenuItem] is kMinInteractiveDimension pixels high. If you use a widget
/// with a different height, it must be specified in the [height] property.
Ian Hickson's avatar
Ian Hickson committed
180
///
181
/// {@tool snippet}
Ian Hickson's avatar
Ian Hickson committed
182 183 184 185 186 187 188
///
/// Here, a [Text] widget is used with a popup menu item. The `WhyFarther` type
/// is an enum, not shown here.
///
/// ```dart
/// const PopupMenuItem<WhyFarther>(
///   value: WhyFarther.harder,
189
///   child: Text('Working a lot harder'),
190
/// )
Ian Hickson's avatar
Ian Hickson committed
191
/// ```
192
/// {@end-tool}
Ian Hickson's avatar
Ian Hickson committed
193 194 195 196 197 198 199
///
/// See the example at [PopupMenuButton] for how this example could be used in a
/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to
/// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child]
/// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem]
/// that use a [ListTile] in their [child] slot.
///
200 201
/// See also:
///
Ian Hickson's avatar
Ian Hickson committed
202 203 204 205 206
///  * [PopupMenuDivider], which can be used to divide items from each other.
///  * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark.
///  * [showMenu], a method to dynamically show a popup menu at a given location.
///  * [PopupMenuButton], an [IconButton] that automatically shows a menu when
///    it is tapped.
Hans Muller's avatar
Hans Muller committed
207
class PopupMenuItem<T> extends PopupMenuEntry<T> {
208 209
  /// Creates an item for a popup menu.
  ///
Ian Hickson's avatar
Ian Hickson committed
210 211
  /// By default, the item is [enabled].
  ///
212
  /// The `enabled` and `height` arguments must not be null.
213
  const PopupMenuItem({
214 215
    Key key,
    this.value,
216
    this.enabled = true,
217
    this.height = kMinInteractiveDimension,
218
    this.textStyle,
219
    @required this.child,
Ian Hickson's avatar
Ian Hickson committed
220 221 222
  }) : assert(enabled != null),
       assert(height != null),
       super(key: key);
223

Ian Hickson's avatar
Ian Hickson committed
224
  /// The value that will be returned by [showMenu] if this entry is selected.
225
  final T value;
226

227
  /// Whether the user is permitted to select this item.
Ian Hickson's avatar
Ian Hickson committed
228 229 230
  ///
  /// Defaults to true. If this is false, then the item will not react to
  /// touches.
231
  final bool enabled;
232

233
  /// The minimum height height of the menu item.
Ian Hickson's avatar
Ian Hickson committed
234
  ///
235
  /// Defaults to [kMinInteractiveDimension] pixels.
Ian Hickson's avatar
Ian Hickson committed
236 237 238
  @override
  final double height;

239
  /// The text style of the popup menu item.
240 241
  ///
  /// If this property is null, then [PopupMenuThemeData.textStyle] is used.
242
  /// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subtitle1] is used.
243 244
  final TextStyle textStyle;

245
  /// The widget below this widget in the tree.
Ian Hickson's avatar
Ian Hickson committed
246 247 248 249
  ///
  /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
  /// appropriate [DefaultTextStyle] is put in scope for the child. In either
  /// case, the text should be short enough that it won't wrap.
Hans Muller's avatar
Hans Muller committed
250
  final Widget child;
251

252
  @override
Ian Hickson's avatar
Ian Hickson committed
253
  bool represents(T value) => value == this.value;
Hans Muller's avatar
Hans Muller committed
254

255
  @override
256
  PopupMenuItemState<T, PopupMenuItem<T>> createState() => PopupMenuItemState<T, PopupMenuItem<T>>();
257 258
}

259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
/// The [State] for [PopupMenuItem] subclasses.
///
/// By default this implements the basic styling and layout of Material Design
/// popup menu items.
///
/// The [buildChild] method can be overridden to adjust exactly what gets placed
/// in the menu. By default it returns [PopupMenuItem.child].
///
/// The [handleTap] method can be overridden to adjust exactly what happens when
/// the item is tapped. By default, it uses [Navigator.pop] to return the
/// [PopupMenuItem.value] from the menu route.
///
/// This class takes two type arguments. The second, `W`, is the exact type of
/// the [Widget] that is using this [State]. It must be a subclass of
/// [PopupMenuItem]. The first, `T`, must match the type argument of that widget
/// class, and is the type of values returned from this menu.
class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
  /// The menu item contents.
  ///
  /// Used by the [build] method.
  ///
  /// By default, this returns [PopupMenuItem.child]. Override this to put
  /// something else in the menu entry.
  @protected
283
  Widget buildChild() => widget.child;
284

285 286 287 288 289 290 291
  /// The handler for when the user selects the menu item.
  ///
  /// Used by the [InkWell] inserted by the [build] method.
  ///
  /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from
  /// the menu route.
  @protected
Ian Hickson's avatar
Ian Hickson committed
292
  void handleTap() {
293
    Navigator.pop<T>(context, widget.value);
294 295
  }

296
  @override
297
  Widget build(BuildContext context) {
Hans Muller's avatar
Hans Muller committed
298
    final ThemeData theme = Theme.of(context);
299
    final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
300
    TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subtitle1;
301

302
    if (!widget.enabled)
Hans Muller's avatar
Hans Muller committed
303 304
      style = style.copyWith(color: theme.disabledColor);

305
    Widget item = AnimatedDefaultTextStyle(
306
      style: style,
307
      duration: kThemeChangeDuration,
308 309 310 311
      child: Container(
        alignment: AlignmentDirectional.centerStart,
        constraints: BoxConstraints(minHeight: widget.height),
        padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding),
Ian Hickson's avatar
Ian Hickson committed
312
        child: buildChild(),
313
      ),
314
    );
315

316
    if (!widget.enabled) {
317
      final bool isDark = theme.brightness == Brightness.dark;
318
      item = IconTheme.merge(
319
        data: IconThemeData(opacity: isDark ? 0.5 : 0.38),
Ian Hickson's avatar
Ian Hickson committed
320
        child: item,
321 322 323
      );
    }

324
    return InkWell(
Ian Hickson's avatar
Ian Hickson committed
325
      onTap: widget.enabled ? handleTap : null,
326
      canRequestFocus: widget.enabled,
327
      child: item,
328 329 330
    );
  }
}
331

332 333 334 335 336
/// An item with a checkmark in a material design popup menu.
///
/// To show a popup menu, use the [showMenu] function. To create a button that
/// shows a popup menu, consider using [PopupMenuButton].
///
337
/// A [CheckedPopupMenuItem] is kMinInteractiveDimension pixels high, which
338 339
/// matches the default minimum height of a [PopupMenuItem]. The horizontal
/// layout uses [ListTile]; the checkmark is an [Icons.done] icon, shown in the
340
/// [ListTile.leading] position.
Ian Hickson's avatar
Ian Hickson committed
341
///
342
/// {@tool snippet}
Ian Hickson's avatar
Ian Hickson committed
343 344 345 346 347 348 349 350 351 352
///
/// Suppose a `Commands` enum exists that lists the possible commands from a
/// particular popup menu, including `Commands.heroAndScholar` and
/// `Commands.hurricaneCame`, and further suppose that there is a
/// `_heroAndScholar` member field which is a boolean. The example below shows a
/// menu with one menu item with a checkmark that can toggle the boolean, and
/// one menu item without a checkmark for selecting the second option. (It also
/// shows a divider placed between the two menu items.)
///
/// ```dart
353
/// PopupMenuButton<Commands>(
Ian Hickson's avatar
Ian Hickson committed
354 355 356 357 358 359 360 361 362 363 364 365
///   onSelected: (Commands result) {
///     switch (result) {
///       case Commands.heroAndScholar:
///         setState(() { _heroAndScholar = !_heroAndScholar; });
///         break;
///       case Commands.hurricaneCame:
///         // ...handle hurricane option
///         break;
///       // ...other items handled here
///     }
///   },
///   itemBuilder: (BuildContext context) => <PopupMenuEntry<Commands>>[
366
///     CheckedPopupMenuItem<Commands>(
Ian Hickson's avatar
Ian Hickson committed
367 368 369 370 371 372 373
///       checked: _heroAndScholar,
///       value: Commands.heroAndScholar,
///       child: const Text('Hero and scholar'),
///     ),
///     const PopupMenuDivider(),
///     const PopupMenuItem<Commands>(
///       value: Commands.hurricaneCame,
374
///       child: ListTile(leading: Icon(null), title: Text('Bring hurricane')),
Ian Hickson's avatar
Ian Hickson committed
375 376 377 378 379
///     ),
///     // ...other items listed here
///   ],
/// )
/// ```
380
/// {@end-tool}
Ian Hickson's avatar
Ian Hickson committed
381 382 383 384 385
///
/// In particular, observe how the second menu item uses a [ListTile] with a
/// blank [Icon] in the [ListTile.leading] position to get the same alignment as
/// the item with the checkmark.
///
386 387
/// See also:
///
Ian Hickson's avatar
Ian Hickson committed
388 389 390 391 392 393
///  * [PopupMenuItem], a popup menu entry for picking a command (as opposed to
///    toggling a value).
///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
///  * [showMenu], a method to dynamically show a popup menu at a given location.
///  * [PopupMenuButton], an [IconButton] that automatically shows a menu when
///    it is tapped.
394
class CheckedPopupMenuItem<T> extends PopupMenuItem<T> {
395 396
  /// Creates a popup menu item with a checkmark.
  ///
Ian Hickson's avatar
Ian Hickson committed
397 398 399 400
  /// By default, the menu item is [enabled] but unchecked. To mark the item as
  /// checked, set [checked] to true.
  ///
  /// The `checked` and `enabled` arguments must not be null.
401
  const CheckedPopupMenuItem({
402 403
    Key key,
    T value,
404 405
    this.checked = false,
    bool enabled = true,
Ian Hickson's avatar
Ian Hickson committed
406 407 408
    Widget child,
  }) : assert(checked != null),
       super(
409 410
    key: key,
    value: value,
411
    enabled: enabled,
Ian Hickson's avatar
Ian Hickson committed
412
    child: child,
413
  );
414

415
  /// Whether to display a checkmark next to the menu item.
Ian Hickson's avatar
Ian Hickson committed
416 417 418 419 420 421 422
  ///
  /// Defaults to false.
  ///
  /// When true, an [Icons.done] checkmark is displayed.
  ///
  /// When this popup menu item is selected, the checkmark will fade in or out
  /// as appropriate to represent the implied new state.
423 424
  final bool checked;

Ian Hickson's avatar
Ian Hickson committed
425 426 427 428 429 430 431 432 433 434
  /// The widget below this widget in the tree.
  ///
  /// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for
  /// the child. The text should be short enough that it won't wrap.
  ///
  /// This widget is placed in the [ListTile.title] slot of a [ListTile] whose
  /// [ListTile.leading] slot is an [Icons.done] icon.
  @override
  Widget get child => super.child;

435
  @override
436
  _CheckedPopupMenuItemState<T> createState() => _CheckedPopupMenuItemState<T>();
437 438
}

439
class _CheckedPopupMenuItemState<T> extends PopupMenuItemState<T, CheckedPopupMenuItem<T>> with SingleTickerProviderStateMixin {
440
  static const Duration _fadeDuration = Duration(milliseconds: 150);
441 442 443
  AnimationController _controller;
  Animation<double> get _opacity => _controller.view;

444
  @override
445 446
  void initState() {
    super.initState();
447
    _controller = AnimationController(duration: _fadeDuration, vsync: this)
448
      ..value = widget.checked ? 1.0 : 0.0
449 450 451
      ..addListener(() => setState(() { /* animation changed */ }));
  }

452
  @override
Ian Hickson's avatar
Ian Hickson committed
453
  void handleTap() {
454
    // This fades the checkmark in or out when tapped.
455
    if (widget.checked)
456 457 458
      _controller.reverse();
    else
      _controller.forward();
Ian Hickson's avatar
Ian Hickson committed
459
    super.handleTap();
460 461
  }

462
  @override
463
  Widget buildChild() {
464
    return ListTile(
465
      enabled: widget.enabled,
466
      leading: FadeTransition(
467
        opacity: _opacity,
468
        child: Icon(_controller.isDismissed ? null : Icons.done),
469
      ),
Ian Hickson's avatar
Ian Hickson committed
470
      title: widget.child,
471 472
    );
  }
473 474
}

475
class _PopupMenu<T> extends StatelessWidget {
476
  const _PopupMenu({
477
    Key key,
478 479
    this.route,
    this.semanticLabel,
Adam Barth's avatar
Adam Barth committed
480
  }) : super(key: key);
481

Hixie's avatar
Hixie committed
482
  final _PopupMenuRoute<T> route;
483
  final String semanticLabel;
484

485
  @override
486
  Widget build(BuildContext context) {
487 488
    final double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
    final List<Widget> children = <Widget>[];
489
    final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
490

Ian Hickson's avatar
Ian Hickson committed
491
    for (int i = 0; i < route.items.length; i += 1) {
Hans Muller's avatar
Hans Muller committed
492
      final double start = (i + 1) * unit;
493
      final double end = (start + 1.5 * unit).clamp(0.0, 1.0) as double;
494
      final CurvedAnimation opacity = CurvedAnimation(
495
        parent: route.animation,
496
        curve: Interval(start, end),
497
      );
Hans Muller's avatar
Hans Muller committed
498
      Widget item = route.items[i];
Ian Hickson's avatar
Ian Hickson committed
499
      if (route.initialValue != null && route.items[i].represents(route.initialValue)) {
500
        item = Container(
501
          color: Theme.of(context).highlightColor,
Ian Hickson's avatar
Ian Hickson committed
502
          child: item,
Hans Muller's avatar
Hans Muller committed
503 504
        );
      }
505 506 507 508 509 510 511 512 513 514 515
      children.add(
        _MenuItem(
          onLayout: (Size size) {
            route.itemSizes[i] = size;
          },
          child: FadeTransition(
            opacity: opacity,
            child: item,
          ),
        ),
      );
516
    }
517

518 519 520
    final CurveTween opacity = CurveTween(curve: const Interval(0.0, 1.0 / 3.0));
    final CurveTween width = CurveTween(curve: Interval(0.0, unit));
    final CurveTween height = CurveTween(curve: Interval(0.0, unit * route.items.length));
521

522
    final Widget child = ConstrainedBox(
523
      constraints: const BoxConstraints(
524
        minWidth: _kMenuMinWidth,
525
        maxWidth: _kMenuMaxWidth,
526
      ),
527
      child: IntrinsicWidth(
528
        stepWidth: _kMenuWidthStep,
529
        child: Semantics(
530 531 532 533
          scopesRoute: true,
          namesRoute: true,
          explicitChildNodes: true,
          label: semanticLabel,
534
          child: SingleChildScrollView(
535 536 537
            padding: const EdgeInsets.symmetric(
              vertical: _kMenuVerticalPadding
            ),
538
            child: ListBody(children: children),
539
          ),
540 541
        ),
      ),
542
    );
Adam Barth's avatar
Adam Barth committed
543

544
    return AnimatedBuilder(
545
      animation: route.animation,
546
      builder: (BuildContext context, Widget child) {
547
        return Opacity(
548
          opacity: opacity.evaluate(route.animation),
549
          child: Material(
550 551
            shape: route.shape ?? popupMenuTheme.shape,
            color: route.color ?? popupMenuTheme.color,
552
            type: MaterialType.card,
553
            elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0,
554
            child: Align(
Ian Hickson's avatar
Ian Hickson committed
555
              alignment: AlignmentDirectional.topEnd,
556 557
              widthFactor: width.evaluate(route.animation),
              heightFactor: height.evaluate(route.animation),
558
              child: child,
559 560
            ),
          ),
561
        );
562
      },
Ian Hickson's avatar
Ian Hickson committed
563
      child: child,
564 565 566
    );
  }
}
567

Ian Hickson's avatar
Ian Hickson committed
568
// Positioning of the menu on the screen.
569
class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
570
  _PopupMenuRouteLayout(this.position, this.itemSizes, this.selectedItemIndex, this.textDirection);
Hans Muller's avatar
Hans Muller committed
571

Ian Hickson's avatar
Ian Hickson committed
572
  // Rectangle of underlying button, relative to the overlay's dimensions.
573
  final RelativeRect position;
Ian Hickson's avatar
Ian Hickson committed
574

575 576 577 578 579 580 581
  // The sizes of each item are computed when the menu is laid out, and before
  // the route is laid out.
  List<Size> itemSizes;

  // The index of the selected item, or null if PopupMenuButton.initialValue
  // was not specified.
  final int selectedItemIndex;
Hans Muller's avatar
Hans Muller committed
582

Ian Hickson's avatar
Ian Hickson committed
583 584 585 586 587 588 589
  // Whether to prefer going to the left or to the right.
  final TextDirection textDirection;

  // We put the child wherever position specifies, so long as it will fit within
  // the specified parent size padded (inset) by 8. If necessary, we adjust the
  // child's position so that it fits.

590
  @override
Hans Muller's avatar
Hans Muller committed
591
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
Ian Hickson's avatar
Ian Hickson committed
592 593
    // The menu can be at most the size of the overlay minus 8.0 pixels in each
    // direction.
594 595 596
    return BoxConstraints.loose(
      constraints.biggest - const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0) as Size,
    );
Hans Muller's avatar
Hans Muller committed
597 598
  }

599
  @override
Hans Muller's avatar
Hans Muller committed
600
  Offset getPositionForChild(Size size, Size childSize) {
Ian Hickson's avatar
Ian Hickson committed
601 602 603 604 605
    // size: The size of the overlay.
    // childSize: The size of the menu, when fully open, as determined by
    // getConstraintsForChild.

    // Find the ideal vertical position.
606 607 608 609 610 611
    double y = position.top;
    if (selectedItemIndex != null && itemSizes != null) {
      double selectedItemOffset = _kMenuVerticalPadding;
      for (int index = 0; index < selectedItemIndex; index += 1)
        selectedItemOffset += itemSizes[index].height;
      selectedItemOffset += itemSizes[selectedItemIndex].height / 2;
Ian Hickson's avatar
Ian Hickson committed
612 613
      y = position.top + (size.height - position.top - position.bottom) / 2.0 - selectedItemOffset;
    }
Hans Muller's avatar
Hans Muller committed
614

Ian Hickson's avatar
Ian Hickson committed
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634
    // Find the ideal horizontal position.
    double x;
    if (position.left > position.right) {
      // Menu button is closer to the right edge, so grow to the left, aligned to the right edge.
      x = size.width - position.right - childSize.width;
    } else if (position.left < position.right) {
      // Menu button is closer to the left edge, so grow to the right, aligned to the left edge.
      x = position.left;
    } else {
      // Menu button is equidistant from both edges, so grow in reading direction.
      assert(textDirection != null);
      switch (textDirection) {
        case TextDirection.rtl:
          x = size.width - position.right - childSize.width;
          break;
        case TextDirection.ltr:
          x = position.left;
          break;
      }
    }
Hans Muller's avatar
Hans Muller committed
635

Ian Hickson's avatar
Ian Hickson committed
636 637
    // Avoid going outside an area defined as the rectangle 8.0 pixels from the
    // edge of the screen in every direction.
Hans Muller's avatar
Hans Muller committed
638 639
    if (x < _kMenuScreenPadding)
      x = _kMenuScreenPadding;
Ian Hickson's avatar
Ian Hickson committed
640
    else if (x + childSize.width > size.width - _kMenuScreenPadding)
Hans Muller's avatar
Hans Muller committed
641 642 643
      x = size.width - childSize.width - _kMenuScreenPadding;
    if (y < _kMenuScreenPadding)
      y = _kMenuScreenPadding;
Ian Hickson's avatar
Ian Hickson committed
644
    else if (y + childSize.height > size.height - _kMenuScreenPadding)
Hans Muller's avatar
Hans Muller committed
645
      y = size.height - childSize.height - _kMenuScreenPadding;
646
    return Offset(x, y);
Hans Muller's avatar
Hans Muller committed
647 648
  }

649
  @override
Hans Muller's avatar
Hans Muller committed
650
  bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) {
651 652 653 654 655 656 657 658 659
    // If called when the old and new itemSizes have been initialized then
    // we expect them to have the same length because there's no practical
    // way to change length of the items list once the menu has been shown.
    assert(itemSizes.length == oldDelegate.itemSizes.length);

    return position != oldDelegate.position
        || selectedItemIndex != oldDelegate.selectedItemIndex
        || textDirection != oldDelegate.textDirection
        || !listEquals(itemSizes, oldDelegate.itemSizes);
Hans Muller's avatar
Hans Muller committed
660 661 662
  }
}

Hixie's avatar
Hixie committed
663 664 665 666
class _PopupMenuRoute<T> extends PopupRoute<T> {
  _PopupMenuRoute({
    this.position,
    this.items,
Hans Muller's avatar
Hans Muller committed
667
    this.initialValue,
668
    this.elevation,
669
    this.theme,
670
    this.popupMenuTheme,
671
    this.barrierLabel,
672
    this.semanticLabel,
673 674
    this.shape,
    this.color,
675 676
    this.showMenuContext,
    this.captureInheritedThemes,
677
  }) : itemSizes = List<Size>(items.length);
678

679
  final RelativeRect position;
Hans Muller's avatar
Hans Muller committed
680
  final List<PopupMenuEntry<T>> items;
681
  final List<Size> itemSizes;
682
  final T initialValue;
683
  final double elevation;
684
  final ThemeData theme;
685
  final String semanticLabel;
686 687 688
  final ShapeBorder shape;
  final Color color;
  final PopupMenuThemeData popupMenuTheme;
689 690
  final BuildContext showMenuContext;
  final bool captureInheritedThemes;
691

692
  @override
693
  Animation<double> createAnimation() {
694
    return CurvedAnimation(
695
      parent: super.createAnimation(),
696
      curve: Curves.linear,
697
      reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd),
698
    );
699 700
  }

701
  @override
702
  Duration get transitionDuration => _kMenuDuration;
703 704

  @override
705
  bool get barrierDismissible => true;
706 707

  @override
Hixie's avatar
Hixie committed
708
  Color get barrierColor => null;
709

710 711 712
  @override
  final String barrierLabel;

713
  @override
714
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
715 716

    int selectedItemIndex;
Hans Muller's avatar
Hans Muller committed
717
    if (initialValue != null) {
718 719 720
      for (int index = 0; selectedItemIndex == null && index < items.length; index += 1) {
        if (items[index].represents(initialValue))
          selectedItemIndex = index;
Hans Muller's avatar
Hans Muller committed
721
      }
Hans Muller's avatar
Hans Muller committed
722
    }
723

724
    Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
725 726 727 728 729
    if (captureInheritedThemes) {
      menu = InheritedTheme.captureAll(showMenuContext, menu);
    } else {
      // For the sake of backwards compatibility. An (unlikely) app that relied
      // on having menus only inherit from the material Theme could set
730
      // captureInheritedThemes to false and get the original behavior.
731 732 733
      if (theme != null)
        menu = Theme(data: theme, child: menu);
    }
734

735
    return MediaQuery.removePadding(
736 737 738 739 740
      context: context,
      removeTop: true,
      removeBottom: true,
      removeLeft: true,
      removeRight: true,
741
      child: Builder(
742
        builder: (BuildContext context) {
743 744
          return CustomSingleChildLayout(
            delegate: _PopupMenuRouteLayout(
745
              position,
746 747
              itemSizes,
              selectedItemIndex,
748 749 750 751 752 753
              Directionality.of(context),
            ),
            child: menu,
          );
        },
      ),
Hans Muller's avatar
Hans Muller committed
754
    );
755
  }
756 757
}

Ian Hickson's avatar
Ian Hickson committed
758
/// Show a popup menu that contains the `items` at `position`.
759
///
760 761
/// `items` should be non-null and not empty.
///
Ian Hickson's avatar
Ian Hickson committed
762 763 764 765
/// If `initialValue` is specified then the first item with a matching value
/// will be highlighted and the value of `position` gives the rectangle whose
/// vertical center will be aligned with the vertical center of the highlighted
/// item (when possible).
766
///
Ian Hickson's avatar
Ian Hickson committed
767 768 769 770 771 772 773 774 775 776 777 778 779
/// If `initialValue` is not specified then the top of the menu will be aligned
/// with the top of the `position` rectangle.
///
/// In both cases, the menu position will be adjusted if necessary to fit on the
/// screen.
///
/// Horizontally, the menu is positioned so that it grows in the direction that
/// has the most room. For example, if the `position` describes a rectangle on
/// the left edge of the screen, then the left edge of the menu is aligned with
/// the left edge of the `position`, and the menu grows to the right. If both
/// edges of the `position` are equidistant from the opposite edge of the
/// screen, then the ambient [Directionality] is used as a tie-breaker,
/// preferring to grow in the reading direction.
Ian Hickson's avatar
Ian Hickson committed
780 781 782 783 784 785 786
///
/// The positioning of the `initialValue` at the `position` is implemented by
/// iterating over the `items` to find the first whose
/// [PopupMenuEntry.represents] method returns true for `initialValue`, and then
/// summing the values of [PopupMenuEntry.height] for all the preceding widgets
/// in the list.
///
Ian Hickson's avatar
Ian Hickson committed
787 788 789 790 791 792 793
/// The `elevation` argument specifies the z-coordinate at which to place the
/// menu. The elevation defaults to 8, the appropriate elevation for popup
/// menus.
///
/// The `context` argument is used to look up the [Navigator] and [Theme] for
/// the menu. It is only used when the method is called. Its corresponding
/// widget can be safely removed from the tree before the popup menu is closed.
794
///
795 796 797 798
/// The `useRootNavigator` argument is used to determine whether to push the
/// menu to the [Navigator] furthest from or nearest to the given `context`. It
/// is `false` by default.
///
799 800 801
/// The `semanticLabel` argument is used by accessibility frameworks to
/// announce screen transitions when the menu is opened and closed. If this
/// label is not provided, it will default to
802
/// [MaterialLocalizations.popupMenuLabel].
803
///
Ian Hickson's avatar
Ian Hickson committed
804 805 806 807 808 809 810
/// See also:
///
///  * [PopupMenuItem], a popup menu entry for a single value.
///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
///  * [CheckedPopupMenuItem], a popup menu item with a checkmark.
///  * [PopupMenuButton], which provides an [IconButton] that shows a menu by
///    calling this method automatically.
811 812
///  * [SemanticsConfiguration.namesRoute], for a description of edge triggered
///    semantics.
813
Future<T> showMenu<T>({
814
  @required BuildContext context,
815
  @required RelativeRect position,
816
  @required List<PopupMenuEntry<T>> items,
817
  T initialValue,
818
  double elevation,
819
  String semanticLabel,
820 821
  ShapeBorder shape,
  Color color,
822
  bool captureInheritedThemes = true,
823
  bool useRootNavigator = false,
Hans Muller's avatar
Hans Muller committed
824 825
}) {
  assert(context != null);
826
  assert(position != null);
827
  assert(useRootNavigator != null);
828
  assert(items != null && items.isNotEmpty);
829
  assert(captureInheritedThemes != null);
830
  assert(debugCheckHasMaterialLocalizations(context));
831

832
  String label = semanticLabel;
833
  switch (Theme.of(context).platform) {
834
    case TargetPlatform.iOS:
835
    case TargetPlatform.macOS:
836 837 838 839 840 841 842
      label = semanticLabel;
      break;
    case TargetPlatform.android:
    case TargetPlatform.fuchsia:
      label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel;
  }

843
  return Navigator.of(context, rootNavigator: useRootNavigator).push(_PopupMenuRoute<T>(
844
    position: position,
Adam Barth's avatar
Adam Barth committed
845
    items: items,
Hans Muller's avatar
Hans Muller committed
846
    initialValue: initialValue,
847
    elevation: elevation,
848
    semanticLabel: label,
849
    theme: Theme.of(context, shadowThemeOnly: true),
850
    popupMenuTheme: PopupMenuTheme.of(context),
851
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
852 853
    shape: shape,
    color: color,
854 855
    showMenuContext: context,
    captureInheritedThemes: captureInheritedThemes,
856 857
  ));
}
Hans Muller's avatar
Hans Muller committed
858

859 860 861 862 863
/// Signature for the callback invoked when a menu item is selected. The
/// argument is the value of the [PopupMenuItem] that caused its menu to be
/// dismissed.
///
/// Used by [PopupMenuButton.onSelected].
864
typedef PopupMenuItemSelected<T> = void Function(T value);
Hans Muller's avatar
Hans Muller committed
865

866 867 868 869
/// Signature for the callback invoked when a [PopupMenuButton] is dismissed
/// without selecting an item.
///
/// Used by [PopupMenuButton.onCanceled].
870
typedef PopupMenuCanceled = void Function();
871

872 873 874 875
/// Signature used by [PopupMenuButton] to lazily construct the items shown when
/// the button is pressed.
///
/// Used by [PopupMenuButton.itemBuilder].
876
typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(BuildContext context);
877

Hans Muller's avatar
Hans Muller committed
878 879
/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed
/// because an item was selected. The value passed to [onSelected] is the value of
880 881 882 883 884 885 886
/// the selected menu item.
///
/// One of [child] or [icon] may be provided, but not both. If [icon] is provided,
/// then [PopupMenuButton] behaves like an [IconButton].
///
/// If both are null, then a standard overflow icon is created (depending on the
/// platform).
Ian Hickson's avatar
Ian Hickson committed
887
///
888
/// {@tool snippet}
889
///
Ian Hickson's avatar
Ian Hickson committed
890 891 892 893 894 895 896 897 898
/// This example shows a menu with four items, selecting between an enum's
/// values and setting a `_selection` field based on the selection.
///
/// ```dart
/// // This is the type used by the popup menu below.
/// enum WhyFarther { harder, smarter, selfStarter, tradingCharter }
///
/// // This menu button widget updates a _selection field (of type WhyFarther,
/// // not shown here).
899
/// PopupMenuButton<WhyFarther>(
Ian Hickson's avatar
Ian Hickson committed
900 901 902 903
///   onSelected: (WhyFarther result) { setState(() { _selection = result; }); },
///   itemBuilder: (BuildContext context) => <PopupMenuEntry<WhyFarther>>[
///     const PopupMenuItem<WhyFarther>(
///       value: WhyFarther.harder,
904
///       child: Text('Working a lot harder'),
Ian Hickson's avatar
Ian Hickson committed
905 906 907
///     ),
///     const PopupMenuItem<WhyFarther>(
///       value: WhyFarther.smarter,
908
///       child: Text('Being a lot smarter'),
Ian Hickson's avatar
Ian Hickson committed
909 910 911
///     ),
///     const PopupMenuItem<WhyFarther>(
///       value: WhyFarther.selfStarter,
912
///       child: Text('Being a self-starter'),
Ian Hickson's avatar
Ian Hickson committed
913 914 915
///     ),
///     const PopupMenuItem<WhyFarther>(
///       value: WhyFarther.tradingCharter,
916
///       child: Text('Placed in charge of trading charter'),
Ian Hickson's avatar
Ian Hickson committed
917 918 919 920
///     ),
///   ],
/// )
/// ```
921
/// {@end-tool}
Ian Hickson's avatar
Ian Hickson committed
922 923 924 925 926 927 928
///
/// See also:
///
///  * [PopupMenuItem], a popup menu entry for a single value.
///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
///  * [CheckedPopupMenuItem], a popup menu item with a checkmark.
///  * [showMenu], a method to dynamically show a popup menu at a given location.
929
class PopupMenuButton<T> extends StatefulWidget {
930 931 932
  /// Creates a button that shows a popup menu.
  ///
  /// The [itemBuilder] argument must not be null.
933
  const PopupMenuButton({
Hans Muller's avatar
Hans Muller committed
934
    Key key,
935
    @required this.itemBuilder,
Hans Muller's avatar
Hans Muller committed
936 937
    this.initialValue,
    this.onSelected,
938
    this.onCanceled,
939
    this.tooltip,
940
    this.elevation,
941
    this.padding = const EdgeInsets.all(8.0),
942 943
    this.child,
    this.icon,
944
    this.offset = Offset.zero,
945
    this.enabled = true,
946 947
    this.shape,
    this.color,
948
    this.captureInheritedThemes = true,
949
  }) : assert(itemBuilder != null),
950
       assert(offset != null),
951
       assert(enabled != null),
952
       assert(captureInheritedThemes != null),
953 954
       assert(!(child != null && icon != null),
           'You can only pass [child] or [icon], not both.'),
955
       super(key: key);
Hans Muller's avatar
Hans Muller committed
956

957 958
  /// Called when the button is pressed to create the items to show in the menu.
  final PopupMenuItemBuilder<T> itemBuilder;
959

960
  /// The value of the menu item, if any, that should be highlighted when the menu opens.
Hans Muller's avatar
Hans Muller committed
961
  final T initialValue;
962

963
  /// Called when the user selects a value from the popup menu created by this button.
964 965 966
  ///
  /// If the popup menu is dismissed without selecting a value, [onCanceled] is
  /// called instead.
Hans Muller's avatar
Hans Muller committed
967
  final PopupMenuItemSelected<T> onSelected;
968

969 970 971 972 973
  /// Called when the user dismisses the popup menu without selecting an item.
  ///
  /// If the user selects a value, [onSelected] is called instead.
  final PopupMenuCanceled onCanceled;

974 975 976 977
  /// Text that describes the action that will occur when the button is pressed.
  ///
  /// This text is displayed when the user long-presses on the button and is
  /// used for accessibility.
Hans Muller's avatar
Hans Muller committed
978
  final String tooltip;
979

980 981 982 983
  /// The z-coordinate at which to place the menu when open. This controls the
  /// size of the shadow below the menu.
  ///
  /// Defaults to 8, the appropriate elevation for popup menus.
984
  final double elevation;
985

986 987 988
  /// Matches IconButton's 8 dps padding by default. In some cases, notably where
  /// this button appears as the trailing element of a list item, it's useful to be able
  /// to set the padding to zero.
989
  final EdgeInsetsGeometry padding;
990

991 992
  /// If provided, [child] is the widget used for this button
  /// and the button will utilize an [InkWell] for taps.
Hans Muller's avatar
Hans Muller committed
993 994
  final Widget child;

995 996 997
  /// If provided, the [icon] is used for this button
  /// and the button will behave like an [IconButton].
  final Widget icon;
998

999 1000 1001 1002 1003 1004
  /// The offset applied to the Popup Menu Button.
  ///
  /// When not set, the Popup Menu Button will be positioned directly next to
  /// the button that was used to create it.
  final Offset offset;

1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
  /// Whether this popup menu button is interactive.
  ///
  /// Must be non-null, defaults to `true`
  ///
  /// If `true` the button will respond to presses by displaying the menu.
  ///
  /// If `false`, the button is styled with the disabled color from the
  /// current [Theme] and will not respond to presses or show the popup
  /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called.
  ///
  /// This can be useful in situations where the app needs to show the button,
  /// but doesn't currently have anything to show in the menu.
  final bool enabled;

1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
  /// If provided, the shape used for the menu.
  ///
  /// If this property is null, then [PopupMenuThemeData.shape] is used.
  /// If [PopupMenuThemeData.shape] is also null, then the default shape for
  /// [MaterialType.card] is used. This default shape is a rectangle with
  /// rounded edges of BorderRadius.circular(2.0).
  final ShapeBorder shape;

  /// If provided, the background color used for the menu.
  ///
  /// If this property is null, then [PopupMenuThemeData.color] is used.
  /// If [PopupMenuThemeData.color] is also null, then
  /// Theme.of(context).cardColor is used.
  final Color color;

1034 1035 1036 1037 1038
  /// If true (the default) then the menu will be wrapped with copies
  /// of the [InheritedThemes], like [Theme] and [PopupMenuTheme], which
  /// are defined above the [BuildContext] where the menu is shown.
  final bool captureInheritedThemes;

1039
  @override
1040
  PopupMenuButtonState<T> createState() => PopupMenuButtonState<T>();
Hans Muller's avatar
Hans Muller committed
1041 1042
}

1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055
/// The [State] for a [PopupMenuButton].
///
/// See [showButtonMenu] for a way to programmatically open the popup menu
/// of your button state.
class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
  /// A method to show a popup menu with the items supplied to
  /// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton].
  ///
  /// By default, it is called when the user taps the button and [PopupMenuButton.enabled]
  /// is set to `true`. Moreover, you can open the button by calling the method manually.
  ///
  /// You would access your [PopupMenuButtonState] using a [GlobalKey] and
  /// show the menu of the button with `globalKey.currentState.showButtonMenu`.
1056
  void showButtonMenu() {
1057
    final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
1058 1059
    final RenderBox button = context.findRenderObject() as RenderBox;
    final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
1060 1061
    final RelativeRect position = RelativeRect.fromRect(
      Rect.fromPoints(
1062
        button.localToGlobal(widget.offset, ancestor: overlay),
Ian Hickson's avatar
Ian Hickson committed
1063 1064 1065 1066
        button.localToGlobal(button.size.bottomRight(Offset.zero), ancestor: overlay),
      ),
      Offset.zero & overlay.size,
    );
1067 1068 1069 1070 1071
    final List<PopupMenuEntry<T>> items = widget.itemBuilder(context);
    // Only show the menu if there is something to show
    if (items.isNotEmpty) {
      showMenu<T>(
        context: context,
1072
        elevation: widget.elevation ?? popupMenuTheme.elevation,
1073 1074 1075
        items: items,
        initialValue: widget.initialValue,
        position: position,
1076 1077
        shape: widget.shape ?? popupMenuTheme.shape,
        color: widget.color ?? popupMenuTheme.color,
1078
        captureInheritedThemes: widget.captureInheritedThemes,
1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091
      )
      .then<void>((T newValue) {
        if (!mounted)
          return null;
        if (newValue == null) {
          if (widget.onCanceled != null)
            widget.onCanceled();
          return null;
        }
        if (widget.onSelected != null)
          widget.onSelected(newValue);
      });
    }
Hans Muller's avatar
Hans Muller committed
1092 1093
  }

1094 1095 1096 1097 1098 1099 1100
  Icon _getIcon(TargetPlatform platform) {
    assert(platform != null);
    switch (platform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        return const Icon(Icons.more_vert);
      case TargetPlatform.iOS:
1101
      case TargetPlatform.macOS:
1102 1103 1104 1105 1106
        return const Icon(Icons.more_horiz);
    }
    return null;
  }

1107
  @override
Hans Muller's avatar
Hans Muller committed
1108
  Widget build(BuildContext context) {
1109
    assert(debugCheckHasMaterialLocalizations(context));
1110 1111 1112 1113 1114

    if (widget.child != null)
      return Tooltip(
        message: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
        child: InkWell(
1115
          onTap: widget.enabled ? showButtonMenu : null,
1116
          canRequestFocus: widget.enabled,
1117
          child: widget.child,
1118 1119 1120 1121 1122 1123 1124 1125 1126
        ),
      );

    return IconButton(
      icon: widget.icon ?? _getIcon(Theme.of(context).platform),
      padding: widget.padding,
      tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
      onPressed: widget.enabled ? showButtonMenu : null,
    );
Hans Muller's avatar
Hans Muller committed
1127 1128
  }
}