Skip to content

Conversation

@gspencergoog
Copy link
Contributor

@gspencergoog gspencergoog commented Aug 8, 2022

Description

This implements a MenuBar widget that can render a Material menu bar, and a createMaterialMenu function 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 MenuBar uses the public global function createMaterialMenu to create a cascading menu that contains widgets representing the menu items. These menu items can be any kind of widget, but are typically SubmenuButtons that host submenus, or MenuItemButtons that have shortcut hints (but don't actually activate the shortcuts) and don't host submenus.

Cascading menus can be created outside of a MenuBar by calling createMaterialMenu. They can be either given a specific location to appear (global coordinate), or they can be located by a new MenuAnchor widget that wraps the control that opens them.

The developer may also create a MenuController to pass to the various menu primitives (MenuBar or createMaterialMenu) 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:

  • Accelerators
  • Showing the "selection trail" for selected menus. Parent menus should show as selected when a child is highlighted.
  • Creating a horizontal menu bar that isn't a MenuBar. This is technically possible in the API, but _MenuPanel isn't public, and to write your own requires access to an internal type to set the orientation. Easy solution is to make _MenuPanel public, but I want to wait until there's a use case.
  • A checkbox menu item that automatically shows and manages its own checkbox. You can technically do it right now with the prefix icon, but it's not very clean.
  • Opening the menu initially with the keyboard (e.g. the Alt key on Linux/Windows, Ctrl-F2 on macOS).

Related Issues

Tests

  • Many tests for all of the menu operations and configurations

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: focus Focus traversal, gaining or losing focus 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 Aug 8, 2022
@gspencergoog gspencergoog changed the title Menu bar iv Implement Material MenuBar Aug 8, 2022
@gspencergoog gspencergoog force-pushed the menu_bar_iv branch 4 times, most recently from 872829a to 71acbb0 Compare August 11, 2022 02:02
@flutter-dashboard flutter-dashboard bot added the f: cupertino flutter/packages/flutter/cupertino repository label Aug 11, 2022
@gspencergoog gspencergoog force-pushed the menu_bar_iv branch 2 times, most recently from 7f8243e to 906860c Compare August 11, 2022 16:10
@gspencergoog gspencergoog marked this pull request as ready for review August 15, 2022 16:50
@gspencergoog gspencergoog force-pushed the menu_bar_iv branch 5 times, most recently from 1fe5420 to 3cd0e1a Compare August 16, 2022 23:25
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.

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.

@gspencergoog gspencergoog force-pushed the menu_bar_iv branch 3 times, most recently from ffa65e8 to de0dccc Compare August 18, 2022 20:07
Copy link
Contributor Author

@gspencergoog gspencergoog left a 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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@gspencergoog gspencergoog force-pushed the menu_bar_iv branch 2 times, most recently from cbf873d to 6fa7d97 Compare September 14, 2022 02:13
@gspencergoog
Copy link
Contributor Author

One thing that isn't clear to me (I probably just missed it in the 8000+ lines 😝): Why do we still need the button rect to position the menu? I thought the LayerLink stuff would take care of most of that...

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 LayerLink takes care of when the view is resized or scrolled after it is positioned, but it doesn't know if the menu is near an edge or not. If you have a menu up and scroll and get the menu too close to an edge, it won't relocate the menu (it'll just scroll off the edge), but that's an edge case that we probably don't need to worry about.

@goderbauer
Copy link
Member

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 LayerLink takes care of when the view is resized or scrolled after it is positioned, but it doesn't know if the menu is near an edge or not. If you have a menu up and scroll and get the menu too close to an edge, it won't relocate the menu (it'll just scroll off the edge), but that's an edge case that we probably don't need to worry about.

Can you document this (and the limitation you describe) in the code somewhere? This may be useful for a future reader.

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.

LGTM

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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?

Copy link
Member

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?

Copy link
Member

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?

@goderbauer
Copy link
Member

goderbauer commented Sep 15, 2022

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...

@gspencergoog
Copy link
Contributor Author

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 create_material_menu.1.dart example shows.

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 createMaterialMenu or a MenuAnchor if you use MenuBar or SubmenuButton directly. The only time you need the lower level API is if you want to have a menu on something other than a plain button, or want to specify the opening location yourself.

However, we might be able to change the MenuAnchor so that it has an API closer to what you propose, because I do like that the menu description appears to be part of the widget tree in the same way as MenuBar. I worry a little that there will be ways in which it won't act the same, and thus be surprising, since it's actually drawn in the Overlay, at least until we get OverlayPortal (I'm mainly thinking of cases where they begin invalidating the widgets at the wrong time in the build).

If we eliminate createMaterialMenu and move its attributes to MenuAnchor, but still allow an optional global position to be specified by the open(), maybe that is sufficient?

I am a bit sad about not committing this yet...but I also don't want to remove API mere days after introducing it.

@goderbauer
Copy link
Member

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 create_material_menu.1.dart example shows.

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 .open call could take an optional offset into the boundingbox defined by the MenuAnchor to position it based on a PointerEvent.

@gspencergoog
Copy link
Contributor Author

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.

@gspencergoog
Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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: cupertino flutter/packages/flutter/cupertino repository 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.

5 participants