Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Implement Material MenuBar #104673

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 43 commits into from
Closed

Conversation

gspencergoog
Copy link
Contributor

@gspencergoog gspencergoog commented May 26, 2022

Description

This implements a prototype of MenuBar widgets that can both render a Material menu bar, and speak to a bundled plugin on the engine that will create and manage system generated menu bars on macOS, Windows, and Linux (a.k.a. PlatformMenuBar, submitted already).

This implementation of the MenuBar uses a MenuBarController to manage most of the communication between widgets that need to occur to implement the menu bar. The MenuBar uses a hierarchy of MenuBarItem widgets which extend MenuItem so that they are also useful for configuring a platform provided menu.

For the Material MenuBar, MenuBarItem widgets have an internal _MenuNode assigned by looking for a wrapping _MenuNodeWrapper, and then they register attributes with that node (things like the focus node associated with the button, and the menu builder function for the submenus). The MenuBarItems update their metadata with the MenuBarController when they are rebuilt so that it knows enough to be able to render the correct menu widgets in the overlay. Widgets don't exist unless they are currently visible, but the nodes always exist.

I've tried eliminating it, but there are some cases where dirtying the controller from the widget tree can affect things on the overlay, so those updates have to happen in a post frame callback.

Related Issues

Tests

  • Many tests for all of the MenuBar operations and configuration.

Design Doc

@flutter-dashboard flutter-dashboard bot added d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos documentation f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. c: contributor-productivity Team-specific productivity, code health, technical debt. labels May 26, 2022
@gspencergoog gspencergoog force-pushed the menu_bar branch 2 times, most recently from 0f98c3e to 79b2cf3 Compare May 26, 2022 23:02
@gspencergoog gspencergoog force-pushed the menu_bar branch 2 times, most recently from aa06ab1 to 53bd65f Compare May 27, 2022 16:31
@gspencergoog
Copy link
Contributor Author

Looks like I started to implement the tests in menu_bar_theme_test.dart, but didn't actually finish. Don't let that hold up the review, but I'm going to write those and add them.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not gotten to the meat of this PR yet (the actual implementation). These are just some nits from getting familiar with the API that this implements.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a full review, will continue next week


/// The background color of the menu bar.
///
/// Defaults to [MenuBarThemeData.barBackgroundColor] if not set.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(here and elsewhere) Do we usually document what this defaults to when nothing is set in the theme?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we seem to, at least sometimes. In this case, I thought it was a good idea, since the properties have different names, and you might not know which background color it corresponds to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I meant do we usually document what default color is used if backgroundColor and MenuBarThemeData.barBackgroundColor is not set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. Should we document it here, or on the theme? On the theme it seems weird, since there isn't really a default there (other than null), so I'm not sure how I would clearly indicate that the default theme for MenuBar has some value for it.

@HansMuller What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaults should be defined and documented by component widgets, not by themes. The default value for all theme properties and for all component widget properties should be null and the implementation should compute the default as needed with something like:

Foo effectiveFoo = widget.foo ?? theme.foo ?? _defaultTheme.foo!;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. That's how they are implemented. I'll adjust the documentation to show the values from the default theme.

How have we worded that in the past? For example, is it something like this? "If unset, uses the [MenuBarTheme] value, if that is unset, defaults to XX" or do we just say "If unset, defaults to XX"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always use "If null, uses the [MenuBarTheme] value, if that is null, defaults to XX". Saying "unset" isn't quite right. It gets a little more complex for state dependent properties (MaterialStateProperty values) because it's also necessary to explain what states the property is resolved against.


/// The background color of the menu bar.
///
/// Defaults to [MenuBarThemeData.barBackgroundColor] if not set.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I meant do we usually document what default color is used if backgroundColor and MenuBarThemeData.barBackgroundColor is not set?

