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( |
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. |
176 | mixin 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. |
211 | abstract 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]. |
270 | typedef 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. |
286 | class 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 | /// ``` |
438 | class 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 | |
474 | class _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]. |
534 | class 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. |
640 | class 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. |
696 | class 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. |
830 | class 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. |
929 | enum 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 |
Definitions
- _kMenuSetMethod
- _kMenuSelectedCallbackMethod
- _kMenuItemOpenedMethod
- _kMenuItemClosedMethod
- _kIdKey
- _kLabelKey
- _kEnabledKey
- _kChildrenKey
- _kIsDividerKey
- _kPlatformDefaultMenuKey
- _kShortcutCharacter
- _kShortcutTrigger
- _kShortcutModifiers
- ShortcutSerialization
- character
- modifier
- trigger
- character
- alt
- control
- meta
- shift
- toChannelRepresentation
- MenuSerializableShortcut
- serializeForMenu
- PlatformMenuDelegate
- PlatformMenuDelegate
- setMenus
- clearMenus
- debugLockDelegate
- debugUnlockDelegate
- DefaultPlatformMenuDelegate
- DefaultPlatformMenuDelegate
- clearMenus
- setMenus
- _getId
- debugLockDelegate
- debugUnlockDelegate
- _methodCallHandler
- PlatformMenuBar
- PlatformMenuBar
- createState
- debugDescribeChildren
- _PlatformMenuBarState
- initState
- dispose
- didUpdateWidget
- _updateMenu
- build
- PlatformMenu
- PlatformMenu
- descendants
- getDescendants
- toChannelRepresentation
- serialize
- debugDescribeChildren
- debugFillProperties
- PlatformMenuItemGroup
- PlatformMenuItemGroup
- toChannelRepresentation
- serialize
- debugFillProperties
- PlatformMenuItem
- PlatformMenuItem
- onOpen
- onClose
- descendants
- members
- toChannelRepresentation
- serialize
- toStringShort
- debugFillProperties
- PlatformProvidedMenuItem
- PlatformProvidedMenuItem
- hasMenu
- toChannelRepresentation
- debugFillProperties
Learn more about Flutter for embedded and desktop on industrialflutter.com