platform_menu_bar.dart 38.5 KB
Newer Older
1 2 3 4 5 6 7 8 9
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

10 11
import 'actions.dart';
import 'basic.dart';
12
import 'binding.dart';
13
import 'focus_manager.dart';
14 15 16 17
import 'framework.dart';
import 'shortcuts.dart';

// "flutter/menu" Method channel methods.
18
const String _kMenuSetMethod = 'Menu.setMenus';
19 20 21 22 23 24 25 26 27 28 29
const String _kMenuSelectedCallbackMethod = 'Menu.selectedCallback';
const String _kMenuItemOpenedMethod = 'Menu.opened';
const String _kMenuItemClosedMethod = 'Menu.closed';

// Keys for channel communication map.
const String _kIdKey = 'id';
const String _kLabelKey = 'label';
const String _kEnabledKey = 'enabled';
const String _kChildrenKey = 'children';
const String _kIsDividerKey = 'isDivider';
const String _kPlatformDefaultMenuKey = 'platformProvidedMenu';
30 31 32
const String _kShortcutCharacter = 'shortcutCharacter';
const String _kShortcutTrigger = 'shortcutTrigger';
const String _kShortcutModifiers = 'shortcutModifiers';
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

/// A class used by [MenuSerializableShortcut] to describe the shortcut for
/// serialization to send to the platform for rendering a [PlatformMenuBar].
///
/// See also:
///
///  * [PlatformMenuBar], a widget that defines a menu bar for the platform to
///    render natively.
///  * [MenuSerializableShortcut], a mixin allowing a [ShortcutActivator] to
///    provide data for serialization of the shortcut for sending to the
///    platform.
class ShortcutSerialization {
  /// Creates a [ShortcutSerialization] representing a single character.
  ///
  /// This is used by a [CharacterActivator] to serialize itself.
  ShortcutSerialization.character(String character)
49
      : _internal = <String, Object?>{_kShortcutCharacter: character},
50
        _character = character,
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
        assert(character.length == 1);

  /// Creates a [ShortcutSerialization] representing a specific
  /// [LogicalKeyboardKey] and modifiers.
  ///
  /// This is used by a [SingleActivator] to serialize itself.
  ShortcutSerialization.modifier(
    LogicalKeyboardKey trigger, {
    bool control = false,
    bool shift = false,
    bool alt = false,
    bool meta = false,
  })  : assert(trigger != LogicalKeyboardKey.shift &&
               trigger != LogicalKeyboardKey.shiftLeft &&
               trigger != LogicalKeyboardKey.shiftRight &&
               trigger != LogicalKeyboardKey.alt &&
               trigger != LogicalKeyboardKey.altLeft &&
               trigger != LogicalKeyboardKey.altRight &&
               trigger != LogicalKeyboardKey.control &&
               trigger != LogicalKeyboardKey.controlLeft &&
               trigger != LogicalKeyboardKey.controlRight &&
               trigger != LogicalKeyboardKey.meta &&
               trigger != LogicalKeyboardKey.metaLeft &&
               trigger != LogicalKeyboardKey.metaRight,
               'Specifying a modifier key as a trigger is not allowed. '
               'Use provided boolean parameters instead.'),
77 78 79 80 81
        _trigger = trigger,
        _control = control,
        _shift = shift,
        _alt = alt,
        _meta = meta,
82
        _internal = <String, Object?>{
83 84
          _kShortcutTrigger: trigger.keyId,
          _kShortcutModifiers: (control ? _shortcutModifierControl : 0) |
85 86 87 88 89 90 91
              (alt ? _shortcutModifierAlt : 0) |
              (shift ? _shortcutModifierShift : 0) |
              (meta ? _shortcutModifierMeta : 0),
        };

  final Map<String, Object?> _internal;

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
  /// The keyboard key that triggers this shortcut, if any.
  LogicalKeyboardKey? get trigger => _trigger;
  LogicalKeyboardKey? _trigger;

  /// The character that triggers this shortcut, if any.
  String? get character => _character;
  String? _character;

  /// If this shortcut has a [trigger], this indicates whether or not the
  /// control modifier needs to be down or not.
  bool? get control => _control;
  bool? _control;

  /// If this shortcut has a [trigger], this indicates whether or not the
  /// shift modifier needs to be down or not.
  bool? get shift => _shift;
  bool? _shift;

  /// If this shortcut has a [trigger], this indicates whether or not the
  /// alt modifier needs to be down or not.
  bool? get alt => _alt;
  bool? _alt;

  /// If this shortcut has a [trigger], this indicates whether or not the meta
  /// (also known as the Windows or Command key) modifier needs to be down or
  /// not.
  bool? get meta => _meta;
  bool? _meta;

121 122 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 162 163 164
  /// The bit mask for the [LogicalKeyboardKey.meta] key (or it's left/right
  /// equivalents) being down.
  static const int _shortcutModifierMeta = 1 << 0;

