diff --git a/examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart b/examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart index 8d988439e98..2914ced97f5 100644 --- a/examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart +++ b/examples/api/lib/material/platform_menu_bar/platform_menu_bar.0.dart @@ -126,7 +126,7 @@ class _MyMenuBarAppState extends State { ], ), ], - body: Center( + child: Center( child: Text(_showMessage ? _message : 'This space intentionally left blank.\n' diff --git a/packages/flutter/lib/src/widgets/platform_menu_bar.dart b/packages/flutter/lib/src/widgets/platform_menu_bar.dart index d9a3bd9739e..77794f537dd 100644 --- a/packages/flutter/lib/src/widgets/platform_menu_bar.dart +++ b/packages/flutter/lib/src/widgets/platform_menu_bar.dart @@ -7,7 +7,10 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'actions.dart'; +import 'basic.dart'; import 'binding.dart'; +import 'focus_manager.dart'; import 'framework.dart'; import 'shortcuts.dart'; @@ -44,6 +47,7 @@ class ShortcutSerialization { /// This is used by a [CharacterActivator] to serialize itself. ShortcutSerialization.character(String character) : _internal = {_kShortcutCharacter: character}, + _character = character, assert(character.length == 1); /// Creates a [ShortcutSerialization] representing a specific @@ -70,6 +74,11 @@ class ShortcutSerialization { trigger != LogicalKeyboardKey.metaRight, 'Specifying a modifier key as a trigger is not allowed. ' 'Use provided boolean parameters instead.'), + _trigger = trigger, + _control = control, + _shift = shift, + _alt = alt, + _meta = meta, _internal = { _kShortcutTrigger: trigger.keyId, _kShortcutModifiers: (control ? _shortcutModifierControl : 0) | @@ -80,6 +89,35 @@ class ShortcutSerialization { final Map _internal; + /// The keyboard key that triggers this shortcut, if any. + LogicalKeyboardKey? get trigger => _trigger; + LogicalKeyboardKey? _trigger; + + /// The character that triggers this shortcut, if any. + String? get character => _character; + String? _character; + + /// If this shortcut has a [trigger], this indicates whether or not the + /// control modifier needs to be down or not. + bool? get control => _control; + bool? _control; + + /// If this shortcut has a [trigger], this indicates whether or not the + /// shift modifier needs to be down or not. + bool? get shift => _shift; + bool? _shift; + + /// If this shortcut has a [trigger], this indicates whether or not the + /// alt modifier needs to be down or not. + bool? get alt => _alt; + bool? _alt; + + /// If this shortcut has a [trigger], this indicates whether or not the meta + /// (also known as the Windows or Command key) modifier needs to be down or + /// not. + bool? get meta => _meta; + bool? _meta; + /// The bit mask for the [LogicalKeyboardKey.meta] key (or it's left/right /// equivalents) being down. static const int _shortcutModifierMeta = 1 << 0; @@ -124,10 +162,13 @@ mixin MenuSerializableShortcut { /// An abstract class for describing cascading menu hierarchies that are part of /// a [PlatformMenuBar]. /// -/// This type is used by [PlatformMenuDelegate.setMenus] to accept the menu +/// This type is also used by [PlatformMenuDelegate.setMenus] to accept the menu /// hierarchy to be sent to the platform, and by [PlatformMenuBar] to define the /// menu hierarchy. /// +/// This class is abstract, and so can't be used directly. Typically subclasses +/// like [PlatformMenuItem] are used. +/// /// See also: /// /// * [PlatformMenuBar], a widget that renders menu items using platform APIs @@ -141,8 +182,8 @@ abstract class MenuItem with Diagnosticable { /// /// The `delegate` is the [PlatformMenuDelegate] that is requesting the /// serialization. The `index` is the position of this menu item in the list - /// of children of the [PlatformMenu] it belongs to, and `count` is the number - /// of children in the [PlatformMenu] it belongs to. + /// of [menus] of the [PlatformMenu] it belongs to, and `count` is the number + /// of [menus] in the [PlatformMenu] it belongs to. /// /// The `getId` parameter is a [MenuItemSerializableIdGenerator] function that /// generates a unique ID for each menu item, which is to be returned in the @@ -152,6 +193,17 @@ abstract class MenuItem with Diagnosticable { required MenuItemSerializableIdGenerator getId, }); + /// The optional shortcut that selects this [MenuItem]. + /// + /// This shortcut is only enabled when [onSelected] is set. + MenuSerializableShortcut? get shortcut => null; + + /// Returns any child [MenuItem]s of this item. + /// + /// Returns an empty list if this type of menu item doesn't have + /// children. + List get menus => const []; + /// Returns all descendant [MenuItem]s of this item. /// /// Returns an empty list if this type of menu item doesn't have @@ -163,9 +215,27 @@ abstract class MenuItem with Diagnosticable { /// /// Only items that do not have submenus will have this callback invoked. /// + /// Only one of [onSelected] or [onSelectedIntent] may be specified. + /// + /// If neither [onSelected] nor [onSelectedIntent] are specified, then this + /// menu item is considered to be disabled. + /// /// The default implementation returns null. VoidCallback? get onSelected => null; + /// Returns an intent, if any, to be invoked if the platform receives a + /// "Menu.selectedCallback" method call from the platform for this item. + /// + /// Only items that do not have submenus will have this intent invoked. + /// + /// Only one of [onSelected] or [onSelectedIntent] may be specified. + /// + /// If neither [onSelected] nor [onSelectedIntent] are specified, then this + /// menu item is considered to be disabled. + /// + /// The default implementation returns null. + Intent? get onSelectedIntent => null; + /// Returns a callback, if any, to be invoked if the platform menu receives a /// "Menu.opened" method call from the platform for this item. /// @@ -181,6 +251,12 @@ abstract class MenuItem with Diagnosticable { /// /// The default implementation returns null. VoidCallback? get onClose => null; + + /// Returns the list of group members if this menu item is a "grouping" menu + /// item, such as [PlatformMenuItemGroup]. + /// + /// Defaults to an empty list. + List get members => const []; } /// An abstract delegate class that can be used to set @@ -383,7 +459,12 @@ class DefaultPlatformMenuDelegate extends PlatformMenuDelegate { } final MenuItem item = _idMap[id]!; if (call.method == _kMenuSelectedCallbackMethod) { + assert(item.onSelected == null || item.onSelectedIntent == null, + 'Only one of MenuItem.onSelected or MenuItem.onSelectedIntent may be specified'); item.onSelected?.call(); + if (item.onSelectedIntent != null) { + Actions.maybeInvoke(FocusManager.instance.primaryFocus!.context!, item.onSelectedIntent!); + } } else if (call.method == _kMenuItemOpenedMethod) { item.onOpen?.call(); } else if (call.method == _kMenuItemClosedMethod) { @@ -407,7 +488,7 @@ class DefaultPlatformMenuDelegate extends PlatformMenuDelegate { /// the platform menu bar. /// /// As far as Flutter is concerned, this widget has no visual representation, -/// and intercepts no events: it just returns the [body] from its build +/// and intercepts no events: it just returns the [child] from its build /// function. This is because all of the rendering, shortcuts, and event /// handling for the menu is handled by the plugin on the host platform. /// @@ -429,18 +510,32 @@ class DefaultPlatformMenuDelegate extends PlatformMenuDelegate { class PlatformMenuBar extends StatefulWidget with DiagnosticableTreeMixin { /// Creates a const [PlatformMenuBar]. /// - /// The [body] and [menus] attributes are required. + /// The [child] and [menus] attributes are required. const PlatformMenuBar({ super.key, - required this.body, required this.menus, - }); + this.child, + @Deprecated( + 'Use the child attribute instead. ' + 'This feature was deprecated after v3.1.0-0.0.pre.' + ) + this.body, + }) : assert(body == null || child == null, + 'The body argument is deprecated, and only one of body or child may be used.'); - /// The widget to be rendered in the Flutter window that these platform menus - /// are associated with. + /// The widget below this widget in the tree. /// - /// This is typically the body of the application's UI. - final Widget body; + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget? child; + + /// The widget below this widget in the tree. + /// + /// This attribute is deprecated, use [child] instead. + @Deprecated( + 'Use the child attribute instead. ' + 'This feature was deprecated after v3.1.0-0.0.pre.' + ) + final Widget? body; /// The list of menu items that are the top level children of the /// [PlatformMenuBar]. @@ -512,7 +607,7 @@ class _PlatformMenuBarState extends State { Widget build(BuildContext context) { // PlatformMenuBar is really about managing the platform menu bar, and // doesn't do any rendering or event handling in Flutter. - return widget.body; + return widget.child ?? widget.body ?? const SizedBox(); } } @@ -547,6 +642,7 @@ class PlatformMenu extends MenuItem with DiagnosticableTreeMixin { /// The menu items in the submenu opened by this menu item. /// /// If this is an empty list, this [PlatformMenu] will be disabled. + @override final List menus; /// Returns all descendant [MenuItem]s of this item. @@ -646,6 +742,7 @@ class PlatformMenuItemGroup extends MenuItem { /// The [MenuItem]s that are members of this menu item group. /// /// An assertion will be thrown if there isn't at least one member of the group. + @override final List members; @override @@ -654,19 +751,32 @@ class PlatformMenuItemGroup extends MenuItem { required MenuItemSerializableIdGenerator getId, }) { assert(members.isNotEmpty, 'There must be at least one member in a PlatformMenuItemGroup'); + return serialize(this, delegate, getId: getId); + } + + /// Converts the supplied object to the correct channel representation for the + /// 'flutter/menu' channel. + /// + /// This API is supplied so that implementers of [PlatformMenuItemGroup] can share + /// this implementation. + static Iterable> serialize( + MenuItem group, + PlatformMenuDelegate delegate, { + required MenuItemSerializableIdGenerator getId, + }) { final List> result = >[]; result.add({ - _kIdKey: getId(this), + _kIdKey: getId(group), _kIsDividerKey: true, }); - for (final MenuItem item in members) { + for (final MenuItem item in group.members) { result.addAll(item.toChannelRepresentation( delegate, getId: getId, )); } result.add({ - _kIdKey: getId(this), + _kIdKey: getId(group), _kIsDividerKey: true, }); return result; @@ -697,7 +807,8 @@ class PlatformMenuItem extends MenuItem { required this.label, this.shortcut, this.onSelected, - }); + this.onSelectedIntent, + }) : assert(onSelected == null || onSelectedIntent == null, 'Only one of onSelected or onSelectedIntent may be specified'); /// The required label used for rendering the menu item. final String label; @@ -705,6 +816,7 @@ class PlatformMenuItem extends MenuItem { /// The optional shortcut that selects this [PlatformMenuItem]. /// /// This shortcut is only enabled when [onSelected] is set. + @override final MenuSerializableShortcut? shortcut; /// An optional callback that is called when this [PlatformMenuItem] is @@ -714,6 +826,13 @@ class PlatformMenuItem extends MenuItem { @override final VoidCallback? onSelected; + /// An optional intent that is invoked when this [PlatformMenuItem] is + /// selected. + /// + /// If unset, this menu item will be disabled. + @override + final Intent? onSelectedIntent; + @override Iterable> toChannelRepresentation( PlatformMenuDelegate delegate, { @@ -741,6 +860,9 @@ class PlatformMenuItem extends MenuItem { }; } + @override + String toStringShort() => '${describeIdentity(this)}($label)'; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -779,9 +901,7 @@ class PlatformProvidedMenuItem extends PlatformMenuItem { const PlatformProvidedMenuItem({ required this.type, this.enabled = true, - }) : super( - label: '', // The label is ignored for standard menus. - ); + }) : super(label: ''); // The label is ignored for platform provided menus. /// The type of default menu this is. /// @@ -832,7 +952,7 @@ class PlatformProvidedMenuItem extends PlatformMenuItem { assert(() { if (!hasMenu(type)) { throw ArgumentError( - 'Platform ${defaultTargetPlatform.name} has no standard menu for ' + 'Platform ${defaultTargetPlatform.name} has no platform provided menu for ' '$type. Call PlatformProvidedMenuItem.hasMenu to determine this before ' 'instantiating one.', ); @@ -856,7 +976,8 @@ class PlatformProvidedMenuItem extends PlatformMenuItem { } } -/// The list of possible standard, prebuilt menus for use in a [PlatformMenuBar]. +/// The list of possible platform provided, prebuilt menus for use in a +/// [PlatformMenuBar]. /// /// These are menus that the platform typically provides that cannot be /// reproduced in Flutter without calling platform functions, but are standard @@ -870,7 +991,7 @@ class PlatformProvidedMenuItem extends PlatformMenuItem { /// Add these to your [PlatformMenuBar] using the [PlatformProvidedMenuItem] /// class. /// -/// You can tell if the platform supports the given standard menu using the +/// You can tell if the platform provides the given menu using the /// [PlatformProvidedMenuItem.hasMenu] method. // Must be kept in sync with the plugin code's enum of the same name. enum PlatformProvidedMenuItemType { diff --git a/packages/flutter/test/widgets/platform_menu_bar_test.dart b/packages/flutter/test/widgets/platform_menu_bar_test.dart index cd0a02c9e53..b8ec130f91e 100644 --- a/packages/flutter/test/widgets/platform_menu_bar_test.dart +++ b/packages/flutter/test/widgets/platform_menu_bar_test.dart @@ -51,7 +51,6 @@ void main() { MaterialApp( home: Material( child: PlatformMenuBar( - body: const Center(child: Text('Body')), menus: createTestMenus( onActivate: onActivate, onOpen: onOpen, @@ -63,6 +62,7 @@ void main() { subSubMenu10[3]: const SingleActivator(LogicalKeyboardKey.keyD, meta: true), }, ), + child: const Center(child: Text('Body')), ), ), ), @@ -163,11 +163,11 @@ void main() { const MaterialApp( home: Material( child: PlatformMenuBar( - body: PlatformMenuBar( - body: SizedBox(), - menus: [], - ), menus: [], + child: PlatformMenuBar( + menus: [], + child: SizedBox(), + ), ), ), ), @@ -180,8 +180,8 @@ void main() { shortcut: SingleActivator(LogicalKeyboardKey.keyA), ); const PlatformMenuBar menuBar = PlatformMenuBar( - body: SizedBox(), menus: [item], + child: SizedBox(), ); await tester.pumpWidget( @@ -197,7 +197,7 @@ void main() { menuBar.toStringDeep(), equalsIgnoringHashCodes( 'PlatformMenuBar#00000\n' - ' └─PlatformMenuItem#00000\n' + ' └─PlatformMenuItem#00000(label2)\n' ' label: "label2"\n' ' shortcut: SingleActivator#00000(keys: Key A)\n' ' DISABLED\n',