| 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 | |