  /// The bit mask for the [LogicalKeyboardKey.shift] key (or it's left/right
  /// equivalents) being down.
  static const int _shortcutModifierShift = 1 << 1;

  /// The bit mask for the [LogicalKeyboardKey.alt] key (or it's left/right
  /// equivalents) being down.
  static const int _shortcutModifierAlt = 1 << 2;

  /// The bit mask for the [LogicalKeyboardKey.alt] key (or it's left/right
  /// equivalents) being down.
  static const int _shortcutModifierControl = 1 << 3;

  /// Converts the internal representation to the format needed for a [MenuItem]
  /// to include it in its serialized form for sending to the platform.
  Map<String, Object?> toChannelRepresentation() => _internal;
}

/// A mixin allowing a [ShortcutActivator] to provide data for serialization of
/// the shortcut when sending to the platform.
///
/// This is meant for those who have written their own [ShortcutActivator]
/// subclass, and would like to have it work for menus in a [PlatformMenuBar] as
/// well.
///
/// Keep in mind that there are limits to the capabilities of the platform APIs,
/// and not all kinds of [ShortcutActivator]s will work with them.
///
/// See also:
///
///  * [SingleActivator], a [ShortcutActivator] which implements this mixin.
///  * [CharacterActivator], another [ShortcutActivator] which implements this mixin.
mixin MenuSerializableShortcut {
  /// Implement this in a [ShortcutActivator] subclass to allow it to be
  /// serialized for use in a [PlatformMenuBar].
  ShortcutSerialization serializeForMenu();
}

/// An abstract class for describing cascading menu hierarchies that are part of
/// a [PlatformMenuBar].
///
165
/// This type is also used by [PlatformMenuDelegate.setMenus] to accept the menu
166 167 168
/// hierarchy to be sent to the platform, and by [PlatformMenuBar] to define the
/// menu hierarchy.
///
169 170 171
/// This class is abstract, and so can't be used directly. Typically subclasses
/// like [PlatformMenuItem] are used.
///
172 173 174 175 176 177 178 179 180 181 182 183 184
/// See also:
///
///  * [PlatformMenuBar], a widget that renders menu items using platform APIs
///    instead of Flutter.
abstract class MenuItem with Diagnosticable {
  /// Allows subclasses to have const constructors.
  const MenuItem();

  /// Converts the representation of this item into a map suitable for sending
  /// over the default "flutter/menu" channel used by [DefaultPlatformMenuDelegate].
  ///
  /// The `delegate` is the [PlatformMenuDelegate] that is requesting the
  /// serialization. The `index` is the position of this menu item in the list
185 186
  /// of [menus] of the [PlatformMenu] it belongs to, and `count` is the number
  /// of [menus] in the [PlatformMenu] it belongs to.
187 188 189 190 191 192 193 194 195
  ///
  /// The `getId` parameter is a [MenuItemSerializableIdGenerator] function that
  /// generates a unique ID for each menu item, which is to be returned in the
  /// "id" field of the menu item data.
  Iterable<Map<String, Object?>> toChannelRepresentation(
    PlatformMenuDelegate delegate, {
    required MenuItemSerializableIdGenerator getId,
  });

196 197 198 199 200 201 202 203 204 205 206
  /// The optional shortcut that selects this [MenuItem].
  ///
  /// This shortcut is only enabled when [onSelected] is set.
  MenuSerializableShortcut? get shortcut => null;

  /// Returns any child [MenuItem]s of this item.
  ///
  /// Returns an empty list if this type of menu item doesn't have
  /// children.
  List<MenuItem> get menus => const <MenuItem>[];

207 208 209 210 211 212 213 214 215 216 217
  /// Returns all descendant [MenuItem]s of this item.
  ///
  /// Returns an empty list if this type of menu item doesn't have
  /// descendants.
  List<MenuItem> get descendants => const <MenuItem>[];

  /// Returns a callback, if any, to be invoked if the platform menu receives a
  /// "Menu.selectedCallback" method call from the platform for this item.
  ///
  /// Only items that do not have submenus will have this callback invoked.
  ///
218 219 220 221 222
  /// Only one of [onSelected] or [onSelectedIntent] may be specified.
  ///
  /// If neither [onSelected] nor [onSelectedIntent] are specified, then this
  /// menu item is considered to be disabled.
  ///
223 224 225
  /// The default implementation returns null.
  VoidCallback? get onSelected => null;

226 227 228 229 230 231 232 233 234 235 236 237 238
  /// Returns an intent, if any, to be invoked if the platform receives a
  /// "Menu.selectedCallback" method call from the platform for this item.
  ///
  /// Only items that do not have submenus will have this intent invoked.
  ///
  /// Only one of [onSelected] or [onSelectedIntent] may be specified.
  ///
  /// If neither [onSelected] nor [onSelectedIntent] are specified, then this
  /// menu item is considered to be disabled.
  ///
  /// The default implementation returns null.
  Intent? get onSelectedIntent => null;

239 240 241
  /// Returns a callback, if any, to be invoked if the platform menu receives a
  /// "Menu.opened" method call from the platform for this item.
  ///
242
  /// Only items that have submenus will have this callback invoked.
243 244 245 246 247
  ///
  /// The default implementation returns null.
  VoidCallback? get onOpen => null;