if (openMenu?.topLevel != null) {
if (_overlayEntry == null) {
_overlayEntry = OverlayEntry(builder: (BuildContext context) => _MenuStack(this));
Navigator.of(menuBarContext).overlay!.insert(_overlayEntry!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incidentally, why isn't that called Overlay.maybeOf?

Looks like we missed this one when we cleaned up the other of/maybeOf's.

@goderbauer
Copy link
Member

(There's a lot of unhappiness with the checks)

@gspencergoog gspencergoog force-pushed the menu_bar branch 2 times, most recently from ee2054e to 0c93cc5 Compare June 2, 2022 02:07
/// adopts a height large enough to accommodate all the children.
///
/// It is used by [MenuBarMenu] to render its child items.
class _MenuBarMenuList extends StatefulWidget {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note for myself: reviewed upto here.

@gspencergoog gspencergoog force-pushed the menu_bar branch 2 times, most recently from fad9b30 to 4ea6afb Compare June 3, 2022 00:22
Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just looked at the first example, getting my feet wet.

case MenuSelection.hideMessage:
showingMessage = false;
break;
case MenuSelection.quit:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to think carefully about how desktop applications should "quit". Most toolkits provide some support for orderly shutdown, including bugging the user about unsaved changes etc. What if we left this aspect of the demo out - users could still just close the window.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I added this so that it would be similar to the platform menu example, which uses the Apple-defined quit menu.

On mobile, popping the last route will exit the app, but doesn't appear to on desktop (we should fix that).

MenuBarMenu(
autofocus: true,
label: 'Menu App',
menus: <MenuBarItem>[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found it a little odd that the value of "menus" is a list of MenuBar_Items_. Maybe "items" would be better? For menu bars that aren't native, it seems like apps might want to include other widgets (buttons, dividers, labels) rather than just menus.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has to be that way so that I can traverse the hierarchy and collect the shortcuts that need to be handled even if the widget doesn't exist. I plan on creating a CustomMenuBarItem that takes a builder so you can draw whatever you want, but it handles the shortcut and activation functionality.

I didn't put it into this PR because it was already too huge.

autofocus: true,
label: 'Menu App',
menus: <MenuBarItem>[
MenuBarButton(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to find a way to unify this with Tab, NavigationDestination, NavigationRailDestination, PopupMenuItem, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unify in what way? Make them all be/wrap the same class?

label: MenuSelection.about.label,
onSelected: () => _activate(MenuSelection.about),
),
MenuBarButton(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful to demo a MenuBarButton that could be disabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

label: MenuSelection.colorRed.label,
shortcut: const SingleActivator(LogicalKeyboardKey.keyR, control: true),
),
MenuBarButton(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about menu item mnemonics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not yet implemented. There are localized, adaptive, shortcut labels derived from the shortcut, however.


/// The background color of the menu bar.
///
/// Defaults to [MenuBarThemeData.barBackgroundColor] if not set.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always use "If null, uses the [MenuBarTheme] value, if that is null, defaults to XX". Saying "unset" isn't quite right. It gets a little more complex for state dependent properties (MaterialStateProperty values) because it's also necessary to explain what states the property is resolved against.

Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still in the middle of all of this.

Please imagine that I'd prefaced each comment with "In Hans's humble opinion".

I still haven't ready all of the code and I'm continuing to have trouble unifying this whole thing with the need for cascading menus in general, outside of menu bars.

// select whether we want Ctrl or Meta for the modifier key on our
// shortcuts.
final bool isAppleOS;
switch (defaultTargetPlatform) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be a static utility method somewhere. It's likely to be very common in portable desktop apps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know.. We already have Platform and defaultTargetPlatform available. I don't think adding isAppleOS as a static clears things up much.

I could also use Platform.isMacOS || Platform.isIOS instead, but that's less testable.

members: <MenuBarItem>[
MenuBarButton(
label: MenuSelection.open.label,
shortcut: SingleActivator(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if SingleActivator provided a constructor parameter that meant "choose meta if iOS and macOS, control otherwise". CC @dkwingsmt

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the problem has been raised in #83733 (comment). I once attempted to draft a design doc but realized there are more things we might want to cover than to swap just Ctrl and Cmd, so I didn't proceed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the design was open-ended, a sort-of shortcut "localization, maybe we could provide the feature and then extend its scope later.

import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('Menu contains the right things', (WidgetTester tester) async {
Copy link
Contributor

@HansMuller HansMuller Jun 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing the menu items and shortcuts would be easy if you updated the value of a Text widget in _onSelected(). Probably more useful than debugPrint for an example too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! Done, and for the other example too.

import 'menu_bar.dart';
import 'theme.dart';

/// Defines the visual properties of [MenuBar], [MenuBarMenu] and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This theme includes some properties that are specific to menu bars, but it seems to mostly be about menus. Maybe what we really need is MenuTheme with a few menu bar properties? Or maybe I've misunderstood what this theme is for; however I don't see a separate menu theme.

We need to explain how this theme relates to PopupMenus and PopupMenuTheme. Surely they're not completely different animals?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with calling this MenuTheme. Just didn't want to overstep my mandate. :-)

///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theme information => theme

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (other.runtimeType != runtimeType) {
return false;
}
return other is MenuBarThemeData &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conventional formatting for this kind of thing is like:

    return other is ChipThemeData
        && other.backgroundColor == backgroundColor
        && other.deleteIconColor == deleteIconColor
        ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I know. Once the changes settle down a bit I'll make a formatting pass to normalize the formatting before submitting.


/// A menu bar with cascading child menus.
///
/// This is a Material Design menu bar that resides above the main body of an
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't it really go anywhere at all? Maybe replace "that resides" with "that typically resides".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, now it can. The comment is just out of date.

///
/// This is a Material Design menu bar that resides above the main body of an
/// application that defines a menu system for invoking callbacks or firing
/// [Intent]s in response to user selection of the menu item.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of the menu item => of a menu item

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// application that defines a menu system for invoking callbacks or firing
/// [Intent]s in response to user selection of the menu item.
///
/// The menu can be navigated by the user using the arrow keys, and can be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the user using the arrow keys => by using the arrow keys. It can be

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// Menu items can have a [SingleActivator] or [CharacterActivator] assigned to
/// them as their [MenuBarButton.shortcut], so that if the shortcut key sequence
/// is pressed, the menu item corresponding to that shortcut will be selected
/// even if the menu is closed. Shortcuts must be unique in the ambient
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the menu => its menu

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +2242 to +2245
final double horizontalPadding = math.max(
_kLabelItemMinSpacing,
_kLabelItemDefaultSpacing + density.horizontal * 2,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of this, would it be possible to customize it? Different people might do horizontalPadding differently on different platforms, and having the visualDensity there adds some "randomness" that's not easy for users to "fix" if they don't like it. Since it is used for everything, I think it is too important to not be user configurable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can set the density to a fixed value, so it's not really "random", but I know what you mean. Sure, I'll see if I can find a good way to make it configurable.

@gspencergoog
Copy link
Contributor Author

Closing in favor of #109179

@flutter-dashboard flutter-dashboard bot added a: tests "flutter test", flutter_test, or one of our tests f: focus Focus traversal, gaining or losing focus labels Oct 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: tests "flutter test", flutter_test, or one of our tests c: contributor-productivity Team-specific productivity, code health, technical debt. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: focus Focus traversal, gaining or losing focus f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants