ui/views/controls/menu/README.md
This document outlines how menus are implemented in Views. You should probably read the Views overview if you haven't yet.
[TOC]
Conceptually, client code uses Views menus like so:
MenuRunner runner(model, ...);
runner.RunMenuAt(...);
The constructor of MenuRunner does little actual work; it is primarily concerned with choosing an appropriate MenuRunnerImplInterface depending on the platform and the requested type of menu. This document will mostly focus on MenuRunnerImpl, which runs a Views menu; the only other MenuRunnerImplInterface implementation, named MenuRunnerImplCocoa, runs a native Mac menu instead which has extremely different behavior.
The call to MenuRunnerImpl::RunMenuAt is responsible for determining which
MenuController will control this menu invocation, which may involve either
nesting the menu off an existing parent menu or cancelling an existing menu
first. In this context, "nesting" specifically means a fully separate menu that
runs while another menu is still running. Here's an example menu:
Item 1
Item 1.1
Item 1.2
Item 2
Item 3
Mousing over Item 1 would open the submenu containing Items 1.1 and 1.2, but that submenu is controlled by the same MenuController instance as the original menu containing Item 1. However, if the user right-clicked on Item 2 to open its context menu, that would create a nested MenuController to run the context menu. At any given time, only one MenuController can be active, since menus are conceptually modal.
Once that's done, MenuController::Run takes over.
That method has many responsibilities, but explaining them will require a brief detour into the structure of MenuController itself.
MenuController is the "uber class" of the Views menu system. It has a large amount of state and nearly all interesting logic is delegated to it, but the most important things it stores are:
The MenuController also contains the logic to allow dragging within menus, which is used in bookmark menus to support reordering items.
Back to MenuController::Run: after setting up some state, this method invokes
MenuController::SetSelection. That method is responsible for changing the
selection from the current selected MenuItemView (which may be nullptr) to a
provided new MenuItemView (which again may be nullptr), which involves closing
and opening submenus as needed and notifying accessibility events up and down
the menu tree. For example, if the current menu is:
A [open]
A1
A2 [open]
A2.1
A2.2 [selected]
A3
A3.1
and the new selected node is A3.1, MenuController::SetSelection would be
responsible for:
Note that, if the selection is not immediate, this fills the pending selection but not the actual selection until later, to allow for menus to animate out and in a bit; if this isn't done, menus flicker in and out as the mouse moves over multiple items with submenus.
When the selection is immediate, as it is when invoked by
MenuController::Run, SetSelection will end up opening the root menu for this
MenuController (via MenuController::CommitPendingSelection). This method
handles actually closing and opening menus as needed to make the pending
selection visible. Since there is no existing open menu during the initial call,
practically this calls MenuController::OpenMenu on the first selectable item
in the menu.
That method, in turn, ultimately calls SubmenuView::ShowAt on the root
MenuItemView's submenu, which is the root submenu. That method constructs a
MenuHost, which is a special kind of Widget that contains a SubmenuView. The
MenuHost is responsible for:
Once MenuHost::InitMenuHost and MenuHost::ShowMenuHost are done, the menu is
on screen!
Once a menu is on screen, its view tree looks like this:
MenuHost (Widget)
MenuHostRootView
MenuItemView (root)
MenuScrollViewContainer
SubmenuView
MenuItemView
MenuItemView
...
None of these have any visuals except the MenuItemView, which contains:
None of these things are separate Views - instead they are drawn directly by
MenuItemView::OnPaint, so the painting step for MenuItemView is also the
layout step.
MenuController centralizes input event handling for menus. Key events enter MenuController from multiple sources:
MenuController::OnWillDispatchKeyEvent is one of the entry points to this logic, but MenuController::OnKeyPressed handles all of the navigation and functional keys, and MenuController::SelectByChar implements incremental searching and menu accelerators.
Mouse events are mostly handled via the normal Views flow, except that pre-target handlers can deliver mouse events to MenuController that did not actually target the menu's widget, which MenuController uses to close itself in response to those events.
Events can also be "reposted", where the menu decides to dismiss itself in response to them and then propagate them up to the menu's parent widget. This behavior is platform-specific and tricky, so it's best to read the code to understand it.
TODO(ellyjones): How does this work?
TODO(ellyjones): How does this work?
Unlike most other UI code, menus decouple their lifetimes from that of the
Widgets containing them as best they can by marking the members displayed
in Widgets by using View::set_owned_by_client(). The below diagram gives an
overview of the ownership relationships between the key menu classes.
┌──────────────────────────┐
│ MenuHost : Widget │
│ │
│ │
│ │
└──┬───────────────────────┘
│
│ Owns 1
│
┌──────────────────────────┐Raw pointer ┌──────────────────────────┐ ┌──▼───────────────────────┐
│ ui::MenuModel ├──────────────► MenuModelAdapter │ │ MenuHostRootView : View │
│ │ │ │ │ │
│ │Raw pointer │ Implements MenuDelegate, │ │ │
│ ◄──────────────┤ ui::MenuModelDelegate. │ │ │
└──────────────────────────┘ └───▲──────────────────────┘ └──┬───────────────────────┘
│ │
│ Raw pointer to │ Contains, but does not
│ MenuDelegate. │ own
┌──────────────────────────┐ │ ┌──▼───────────────────────┐
│ MenuRunner │ │ │ MenuScrollViewContainer :│
│ │ │ │ View │
│ │ │ │ │
│ │ │ │ Is client-owned. │
└──┬───────────────────────┘ │ └──▲────────────┬──────────┘
│ Owns (de facto) │ │ │
│ │ │Owns 1 │ Contains
│ │ │ │
┌──▼───────────────────────┐ Owns 1 ┌───┴──────────────────────┐ Owns 0 to 1 ┌──┴────────────▼──────────┐ Owns n ┌──────────────────────────┐
│ MenuRunnerImpl ├──────────────► MenuItemView : View ├──────────────► SubMenuView : View ├────────────► View (including ├───► Continue
│ │ Owns n to n │ │ │ │ │ MenuItemView) │ recursively
│ ├───────────┐ │ The main menu. │ │ │ │ │ (tree of
│ │ │ │ ├───────────┐ │ Is client-owned. │ │ │ submenus)
└──┬───────────────────────┘ │ └──┬───────────────────────┘ Owns n │ └──────────────────────────┘ └──────────────────────────┘
│ Creates and deletes Weak ptr. │ (as views│
│ (de facto) ┌───────┬─────┘ children)│
│ │ │ │
┌──▼───────────────────────┐ │ │ ┌──────────────────────────┐ │ ┌──────────────────────────┐
│ MenuController ◄───┘ └──► MenuItemView : View │ └──► View │
│ │ │ │ │ │
│ Singleton, at most one │ │ Sibling menus (relevant ├───┐ │ │
│ instance active globally.│ │ for drag & drop). │ │ │ │
└──────────────────────────┘ └──────────────────────────┘ │ └──────────────────────────┘
│
Same type of
children as
main menu