  /// Returns a callback, if any, to be invoked if the platform menu receives a
248
  /// "Menu.closed" method call from the platform for this item.
249 250 251 252 253
  ///
  /// Only items that have submenus will have this callback invoked.
  ///
  /// The default implementation returns null.
  VoidCallback? get onClose => null;
254 255 256 257 258 259

  /// Returns the list of group members if this menu item is a "grouping" menu
  /// item, such as [PlatformMenuItemGroup].
  ///
  /// Defaults to an empty list.
  List<MenuItem> get members => const <MenuItem>[];
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
}

/// An abstract delegate class that can be used to set
/// [WidgetsBinding.platformMenuDelegate] to provide for managing platform
/// menus.
///
/// This can be subclassed to provide a different menu plugin than the default
/// system-provided plugin for managing [PlatformMenuBar] menus.
///
/// The [setMenus] method allows for setting of the menu hierarchy when the
/// [PlatformMenuBar] menu hierarchy changes.
///
/// This delegate doesn't handle the results of clicking on a menu item, which
/// is left to the implementor of subclasses of `PlatformMenuDelegate` to
/// handle for their implementation.
///
/// This delegate typically knows how to serialize a [PlatformMenu]
/// hierarchy, send it over a channel, and register for calls from the channel
/// when a menu is invoked or a submenu is opened or closed.
///
/// See [DefaultPlatformMenuDelegate] for an example of implementing one of
/// these.
///
/// See also:
///
///  * [PlatformMenuBar], the widget that adds a platform menu bar to an
///    application, and uses [setMenus] to send the menus to the platform.
///  * [PlatformMenu], the class that describes a menu item with children
///    that appear in a cascading menu.
///  * [PlatformMenuItem], the class that describes the leaves of a menu
///    hierarchy.
abstract class PlatformMenuDelegate {
  /// A const constructor so that subclasses can have const constructors.
  const PlatformMenuDelegate();

  /// Sets the entire menu hierarchy for a platform-rendered menu bar.
  ///
  /// The `topLevelMenus` argument is the list of menus that appear in the menu
  /// bar, which themselves can have children.
  ///
  /// To update the menu hierarchy or menu item state, call `setMenus` with the
  /// modified hierarchy, and it will overwrite the previous menu state.
  ///
  /// See also:
  ///
  ///  * [PlatformMenuBar], the widget that adds a platform menu bar to an
  ///    application.
  ///  * [PlatformMenu], the class that describes a menu item with children
  ///    that appear in a cascading menu.
  ///  * [PlatformMenuItem], the class that describes the leaves of a menu
  ///    hierarchy.
  void setMenus(List<MenuItem> topLevelMenus);

  /// Clears any existing platform-rendered menus and leaves the application
  /// with no menus.
  ///
  /// It is not necessary to call this before updating the menu with [setMenus].
  void clearMenus();

  /// This is called by [PlatformMenuBar] when it is initialized, to be sure that
  /// only one is active at a time.
  ///
  /// The `debugLockDelegate` function should be called before the first call to
  /// [setMenus].
  ///
  /// If the lock is successfully acquired, `debugLockDelegate` will return
  /// true.
  ///
  /// If your implementation of a [PlatformMenuDelegate] can have only limited
  /// active instances, enforce it when you override this function.
  ///
  /// See also:
  ///
  ///  * [debugUnlockDelegate], where the delegate is unlocked.
  bool debugLockDelegate(BuildContext context);

  /// This is called by [PlatformMenuBar] when it is disposed, so that another
  /// one can take over.
  ///
  /// If the `debugUnlockDelegate` successfully unlocks the delegate, it will
  /// return true.
  ///
  /// See also:
  ///
  ///  * [debugLockDelegate], where the delegate is locked.
  bool debugUnlockDelegate(BuildContext context);
}

/// The signature for a function that generates unique menu item IDs for
/// serialization of a [MenuItem].
typedef MenuItemSerializableIdGenerator = int Function(MenuItem item);

/// The platform menu delegate that handles the built-in macOS platform menu
/// generation using the 'flutter/menu' channel.
///
/// An instance of this class is set on [WidgetsBinding.platformMenuDelegate] by
/// default when the [WidgetsBinding] is initialized.
///
/// See also:
///
///  * [PlatformMenuBar], the widget that adds a platform menu bar to an
///    application.
///  * [PlatformMenu], the class that describes a menu item with children
///    that appear in a cascading menu.
///  * [PlatformMenuItem], the class that describes the leaves of a menu
///    hierarchy.
class DefaultPlatformMenuDelegate extends PlatformMenuDelegate {
  /// Creates a const [DefaultPlatformMenuDelegate].
  ///
  /// The optional [channel] argument defines the channel used to communicate
  /// with the platform. It defaults to [SystemChannels.menu] if not supplied.
  DefaultPlatformMenuDelegate({MethodChannel? channel})
      : channel = channel ?? SystemChannels.menu,
        _idMap = <int, MenuItem>{} {
    this.channel.setMethodCallHandler(_methodCallHandler);
  }

