Unverified Commit 2d9ad260 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Implements a PlatformMenuBar widget and associated data structures (#100274)

Implements a PlatformMenuBar widget and associated data structures for defining menu bars that use native APIs for rendering.

This PR includes:
A PlatformMenuBar class, which is a widget that menu bar data can be attached to for sending to the platform.
A PlatformMenuDelegate base, which is the type taken by a new WidgetsBinding.platformMenuDelegate.
An implementation of the above in DefaultPlatformMenuDelegate that talks to the built-in "flutter/menu" channel to talk to the built-in platform implementation. The delegate is so that a plugin could override with its own delegate and provide other platforms with native menu support using the same widgets to define the menus.
This is the framework part of the implementation. The engine part will be in flutter/engine#32080 (and flutter/engine#32358)
parent 2af2c9a6
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for PlatformMenuBar
////////////////////////////////////
// THIS SAMPLE ONLY WORKS ON MACOS.
////////////////////////////////////
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const SampleApp());
enum MenuSelection {
about,
showMessage,
}
class SampleApp extends StatelessWidget {
const SampleApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(body: MyMenuBarApp()),
);
}
}
class MyMenuBarApp extends StatefulWidget {
const MyMenuBarApp({Key? key}) : super(key: key);
@override
State<MyMenuBarApp> createState() => _MyMenuBarAppState();
}
class _MyMenuBarAppState extends State<MyMenuBarApp> {
String _message = 'Hello';
bool _showMessage = false;
void _handleMenuSelection(MenuSelection value) {
switch (value) {
case MenuSelection.about:
showAboutDialog(
context: context,
applicationName: 'MenuBar Sample',
applicationVersion: '1.0.0',
);
break;
case MenuSelection.showMessage:
setState(() {
_showMessage = !_showMessage;
});
break;
}
}
@override
Widget build(BuildContext context) {
////////////////////////////////////
// THIS SAMPLE ONLY WORKS ON MACOS.
////////////////////////////////////
// This builds a menu hierarchy that looks like this:
// Flutter API Sample
// ├ About
// ├ ──────── (group divider)
// ├ Hide/Show Message
// ├ Messages
// │ ├ I am not throwing away my shot.
// │ └ There's a million things I haven't done, but just you wait.
// └ Quit
return PlatformMenuBar(
menus: <MenuItem>[
PlatformMenu(
label: 'Flutter API Sample',
menus: <MenuItem>[
PlatformMenuItemGroup(
members: <MenuItem>[
PlatformMenuItem(
label: 'About',
onSelected: () {
_handleMenuSelection(MenuSelection.about);
},
)
],
),
PlatformMenuItemGroup(
members: <MenuItem>[
PlatformMenuItem(
onSelected: () {
_handleMenuSelection(MenuSelection.showMessage);
},
shortcut: const CharacterActivator('m'),
label: _showMessage ? 'Hide Message' : 'Show Message',
),
PlatformMenu(
label: 'Messages',
menus: <MenuItem>[
PlatformMenuItem(
label: 'I am not throwing away my shot.',
shortcut: const SingleActivator(LogicalKeyboardKey.digit1, meta: true),
onSelected: () {
setState(() {
_message = 'I am not throwing away my shot.';
});
},
),
PlatformMenuItem(
label: "There's a million things I haven't done, but just you wait.",
shortcut: const SingleActivator(LogicalKeyboardKey.digit2, meta: true),
onSelected: () {
setState(() {
_message = "There's a million things I haven't done, but just you wait.";
});
},
),
],
)
],
),
if (PlatformProvidedMenuItem.hasMenu(PlatformProvidedMenuItemType.quit))
const PlatformProvidedMenuItem(type: PlatformProvidedMenuItemType.quit),
],
),
],
body: Center(
child: Text(_showMessage
? _message
: 'This space intentionally left blank.\n'
'Show a message here using the menu.'),
),
);
}
}
......@@ -28,7 +28,7 @@ class ChildLayoutHelper {
/// This method calls [RenderBox.getDryLayout] on the given [RenderBox].
///
/// This method should only be called by the parent of the provided
/// [RenderBox] child as it bounds parent and child together (if the child
/// [RenderBox] child as it binds parent and child together (if the child
/// is marked as dirty, the child will also be marked as dirty).
///
/// See also:
......@@ -46,7 +46,7 @@ class ChildLayoutHelper {
/// `parentUsesSize` set to true to receive its [Size].
///
/// This method should only be called by the parent of the provided
/// [RenderBox] child as it bounds parent and child together (if the child
/// [RenderBox] child as it binds parent and child together (if the child
/// is marked as dirty, the child will also be marked as dirty).
///
/// See also:
......
......@@ -392,4 +392,53 @@ class SystemChannels {
'flutter/localization',
JSONMethodCodec(),
);
/// A [MethodChannel] for platform menu specification and control.
///
/// The following outgoing method is defined for this channel (invoked using
/// [OptionalMethodChannel.invokeMethod]):
///
/// * `Menu.setMenu`: sends the configuration of the platform menu, including
/// labels, enable/disable information, and unique integer identifiers for
/// each menu item. The configuration is sent as a `Map<String, Object?>`
/// encoding the list of top level menu items in window "0", which each
/// have a hierarchy of `Map<String, Object?>` containing the required
/// data, sent via a [StandardMessageCodec]. It is typically generated from
/// a list of [MenuItem]s, and ends up looking like this example:
///
/// ```dart
/// List<Map<String, Object?>> menu = <String, Object?>{
/// '0': <Map<String, Object?>>[
/// <String, Object?>{
/// 'id': 1,
/// 'label': 'First Menu Label',
/// 'enabled': true,
/// 'children': <Map<String, Object?>>[
/// <String, Object?>{
/// 'id': 2,
/// 'label': 'Sub Menu Label',
/// 'enabled': true,
/// },
/// ],
/// },
/// ],
/// };
/// ```
///
/// The following incoming methods are defined for this channel (registered
/// using [MethodChannel.setMethodCallHandler]).
///
/// * `Menu.selectedCallback`: Called when a menu item is selected, along
/// with the unique ID of the menu item selected.
///
/// * `Menu.opened`: Called when a submenu is opened, along with the unique
/// ID of the submenu.
///
/// * `Menu.closed`: Called when a submenu is closed, along with the unique
/// ID of the submenu.
///
/// See also:
///
/// * [DefaultPlatformMenuDelegate], which uses this channel.
static const MethodChannel menu = OptionalMethodChannel('flutter/menu');
}
......@@ -16,6 +16,7 @@ import 'app.dart';
import 'debug.dart';
import 'focus_manager.dart';
import 'framework.dart';
import 'platform_menu_bar.dart';
import 'router.dart';
import 'widget_inspector.dart';
......@@ -294,6 +295,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
FlutterErrorDetails.propertiesTransformers.add(debugTransformDebugCreator);
return true;
}());
platformMenuDelegate = DefaultPlatformMenuDelegate();
}
/// The current [WidgetsBinding], if one has been created.
......@@ -523,6 +525,13 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// See [FocusManager] for more details.
FocusManager get focusManager => _buildOwner!.focusManager;
/// A delegate that communicates with a platform plugin for serializing and
/// managing platform-rendered menu bars created by [PlatformMenuBar].
///
/// This is set by default to a [DefaultPlatformMenuDelegate] instance in
/// [initInstances].
late PlatformMenuDelegate platformMenuDelegate;
final List<WidgetsBindingObserver> _observers = <WidgetsBindingObserver>[];
/// Registers the given object as a binding observer. Binding
......
This diff is collapsed.
......@@ -12,6 +12,7 @@ import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'inherited_notifier.dart';
import 'platform_menu_bar.dart';
/// A set of [KeyboardKey]s that can be used as the keys in a [Map].
///
......@@ -397,7 +398,7 @@ class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Int
///
/// * [CharacterActivator], an activator that represents key combinations
/// that result in the specified character, such as question mark.
class SingleActivator with Diagnosticable implements ShortcutActivator {
class SingleActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
/// Triggered when the [trigger] key is pressed while the modifiers are held.
///
/// The `trigger` should be the non-modifier key that is pressed after all the
......@@ -517,6 +518,17 @@ class SingleActivator with Diagnosticable implements ShortcutActivator {
&& (meta == (pressed.contains(LogicalKeyboardKey.metaLeft) || pressed.contains(LogicalKeyboardKey.metaRight)));
}
@override
ShortcutSerialization serializeForMenu() {
return ShortcutSerialization.modifier(
trigger,
shift: shift,
alt: alt,
meta: meta,
control: control,
);
}
/// Returns a short and readable description of the key combination.
///
/// Intended to be used in debug mode for logging purposes. In release mode,
......@@ -572,7 +584,7 @@ class SingleActivator with Diagnosticable implements ShortcutActivator {
///
/// * [SingleActivator], an activator that represents a single key combined
/// with modifiers, such as `Ctrl+C`.
class CharacterActivator with Diagnosticable implements ShortcutActivator {
class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
/// Create a [CharacterActivator] from the triggering character.
const CharacterActivator(this.character);
......@@ -608,6 +620,11 @@ class CharacterActivator with Diagnosticable implements ShortcutActivator {
return result;
}
@override
ShortcutSerialization serializeForMenu() {
return ShortcutSerialization.character(character);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
......
......@@ -83,6 +83,7 @@ export 'src/widgets/page_view.dart';
export 'src/widgets/pages.dart';
export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart';
export 'src/widgets/platform_menu_bar.dart';
export 'src/widgets/platform_view.dart';
export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart';
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment