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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com