  // Map of distributed IDs to menu items.
  final Map<int, MenuItem> _idMap;
  // An ever increasing value used to dole out IDs.
  int _serial = 0;
  // The context used to "lock" this delegate to a specific instance of
  // PlatformMenuBar to make sure there is only one.
  BuildContext? _lockedContext;

  @override
  void clearMenus() => setMenus(<MenuItem>[]);

  @override
  void setMenus(List<MenuItem> topLevelMenus) {
    _idMap.clear();
    final List<Map<String, Object?>> representation = <Map<String, Object?>>[];
    if (topLevelMenus.isNotEmpty) {
      for (final MenuItem childItem in topLevelMenus) {
394
        representation.addAll(childItem.toChannelRepresentation(this, getId: _getId));
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
      }
    }
    // Currently there's only ever one window, but the channel's format allows
    // more than one window's menu hierarchy to be defined.
    final Map<String, Object?> windowMenu = <String, Object?>{
      '0': representation,
    };
    channel.invokeMethod<void>(_kMenuSetMethod, windowMenu);
  }

  /// Defines the channel that the [DefaultPlatformMenuDelegate] uses to
  /// communicate with the platform.
  ///
  /// Defaults to [SystemChannels.menu].
  final MethodChannel channel;

  /// Get the next serialization ID.
  ///
  /// This is called by each DefaultPlatformMenuDelegateSerializer when
  /// serializing a new object so that it has a unique ID.
  int _getId(MenuItem item) {
    _serial += 1;
    _idMap[_serial] = item;
    return _serial;
  }

  @override
  bool debugLockDelegate(BuildContext context) {
    assert(() {
      // It's OK to lock if the lock isn't set, but not OK if a different
      // context is locking it.
      if (_lockedContext != null && _lockedContext != context) {
        return false;
      }
      _lockedContext = context;
      return true;
    }());
    return true;
  }

  @override
  bool debugUnlockDelegate(BuildContext context) {
    assert(() {
      // It's OK to unlock if the lock isn't set, but not OK if a different
      // context is unlocking it.
      if (_lockedContext != null && _lockedContext != context) {
        return false;
      }
      _lockedContext = null;
      return true;
    }());
    return true;
  }

  // Handles the method calls from the plugin to forward to selection and
  // open/close callbacks.
  Future<void> _methodCallHandler(MethodCall call) async {
    final int id = call.arguments as int;
    assert(
      _idMap.containsKey(id),
      'Received a menu ${call.method} for a menu item with an ID that was not recognized: $id',
    );
    if (!_idMap.containsKey(id)) {
      return;
    }
    final MenuItem item = _idMap[id]!;
    if (call.method == _kMenuSelectedCallbackMethod) {
462 463
      assert(item.onSelected == null || item.onSelectedIntent == null,
        'Only one of MenuItem.onSelected or MenuItem.onSelectedIntent may be specified');
464
      item.onSelected?.call();
465 466 467
      if (item.onSelectedIntent != null) {
        Actions.maybeInvoke(FocusManager.instance.primaryFocus!.context!, item.onSelectedIntent!);
      }
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
    } else if (call.method == _kMenuItemOpenedMethod) {
      item.onOpen?.call();
    } else if (call.method == _kMenuItemClosedMethod) {
      item.onClose?.call();
    }
  }
}

/// A menu bar that uses the platform's native APIs to construct and render a
/// menu described by a [PlatformMenu]/[PlatformMenuItem] hierarchy.
///
/// This widget is especially useful on macOS, where a system menu is a required
/// part of every application. Flutter only includes support for macOS out of
/// the box, but support for other platforms may be provided via plugins that
/// set [WidgetsBinding.platformMenuDelegate] in their initialization.
///
/// The [menus] member contains [MenuItem]s. They will not be part of the
/// widget tree, since they are not required to be widgets (even if they happen
/// to be widgets that implement [MenuItem], they still won't be part of the
/// widget tree). They are provided to configure the properties of the menus on
/// the platform menu bar.
///
/// As far as Flutter is concerned, this widget has no visual representation,
491
/// and intercepts no events: it just returns the [child] from its build
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
/// function. This is because all of the rendering, shortcuts, and event
/// handling for the menu is handled by the plugin on the host platform.
///
/// There can only be one [PlatformMenuBar] at a time using the same
/// [PlatformMenuDelegate]. It will assert if more than one is detected.
///
/// When calling [toStringDeep] on this widget, it will give a tree of
/// [MenuItem]s, not a tree of widgets.
///
/// {@tool sample}
/// This example shows a [PlatformMenuBar] that contains a single top level
/// menu, containing three items for "About", a toggleable menu item for showing
/// a message, a cascading submenu with message choices, and "Quit".
///
/// **This example will only work on macOS.**
///
/// ** See code in examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart **
/// {@end-tool}
class PlatformMenuBar extends StatefulWidget with DiagnosticableTreeMixin {
  /// Creates a const [PlatformMenuBar].
  ///
513
  /// The [child] and [menus] attributes are required.
514
  const PlatformMenuBar({
515
    super.key,
516
    required this.menus,
517 518 519 520 521 522 523 524 525 526 527 528 529
    this.child,
    @Deprecated(
      'Use the child attribute instead. '
      'This feature was deprecated after v3.1.0-0.0.pre.'
    )
    this.body,
  }) : assert(body == null || child == null,
              'The body argument is deprecated, and only one of body or child may be used.');

