-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Implement Material MenuBar #109179
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 #109179
Conversation
872829a to
71acbb0
Compare
7f8243e to
906860c
Compare
1fe5420 to
3cd0e1a
Compare
HansMuller
left a comment
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 all looks good; most of the nit-picking is to prove that I read the code.
Just a progress report, this is a big PR.
ffa65e8 to
de0dccc
Compare
2dc1e8a to
0936969
Compare
gspencergoog
left a comment
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.
Still need to separate out the unrelated MaterialStateProperty changes.
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.
When state in this widget changes, then we have to rebuild the menu, so that lines like:
showingMessage ? MenuSelection.hideMessage.label : MenuSelection.showMessage.label,update, based on the value of showingMessage. Since it needs to happen any time state changes, it seemed to make sense to do it in build.
If the menu definition here wasn't dependent upon state in this widget, it wouldn't need to be rebuilt.
Since menus can have regular widgets, if the menu definition incorporates an AnimatedBuilder then it would rebuild just the items that changed (but will have to listen to this state object). I'll give that a try.
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.
MenuHandles are for individual menus. MenuControllers allow you to link otherwise unrelated menus so that you can close them all at once and tab between them when they are open.
I thought about making MenuController just be the root MenuHandle (it basically is anyhow), but then I wasn't sure what it meant to "open" a menu that already existed and can't be "closed", as you would be able to do for a MenuBar that you gave one of these to, so I kept the MenuController a separate API, since then I can avoid having an "open" on the MenuController, and MenuController.closeAll can mean something different from MenuHandle.close. They do essentially the same thing, but the menu controller doesn't "close" the menu bar or menu button it is attached to (since it can't).
I can merge the two concepts, though, if you think it's enough to just document that close can't close menu bars or buttons that have a MenuAnchor, and the root open has no effect on them.
cbf873d to
6fa7d97
Compare
We still need the button rect because the custom layout needs to be able to position the menu by looking at the global coordinates of the menu button: it needs to know if the menu is too close to the edge of the view or not. The |
8ee5484 to
e01e367
Compare
Can you document this (and the limitation you describe) in the code somewhere? This may be useful for a future reader. |
goderbauer
left a comment
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.
LGTM
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 do you need the DiagnosticableTreeMixin for? I am a little afraid that this introduces bugs similar to what we saw with the TapRegion.
You can override debugDescribeChildren and debugFillProperties without adding this mixin.
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 believe this little nit is still unaddressed:
I noticed below that depending on how the menu is opened, different contexts are used. Sometimes its the Builder context, sometimes its the GlobalKey context. Maybe we can unify that since I believe there is no meaning in those differences?
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.
Maybe document that this MenuAnchor is looked up via the provided context.
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.
Maybe document in general what the context is for?
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 if it is given a different context - which could result in a different position?
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.
Maybe it would be easier to define that an open menu cannot be opend again?
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.
Isn't _MenuHandleMarker an inheritedwidget that could/should be looked up via one of the getFoo methods instead of findFoo?
|
Just as I stamped this LGTM I actually had one more idea that may improve the API and may make it easier to integrate this with OverlayPortal and/or multi window in the future. I think it would do away with MenuHandle and createMaterialMenu altogether and would avoid passing contexts around. Rough API sketch: final MenuController controller = MenuController();
@override
Widget build(BuildContext context) {
return MenuAnchor(
controller: controller,
// ... other config options that would have been passed to createMaterialMenu before
menuBuilder: (BuildContext context) {
return <Widget>[
MenuItemButton(...),
SubMenuButton(...),
// ...
],
},
child: TextButton(
onPressed: () {
if (!controller.isOpen) {
// maybe open needs to take an optional offset param to allow specifying where within the MenuAnchor's bounding box the menu should be shown...
controller.open();
} else {
controller.close();
}
},
child: Text('Toggle menu'),
)
);
}Basically, the menu is always anchored at the MenuAnchor. It's context is used for positioning (so you want to wrap it tightly e.g. around a button or whatever UI element is supposed to be aligned with the menu). Now the widgets that make up the menu entries actually look like they are part of the widget tree and when we move menues over to OverlayPortal or multi window it is immediately obvious that they can access all inherited widgets above the MenuAnchor, because that's where they are anchored. Also, I think this will make it easier to move to OverlayPortal later (and multi window as well) because for both you kinda need an anchor in the widget tree. It also simplifies the API by removing concepts like MenuHandle and createMaterialMenu from the public API. This is similar to how Tooltips already work today (except that you get a controller to decide when to bring up the menu) and parallels the API we are designing for OverlayPortal and multi window. Curious to hear what you think... |
This might make it easier to integrate, but it disables one of the use cases that we can currently allow: creating context menus that are placed with a global position (often from a pointer position, not a widget position), which is what the Also, unless you need the global positioning use case, we already have most of what you're talking about: you don't need to use However, we might be able to change the If we eliminate I am a bit sad about not committing this yet...but I also don't want to remove API mere days after introducing it. |
For that my thinking was that you'd wrap the entire area in which you want the context menu to appear in the MenuAnchor and then the |
31d3ea1 to
f4fbb71
Compare
|
Michael and I talked about this, and I'm going to give it a try and see if I can refine the API surface to match his idea more closely. |
|
Closing in favor of #112239 since GitHub is just getting too slow to be able to handle this PR with more comments. Moving to a new PR in the hope that it'll be faster without the history, and also to preserve this iteration. |
Description
This implements a
MenuBarwidget that can render a Material menu bar, and acreateMaterialMenufunction to create a cascading menu anywhere. The menus are drawn in the overlay, while the menu bar itself is in the regular widget tree. Keyboard traversal works between the two.This implementation of the
MenuBaruses the public global functioncreateMaterialMenuto create a cascading menu that contains widgets representing the menu items. These menu items can be any kind of widget, but are typicallySubmenuButtons that host submenus, orMenuItemButtons that have shortcut hints (but don't actually activate the shortcuts) and don't host submenus.Cascading menus can be created outside of a
MenuBarby callingcreateMaterialMenu. They can be either given a specific location to appear (global coordinate), or they can be located by a newMenuAnchorwidget that wraps the control that opens them.The developer may also create a
MenuControllerto pass to the various menu primitives (MenuBarorcreateMaterialMenu) to associate menus so that they can be traversed together and closed together. Creating a controller is not required.Things that are not implemented in this PR, but will need to be:
MenuBar. This is technically possible in the API, but_MenuPanelisn't public, and to write your own requires access to an internal type to set the orientation. Easy solution is to make_MenuPanelpublic, but I want to wait until there's a use case.Related Issues
Tests
Design Doc