1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/services.dart';
9
10import 'actions.dart';
11import 'basic.dart';
12import 'binding.dart';
13import 'focus_manager.dart';
14import 'framework.dart';
15import 'shortcuts.dart';
16
17// "flutter/menu" Method channel methods.
18const String _kMenuSetMethod = 'Menu.setMenus';
19const String _kMenuSelectedCallbackMethod = 'Menu.selectedCallback';
20const String _kMenuItemOpenedMethod = 'Menu.opened';
21const String _kMenuItemClosedMethod = 'Menu.closed';
22
23// Keys for channel communication map.
24const String _kIdKey = 'id';
25const String _kLabelKey = 'label';
26const String _kEnabledKey = 'enabled';
27const String _kChildrenKey = 'children';
28const String _kIsDividerKey = 'isDivider';
29const String _kPlatformDefaultMenuKey = 'platformProvidedMenu';
30const String _kShortcutCharacter = 'shortcutCharacter';
31const String _kShortcutTrigger = 'shortcutTrigger';
32const String _kShortcutModifiers = 'shortcutModifiers';
33
34/// A class used by [MenuSerializableShortcut] to describe the shortcut for
35/// serialization to send to the platform for rendering a [PlatformMenuBar].
36///
37/// See also:
38///
39/// * [PlatformMenuBar], a widget that defines a menu bar for the platform to
40/// render natively.
41/// * [MenuSerializableShortcut], a mixin allowing a [ShortcutActivator] to
42/// provide data for serialization of the shortcut for sending to the
43/// platform.
44class ShortcutSerialization {
45 /// Creates a [ShortcutSerialization] representing a single character.
46 ///
47 /// This is used by a [CharacterActivator] to serialize itself.
48 ShortcutSerialization.character(String character, {
49 bool alt = false,
50 bool control = false,
51 bool meta = false,
52 }) : assert(character.length == 1),
53 _character = character,
54 _trigger = null,
55 _alt = alt,
56 _control = control,
57 _meta = meta,
58 _shift = null,
59 _internal = <String, Object?>{
60 _kShortcutCharacter: character,
61 _kShortcutModifiers: (control ? _shortcutModifierControl : 0) |
62 (alt ? _shortcutModifierAlt : 0) |
63 (meta ? _shortcutModifierMeta : 0),
64 };
65
66 /// Creates a [ShortcutSerialization] representing a specific
67 /// [LogicalKeyboardKey] and modifiers.
68 ///
69 /// This is used by a [SingleActivator] to serialize itself.
70 ShortcutSerialization.modifier(
71 LogicalKeyboardKey trigger, {
72 bool alt = false,
73 bool control = false,
74 bool meta = false,
75 bool shift = false,
76 }) : assert(trigger != LogicalKeyboardKey.alt &&
77 trigger != LogicalKeyboardKey.altLeft &&
78 trigger != LogicalKeyboardKey.altRight &&
79 trigger != LogicalKeyboardKey.control &&
80 trigger != LogicalKeyboardKey.controlLeft &&
81 trigger != LogicalKeyboardKey.controlRight &&
82 trigger != LogicalKeyboardKey.meta &&
83 trigger != LogicalKeyboardKey.metaLeft &&
84 trigger != LogicalKeyboardKey.metaRight &&
85 trigger != LogicalKeyboardKey.shift &&
86 trigger != LogicalKeyboardKey.shiftLeft &&
87 trigger != LogicalKeyboardKey.shiftRight,
88 'Specifying a modifier key as a trigger is not allowed. '
89 'Use provided boolean parameters instead.'),
90 _trigger = trigger,
91 _character = null,
92 _alt = alt,
93 _control = control,
94 _meta = meta,
95 _shift = shift,
96 _internal = <String, Object?>{
97 _kShortcutTrigger: trigger.keyId,
98 _kShortcutModifiers: (alt ? _shortcutModifierAlt : 0) |
99 (control ? _shortcutModifierControl : 0) |
100 (meta ? _shortcutModifierMeta : 0) |
101 (shift ? _shortcutModifierShift : 0),
102 };
103
104 final Map<String, Object?> _internal;
105
106 /// The keyboard key that triggers this shortcut, if any.
107 LogicalKeyboardKey? get trigger => _trigger;
108 final LogicalKeyboardKey? _trigger;
109
110 /// The character that triggers this shortcut, if any.
111 String? get character => _character;
112 final String? _character;
113
114 /// If this shortcut has a [trigger], this indicates whether or not the
115 /// alt modifier needs to be down or not.
116 bool? get alt => _alt;
117 final bool? _alt;
118
119 /// If this shortcut has a [trigger], this indicates whether or not the
120 /// control modifier needs to be down or not.
121 bool? get control => _control;
122 final bool? _control;
123
124 /// If this shortcut has a [trigger], this indicates whether or not the meta
125 /// (also known as the Windows or Command key) modifier needs to be down or
126 /// not.
127 bool? get meta => _meta;
128 final bool? _meta;
129
130 /// If this shortcut has a [trigger], this indicates whether or not the
131 /// shift modifier needs to be down or not.
132 bool? get shift => _shift;
133 final bool? _shift;
134
135 /// The bit mask for the [LogicalKeyboardKey.alt] key (or it's left/right
136 /// equivalents) being down.
137 static const int _shortcutModifierAlt = 1 << 2;
138
139 /// The bit mask for the [LogicalKeyboardKey.control] key (or it's left/right
140 /// equivalents) being down.
141 static const int _shortcutModifierControl = 1 << 3;
142
143 /// The bit mask for the [LogicalKeyboardKey.meta] key (or it's left/right
144 /// equivalents) being down.
145 static const int _shortcutModifierMeta = 1 << 0;
146
147 /// The bit mask for the [LogicalKeyboardKey.shift] key (or it's left/right
148 /// equivalents) being down.
149 static const int _shortcutModifierShift = 1 << 1;
150
151 /// Converts the internal representation to the format needed for a
152 /// [PlatformMenuItem] to include it in its serialized form for sending to the
153 /// platform.
154 Map<String, Object?> toChannelRepresentation() => _internal;
155}
156
157/// A mixin allowing a [ShortcutActivator] to provide data for serialization of
158/// the shortcut when sending to the platform.
159///
160/// This is meant for those who have written their own [ShortcutActivator]
161/// subclass, and would like to have it work for menus in a [PlatformMenuBar] as
162/// well.
163///
164/// Keep in mind that there are limits to the capabilities of the platform APIs,
165/// and not all kinds of [ShortcutActivator]s will work with them.
166///
167/// See also:
168///
169/// * [SingleActivator], a [ShortcutActivator] which implements this mixin.
170/// * [CharacterActivator], another [ShortcutActivator] which implements this mixin.
171mixin MenuSerializableShortcut implements ShortcutActivator {
172 /// Implement this in a [ShortcutActivator] subclass to allow it to be
173 /// serialized for use in a [PlatformMenuBar].
174 ShortcutSerialization serializeForMenu();
175}
176
177/// An abstract delegate class that can be used to set
178/// [WidgetsBinding.platformMenuDelegate] to provide for managing platform
179/// menus.
180///
181/// This can be subclassed to provide a different menu plugin than the default
182/// system-provided plugin for managing [PlatformMenuBar] menus.
183///
184/// The [setMenus] method allows for setting of the menu hierarchy when the
185/// [PlatformMenuBar] menu hierarchy changes.
186///
187/// This delegate doesn't handle the results of clicking on a menu item, which
188/// is left to the implementor of subclasses of [PlatformMenuDelegate] to
189/// handle for their implementation.
190///
191/// This delegate typically knows how to serialize a [PlatformMenu]
192/// hierarchy, send it over a channel, and register for calls from the channel
193/// when a menu is invoked or a submenu is opened or closed.
194///
195/// See [DefaultPlatformMenuDelegate] for an example of implementing one of
196/// these.
197///
198/// See also:
199///
200/// * [PlatformMenuBar], the widget that adds a platform menu bar to an
201/// application, and uses [setMenus] to send the menus to the platform.
202/// * [PlatformMenu], the class that describes a menu item with children
203/// that appear in a cascading menu.
204/// * [PlatformMenuItem], the class that describes the leaves of a menu
205/// hierarchy.
206abstract class PlatformMenuDelegate {
207 /// A const constructor so that subclasses can have const constructors.
208 const PlatformMenuDelegate();
209
210 /// Sets the entire menu hierarchy for a platform-rendered menu bar.
211 ///
212 /// The `topLevelMenus` argument is the list of menus that appear in the menu
213 /// bar, which themselves can have children.
214 ///
215 /// To update the menu hierarchy or menu item state, call [setMenus] with the
216 /// modified hierarchy, and it will overwrite the previous menu state.
217 ///
218 /// See also:
219 ///
220 /// * [PlatformMenuBar], the widget that adds a platform menu bar to an
221 /// application.
222 /// * [PlatformMenu], the class that describes a menu item with children
223 /// that appear in a cascading menu.
224 /// * [PlatformMenuItem], the class that describes the leaves of a menu
225 /// hierarchy.
226 void setMenus(List<PlatformMenuItem> topLevelMenus);
227
228 /// Clears any existing platform-rendered menus and leaves the application
229 /// with no menus.
230 ///
231 /// It is not necessary to call this before updating the menu with [setMenus].
232 void clearMenus();
233
234 /// This is called by [PlatformMenuBar] when it is initialized, to be sure that
235 /// only one is active at a time.
236 ///
237 /// The [debugLockDelegate] function should be called before the first call to
238 /// [setMenus].
239 ///
240 /// If the lock is successfully acquired, [debugLockDelegate] will return
241 /// true.
242 ///
243 /// If your implementation of a [PlatformMenuDelegate] can have only limited
244 /// active instances, enforce it when you override this function.
245 ///
246 /// See also:
247 ///
248 /// * [debugUnlockDelegate], where the delegate is unlocked.
249 bool debugLockDelegate(BuildContext context);
250
251 /// This is called by [PlatformMenuBar] when it is disposed, so that another
252 /// one can take over.
253 ///
254 /// If the [debugUnlockDelegate] successfully unlocks the delegate, it will
255 /// return true.
256 ///
257 /// See also:
258 ///
259 /// * [debugLockDelegate], where the delegate is locked.
260 bool debugUnlockDelegate(BuildContext context);
261}
262
263/// The signature for a function that generates unique menu item IDs for
264/// serialization of a [PlatformMenuItem].
265typedef MenuItemSerializableIdGenerator = int Function(PlatformMenuItem item);
266
267/// The platform menu delegate that handles the built-in macOS platform menu
268/// generation using the 'flutter/menu' channel.
269///
270/// An instance of this class is set on [WidgetsBinding.platformMenuDelegate] by
271/// default when the [WidgetsBinding] is initialized.
272///
273/// See also:
274///
275/// * [PlatformMenuBar], the widget that adds a platform menu bar to an
276/// application.
277/// * [PlatformMenu], the class that describes a menu item with children
278/// that appear in a cascading menu.
279/// * [PlatformMenuItem], the class that describes the leaves of a menu
280/// hierarchy.
281class DefaultPlatformMenuDelegate extends PlatformMenuDelegate {
282 /// Creates a const [DefaultPlatformMenuDelegate].
283 ///
284 /// The optional [channel] argument defines the channel used to communicate
285 /// with the platform. It defaults to [SystemChannels.menu] if not supplied.
286 DefaultPlatformMenuDelegate({MethodChannel? channel})
287 : channel = channel ?? SystemChannels.menu,
288 _idMap = <int, PlatformMenuItem>{} {
289 this.channel.setMethodCallHandler(_methodCallHandler);
290 }
291
292 // Map of distributed IDs to menu items.
293 final Map<int, PlatformMenuItem> _idMap;
294 // An ever increasing value used to dole out IDs.
295 int _serial = 0;
296 // The context used to "lock" this delegate to a specific instance of
297 // PlatformMenuBar to make sure there is only one.
298 BuildContext? _lockedContext;
299
300 @override
301 void clearMenus() => setMenus(<PlatformMenuItem>[]);
302
303 @override
304 void setMenus(List<PlatformMenuItem> topLevelMenus) {
305 _idMap.clear();
306 final List<Map<String, Object?>> representation = <Map<String, Object?>>[];
307 if (topLevelMenus.isNotEmpty) {
308 for (final PlatformMenuItem childItem in topLevelMenus) {
309 representation.addAll(childItem.toChannelRepresentation(this, getId: _getId));
310 }
311 }
312 // Currently there's only ever one window, but the channel's format allows
313 // more than one window's menu hierarchy to be defined.
314 final Map<String, Object?> windowMenu = <String, Object?>{
315 '0': representation,
316 };
317 channel.invokeMethod<void>(_kMenuSetMethod, windowMenu);
318 }
319
320 /// Defines the channel that the [DefaultPlatformMenuDelegate] uses to
321 /// communicate with the platform.
322 ///
323 /// Defaults to [SystemChannels.menu].
324 final MethodChannel channel;
325
326 /// Get the next serialization ID.
327 ///
328 /// This is called by each DefaultPlatformMenuDelegateSerializer when
329 /// serializing a new object so that it has a unique ID.
330 int _getId(PlatformMenuItem item) {
331 _serial += 1;
332 _idMap[_serial] = item;
333 return _serial;
334 }
335
336 @override
337 bool debugLockDelegate(BuildContext context) {
338 assert(() {
339 // It's OK to lock if the lock isn't set, but not OK if a different
340 // context is locking it.
341 if (_lockedContext != null && _lockedContext != context) {
342 return false;
343 }
344 _lockedContext = context;
345 return true;
346 }());
347 return true;
348 }
349
350 @override
351 bool debugUnlockDelegate(BuildContext context) {
352 assert(() {
353 // It's OK to unlock if the lock isn't set, but not OK if a different
354 // context is unlocking it.
355 if (_lockedContext != null && _lockedContext != context) {
356 return false;
357 }
358 _lockedContext = null;
359 return true;
360 }());
361 return true;
362 }
363
364 // Handles the method calls from the plugin to forward to selection and
365 // open/close callbacks.
366 Future<void> _methodCallHandler(MethodCall call) async {
367 final int id = call.arguments as int;
368 assert(
369 _idMap.containsKey(id),
370 'Received a menu ${call.method} for a menu item with an ID that was not recognized: $id',
371 );
372 if (!_idMap.containsKey(id)) {
373 return;
374 }
375 final PlatformMenuItem item = _idMap[id]!;
376 if (call.method == _kMenuSelectedCallbackMethod) {
377 assert(item.onSelected == null || item.onSelectedIntent == null,
378 'Only one of PlatformMenuItem.onSelected or PlatformMenuItem.onSelectedIntent may be specified');
379 item.onSelected?.call();
380 if (item.onSelectedIntent != null) {
381 Actions.maybeInvoke(FocusManager.instance.primaryFocus!.context!, item.onSelectedIntent!);
382 }
383 } else if (call.method == _kMenuItemOpenedMethod) {
384 item.onOpen?.call();
385 } else if (call.method == _kMenuItemClosedMethod) {
386 item.onClose?.call();
387 }
388 }
389}
390
391/// A menu bar that uses the platform's native APIs to construct and render a
392/// menu described by a [PlatformMenu]/[PlatformMenuItem] hierarchy.
393///
394/// This widget is especially useful on macOS, where a system menu is a required
395/// part of every application. Flutter only includes support for macOS out of
396/// the box, but support for other platforms may be provided via plugins that
397/// set [WidgetsBinding.platformMenuDelegate] in their initialization.
398///
399/// The [menus] member contains [PlatformMenuItem]s, which configure the
400/// properties of the menus on the platform menu bar.
401///
402/// As far as Flutter is concerned, this widget has no visual representation,
403/// and intercepts no events: it just returns the [child] from its build
404/// function. This is because all of the rendering, shortcuts, and event
405/// handling for the menu is handled by the plugin on the host platform. It is
406/// only part of the widget tree to provide a convenient refresh mechanism for
407/// the menu data.
408///
409/// There can only be one [PlatformMenuBar] at a time using the same
410/// [PlatformMenuDelegate]. It will assert if more than one is detected.
411///
412/// When calling [toStringDeep] on this widget, it will give a tree of
413/// [PlatformMenuItem]s, not a tree of widgets.
414///
415/// {@tool sample} This example shows a [PlatformMenuBar] that contains a single
416/// top level menu, containing three items for "About", a toggleable menu item
417/// for showing a message, a cascading submenu with message choices, and "Quit".
418///
419/// **This example will only work on macOS.**
420///
421/// ** See code in examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart **
422/// {@end-tool}
423///
424/// The menus could just as effectively be managed without using the widget tree
425/// by using the following code, but mixing this usage with [PlatformMenuBar] is
426/// not recommended, since it will overwrite the menu configuration when it is
427/// rebuilt:
428///
429/// ```dart
430/// List<PlatformMenuItem> menus = <PlatformMenuItem>[ /* Define menus... */ ];
431/// WidgetsBinding.instance.platformMenuDelegate.setMenus(menus);
432/// ```
433class PlatformMenuBar extends StatefulWidget with DiagnosticableTreeMixin {
434 /// Creates a const [PlatformMenuBar].
435 ///
436 /// The [child] and [menus] attributes are required.
437 const PlatformMenuBar({
438 super.key,
439 required this.menus,
440 this.child,
441 });
442
443 /// The widget below this widget in the tree.
444 ///
445 /// {@macro flutter.widgets.ProxyWidget.child}
446 final Widget? child;
447
448 /// The list of menu items that are the top level children of the
449 /// [PlatformMenuBar].
450 ///
451 /// The [menus] member contains [PlatformMenuItem]s. They will not be part of
452 /// the widget tree, since they are not widgets. They are provided to
453 /// configure the properties of the menus on the platform menu bar.
454 ///
455 /// Also, a Widget in Flutter is immutable, so directly modifying the
456 /// [menus] with `List` APIs such as
457 /// `somePlatformMenuBarWidget.menus.add(...)` will result in incorrect
458 /// behaviors. Whenever the menus list is modified, a new list object
459 /// should be provided.
460 final List<PlatformMenuItem> menus;
461
462 @override
463 State<PlatformMenuBar> createState() => _PlatformMenuBarState();
464
465 @override
466 List<DiagnosticsNode> debugDescribeChildren() {
467 return menus.map<DiagnosticsNode>((PlatformMenuItem child) => child.toDiagnosticsNode()).toList();
468 }
469}
470
471class _PlatformMenuBarState extends State<PlatformMenuBar> {
472 List<PlatformMenuItem> descendants = <PlatformMenuItem>[];
473
474 @override
475 void initState() {
476 super.initState();
477 assert(
478 WidgetsBinding.instance.platformMenuDelegate.debugLockDelegate(context),
479 'More than one active $PlatformMenuBar detected. Only one active '
480 'platform-rendered menu bar is allowed at a time.');
481 WidgetsBinding.instance.platformMenuDelegate.clearMenus();
482 _updateMenu();
483 }
484
485 @override
486 void dispose() {
487 assert(WidgetsBinding.instance.platformMenuDelegate.debugUnlockDelegate(context),
488 'tried to unlock the $DefaultPlatformMenuDelegate more than once with context $context.');
489 WidgetsBinding.instance.platformMenuDelegate.clearMenus();
490 super.dispose();
491 }
492
493 @override
494 void didUpdateWidget(PlatformMenuBar oldWidget) {
495 super.didUpdateWidget(oldWidget);
496 final List<PlatformMenuItem> newDescendants = <PlatformMenuItem>[
497 for (final PlatformMenuItem item in widget.menus) ...<PlatformMenuItem>[
498 item,
499 ...item.descendants,
500 ],
501 ];
502 if (!listEquals(newDescendants, descendants)) {
503 descendants = newDescendants;
504 _updateMenu();
505 }
506 }
507
508 // Updates the data structures for the menu and send them to the platform
509 // plugin.
510 void _updateMenu() {
511 WidgetsBinding.instance.platformMenuDelegate.setMenus(widget.menus);
512 }
513
514 @override
515 Widget build(BuildContext context) {
516 // PlatformMenuBar is really about managing the platform menu bar, and
517 // doesn't do any rendering or event handling in Flutter.
518 return widget.child ?? const SizedBox();
519 }
520}
521
522/// A class for representing menu items that have child submenus.
523///
524/// See also:
525///
526/// * [PlatformMenuItem], a class representing a leaf menu item in a
527/// [PlatformMenuBar].
528class PlatformMenu extends PlatformMenuItem with DiagnosticableTreeMixin {
529 /// Creates a const [PlatformMenu].
530 ///
531 /// The [label] and [menus] fields are required.
532 const PlatformMenu({
533 required super.label,
534 this.onOpen,
535 this.onClose,
536 required this.menus,
537 });
538
539 @override
540 final VoidCallback? onOpen;
541
542 @override
543 final VoidCallback? onClose;
544
545 /// The menu items in the submenu opened by this menu item.
546 ///
547 /// If this is an empty list, this [PlatformMenu] will be disabled.
548 final List<PlatformMenuItem> menus;
549
550 /// Returns all descendant [PlatformMenuItem]s of this item.
551 @override
552 List<PlatformMenuItem> get descendants => getDescendants(this);
553
554 /// Returns all descendants of the given item.
555 ///
556 /// This API is supplied so that implementers of [PlatformMenu] can share
557 /// this implementation.
558 static List<PlatformMenuItem> getDescendants(PlatformMenu item) {
559 return <PlatformMenuItem>[
560 for (final PlatformMenuItem child in item.menus) ...<PlatformMenuItem>[
561 child,
562 ...child.descendants,
563 ],
564 ];
565 }
566
567 @override
568 Iterable<Map<String, Object?>> toChannelRepresentation(
569 PlatformMenuDelegate delegate, {
570 required MenuItemSerializableIdGenerator getId,
571 }) {
572 return <Map<String, Object?>>[serialize(this, delegate, getId)];
573 }
574
575 /// Converts the supplied object to the correct channel representation for the
576 /// 'flutter/menu' channel.
577 ///
578 /// This API is supplied so that implementers of [PlatformMenu] can share
579 /// this implementation.
580 static Map<String, Object?> serialize(
581 PlatformMenu item,
582 PlatformMenuDelegate delegate,
583 MenuItemSerializableIdGenerator getId,
584 ) {
585 final List<Map<String, Object?>> result = <Map<String, Object?>>[];
586 for (final PlatformMenuItem childItem in item.menus) {
587 result.addAll(childItem.toChannelRepresentation(
588 delegate,
589 getId: getId,
590 ));
591 }
592 // To avoid doing type checking for groups, just filter out when there are
593 // multiple sequential dividers, or when they are first or last, since
594 // groups may be interleaved with non-groups, and non-groups may also add
595 // dividers.
596 Map<String, Object?>? previousItem;
597 result.removeWhere((Map<String, Object?> item) {
598 if (previousItem == null && item[_kIsDividerKey] == true) {
599 // Strip any leading dividers.
600 return true;
601 }
602 if (previousItem != null && previousItem![_kIsDividerKey] == true && item[_kIsDividerKey] == true) {
603 // Strip any duplicate dividers.
604 return true;
605 }
606 previousItem = item;
607 return false;
608 });
609 if (result.isNotEmpty && result.last[_kIsDividerKey] == true) {
610 result.removeLast();
611 }
612 return <String, Object?>{
613 _kIdKey: getId(item),
614 _kLabelKey: item.label,
615 _kEnabledKey: item.menus.isNotEmpty,
616 _kChildrenKey: result,
617 };
618 }
619
620 @override
621 List<DiagnosticsNode> debugDescribeChildren() {
622 return menus.map<DiagnosticsNode>((PlatformMenuItem child) => child.toDiagnosticsNode()).toList();
623 }
624
625 @override
626 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
627 super.debugFillProperties(properties);
628 properties.add(StringProperty('label', label));
629 properties.add(FlagProperty('enabled', value: menus.isNotEmpty, ifFalse: 'DISABLED'));
630 }
631}
632
633/// A class that groups other menu items into sections delineated by dividers.
634///
635/// Visual dividers will be added before and after this group if other menu
636/// items appear in the [PlatformMenu], and the leading one omitted if it is
637/// first and the trailing one omitted if it is last in the menu.
638class PlatformMenuItemGroup extends PlatformMenuItem {
639 /// Creates a const [PlatformMenuItemGroup].
640 ///
641 /// The [members] field is required.
642 const PlatformMenuItemGroup({required this.members}) : super(label: '');
643
644 /// The [PlatformMenuItem]s that are members of this menu item group.
645 ///
646 /// An assertion will be thrown if there isn't at least one member of the group.
647 @override
648 final List<PlatformMenuItem> members;
649
650 @override
651 Iterable<Map<String, Object?>> toChannelRepresentation(
652 PlatformMenuDelegate delegate, {
653 required MenuItemSerializableIdGenerator getId,
654 }) {
655 assert(members.isNotEmpty, 'There must be at least one member in a PlatformMenuItemGroup');
656 return serialize(this, delegate, getId: getId);
657 }
658
659 /// Converts the supplied object to the correct channel representation for the
660 /// 'flutter/menu' channel.
661 ///
662 /// This API is supplied so that implementers of [PlatformMenuItemGroup] can share
663 /// this implementation.
664 static Iterable<Map<String, Object?>> serialize(
665 PlatformMenuItem group,
666 PlatformMenuDelegate delegate, {
667 required MenuItemSerializableIdGenerator getId,
668 }) {
669 final List<Map<String, Object?>> result = <Map<String, Object?>>[];
670 result.add(<String, Object?>{
671 _kIdKey: getId(group),
672 _kIsDividerKey: true,
673 });
674 for (final PlatformMenuItem item in group.members) {
675 result.addAll(item.toChannelRepresentation(
676 delegate,
677 getId: getId,
678 ));
679 }
680 result.add(<String, Object?>{
681 _kIdKey: getId(group),
682 _kIsDividerKey: true,
683 });
684 return result;
685 }
686
687 @override
688 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
689 super.debugFillProperties(properties);
690 properties.add(IterableProperty<PlatformMenuItem>('members', members));
691 }
692}
693
694/// A class for [PlatformMenuItem]s that do not have submenus (as a [PlatformMenu]
695/// would), but can be selected.
696///
697/// These [PlatformMenuItem]s are the leaves of the menu item tree, and [onSelected]
698/// will be called when they are selected by clicking on them, or via an
699/// optional keyboard [shortcut].
700///
701/// See also:
702///
703/// * [PlatformMenu], a menu item that opens a submenu.
704class PlatformMenuItem with Diagnosticable {
705 /// Creates a const [PlatformMenuItem].
706 ///
707 /// The [label] attribute is required.
708 const PlatformMenuItem({
709 required this.label,
710 this.shortcut,
711 this.onSelected,
712 this.onSelectedIntent,
713 }) : assert(onSelected == null || onSelectedIntent == null, 'Only one of onSelected or onSelectedIntent may be specified');
714
715 /// The required label used for rendering the menu item.
716 final String label;
717
718 /// The optional shortcut that selects this [PlatformMenuItem].
719 ///
720 /// This shortcut is only enabled when [onSelected] is set.
721 final MenuSerializableShortcut? shortcut;
722
723 /// An optional callback that is called when this [PlatformMenuItem] is
724 /// selected.
725 ///
726 /// At most one of [onSelected] and [onSelectedIntent] may be set. If neither
727 /// field is set, this menu item will be disabled.
728 final VoidCallback? onSelected;
729
730 /// Returns a callback, if any, to be invoked if the platform menu receives a
731 /// "Menu.opened" method call from the platform for this item.
732 ///
733 /// Only items that have submenus will have this callback invoked.
734 ///
735 /// The default implementation returns null.
736 VoidCallback? get onOpen => null;
737
738 /// Returns a callback, if any, to be invoked if the platform menu receives a
739 /// "Menu.closed" method call from the platform for this item.
740 ///
741 /// Only items that have submenus will have this callback invoked.
742 ///
743 /// The default implementation returns null.
744 VoidCallback? get onClose => null;
745
746 /// An optional intent that is invoked when this [PlatformMenuItem] is
747 /// selected.
748 ///
749 /// At most one of [onSelected] and [onSelectedIntent] may be set. If neither
750 /// field is set, this menu item will be disabled.
751 final Intent? onSelectedIntent;
752
753 /// Returns all descendant [PlatformMenuItem]s of this item.
754 ///
755 /// Returns an empty list if this type of menu item doesn't have
756 /// descendants.
757 List<PlatformMenuItem> get descendants => const <PlatformMenuItem>[];
758
759 /// Returns the list of group members if this menu item is a "grouping" menu
760 /// item, such as [PlatformMenuItemGroup].
761 ///
762 /// Defaults to an empty list.
763 List<PlatformMenuItem> get members => const <PlatformMenuItem>[];
764
765 /// Converts the representation of this item into a map suitable for sending
766 /// over the default "flutter/menu" channel used by [DefaultPlatformMenuDelegate].
767 ///
768 /// The `delegate` is the [PlatformMenuDelegate] that is requesting the
769 /// serialization.
770 ///
771 /// The `getId` parameter is a [MenuItemSerializableIdGenerator] function that
772 /// generates a unique ID for each menu item, which is to be returned in the
773 /// "id" field of the menu item data.
774 Iterable<Map<String, Object?>> toChannelRepresentation(
775 PlatformMenuDelegate delegate, {
776 required MenuItemSerializableIdGenerator getId,
777 }) {
778 return <Map<String, Object?>>[PlatformMenuItem.serialize(this, delegate, getId)];
779 }
780
781 /// Converts the given [PlatformMenuItem] into a data structure accepted by
782 /// the 'flutter/menu' method channel method 'Menu.SetMenu'.
783 ///
784 /// This API is supplied so that implementers of [PlatformMenuItem] can share
785 /// this implementation.
786 static Map<String, Object?> serialize(
787 PlatformMenuItem item,
788 PlatformMenuDelegate delegate,
789 MenuItemSerializableIdGenerator getId,
790 ) {
791 final MenuSerializableShortcut? shortcut = item.shortcut;
792 return <String, Object?>{
793 _kIdKey: getId(item),
794 _kLabelKey: item.label,
795 _kEnabledKey: item.onSelected != null || item.onSelectedIntent != null,
796 if (shortcut != null)...shortcut.serializeForMenu().toChannelRepresentation(),
797 };
798 }
799
800 @override
801 String toStringShort() => '${describeIdentity(this)}($label)';
802
803 @override
804 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
805 super.debugFillProperties(properties);
806 properties.add(StringProperty('label', label));
807 properties.add(DiagnosticsProperty<MenuSerializableShortcut?>('shortcut', shortcut, defaultValue: null));
808 properties.add(FlagProperty('enabled', value: onSelected != null, ifFalse: 'DISABLED'));
809 }
810}
811
812/// A class that represents a menu item that is provided by the platform.
813///
814/// This is used to add things like the "About" and "Quit" menu items to a
815/// platform menu.
816///
817/// The [type] enum determines which type of platform defined menu will be
818/// added.
819///
820/// This is most useful on a macOS platform where there are many different types
821/// of platform provided menu items in the standard menu setup.
822///
823/// In order to know if a [PlatformProvidedMenuItem] is available on a
824/// particular platform, call [PlatformProvidedMenuItem.hasMenu].
825///
826/// If the platform does not support the given [type], then the menu item will
827/// throw an [ArgumentError] when it is sent to the platform.
828///
829/// See also:
830///
831/// * [PlatformMenuBar] which takes these items for inclusion in a
832/// platform-rendered menu bar.
833class PlatformProvidedMenuItem extends PlatformMenuItem {
834 /// Creates a const [PlatformProvidedMenuItem] of the appropriate type. Throws if the
835 /// platform doesn't support the given default menu type.
836 ///
837 /// The [type] argument is required.
838 const PlatformProvidedMenuItem({
839 required this.type,
840 this.enabled = true,
841 }) : super(label: ''); // The label is ignored for platform provided menus.
842
843 /// The type of default menu this is.
844 ///
845 /// See [PlatformProvidedMenuItemType] for the different types available. Not
846 /// all of the types will be available on every platform. Use [hasMenu] to
847 /// determine if the current platform has a given default menu item.
848 ///
849 /// If the platform does not support the given [type], then the menu item will
850 /// throw an [ArgumentError] in debug mode.
851 final PlatformProvidedMenuItemType type;
852
853 /// True if this [PlatformProvidedMenuItem] should be enabled or not.
854 final bool enabled;
855
856 /// Checks to see if the given default menu type is supported on this
857 /// platform.
858 static bool hasMenu(PlatformProvidedMenuItemType menu) {
859 switch (defaultTargetPlatform) {
860 case TargetPlatform.android:
861 case TargetPlatform.iOS:
862 case TargetPlatform.fuchsia:
863 case TargetPlatform.linux:
864 case TargetPlatform.windows:
865 return false;
866 case TargetPlatform.macOS:
867 return const <PlatformProvidedMenuItemType>{
868 PlatformProvidedMenuItemType.about,
869 PlatformProvidedMenuItemType.quit,
870 PlatformProvidedMenuItemType.servicesSubmenu,
871 PlatformProvidedMenuItemType.hide,
872 PlatformProvidedMenuItemType.hideOtherApplications,
873 PlatformProvidedMenuItemType.showAllApplications,
874 PlatformProvidedMenuItemType.startSpeaking,
875 PlatformProvidedMenuItemType.stopSpeaking,
876 PlatformProvidedMenuItemType.toggleFullScreen,
877 PlatformProvidedMenuItemType.minimizeWindow,
878 PlatformProvidedMenuItemType.zoomWindow,
879 PlatformProvidedMenuItemType.arrangeWindowsInFront,
880 }.contains(menu);
881 }
882 }
883
884 @override
885 Iterable<Map<String, Object?>> toChannelRepresentation(
886 PlatformMenuDelegate delegate, {
887 required MenuItemSerializableIdGenerator getId,
888 }) {
889 assert(() {
890 if (!hasMenu(type)) {
891 throw ArgumentError(
892 'Platform ${defaultTargetPlatform.name} has no platform provided menu for '
893 '$type. Call PlatformProvidedMenuItem.hasMenu to determine this before '
894 'instantiating one.',
895 );
896 }
897 return true;
898 }());
899
900 return <Map<String, Object?>>[
901 <String, Object?>{
902 _kIdKey: getId(this),
903 _kEnabledKey: enabled,
904 _kPlatformDefaultMenuKey: type.index,
905 },
906 ];
907 }
908
909 @override
910 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
911 super.debugFillProperties(properties);
912 properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED'));
913 }
914}
915
916/// The list of possible platform provided, prebuilt menus for use in a
917/// [PlatformMenuBar].
918///
919/// These are menus that the platform typically provides that cannot be
920/// reproduced in Flutter without calling platform functions, but are standard
921/// on the platform.
922///
923/// Examples include things like the "Quit" or "Services" menu items on macOS.
924/// Not all platforms support all menu item types. Use
925/// [PlatformProvidedMenuItem.hasMenu] to know if a particular type is supported
926/// on a the current platform.
927///
928/// Add these to your [PlatformMenuBar] using the [PlatformProvidedMenuItem]
929/// class.
930///
931/// You can tell if the platform provides the given menu using the
932/// [PlatformProvidedMenuItem.hasMenu] method.
933// Must be kept in sync with the plugin code's enum of the same name.
934enum PlatformProvidedMenuItemType {
935 /// The system provided "About" menu item.
936 ///
937 /// On macOS, this is the `orderFrontStandardAboutPanel` default menu.
938 about,
939
940 /// The system provided "Quit" menu item.
941 ///
942 /// On macOS, this is the `terminate` default menu.
943 ///
944 /// This menu item will exit the application when activated.
945 quit,
946
947 /// The system provided "Services" submenu.
948 ///
949 /// This submenu provides a list of system provided application services.
950 ///
951 /// This default menu is only supported on macOS.
952 servicesSubmenu,
953
954 /// The system provided "Hide" menu item.
955 ///
956 /// This menu item hides the application window.
957 ///
958 /// On macOS, this is the `hide` default menu.
959 ///
960 /// This default menu is only supported on macOS.
961 hide,
962
963 /// The system provided "Hide Others" menu item.
964 ///
965 /// This menu item hides other application windows.
966 ///
967 /// On macOS, this is the `hideOtherApplications` default menu.
968 ///
969 /// This default menu is only supported on macOS.
970 hideOtherApplications,
971
972 /// The system provided "Show All" menu item.
973 ///
974 /// This menu item shows all hidden application windows.
975 ///
976 /// On macOS, this is the `unhideAllApplications` default menu.
977 ///
978 /// This default menu is only supported on macOS.
979 showAllApplications,
980
981 /// The system provided "Start Dictation..." menu item.
982 ///
983 /// This menu item tells the system to start the screen reader.
984 ///
985 /// On macOS, this is the `startSpeaking` default menu.
986 ///
987 /// This default menu is currently only supported on macOS.
988 startSpeaking,
989
990 /// The system provided "Stop Dictation..." menu item.
991 ///
992 /// This menu item tells the system to stop the screen reader.
993 ///
994 /// On macOS, this is the `stopSpeaking` default menu.
995 ///
996 /// This default menu is currently only supported on macOS.
997 stopSpeaking,
998
999 /// The system provided "Enter Full Screen" menu item.
1000 ///
1001 /// This menu item tells the system to toggle full screen mode for the window.
1002 ///
1003 /// On macOS, this is the `toggleFullScreen` default menu.
1004 ///
1005 /// This default menu is currently only supported on macOS.
1006 toggleFullScreen,
1007
1008 /// The system provided "Minimize" menu item.
1009 ///
1010 /// This menu item tells the system to minimize the window.
1011 ///
1012 /// On macOS, this is the `performMiniaturize` default menu.
1013 ///
1014 /// This default menu is currently only supported on macOS.
1015 minimizeWindow,
1016
1017 /// The system provided "Zoom" menu item.
1018 ///
1019 /// This menu item tells the system to expand the window size.
1020 ///
1021 /// On macOS, this is the `performZoom` default menu.
1022 ///
1023 /// This default menu is currently only supported on macOS.
1024 zoomWindow,
1025
1026 /// The system provided "Bring To Front" menu item.
1027 ///
1028 /// This menu item tells the system to stack the window above other windows.
1029 ///
1030 /// On macOS, this is the `arrangeInFront` default menu.
1031 ///
1032 /// This default menu is currently only supported on macOS.
1033 arrangeWindowsInFront,
1034}
1035