  /// The widget below this widget in the tree.
  ///
  /// {@macro flutter.widgets.ProxyWidget.child}
  final Widget? child;
530

531
  /// The widget below this widget in the tree.
532
  ///
533 534 535 536 537 538
  /// This attribute is deprecated, use [child] instead.
  @Deprecated(
    'Use the child attribute instead. '
    'This feature was deprecated after v3.1.0-0.0.pre.'
  )
  final Widget? body;
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609

  /// The list of menu items that are the top level children of the
  /// [PlatformMenuBar].
  ///
  /// The `menus` member contains [MenuItem]s. They will not be part
  /// of the widget tree, since they are not widgets. They are provided to
  /// configure the properties of the menus on the platform menu bar.
  ///
  /// Also, a Widget in Flutter is immutable, so directly modifying the
  /// `menus` with `List` APIs such as
  /// `somePlatformMenuBarWidget.menus.add(...)` will result in incorrect
  /// behaviors. Whenever the menus list is modified, a new list object
  /// should be provided.
  final List<MenuItem> menus;

  @override
  State<PlatformMenuBar> createState() => _PlatformMenuBarState();

  @override
  List<DiagnosticsNode> debugDescribeChildren() {
    return menus.map<DiagnosticsNode>((MenuItem child) => child.toDiagnosticsNode()).toList();
  }
}

class _PlatformMenuBarState extends State<PlatformMenuBar> {
  List<MenuItem> descendants = <MenuItem>[];

  @override
  void initState() {
    super.initState();
    assert(
        WidgetsBinding.instance.platformMenuDelegate.debugLockDelegate(context),
        'More than one active $PlatformMenuBar detected. Only one active '
        'platform-rendered menu bar is allowed at a time.');
    WidgetsBinding.instance.platformMenuDelegate.clearMenus();
    _updateMenu();
  }

  @override
  void dispose() {
    assert(WidgetsBinding.instance.platformMenuDelegate.debugUnlockDelegate(context),
        'tried to unlock the $DefaultPlatformMenuDelegate more than once with context $context.');
    WidgetsBinding.instance.platformMenuDelegate.clearMenus();
    super.dispose();
  }

  @override
  void didUpdateWidget(PlatformMenuBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    final List<MenuItem> newDescendants = <MenuItem>[
      for (final MenuItem item in widget.menus) ...<MenuItem>[
        item,
        ...item.descendants,
      ],
    ];
    if (!listEquals(newDescendants, descendants)) {
      descendants = newDescendants;
      _updateMenu();
    }
  }

  // Updates the data structures for the menu and send them to the platform
  // plugin.
  void _updateMenu() {
    WidgetsBinding.instance.platformMenuDelegate.setMenus(widget.menus);
  }

  @override
  Widget build(BuildContext context) {
    // PlatformMenuBar is really about managing the platform menu bar, and
    // doesn't do any rendering or event handling in Flutter.
610
    return widget.child ?? widget.body ?? const SizedBox();
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
  }
}

/// A class for representing menu items that have child submenus.
///
/// See also:
///
///  * [PlatformMenuItem], a class representing a leaf menu item in a
///    [PlatformMenuBar].
class PlatformMenu extends MenuItem with DiagnosticableTreeMixin {
  /// Creates a const [PlatformMenu].
  ///
  /// The [label] and [menus] fields are required.
  const PlatformMenu({
    required this.label,
    this.onOpen,
    this.onClose,
    required this.menus,
  });

  /// The label that will appear on the menu.
  final String label;

  /// The callback that is called when this menu is opened.
  @override
  final VoidCallback? onOpen;

  /// The callback that is called when this menu is closed.
  @override
  final VoidCallback? onClose;

  /// The menu items in the submenu opened by this menu item.
  ///
  /// If this is an empty list, this [PlatformMenu] will be disabled.
645
  @override
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
  final List<MenuItem> menus;

  /// Returns all descendant [MenuItem]s of this item.
  @override
  List<MenuItem> get descendants => getDescendants(this);

  /// Returns all descendants of the given item.
  ///
  /// This API is supplied so that implementers of [PlatformMenu] can share
  /// this implementation.
  static List<MenuItem> getDescendants(PlatformMenu item) {
    return <MenuItem>[
      for (final MenuItem child in item.menus) ...<MenuItem>[
        child,
        ...child.descendants,
      ],
    ];
  }

  @override
  Iterable<Map<String, Object?>> toChannelRepresentation(
    PlatformMenuDelegate delegate, {
    required MenuItemSerializableIdGenerator getId,
  }) {
    return <Map<String, Object?>>[serialize(this, delegate, getId)];
  }

  /// Converts the supplied object to the correct channel representation for the
  /// 'flutter/menu' channel.
  ///
  /// This API is supplied so that implementers of [PlatformMenu] can share
  /// this implementation.
  static Map<String, Object?> serialize(
    PlatformMenu item,
    PlatformMenuDelegate delegate,
    MenuItemSerializableIdGenerator getId,
  ) {
    final List<Map<String, Object?>> result = <Map<String, Object?>>[];
    for (final MenuItem childItem in item.menus) {
      result.addAll(childItem.toChannelRepresentation(
        delegate,
        getId: getId,
      ));
689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
    }
    // To avoid doing type checking for groups, just filter out when there are
    // multiple sequential dividers, or when they are first or last, since
    // groups may be interleaved with non-groups, and non-groups may also add
    // dividers.
    Map<String, Object?>? previousItem;
    result.removeWhere((Map<String, Object?> item) {
      if (previousItem == null && item[_kIsDividerKey] == true) {
        // Strip any leading dividers.
        return true;
      }
      if (previousItem != null && previousItem![_kIsDividerKey] == true && item[_kIsDividerKey] == true) {
        // Strip any duplicate dividers.
        return true;
      }
      previousItem = item;
      return false;
    });
    if (result.isNotEmpty && result.last[_kIsDividerKey] == true) {
      result.removeLast();
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
    }
    return <String, Object?>{
      _kIdKey: getId(item),
      _kLabelKey: item.label,
      _kEnabledKey: item.menus.isNotEmpty,
      _kChildrenKey: result,
    };
  }

  @override
  List<DiagnosticsNode> debugDescribeChildren() {
    return menus.map<DiagnosticsNode>((MenuItem child) => child.toDiagnosticsNode()).toList();
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(StringProperty('label', label));
    properties.add(FlagProperty('enabled', value: menus.isNotEmpty, ifFalse: 'DISABLED'));
  }
}

/// A class that groups other menu items into sections delineated by dividers.
///
/// Visual dividers will be added before and after this group if other menu
/// items appear in the [PlatformMenu], and the leading one omitted if it is
/// first and the trailing one omitted if it is last in the menu.
class PlatformMenuItemGroup extends MenuItem {
  /// Creates a const [PlatformMenuItemGroup].
  ///
  /// The [members] field is required.
  const PlatformMenuItemGroup({required this.members});

  /// The [MenuItem]s that are members of this menu item group.
  ///
  /// An assertion will be thrown if there isn't at least one member of the group.
745
  @override
746 747 748 749 750 751 752 753
  final List<MenuItem> members;

  @override
  Iterable<Map<String, Object?>> toChannelRepresentation(
    PlatformMenuDelegate delegate, {
    required MenuItemSerializableIdGenerator getId,
  }) {
    assert(members.isNotEmpty, 'There must be at least one member in a PlatformMenuItemGroup');
754 755 756 757 758 759 760 761 762 763 764 765 766
    return serialize(this, delegate, getId: getId);
  }

  /// Converts the supplied object to the correct channel representation for the
  /// 'flutter/menu' channel.
  ///
  /// This API is supplied so that implementers of [PlatformMenuItemGroup] can share
  /// this implementation.
  static Iterable<Map<String, Object?>> serialize(
    MenuItem group,
    PlatformMenuDelegate delegate, {
    required MenuItemSerializableIdGenerator getId,
  }) {
767
    final List<Map<String, Object?>> result = <Map<String, Object?>>[];
768
    result.add(<String, Object?>{
769
      _kIdKey: getId(group),
770 771
      _kIsDividerKey: true,
    });
772
    for (final MenuItem item in group.members) {
773 774 775 776 777
      result.addAll(item.toChannelRepresentation(
        delegate,
        getId: getId,
      ));
    }
778
    result.add(<String, Object?>{
779
      _kIdKey: getId(group),
780 781
      _kIsDividerKey: true,
    });
782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809
    return result;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(IterableProperty<MenuItem>('members', members));
  }
}

/// A class for [MenuItem]s that do not have submenus (as a [PlatformMenu]
/// would), but can be selected.
///
/// These [MenuItem]s are the leaves of the menu item tree, and [onSelected]
/// will be called when they are selected by clicking on them, or via an
/// optional keyboard [shortcut].
///
/// See also:
///
///  * [PlatformMenu], a menu item that opens a submenu.
class PlatformMenuItem extends MenuItem {
  /// Creates a const [PlatformMenuItem].
  ///
  /// The [label] attribute is required.
  const PlatformMenuItem({
    required this.label,
    this.shortcut,
    this.onSelected,
810 811
    this.onSelectedIntent,
  }) : assert(onSelected == null || onSelectedIntent == null, 'Only one of onSelected or onSelectedIntent may be specified');
812 813 814 815 816 817 818

  /// The required label used for rendering the menu item.
  final String label;

  /// The optional shortcut that selects this [PlatformMenuItem].
  ///
  /// This shortcut is only enabled when [onSelected] is set.
819
  @override
820 821 822 823 824 825 826 827 828
  final MenuSerializableShortcut? shortcut;

  /// An optional callback that is called when this [PlatformMenuItem] is
  /// selected.
  ///
  /// If unset, this menu item will be disabled.
  @override
  final VoidCallback? onSelected;

829 830 831 832 833 834 835
  /// An optional intent that is invoked when this [PlatformMenuItem] is
  /// selected.
  ///
  /// If unset, this menu item will be disabled.
  @override
  final Intent? onSelectedIntent;

836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862
  @override
  Iterable<Map<String, Object?>> toChannelRepresentation(
    PlatformMenuDelegate delegate, {
    required MenuItemSerializableIdGenerator getId,
  }) {
    return <Map<String, Object?>>[PlatformMenuItem.serialize(this, delegate, getId)];
  }

  /// Converts the given [PlatformMenuItem] into a data structure accepted by
  /// the 'flutter/menu' method channel method 'Menu.SetMenu'.
  ///
  /// This API is supplied so that implementers of [PlatformMenuItem] can share
  /// this implementation.
  static Map<String, Object?> serialize(
    PlatformMenuItem item,
    PlatformMenuDelegate delegate,
    MenuItemSerializableIdGenerator getId,
  ) {
    final MenuSerializableShortcut? shortcut = item.shortcut;
    return <String, Object?>{
      _kIdKey: getId(item),
      _kLabelKey: item.label,
      _kEnabledKey: item.onSelected != null,
      if (shortcut != null)...shortcut.serializeForMenu().toChannelRepresentation(),
    };
  }

863 864 865
  @override
  String toStringShort() => '${describeIdentity(this)}($label)';

866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(StringProperty('label', label));
    properties.add(DiagnosticsProperty<MenuSerializableShortcut?>('shortcut', shortcut, defaultValue: null));
    properties.add(FlagProperty('enabled', value: onSelected != null, ifFalse: 'DISABLED'));
  }
}

/// A class that represents a menu item that is provided by the platform.
///
/// This is used to add things like the "About" and "Quit" menu items to a
/// platform menu.
///
/// The [type] enum determines which type of platform defined menu will be
/// added.
///
/// This is most useful on a macOS platform where there are many different types
/// of platform provided menu items in the standard menu setup.
///
/// In order to know if a [PlatformProvidedMenuItem] is available on a
/// particular platform, call [PlatformProvidedMenuItem.hasMenu].
///
/// If the platform does not support the given [type], then the menu item will
/// throw an [ArgumentError] when it is sent to the platform.
///
/// See also:
///
///  * [PlatformMenuBar] which takes these items for inclusion in a
///    platform-rendered menu bar.
class PlatformProvidedMenuItem extends PlatformMenuItem {
  /// Creates a const [PlatformProvidedMenuItem] of the appropriate type. Throws if the
  /// platform doesn't support the given default menu type.
  ///
  /// The [type] argument is required.
  const PlatformProvidedMenuItem({
    required this.type,
    this.enabled = true,
904
  }) : super(label: ''); // The label is ignored for platform provided menus.
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954

  /// The type of default menu this is.
  ///
  /// See [PlatformProvidedMenuItemType] for the different types available. Not
  /// all of the types will be available on every platform. Use [hasMenu] to
  /// determine if the current platform has a given default menu item.
  ///
  /// If the platform does not support the given [type], then the menu item will
  /// throw an [ArgumentError] in debug mode.
  final PlatformProvidedMenuItemType type;

  /// True if this [PlatformProvidedMenuItem] should be enabled or not.
  final bool enabled;

