-
Notifications
You must be signed in to change notification settings - Fork 28.7k
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
Implement Material MenuBar #104673
Conversation
0f98c3e
to
79b2cf3
Compare
aa06ab1
to
53bd65f
Compare
Looks like I started to implement the tests in |
There was a problem hiding this 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.
There was a problem hiding this 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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!;
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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!); |
There was a problem hiding this comment.
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.
(There's a lot of unhappiness with the checks) |
ee2054e
to
0c93cc5
Compare
/// adopts a height large enough to accommodate all the children. | ||
/// | ||
/// It is used by [MenuBarMenu] to render its child items. | ||
class _MenuBarMenuList extends StatefulWidget { |
There was a problem hiding this comment.
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.
fad9b30
to
4ea6afb
Compare
There was a problem hiding this 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>[ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
theme information => theme
There was a problem hiding this comment.
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 && |
There was a problem hiding this comment.
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
...
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the menu => its menu
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
final double horizontalPadding = math.max( | ||
_kLabelItemMinSpacing, | ||
_kLabelItemDefaultSpacing + density.horizontal * 2, | ||
); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Closing in favor of #109179 |
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 aMenuBarController
to manage most of the communication between widgets that need to occur to implement the menu bar. TheMenuBar
uses a hierarchy ofMenuBarItem
widgets which extendMenuItem
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). TheMenuBarItem
s update their metadata with theMenuBarController
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
MenuBar
operations and configuration.Design Doc