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 | |
5 | import 'dart:async'; |
6 | |
7 | import 'package:flutter/foundation.dart'; |
8 | import 'package:flutter/services.dart'; |
9 | |
10 | import 'actions.dart'; |
11 | import 'basic.dart'; |
12 | import 'binding.dart'; |
13 | import 'focus_manager.dart'; |
14 | import 'framework.dart'; |
15 | import 'shortcuts.dart'; |
16 | |
17 | // "flutter/menu" Method channel methods. |
18 | const String _kMenuSetMethod = 'Menu.setMenus' ; |
19 | const String _kMenuSelectedCallbackMethod = 'Menu.selectedCallback' ; |
20 | const String _kMenuItemOpenedMethod = 'Menu.opened' ; |
21 | const String _kMenuItemClosedMethod = 'Menu.closed' ; |
22 | |
23 | // Keys for channel communication map. |
24 | const String _kIdKey = 'id' ; |
25 | const String _kLabelKey = 'label' ; |
26 | const String _kEnabledKey = 'enabled' ; |
27 | const String _kChildrenKey = 'children' ; |
28 | const String _kIsDividerKey = 'isDivider' ; |
29 | const String _kPlatformDefaultMenuKey = 'platformProvidedMenu' ; |
30 | const String _kShortcutCharacter = 'shortcutCharacter' ; |
31 | const String _kShortcutTrigger = 'shortcutTrigger' ; |
32 | const 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. |
44 | class 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. |
171 | mixin 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. |
206 | abstract 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]. |
265 | typedef 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. |
281 | class 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 | /// ``` |
433 | class 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 | |
471 | class _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]. |
528 | class 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. |
638 | class 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. |
704 | class 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. |
833 | class 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. |
934 | enum 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 | |