  /// Checks to see if the given default menu type is supported on this
  /// platform.
  static bool hasMenu(PlatformProvidedMenuItemType menu) {
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.iOS:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        return false;
      case TargetPlatform.macOS:
        return const <PlatformProvidedMenuItemType>{
          PlatformProvidedMenuItemType.about,
          PlatformProvidedMenuItemType.quit,
          PlatformProvidedMenuItemType.servicesSubmenu,
          PlatformProvidedMenuItemType.hide,
          PlatformProvidedMenuItemType.hideOtherApplications,
          PlatformProvidedMenuItemType.showAllApplications,
          PlatformProvidedMenuItemType.startSpeaking,
          PlatformProvidedMenuItemType.stopSpeaking,
          PlatformProvidedMenuItemType.toggleFullScreen,
          PlatformProvidedMenuItemType.minimizeWindow,
          PlatformProvidedMenuItemType.zoomWindow,
          PlatformProvidedMenuItemType.arrangeWindowsInFront,
        }.contains(menu);
    }
  }

  @override
  Iterable<Map<String, Object?>> toChannelRepresentation(
    PlatformMenuDelegate delegate, {
    required MenuItemSerializableIdGenerator getId,
  }) {
    assert(() {
      if (!hasMenu(type)) {
        throw ArgumentError(
955
          'Platform ${defaultTargetPlatform.name} has no platform provided menu for '
956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978
          '$type. Call PlatformProvidedMenuItem.hasMenu to determine this before '
          'instantiating one.',
        );
      }
      return true;
    }());

    return <Map<String, Object?>>[
      <String, Object?>{
        _kIdKey: getId(this),
        _kEnabledKey: enabled,
        _kPlatformDefaultMenuKey: type.index,
      },
    ];
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED'));
  }
}

979 980
/// The list of possible platform provided, prebuilt menus for use in a
/// [PlatformMenuBar].
981 982 983 984 985 986 987 988 989 990 991 992 993
///
/// These are menus that the platform typically provides that cannot be
/// reproduced in Flutter without calling platform functions, but are standard
/// on the platform.
///
/// Examples include things like the "Quit" or "Services" menu items on macOS.
/// Not all platforms support all menu item types. Use
/// [PlatformProvidedMenuItem.hasMenu] to know if a particular type is supported
/// on a the current platform.
///
/// Add these to your [PlatformMenuBar] using the [PlatformProvidedMenuItem]
/// class.
///
994
/// You can tell if the platform provides the given menu using the
995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097
/// [PlatformProvidedMenuItem.hasMenu] method.
// Must be kept in sync with the plugin code's enum of the same name.
enum PlatformProvidedMenuItemType {
  /// The system provided "About" menu item.
  ///
  /// On macOS, this is the `orderFrontStandardAboutPanel` default menu.
  about,

  /// The system provided "Quit" menu item.
  ///
  /// On macOS, this is the `terminate` default menu.
  ///
  /// This menu item will simply exit the application when activated.
  quit,

  /// The system provided "Services" submenu.
  ///
  /// This submenu provides a list of system provided application services.
  ///
  /// This default menu is only supported on macOS.
  servicesSubmenu,

  /// The system provided "Hide" menu item.
  ///
  /// This menu item hides the application window.
  ///
  /// On macOS, this is the `hide` default menu.
  ///
  /// This default menu is only supported on macOS.
  hide,

  /// The system provided "Hide Others" menu item.
  ///
  /// This menu item hides other application windows.
  ///
  /// On macOS, this is the `hideOtherApplications` default menu.
  ///
  /// This default menu is only supported on macOS.
  hideOtherApplications,

  /// The system provided "Show All" menu item.
  ///
  /// This menu item shows all hidden application windows.
  ///
  /// On macOS, this is the `unhideAllApplications` default menu.
  ///
  /// This default menu is only supported on macOS.
  showAllApplications,

  /// The system provided "Start Dictation..." menu item.
  ///
  /// This menu item tells the system to start the screen reader.
  ///
  /// On macOS, this is the `startSpeaking` default menu.
  ///
  /// This default menu is currently only supported on macOS.
  startSpeaking,

  /// The system provided "Stop Dictation..." menu item.
  ///
  /// This menu item tells the system to stop the screen reader.
  ///
  /// On macOS, this is the `stopSpeaking` default menu.
  ///
  /// This default menu is currently only supported on macOS.
  stopSpeaking,

  /// The system provided "Enter Full Screen" menu item.
  ///
  /// This menu item tells the system to toggle full screen mode for the window.
  ///
  /// On macOS, this is the `toggleFullScreen` default menu.
  ///
  /// This default menu is currently only supported on macOS.
  toggleFullScreen,

  /// The system provided "Minimize" menu item.
  ///
  /// This menu item tells the system to minimize the window.
  ///
  /// On macOS, this is the `performMiniaturize` default menu.
  ///
  /// This default menu is currently only supported on macOS.
  minimizeWindow,

  /// The system provided "Zoom" menu item.
  ///
  /// This menu item tells the system to expand the window size.
  ///
  /// On macOS, this is the `performZoom` default menu.
  ///
  /// This default menu is currently only supported on macOS.
  zoomWindow,

  /// The system provided "Bring To Front" menu item.
  ///
  /// This menu item tells the system to stack the window above other windows.
  ///
  /// On macOS, this is the `arrangeInFront` default menu.
  ///
  /// This default menu is currently only supported on macOS.
  arrangeWindowsInFront,
}