blob: f5ded7a121e853d7a72b4fcb0cbdd4b64da6c803 [file] [log] [blame]
// 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';
import 'actions.dart';
import 'basic.dart';
import 'binding.dart';
import 'focus_manager.dart';
import 'framework.dart';
import 'shortcuts.dart';
// "flutter/menu" Method channel methods.
const String _kMenuSetMethod = 'Menu.setMenus';
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';
const String _kShortcutCharacter = 'shortcutCharacter';
const String _kShortcutTrigger = 'shortcutTrigger';
const String _kShortcutModifiers = 'shortcutModifiers';
/// 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)
: _internal = <String, Object?>{_kShortcutCharacter: character},
_character = character,
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.'),
_trigger = trigger,
_control = control,
_shift = shift,
_alt = alt,
_meta = meta,
_internal = <String, Object?>{
_kShortcutTrigger: trigger.keyId,
_kShortcutModifiers: (control ? _shortcutModifierControl : 0) |
(alt ? _shortcutModifierAlt : 0) |
(shift ? _shortcutModifierShift : 0) |
(meta ? _shortcutModifierMeta : 0),
};
final Map<String, Object?> _internal;
/// 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;
/// 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
/// [PlatformMenuItem] 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 implements ShortcutActivator {
/// Implement this in a [ShortcutActivator] subclass to allow it to be
/// serialized for use in a [PlatformMenuBar].
ShortcutSerialization serializeForMenu();
}
/// 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<PlatformMenuItem> 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 [PlatformMenuItem].
typedef MenuItemSerializableIdGenerator = int Function(PlatformMenuItem 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, PlatformMenuItem>{} {
this.channel.setMethodCallHandler(_methodCallHandler);
}
// Map of distributed IDs to menu items.
final Map<int, PlatformMenuItem> _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(<PlatformMenuItem>[]);
@override
void setMenus(List<PlatformMenuItem> topLevelMenus) {
_idMap.clear();
final List<Map<String, Object?>> representation = <Map<String, Object?>>[];
if (topLevelMenus.isNotEmpty) {
for (final PlatformMenuItem childItem in topLevelMenus) {
representation.addAll(childItem.toChannelRepresentation(this, getId: _getId));
}
}
// 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(PlatformMenuItem 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 PlatformMenuItem item = _idMap[id]!;
if (call.method == _kMenuSelectedCallbackMethod) {
assert(item.onSelected == null || item.onSelectedIntent == null,
'Only one of PlatformMenuItem.onSelected or PlatformMenuItem.onSelectedIntent may be specified');
item.onSelected?.call();
if (item.onSelectedIntent != null) {
Actions.maybeInvoke(FocusManager.instance.primaryFocus!.context!, item.onSelectedIntent!);
}
} 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 [PlatformMenuItem]s, which configure the
/// properties of the menus on the platform menu bar.
///
/// As far as Flutter is concerned, this widget has no visual representation,
/// and intercepts no events: it just returns the [child] from its build
/// function. This is because all of the rendering, shortcuts, and event
/// handling for the menu is handled by the plugin on the host platform. It is
/// only part of the widget tree to provide a convenient refresh mechanism for
/// the menu data.
///
/// 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
/// [PlatformMenuItem]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}
///
/// The menus could just as effectively be managed without using the widget tree
/// by using the following code, but mixing this usage with [PlatformMenuBar] is
/// not recommended, since it will overwrite the menu configuration when it is
/// rebuilt:
///
/// ```dart
/// List<PlatformMenuItem> menus = <PlatformMenuItem>[ /* Define menus... */ ];
/// WidgetsBinding.instance.platformMenuDelegate.setMenus(menus);
/// ```
class PlatformMenuBar extends StatefulWidget with DiagnosticableTreeMixin {
/// Creates a const [PlatformMenuBar].
///
/// The [child] and [menus] attributes are required.
const PlatformMenuBar({
super.key,
required this.menus,
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;
/// The widget below this widget in the tree.
///
/// 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;
/// The list of menu items that are the top level children of the
/// [PlatformMenuBar].
///
/// The [menus] member contains [PlatformMenuItem]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<PlatformMenuItem> menus;
@override
State<PlatformMenuBar> createState() => _PlatformMenuBarState();
@override
List<DiagnosticsNode> debugDescribeChildren() {
return menus.map<DiagnosticsNode>((PlatformMenuItem child) => child.toDiagnosticsNode()).toList();
}
}
class _PlatformMenuBarState extends State<PlatformMenuBar> {
List<PlatformMenuItem> descendants = <PlatformMenuItem>[];
@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<PlatformMenuItem> newDescendants = <PlatformMenuItem>[
for (final PlatformMenuItem item in widget.menus) ...<PlatformMenuItem>[
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.
return widget.child ?? widget.body ?? const SizedBox();
}
}
/// 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 PlatformMenuItem with DiagnosticableTreeMixin {
/// Creates a const [PlatformMenu].
///
/// The [label] and [menus] fields are required.
const PlatformMenu({
required super.label,
this.onOpen,
this.onClose,
required this.menus,
});
@override
final VoidCallback? onOpen;
@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.
final List<PlatformMenuItem> menus;
/// Returns all descendant [PlatformMenuItem]s of this item.
@override
List<PlatformMenuItem> 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<PlatformMenuItem> getDescendants(PlatformMenu item) {
return <PlatformMenuItem>[
for (final PlatformMenuItem child in item.menus) ...<PlatformMenuItem>[
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 PlatformMenuItem childItem in item.menus) {
result.addAll(childItem.toChannelRepresentation(
delegate,
getId: getId,
));
}
// 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();
}
return <String, Object?>{
_kIdKey: getId(item),
_kLabelKey: item.label,
_kEnabledKey: item.menus.isNotEmpty,
_kChildrenKey: result,
};
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
return menus.map<DiagnosticsNode>((PlatformMenuItem 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 PlatformMenuItem {
/// Creates a const [PlatformMenuItemGroup].
///
/// The [members] field is required.
const PlatformMenuItemGroup({required this.members}) : super(label: '');
/// The [PlatformMenuItem]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.
@override
final List<PlatformMenuItem> 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');
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(
PlatformMenuItem group,
PlatformMenuDelegate delegate, {
required MenuItemSerializableIdGenerator getId,
}) {
final List<Map<String, Object?>> result = <Map<String, Object?>>[];
result.add(<String, Object?>{
_kIdKey: getId(group),
_kIsDividerKey: true,
});
for (final PlatformMenuItem item in group.members) {
result.addAll(item.toChannelRepresentation(
delegate,
getId: getId,
));
}
result.add(<String, Object?>{
_kIdKey: getId(group),
_kIsDividerKey: true,
});
return result;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IterableProperty<PlatformMenuItem>('members', members));
}
}
/// A class for [PlatformMenuItem]s that do not have submenus (as a [PlatformMenu]
/// would), but can be selected.
///
/// These [PlatformMenuItem]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 with Diagnosticable {
/// Creates a const [PlatformMenuItem].
///
/// The [label] attribute is required.
const PlatformMenuItem({
required this.label,
this.shortcut,
this.onSelected,
this.onSelectedIntent,
}) : assert(onSelected == null || onSelectedIntent == null, 'Only one of onSelected or onSelectedIntent may be specified');
/// 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.
final MenuSerializableShortcut? shortcut;
/// An optional callback that is called when this [PlatformMenuItem] is
/// selected.
///
/// If unset, this menu item will be disabled.
final VoidCallback? onSelected;
/// Returns a callback, if any, to be invoked if the platform menu receives a
/// "Menu.opened" method call from the platform for this item.
///
/// Only items that have submenus will have this callback invoked.
///
/// The default implementation returns null.
VoidCallback? get onOpen => null;
/// Returns a callback, if any, to be invoked if the platform menu receives a
/// "Menu.closed" method call from the platform for this item.
///
/// Only items that have submenus will have this callback invoked.
///
/// The default implementation returns null.
VoidCallback? get onClose => null;
/// An optional intent that is invoked when this [PlatformMenuItem] is
/// selected.
///
/// If unset, this menu item will be disabled.
final Intent? onSelectedIntent;
/// Returns all descendant [PlatformMenuItem]s of this item.
///
/// Returns an empty list if this type of menu item doesn't have
/// descendants.
List<PlatformMenuItem> get descendants => const <PlatformMenuItem>[];
/// Returns the list of group members if this menu item is a "grouping" menu
/// item, such as [PlatformMenuItemGroup].
///
/// Defaults to an empty list.
List<PlatformMenuItem> get members => const <PlatformMenuItem>[];
/// 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 `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,
}) {
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(),
};
}
@override
String toStringShort() => '${describeIdentity(this)}($label)';
@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,
}) : super(label: ''); // The label is ignored for platform provided menus.
/// 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(
'Platform ${defaultTargetPlatform.name} has no platform provided menu for '
'$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'));
}
}
/// The list of possible platform provided, prebuilt menus for use in a
/// [PlatformMenuBar].
///
/// 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.
///
/// You can tell if the platform provides the given menu using the
/// [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,
}