1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/views/controls/menu/menu_item_view.h"
6 
7 #include <math.h>
8 #include <stddef.h>
9 
10 #include <algorithm>
11 #include <memory>
12 #include <numeric>
13 
14 #include "base/containers/adapters.h"
15 #include "base/i18n/case_conversion.h"
16 #include "base/macros.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "ui/accessibility/ax_action_data.h"
19 #include "ui/accessibility/ax_enums.mojom.h"
20 #include "ui/accessibility/ax_node_data.h"
21 #include "ui/base/l10n/l10n_util.h"
22 #include "ui/base/models/menu_model.h"
23 #include "ui/base/ui_base_features.h"
24 #include "ui/events/base_event_utils.h"
25 #include "ui/events/keycodes/dom/dom_code.h"
26 #include "ui/gfx/animation/animation.h"
27 #include "ui/gfx/canvas.h"
28 #include "ui/gfx/color_utils.h"
29 #include "ui/gfx/geometry/rect.h"
30 #include "ui/gfx/geometry/vector2d.h"
31 #include "ui/gfx/image/image.h"
32 #include "ui/gfx/paint_vector_icon.h"
33 #include "ui/gfx/text_utils.h"
34 #include "ui/native_theme/native_theme.h"
35 #include "ui/strings/grit/ui_strings.h"
36 #include "ui/views/controls/button/menu_button.h"
37 #include "ui/views/controls/image_view.h"
38 #include "ui/views/controls/menu/menu_config.h"
39 #include "ui/views/controls/menu/menu_controller.h"
40 #include "ui/views/controls/menu/menu_image_util.h"
41 #include "ui/views/controls/menu/menu_scroll_view_container.h"
42 #include "ui/views/controls/menu/menu_separator.h"
43 #include "ui/views/controls/menu/submenu_view.h"
44 #include "ui/views/controls/separator.h"
45 #include "ui/views/style/typography.h"
46 #include "ui/views/vector_icons.h"
47 #include "ui/views/view_class_properties.h"
48 #include "ui/views/widget/widget.h"
49 
50 namespace views {
51 
52 namespace {
53 
54 // EmptyMenuMenuItem ---------------------------------------------------------
55 
56 // EmptyMenuMenuItem is used when a menu has no menu items. EmptyMenuMenuItem
57 // is itself a MenuItemView, but it uses a different ID so that it isn't
58 // identified as a MenuItemView.
59 
60 class EmptyMenuMenuItem : public MenuItemView {
61  public:
EmptyMenuMenuItem(MenuItemView * parent)62   explicit EmptyMenuMenuItem(MenuItemView* parent)
63       : MenuItemView(parent, 0, Type::kEmpty) {
64     // Set this so that we're not identified as a normal menu item.
65     SetID(kEmptyMenuItemViewID);
66     SetTitle(l10n_util::GetStringUTF16(IDS_APP_MENU_EMPTY_SUBMENU));
67     SetEnabled(false);
68   }
69 
GetTooltipText(const gfx::Point & p) const70   base::string16 GetTooltipText(const gfx::Point& p) const override {
71     // Empty menu items shouldn't have a tooltip.
72     return base::string16();
73   }
74 
75  private:
76   DISALLOW_COPY_AND_ASSIGN(EmptyMenuMenuItem);
77 };
78 
79 }  // namespace
80 
81 // Padding between child views.
82 static constexpr int kChildXPadding = 8;
83 
84 // MenuItemView ---------------------------------------------------------------
85 
86 // static
87 const int MenuItemView::kMenuItemViewID = 1001;
88 
89 // static
90 const int MenuItemView::kEmptyMenuItemViewID =
91     MenuItemView::kMenuItemViewID + 1;
92 
93 // static
94 int MenuItemView::icon_area_width_ = 0;
95 
96 // static
97 int MenuItemView::label_start_;
98 
99 // static
100 int MenuItemView::item_right_margin_;
101 
102 // static
103 int MenuItemView::pref_menu_height_;
104 
MenuItemView(MenuDelegate * delegate)105 MenuItemView::MenuItemView(MenuDelegate* delegate) : delegate_(delegate) {
106   // NOTE: don't check the delegate for NULL, UpdateMenuPartSizes() supplies a
107   // NULL delegate.
108   Init(nullptr, 0, Type::kSubMenu);
109 }
110 
ChildPreferredSizeChanged(View * child)111 void MenuItemView::ChildPreferredSizeChanged(View* child) {
112   invalidate_dimensions();
113   PreferredSizeChanged();
114 }
115 
GetTooltipText(const gfx::Point & p) const116 base::string16 MenuItemView::GetTooltipText(const gfx::Point& p) const {
117   if (!tooltip_.empty())
118     return tooltip_;
119 
120   if (type_ == Type::kSeparator)
121     return base::string16();
122 
123   const MenuController* controller = GetMenuController();
124   if (!controller ||
125       controller->exit_type() != MenuController::ExitType::kNone) {
126     // Either the menu has been closed or we're in the process of closing the
127     // menu. Don't attempt to query the delegate as it may no longer be valid.
128     return base::string16();
129   }
130 
131   const MenuItemView* root_menu_item = GetRootMenuItem();
132   if (root_menu_item->canceled_) {
133     // TODO(sky): if |canceled_| is true, controller->exit_type() should be
134     // something other than ExitType::kNone, but crash reports seem to indicate
135     // otherwise. Figure out why this is needed.
136     return base::string16();
137   }
138 
139   const MenuDelegate* delegate = GetDelegate();
140   CHECK(delegate);
141   gfx::Point location(p);
142   ConvertPointToScreen(this, &location);
143   return delegate->GetTooltipText(command_, location);
144 }
145 
GetAccessibleNodeData(ui::AXNodeData * node_data)146 void MenuItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
147   // Set the role based on the type of menu item.
148   switch (type_) {
149     case Type::kCheckbox:
150       node_data->role = ax::mojom::Role::kMenuItemCheckBox;
151       break;
152     case Type::kRadio:
153       node_data->role = ax::mojom::Role::kMenuItemRadio;
154       break;
155     default:
156       node_data->role = ax::mojom::Role::kMenuItem;
157       break;
158   }
159 
160   base::string16 item_text;
161   if (IsContainer()) {
162     // The first child is taking over, just use its accessible name instead of
163     // |title_|.
164     View* child = children().front();
165     ui::AXNodeData child_node_data;
166     child->GetAccessibleNodeData(&child_node_data);
167     item_text =
168         child_node_data.GetString16Attribute(ax::mojom::StringAttribute::kName);
169   } else {
170     item_text = title_;
171   }
172   node_data->SetName(GetAccessibleNameForMenuItem(item_text, GetMinorText()));
173 
174   switch (type_) {
175     case Type::kSubMenu:
176     case Type::kActionableSubMenu:
177       node_data->SetHasPopup(ax::mojom::HasPopup::kMenu);
178       break;
179     case Type::kCheckbox:
180     case Type::kRadio: {
181       const bool is_checked = GetDelegate()->IsItemChecked(GetCommand());
182       node_data->SetCheckedState(is_checked ? ax::mojom::CheckedState::kTrue
183                                             : ax::mojom::CheckedState::kFalse);
184     } break;
185     case Type::kTitle:
186     case Type::kNormal:
187     case Type::kSeparator:
188     case Type::kEmpty:
189     case Type::kHighlighted:
190       // No additional accessibility states currently for these menu states.
191       break;
192   }
193 
194   base::char16 mnemonic = GetMnemonic();
195   if (mnemonic != '\0') {
196     node_data->AddStringAttribute(
197         ax::mojom::StringAttribute::kKeyShortcuts,
198         base::UTF16ToUTF8(base::string16(1, mnemonic)));
199   }
200 }
201 
HandleAccessibleAction(const ui::AXActionData & action_data)202 bool MenuItemView::HandleAccessibleAction(const ui::AXActionData& action_data) {
203   if (action_data.action != ax::mojom::Action::kDoDefault)
204     return View::HandleAccessibleAction(action_data);
205 
206   // kDoDefault in View would simulate a mouse click in the center of this
207   // MenuItemView. However, mouse events for menus are dispatched via
208   // Widget::SetCapture() to the MenuController rather than to MenuItemView, so
209   // there is no effect. VKEY_RETURN provides a better UX anyway, since it will
210   // move focus to a submenu.
211   ui::KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_RETURN, ui::DomCode::ENTER,
212                      ui::EF_NONE, ui::DomKey::ENTER, ui::EventTimeForNow());
213   GetMenuController()->SetSelection(this, MenuController::SELECTION_DEFAULT);
214   GetMenuController()->OnWillDispatchKeyEvent(&event);
215   return true;
216 }
217 
218 // static
IsBubble(MenuAnchorPosition anchor)219 bool MenuItemView::IsBubble(MenuAnchorPosition anchor) {
220   return anchor == MenuAnchorPosition::kBubbleAbove ||
221          anchor == MenuAnchorPosition::kBubbleLeft ||
222          anchor == MenuAnchorPosition::kBubbleRight;
223 }
224 
225 // static
GetAccessibleNameForMenuItem(const base::string16 & item_text,const base::string16 & minor_text)226 base::string16 MenuItemView::GetAccessibleNameForMenuItem(
227     const base::string16& item_text,
228     const base::string16& minor_text) {
229   base::string16 accessible_name = item_text;
230 
231   // Filter out the "&" for accessibility clients.
232   size_t index = 0;
233   const base::char16 amp = '&';
234   while ((index = accessible_name.find(amp, index)) != base::string16::npos &&
235          index + 1 < accessible_name.length()) {
236     accessible_name.replace(index, accessible_name.length() - index,
237                             accessible_name.substr(index + 1));
238 
239     // Special case for "&&" (escaped for "&").
240     if (accessible_name[index] == '&')
241       ++index;
242   }
243 
244   // Append subtext.
245   if (!minor_text.empty()) {
246     accessible_name.push_back(' ');
247     accessible_name.append(minor_text);
248   }
249 
250   return accessible_name;
251 }
252 
Cancel()253 void MenuItemView::Cancel() {
254   if (controller_ && !canceled_) {
255     canceled_ = true;
256     controller_->Cancel(MenuController::ExitType::kAll);
257   }
258 }
259 
AddMenuItemAt(int index,int item_id,const base::string16 & label,const base::string16 & minor_text,const ui::ThemedVectorIcon & minor_icon,const gfx::ImageSkia & icon,const ui::ThemedVectorIcon & vector_icon,Type type,ui::MenuSeparatorType separator_style)260 MenuItemView* MenuItemView::AddMenuItemAt(
261     int index,
262     int item_id,
263     const base::string16& label,
264     const base::string16& minor_text,
265     const ui::ThemedVectorIcon& minor_icon,
266     const gfx::ImageSkia& icon,
267     const ui::ThemedVectorIcon& vector_icon,
268     Type type,
269     ui::MenuSeparatorType separator_style) {
270   DCHECK_NE(type, Type::kEmpty);
271   DCHECK_GE(index, 0);
272   if (!submenu_)
273     CreateSubmenu();
274   DCHECK_LE(size_t{index}, submenu_->children().size());
275   if (type == Type::kSeparator) {
276     submenu_->AddChildViewAt(std::make_unique<MenuSeparator>(separator_style),
277                              index);
278     return nullptr;
279   }
280   MenuItemView* item = new MenuItemView(this, item_id, type);
281   if (label.empty() && GetDelegate())
282     item->SetTitle(GetDelegate()->GetLabel(item_id));
283   else
284     item->SetTitle(label);
285   item->SetMinorText(minor_text);
286   item->SetMinorIcon(minor_icon);
287   if (!vector_icon.empty()) {
288     DCHECK(icon.isNull());
289     item->SetIcon(vector_icon);
290   }
291   if (!icon.isNull())
292     item->SetIcon(icon);
293   if (type == Type::kSubMenu || type == Type::kActionableSubMenu)
294     item->CreateSubmenu();
295   if (type == Type::kHighlighted) {
296     const MenuConfig& config = MenuConfig::instance();
297     item->SetMargins(config.footnote_vertical_margin,
298                      config.footnote_vertical_margin);
299   }
300   if (GetDelegate() && !GetDelegate()->IsCommandVisible(item_id))
301     item->SetVisible(false);
302   return submenu_->AddChildViewAt(item, index);
303 }
304 
RemoveMenuItem(View * item)305 void MenuItemView::RemoveMenuItem(View* item) {
306   DCHECK(item);
307   DCHECK(submenu_);
308   DCHECK_EQ(submenu_, item->parent());
309   removed_items_.push_back(item);
310   submenu_->RemoveChildView(item);
311 }
312 
RemoveAllMenuItems()313 void MenuItemView::RemoveAllMenuItems() {
314   DCHECK(submenu_);
315 
316   removed_items_.insert(removed_items_.end(), submenu_->children().begin(),
317                         submenu_->children().end());
318 
319   submenu_->RemoveAllChildViews(false);
320 }
321 
AppendMenuItem(int item_id,const base::string16 & label,const gfx::ImageSkia & icon)322 MenuItemView* MenuItemView::AppendMenuItem(int item_id,
323                                            const base::string16& label,
324                                            const gfx::ImageSkia& icon) {
325   return AppendMenuItemImpl(item_id, label, icon, Type::kNormal);
326 }
327 
AppendSubMenu(int item_id,const base::string16 & label,const gfx::ImageSkia & icon)328 MenuItemView* MenuItemView::AppendSubMenu(int item_id,
329                                           const base::string16& label,
330                                           const gfx::ImageSkia& icon) {
331   return AppendMenuItemImpl(item_id, label, icon, Type::kSubMenu);
332 }
333 
AppendSeparator()334 void MenuItemView::AppendSeparator() {
335   AppendMenuItemImpl(0, base::string16(), gfx::ImageSkia(), Type::kSeparator);
336 }
337 
AddSeparatorAt(int index)338 void MenuItemView::AddSeparatorAt(int index) {
339   AddMenuItemAt(index, /*item_id=*/0, /*label=*/base::string16(),
340                 /*minor_text=*/base::string16(),
341                 /*minor_icon=*/ui::ThemedVectorIcon(),
342                 /*icon=*/gfx::ImageSkia(),
343                 /*vector_icon=*/ui::ThemedVectorIcon(),
344                 /*type=*/Type::kSeparator,
345                 /*separator_style=*/ui::NORMAL_SEPARATOR);
346 }
347 
AppendMenuItemImpl(int item_id,const base::string16 & label,const gfx::ImageSkia & icon,Type type)348 MenuItemView* MenuItemView::AppendMenuItemImpl(int item_id,
349                                                const base::string16& label,
350                                                const gfx::ImageSkia& icon,
351                                                Type type) {
352   const int index = submenu_ ? int{submenu_->children().size()} : 0;
353   return AddMenuItemAt(index, item_id, label, base::string16(),
354                        ui::ThemedVectorIcon(), icon, ui::ThemedVectorIcon(),
355                        type, ui::NORMAL_SEPARATOR);
356 }
357 
CreateSubmenu()358 SubmenuView* MenuItemView::CreateSubmenu() {
359   if (!submenu_) {
360     submenu_ = new SubmenuView(this);
361 
362     // Initialize the submenu indicator icon (arrow).
363     submenu_arrow_image_view_ = AddChildView(std::make_unique<ImageView>());
364   }
365 
366   return submenu_;
367 }
368 
HasSubmenu() const369 bool MenuItemView::HasSubmenu() const {
370   return (submenu_ != nullptr);
371 }
372 
GetSubmenu() const373 SubmenuView* MenuItemView::GetSubmenu() const {
374   return submenu_;
375 }
376 
SubmenuIsShowing() const377 bool MenuItemView::SubmenuIsShowing() const {
378   return HasSubmenu() && GetSubmenu()->IsShowing();
379 }
380 
SetTitle(const base::string16 & title)381 void MenuItemView::SetTitle(const base::string16& title) {
382   title_ = title;
383   invalidate_dimensions();  // Triggers preferred size recalculation.
384 }
385 
SetMinorText(const base::string16 & minor_text)386 void MenuItemView::SetMinorText(const base::string16& minor_text) {
387   minor_text_ = minor_text;
388   invalidate_dimensions();  // Triggers preferred size recalculation.
389 }
390 
SetMinorIcon(const ui::ThemedVectorIcon & minor_icon)391 void MenuItemView::SetMinorIcon(const ui::ThemedVectorIcon& minor_icon) {
392   minor_icon_ = minor_icon;
393   invalidate_dimensions();  // Triggers preferred size recalculation.
394 }
395 
SetSelected(bool selected)396 void MenuItemView::SetSelected(bool selected) {
397   selected_ = selected;
398   SchedulePaint();
399 }
400 
SetSelectionOfActionableSubmenu(bool submenu_area_of_actionable_submenu_selected)401 void MenuItemView::SetSelectionOfActionableSubmenu(
402     bool submenu_area_of_actionable_submenu_selected) {
403   DCHECK_EQ(Type::kActionableSubMenu, type_);
404   if (submenu_area_of_actionable_submenu_selected_ ==
405       submenu_area_of_actionable_submenu_selected) {
406     return;
407   }
408 
409   submenu_area_of_actionable_submenu_selected_ =
410       submenu_area_of_actionable_submenu_selected;
411   SchedulePaint();
412 }
413 
SetTooltip(const base::string16 & tooltip,int item_id)414 void MenuItemView::SetTooltip(const base::string16& tooltip, int item_id) {
415   MenuItemView* item = GetMenuItemByID(item_id);
416   DCHECK(item);
417   item->tooltip_ = tooltip;
418 }
419 
SetIcon(const gfx::ImageSkia & icon,int item_id)420 void MenuItemView::SetIcon(const gfx::ImageSkia& icon, int item_id) {
421   MenuItemView* item = GetMenuItemByID(item_id);
422   DCHECK(item);
423   item->SetIcon(icon);
424 }
425 
SetIcon(const gfx::ImageSkia & icon)426 void MenuItemView::SetIcon(const gfx::ImageSkia& icon) {
427   vector_icon_.clear();
428 
429   if (icon.isNull()) {
430     SetIconView(nullptr);
431     return;
432   }
433 
434   auto icon_view = std::make_unique<ImageView>();
435   icon_view->SetImage(&icon);
436   SetIconView(std::move(icon_view));
437 }
438 
SetIcon(const ui::ThemedVectorIcon & icon)439 void MenuItemView::SetIcon(const ui::ThemedVectorIcon& icon) {
440   vector_icon_ = icon;
441 }
442 
UpdateIconViewFromVectorIconAndTheme()443 void MenuItemView::UpdateIconViewFromVectorIconAndTheme() {
444   if (vector_icon_.empty())
445     return;
446 
447   if (!icon_view_)
448     SetIconView(std::make_unique<ImageView>());
449 
450   const bool use_touchable_layout =
451       GetMenuController() && GetMenuController()->use_touchable_layout();
452   const int icon_size = use_touchable_layout ? 20 : 16;
453   icon_view_->SetImage(vector_icon_.GetImageSkia(GetNativeTheme(), icon_size));
454 }
455 
SetIconView(std::unique_ptr<ImageView> icon_view)456 void MenuItemView::SetIconView(std::unique_ptr<ImageView> icon_view) {
457   if (icon_view_) {
458     RemoveChildViewT(icon_view_);
459     icon_view_ = nullptr;
460   }
461 
462   if (icon_view)
463     icon_view_ = AddChildView(std::move(icon_view));
464 
465   InvalidateLayout();
466   SchedulePaint();
467 }
468 
OnPaint(gfx::Canvas * canvas)469 void MenuItemView::OnPaint(gfx::Canvas* canvas) {
470   PaintButton(canvas, PaintButtonMode::kNormal);
471 }
472 
CalculatePreferredSize() const473 gfx::Size MenuItemView::CalculatePreferredSize() const {
474   const MenuItemDimensions& dimensions(GetDimensions());
475   return gfx::Size(dimensions.standard_width + dimensions.children_width,
476                    dimensions.height);
477 }
478 
GetHeightForWidth(int width) const479 int MenuItemView::GetHeightForWidth(int width) const {
480   // If this isn't a container, we can just use the preferred size's height.
481   if (!IsContainer())
482     return GetPreferredSize().height();
483 
484   const gfx::Insets margins = GetContainerMargins();
485   int height = children().front()->GetHeightForWidth(width - margins.width());
486   if (!icon_view_ && GetRootMenuItem()->has_icons())
487     height = std::max(height, MenuConfig::instance().check_height);
488 
489   height += margins.height();
490 
491   return height;
492 }
493 
OnThemeChanged()494 void MenuItemView::OnThemeChanged() {
495   View::OnThemeChanged();
496   UpdateIconViewFromVectorIconAndTheme();
497 }
498 
GetSubmenuAreaOfActionableSubmenu() const499 gfx::Rect MenuItemView::GetSubmenuAreaOfActionableSubmenu() const {
500   DCHECK_EQ(Type::kActionableSubMenu, type_);
501   const MenuConfig& config = MenuConfig::instance();
502   return gfx::Rect(gfx::Point(vertical_separator_->bounds().right(), 0),
503                    gfx::Size(config.actionable_submenu_width, height()));
504 }
505 
GetDimensions() const506 const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() const {
507   if (!is_dimensions_valid())
508     dimensions_ = CalculateDimensions();
509   DCHECK(is_dimensions_valid());
510   return dimensions_;
511 }
512 
GetMenuController()513 MenuController* MenuItemView::GetMenuController() {
514   return GetRootMenuItem()->controller_.get();
515 }
516 
GetMenuController() const517 const MenuController* MenuItemView::GetMenuController() const {
518   return GetRootMenuItem()->controller_.get();
519 }
520 
GetDelegate()521 MenuDelegate* MenuItemView::GetDelegate() {
522   return GetRootMenuItem()->delegate_;
523 }
524 
GetDelegate() const525 const MenuDelegate* MenuItemView::GetDelegate() const {
526   return GetRootMenuItem()->delegate_;
527 }
528 
GetRootMenuItem()529 MenuItemView* MenuItemView::GetRootMenuItem() {
530   return const_cast<MenuItemView*>(
531       static_cast<const MenuItemView*>(this)->GetRootMenuItem());
532 }
533 
GetRootMenuItem() const534 const MenuItemView* MenuItemView::GetRootMenuItem() const {
535   const MenuItemView* item = this;
536   for (const MenuItemView* parent = GetParentMenuItem(); parent;
537        parent = item->GetParentMenuItem())
538     item = parent;
539   return item;
540 }
541 
GetMnemonic()542 base::char16 MenuItemView::GetMnemonic() {
543   if (!GetRootMenuItem()->has_mnemonics_ ||
544       !MenuConfig::instance().use_mnemonics) {
545     return 0;
546   }
547 
548   size_t index = 0;
549   do {
550     index = title_.find('&', index);
551     if (index != base::string16::npos) {
552       if (index + 1 != title_.size() && title_[index + 1] != '&') {
553         base::char16 char_array[] = {title_[index + 1], 0};
554         // TODO(jshin): What about Turkish locale? See http://crbug.com/81719.
555         // If the mnemonic is capital I and the UI language is Turkish,
556         // lowercasing it results in 'small dotless i', which is different
557         // from a 'dotted i'. Similar issues may exist for az and lt locales.
558         return base::i18n::ToLower(char_array)[0];
559       }
560       index++;
561     }
562   } while (index != base::string16::npos);
563   return 0;
564 }
565 
GetMenuItemByID(int id)566 MenuItemView* MenuItemView::GetMenuItemByID(int id) {
567   if (GetCommand() == id)
568     return this;
569   if (!HasSubmenu())
570     return nullptr;
571   for (MenuItemView* item : GetSubmenu()->GetMenuItems()) {
572     MenuItemView* result = item->GetMenuItemByID(id);
573     if (result)
574       return result;
575   }
576   return nullptr;
577 }
578 
ChildrenChanged()579 void MenuItemView::ChildrenChanged() {
580   MenuController* controller = GetMenuController();
581   if (controller) {
582     // Handles the case where we were empty and are no longer empty.
583     RemoveEmptyMenus();
584 
585     // Handles the case where we were not empty, but now are.
586     AddEmptyMenus();
587 
588     controller->MenuChildrenChanged(this);
589 
590     if (submenu_) {
591       // Force a paint and a synchronous layout. This needs a synchronous layout
592       // as UpdateSubmenuSelection() looks at bounds. This handles the case of
593       // the top level window's size remaining the same, resulting in no change
594       // to the submenu's size and no layout.
595       submenu_->Layout();
596       submenu_->SchedulePaint();
597       // Update the menu selection after layout.
598       controller->UpdateSubmenuSelection(submenu_);
599     }
600   }
601 
602   for (auto* item : removed_items_)
603     delete item;
604   removed_items_.clear();
605 }
606 
Layout()607 void MenuItemView::Layout() {
608   if (children().empty())
609     return;
610 
611   if (IsContainer()) {
612     // This MenuItemView is acting as a thin wrapper around the sole child view,
613     // and the size has already been set appropriately for the child's preferred
614     // size and margins. The child's bounds can simply be set to the content
615     // bounds, less the margins.
616     gfx::Rect bounds = GetContentsBounds();
617     bounds.Inset(GetContainerMargins());
618     children().front()->SetBoundsRect(bounds);
619   } else {
620     // Child views are laid out right aligned and given the full height. To
621     // right align start with the last view and progress to the first.
622     int child_x = width() - (use_right_margin_ ? item_right_margin_ : 0);
623     for (View* child : base::Reversed(children())) {
624       if (icon_view_ == child)
625         continue;
626       if (radio_check_image_view_ == child)
627         continue;
628       if (submenu_arrow_image_view_ == child)
629         continue;
630       if (vertical_separator_ == child)
631         continue;
632       int width = child->GetPreferredSize().width();
633       child->SetBounds(child_x - width, 0, width, height());
634       child_x -= width + kChildXPadding;
635     }
636     // Position |icon_view|.
637     const MenuConfig& config = MenuConfig::instance();
638     if (icon_view_) {
639       icon_view_->SizeToPreferredSize();
640       gfx::Size size = icon_view_->GetPreferredSize();
641       int x = config.item_horizontal_padding + left_icon_margin_ +
642               (icon_area_width_ - size.width()) / 2;
643       if (config.icons_in_label || type_ == Type::kCheckbox ||
644           type_ == Type::kRadio)
645         x = label_start_;
646       if (GetMenuController() && GetMenuController()->use_touchable_layout())
647         x = config.touchable_item_horizontal_padding;
648 
649       int y =
650           (height() + GetTopMargin() - GetBottomMargin() - size.height()) / 2;
651       icon_view_->SetPosition(gfx::Point(x, y));
652     }
653 
654     if (radio_check_image_view_) {
655       int x = config.item_horizontal_padding + left_icon_margin_;
656       if (GetMenuController() && GetMenuController()->use_touchable_layout())
657         x = config.touchable_item_horizontal_padding;
658       int y =
659           (height() + GetTopMargin() - GetBottomMargin() - kMenuCheckSize) / 2;
660       radio_check_image_view_->SetBounds(x, y, kMenuCheckSize, kMenuCheckSize);
661     }
662 
663     if (submenu_arrow_image_view_) {
664       int x = width() - config.arrow_width -
665               (type_ == Type::kActionableSubMenu
666                    ? config.actionable_submenu_arrow_to_edge_padding
667                    : config.arrow_to_edge_padding);
668       int y =
669           (height() + GetTopMargin() - GetBottomMargin() - kSubmenuArrowSize) /
670           2;
671       submenu_arrow_image_view_->SetBounds(x, y, config.arrow_width,
672                                            kSubmenuArrowSize);
673     }
674 
675     if (vertical_separator_) {
676       const gfx::Size preferred_size = vertical_separator_->GetPreferredSize();
677       int x = width() - config.actionable_submenu_width -
678               config.actionable_submenu_vertical_separator_width;
679       int y = (height() - preferred_size.height()) / 2;
680       vertical_separator_->SetBoundsRect(
681           gfx::Rect(gfx::Point(x, y), preferred_size));
682     }
683   }
684 }
685 
SetMargins(int top_margin,int bottom_margin)686 void MenuItemView::SetMargins(int top_margin, int bottom_margin) {
687   top_margin_ = top_margin;
688   bottom_margin_ = bottom_margin;
689 
690   invalidate_dimensions();
691 }
692 
SetForcedVisualSelection(bool selected)693 void MenuItemView::SetForcedVisualSelection(bool selected) {
694   forced_visual_selection_ = selected;
695   SchedulePaint();
696 }
697 
SetCornerRadius(int radius)698 void MenuItemView::SetCornerRadius(int radius) {
699   DCHECK_EQ(Type::kHighlighted, type_);
700   corner_radius_ = radius;
701   invalidate_dimensions();  // Triggers preferred size recalculation.
702 }
703 
SetAlerted()704 void MenuItemView::SetAlerted() {
705   is_alerted_ = true;
706   SchedulePaint();
707 }
708 
MenuItemView(MenuItemView * parent,int command,MenuItemView::Type type)709 MenuItemView::MenuItemView(MenuItemView* parent,
710                            int command,
711                            MenuItemView::Type type) {
712   Init(parent, command, type);
713 }
714 
~MenuItemView()715 MenuItemView::~MenuItemView() {
716   if (GetMenuController())
717     GetMenuController()->OnMenuItemDestroying(this);
718   delete submenu_;
719   for (auto* item : removed_items_)
720     delete item;
721 }
722 
723 // Calculates all sizes that we can from the OS.
724 //
725 // This is invoked prior to Running a menu.
UpdateMenuPartSizes()726 void MenuItemView::UpdateMenuPartSizes() {
727   const MenuConfig& config = MenuConfig::instance();
728 
729   item_right_margin_ = config.label_to_arrow_padding + config.arrow_width +
730                        config.arrow_to_edge_padding;
731   icon_area_width_ = config.check_width;
732   if (has_icons_)
733     icon_area_width_ = std::max(icon_area_width_, GetMaxIconViewWidth());
734 
735   const bool use_touchable_layout =
736       GetMenuController() && GetMenuController()->use_touchable_layout();
737   label_start_ =
738       (use_touchable_layout ? config.touchable_item_horizontal_padding
739                             : config.item_horizontal_padding) +
740       icon_area_width_;
741   int padding = 0;
742   if (config.always_use_icon_to_label_padding) {
743     padding = config.item_horizontal_padding;
744   } else if (!config.icons_in_label) {
745     padding = (has_icons_ || HasChecksOrRadioButtons())
746                   ? config.item_horizontal_padding
747                   : 0;
748   }
749   if (use_touchable_layout)
750     padding = config.touchable_item_horizontal_padding;
751 
752   label_start_ += padding;
753 
754   EmptyMenuMenuItem menu_item(this);
755   menu_item.set_controller(GetMenuController());
756   pref_menu_height_ = menu_item.GetPreferredSize().height();
757 
758   UpdateIconViewFromVectorIconAndTheme();
759 }
760 
Init(MenuItemView * parent,int command,MenuItemView::Type type)761 void MenuItemView::Init(MenuItemView* parent,
762                         int command,
763                         MenuItemView::Type type) {
764   parent_menu_item_ = parent;
765   type_ = type;
766   command_ = command;
767   // Assign our ID, this allows SubmenuItemView to find MenuItemViews.
768   SetID(kMenuItemViewID);
769   has_icons_ = false;
770 
771   if (type_ == Type::kCheckbox || type_ == Type::kRadio) {
772     radio_check_image_view_ = AddChildView(std::make_unique<ImageView>());
773     bool show_check_radio_icon =
774         type_ == Type::kRadio || (type_ == Type::kCheckbox &&
775                                   GetDelegate()->IsItemChecked(GetCommand()));
776     radio_check_image_view_->SetVisible(show_check_radio_icon);
777     radio_check_image_view_->set_can_process_events_within_subtree(false);
778   }
779 
780   if (type_ == Type::kActionableSubMenu) {
781     vertical_separator_ = AddChildView(std::make_unique<Separator>());
782     vertical_separator_->SetVisible(true);
783     vertical_separator_->SetFocusBehavior(FocusBehavior::NEVER);
784     const MenuConfig& config = MenuConfig::instance();
785     vertical_separator_->SetColor(GetNativeTheme()->GetSystemColor(
786         ui::NativeTheme::kColorId_MenuSeparatorColor));
787     vertical_separator_->SetPreferredSize(
788         gfx::Size(config.actionable_submenu_vertical_separator_width,
789                   config.actionable_submenu_vertical_separator_height));
790     vertical_separator_->set_can_process_events_within_subtree(false);
791   }
792 
793   if (submenu_arrow_image_view_)
794     submenu_arrow_image_view_->SetVisible(HasSubmenu());
795 
796   // Don't request enabled status from the root menu item as it is just
797   // a container for real items. kEmpty items will be disabled.
798   MenuDelegate* root_delegate = GetDelegate();
799   if (parent && type != Type::kEmpty && root_delegate)
800     SetEnabled(root_delegate->IsCommandEnabled(command));
801 }
802 
PrepareForRun(bool is_first_menu,bool has_mnemonics,bool show_mnemonics)803 void MenuItemView::PrepareForRun(bool is_first_menu,
804                                  bool has_mnemonics,
805                                  bool show_mnemonics) {
806   // Currently we only support showing the root.
807   DCHECK(!parent_menu_item_);
808 
809   // Force us to have a submenu.
810   CreateSubmenu();
811   actual_menu_position_ = requested_menu_position_;
812   canceled_ = false;
813 
814   has_mnemonics_ = has_mnemonics;
815   show_mnemonics_ = has_mnemonics && show_mnemonics;
816 
817   AddEmptyMenus();
818 
819   if (is_first_menu) {
820     // Only update the menu size if there are no menus showing, otherwise
821     // things may shift around.
822     UpdateMenuPartSizes();
823   }
824 }
825 
GetDrawStringFlags()826 int MenuItemView::GetDrawStringFlags() {
827   int flags = 0;
828   if (base::i18n::IsRTL())
829     flags |= gfx::Canvas::TEXT_ALIGN_RIGHT;
830   else
831     flags |= gfx::Canvas::TEXT_ALIGN_LEFT;
832 
833   if (GetRootMenuItem()->has_mnemonics_) {
834     if (MenuConfig::instance().show_mnemonics ||
835         GetRootMenuItem()->show_mnemonics_) {
836       flags |= gfx::Canvas::SHOW_PREFIX;
837     } else {
838       flags |= gfx::Canvas::HIDE_PREFIX;
839     }
840   }
841   return flags;
842 }
843 
GetLabelStyle(MenuDelegate::LabelStyle * style) const844 void MenuItemView::GetLabelStyle(MenuDelegate::LabelStyle* style) const {
845   // Start with the default font:
846   style->font_list = MenuConfig::instance().font_list;
847 
848   // Replace it with the touchable font in touchable menus:
849   if (GetMenuController() && GetMenuController()->use_touchable_layout()) {
850     style->font_list =
851         style::GetFont(style::CONTEXT_TOUCH_MENU, style::STYLE_PRIMARY);
852   }
853 
854   // Then let the delegate replace any part of |style|.
855   const MenuDelegate* delegate = GetDelegate();
856   if (delegate)
857     delegate->GetLabelStyle(GetCommand(), style);
858 }
859 
AddEmptyMenus()860 void MenuItemView::AddEmptyMenus() {
861   DCHECK(HasSubmenu());
862   if (!submenu_->HasVisibleChildren() && !submenu_->HasEmptyMenuItemView()) {
863     submenu_->AddChildViewAt(std::make_unique<EmptyMenuMenuItem>(this), 0);
864   } else {
865     for (MenuItemView* item : submenu_->GetMenuItems()) {
866       if (item->HasSubmenu())
867         item->AddEmptyMenus();
868     }
869   }
870 }
871 
RemoveEmptyMenus()872 void MenuItemView::RemoveEmptyMenus() {
873   DCHECK(HasSubmenu());
874   // Copy the children, since we may mutate them as we go.
875   const Views children = submenu_->children();
876   for (View* child : children) {
877     if (child->GetID() == MenuItemView::kMenuItemViewID) {
878       MenuItemView* menu_item = static_cast<MenuItemView*>(child);
879       if (menu_item->HasSubmenu())
880         menu_item->RemoveEmptyMenus();
881     } else if (child->GetID() == EmptyMenuMenuItem::kEmptyMenuItemViewID) {
882       submenu_->RemoveChildView(child);
883       delete child;
884     }
885   }
886 }
887 
AdjustBoundsForRTLUI(gfx::Rect * rect) const888 void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
889   rect->set_x(GetMirroredXForRect(*rect));
890 }
891 
PaintButton(gfx::Canvas * canvas,PaintButtonMode mode)892 void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) {
893   const MenuConfig& config = MenuConfig::instance();
894   bool render_selection =
895       (mode == PaintButtonMode::kNormal && IsSelected() &&
896        parent_menu_item_->GetSubmenu()->GetShowSelection(this) &&
897        (NonIconChildViewsCount() == 0));
898   if (forced_visual_selection_.has_value())
899     render_selection = *forced_visual_selection_;
900 
901   MenuDelegate* delegate = GetDelegate();
902   // Render the background. As MenuScrollViewContainer draws the background, we
903   // only need the background when we want it to look different, as when we're
904   // selected.
905   PaintBackground(canvas, mode, render_selection);
906 
907   const int top_margin = GetTopMargin();
908   const int bottom_margin = GetBottomMargin();
909   const int available_height = height() - top_margin - bottom_margin;
910 
911   // Calculate some colors.
912   MenuDelegate::LabelStyle style;
913   style.foreground = GetTextColor(false, render_selection);
914   GetLabelStyle(&style);
915 
916   SkColor icon_color = color_utils::DeriveDefaultIconColor(style.foreground);
917 
918   // Render the check.
919   if (type_ == Type::kCheckbox && delegate->IsItemChecked(GetCommand())) {
920     radio_check_image_view_->SetImage(GetMenuCheckImage(icon_color));
921   } else if (type_ == Type::kRadio) {
922     const bool toggled = delegate->IsItemChecked(GetCommand());
923     const gfx::VectorIcon& radio_icon =
924         toggled ? kMenuRadioSelectedIcon : kMenuRadioEmptyIcon;
925     const SkColor radio_icon_color = GetNativeTheme()->GetSystemColor(
926         toggled ? ui::NativeTheme::kColorId_ButtonEnabledColor
927                 : ui::NativeTheme::kColorId_ButtonUncheckedColor);
928     radio_check_image_view_->SetImage(
929         gfx::CreateVectorIcon(radio_icon, kMenuCheckSize, radio_icon_color));
930   }
931 
932   // Render the foreground.
933   int accel_width = parent_menu_item_->GetSubmenu()->max_minor_text_width();
934   int label_start = GetLabelStartForThisItem();
935 
936   int width = this->width() - label_start - accel_width -
937               (!delegate || delegate->ShouldReserveSpaceForSubmenuIndicator()
938                    ? item_right_margin_
939                    : config.arrow_to_edge_padding);
940   gfx::Rect text_bounds(label_start, top_margin, width, available_height);
941   text_bounds.set_x(GetMirroredXForRect(text_bounds));
942   int flags = GetDrawStringFlags();
943   if (mode == PaintButtonMode::kForDrag)
944     flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING;
945   canvas->DrawStringRectWithFlags(title(), style.font_list, style.foreground,
946                                   text_bounds, flags);
947   PaintMinorIconAndText(canvas, style);
948 
949   // Set the submenu indicator (arrow) image and color.
950   if (HasSubmenu())
951     submenu_arrow_image_view_->SetImage(GetSubmenuArrowImage(icon_color));
952 }
953 
PaintBackground(gfx::Canvas * canvas,PaintButtonMode mode,bool render_selection)954 void MenuItemView::PaintBackground(gfx::Canvas* canvas,
955                                    PaintButtonMode mode,
956                                    bool render_selection) {
957   if (type_ == Type::kHighlighted || is_alerted_) {
958     SkColor color = gfx::kPlaceholderColor;
959 
960     if (type_ == Type::kHighlighted) {
961       const ui::NativeTheme::ColorId color_id =
962           render_selection
963               ? ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor
964               : ui::NativeTheme::kColorId_HighlightedMenuItemBackgroundColor;
965       color = GetNativeTheme()->GetSystemColor(color_id);
966     } else {
967       const auto* animation = GetMenuController()->GetAlertAnimation();
968       color = gfx::Tween::ColorValueBetween(
969           animation->GetCurrentValue(),
970           GetNativeTheme()->GetSystemColor(
971               ui::NativeTheme::kColorId_MenuItemInitialAlertBackgroundColor),
972           GetNativeTheme()->GetSystemColor(
973               ui::NativeTheme::kColorId_MenuItemTargetAlertBackgroundColor));
974     }
975 
976     DCHECK_NE(color, gfx::kPlaceholderColor);
977 
978     cc::PaintFlags flags;
979     flags.setAntiAlias(true);
980     flags.setStyle(cc::PaintFlags::kFill_Style);
981     flags.setColor(color);
982     // Draw a rounded rect that spills outside of the clipping area, so that the
983     // rounded corners only show in the bottom 2 corners. Note that
984     // |corner_radius_| should only be set when the highlighted item is at the
985     // end of the menu.
986     gfx::RectF spilling_rect(GetLocalBounds());
987     spilling_rect.set_y(spilling_rect.y() - corner_radius_);
988     spilling_rect.set_height(spilling_rect.height() + corner_radius_);
989     canvas->DrawRoundRect(spilling_rect, corner_radius_, flags);
990   } else if (render_selection) {
991     gfx::Rect item_bounds = GetLocalBounds();
992     if (type_ == Type::kActionableSubMenu) {
993       if (submenu_area_of_actionable_submenu_selected_) {
994         item_bounds = GetSubmenuAreaOfActionableSubmenu();
995       } else {
996         item_bounds.set_width(item_bounds.width() -
997                               MenuConfig::instance().actionable_submenu_width -
998                               1);
999       }
1000     }
1001     AdjustBoundsForRTLUI(&item_bounds);
1002 
1003     GetNativeTheme()->Paint(
1004         canvas->sk_canvas(), ui::NativeTheme::kMenuItemBackground,
1005         ui::NativeTheme::kHovered, item_bounds, ui::NativeTheme::ExtraParams());
1006   }
1007 }
1008 
PaintMinorIconAndText(gfx::Canvas * canvas,const MenuDelegate::LabelStyle & style)1009 void MenuItemView::PaintMinorIconAndText(
1010     gfx::Canvas* canvas,
1011     const MenuDelegate::LabelStyle& style) {
1012   base::string16 minor_text = GetMinorText();
1013   const ui::ThemedVectorIcon minor_icon = GetMinorIcon();
1014   if (minor_text.empty() && minor_icon.empty())
1015     return;
1016 
1017   int available_height = height() - GetTopMargin() - GetBottomMargin();
1018   int max_minor_text_width =
1019       parent_menu_item_->GetSubmenu()->max_minor_text_width();
1020   const MenuConfig& config = MenuConfig::instance();
1021   int minor_text_right_margin = config.align_arrow_and_shortcut
1022                                     ? config.arrow_to_edge_padding
1023                                     : item_right_margin_;
1024   gfx::Rect minor_text_bounds(
1025       width() - minor_text_right_margin - max_minor_text_width, GetTopMargin(),
1026       max_minor_text_width, available_height);
1027   minor_text_bounds.set_x(GetMirroredXForRect(minor_text_bounds));
1028 
1029   std::unique_ptr<gfx::RenderText> render_text =
1030       gfx::RenderText::CreateRenderText();
1031   if (!minor_text.empty()) {
1032     render_text->SetText(minor_text);
1033     render_text->SetFontList(style.font_list);
1034     render_text->SetColor(style.foreground);
1035     render_text->SetDisplayRect(minor_text_bounds);
1036     render_text->SetHorizontalAlignment(base::i18n::IsRTL() ? gfx::ALIGN_LEFT
1037                                                             : gfx::ALIGN_RIGHT);
1038     render_text->Draw(canvas);
1039   }
1040 
1041   if (!minor_icon.empty()) {
1042     gfx::ImageSkia image = minor_icon.GetImageSkia(style.foreground);
1043 
1044     int image_x = GetMirroredRect(minor_text_bounds).right() -
1045                   render_text->GetContentWidth() -
1046                   (minor_text.empty() ? 0 : config.item_horizontal_padding) -
1047                   image.width();
1048     int minor_text_center_y =
1049         minor_text_bounds.y() + minor_text_bounds.height() / 2;
1050     int image_y = minor_text_center_y - image.height() / 2;
1051     canvas->DrawImageInt(
1052         image, GetMirroredXWithWidthInView(image_x, image.width()), image_y);
1053   }
1054 }
1055 
GetTextColor(bool minor,bool render_selection) const1056 SkColor MenuItemView::GetTextColor(bool minor, bool render_selection) const {
1057   style::TextContext context =
1058       GetMenuController() && GetMenuController()->use_touchable_layout()
1059           ? style::CONTEXT_TOUCH_MENU
1060           : style::CONTEXT_MENU;
1061 
1062   style::TextStyle text_style = style::STYLE_PRIMARY;
1063   if (type_ == Type::kTitle)
1064     text_style = style::STYLE_PRIMARY;
1065   else if (type_ == Type::kHighlighted)
1066     text_style = style::STYLE_HIGHLIGHTED;
1067   else if (!GetEnabled())
1068     text_style = style::STYLE_DISABLED;
1069   else if (render_selection)
1070     text_style = style::STYLE_SELECTED;
1071   else if (minor)
1072     text_style = style::STYLE_SECONDARY;
1073 
1074   return style::GetColor(*this, context, text_style);
1075 }
1076 
DestroyAllMenuHosts()1077 void MenuItemView::DestroyAllMenuHosts() {
1078   if (!HasSubmenu())
1079     return;
1080 
1081   submenu_->Close();
1082   for (MenuItemView* item : submenu_->GetMenuItems())
1083     item->DestroyAllMenuHosts();
1084 }
1085 
GetTopMargin() const1086 int MenuItemView::GetTopMargin() const {
1087   int margin = top_margin_;
1088   if (margin < 0) {
1089     const MenuItemView* root = GetRootMenuItem();
1090     margin = root && root->has_icons_
1091                  ? MenuConfig::instance().item_top_margin
1092                  : MenuConfig::instance().item_no_icon_top_margin;
1093   }
1094 
1095   return margin;
1096 }
1097 
GetBottomMargin() const1098 int MenuItemView::GetBottomMargin() const {
1099   int margin = bottom_margin_;
1100   if (margin < 0) {
1101     const MenuItemView* root = GetRootMenuItem();
1102     margin = root && root->has_icons_
1103                  ? MenuConfig::instance().item_bottom_margin
1104                  : MenuConfig::instance().item_no_icon_bottom_margin;
1105   }
1106 
1107   return margin;
1108 }
1109 
GetChildPreferredSize() const1110 gfx::Size MenuItemView::GetChildPreferredSize() const {
1111   if (children().empty())
1112     return gfx::Size();
1113 
1114   if (IsContainer())
1115     return children().front()->GetPreferredSize();
1116 
1117   const auto add_width = [this](int width, const View* child) {
1118     if (child == icon_view_ || child == radio_check_image_view_ ||
1119         child == submenu_arrow_image_view_ || child == vertical_separator_)
1120       return width;
1121     if (width)
1122       width += kChildXPadding;
1123     return width + child->GetPreferredSize().width();
1124   };
1125   const int width =
1126       std::accumulate(children().cbegin(), children().cend(), 0, add_width);
1127 
1128   // If there is no icon view it returns a height of 0 to indicate that
1129   // we should use the title height instead.
1130   const int height = icon_view_ ? icon_view_->GetPreferredSize().height() : 0;
1131 
1132   return gfx::Size(width, height);
1133 }
1134 
CalculateDimensions() const1135 MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const {
1136   gfx::Size child_size = GetChildPreferredSize();
1137 
1138   MenuItemDimensions dimensions;
1139   dimensions.children_width = child_size.width();
1140   const MenuConfig& menu_config = MenuConfig::instance();
1141 
1142   if (GetMenuController() && GetMenuController()->use_touchable_layout()) {
1143     dimensions.height = menu_config.touchable_menu_height;
1144 
1145     // For container MenuItemViews, the width components should only include the
1146     // |children_width|. Setting a |standard_width| would result in additional
1147     // width being added to the container because the total width used in layout
1148     // is |children_width| + |standard_width|.
1149     if (IsContainer())
1150       return dimensions;
1151 
1152     dimensions.standard_width = menu_config.touchable_menu_width;
1153 
1154     if (icon_view_) {
1155       dimensions.height = icon_view_->GetPreferredSize().height() +
1156                           2 * menu_config.vertical_touchable_menu_item_padding;
1157     }
1158     return dimensions;
1159   }
1160 
1161   MenuDelegate::LabelStyle style;
1162   GetLabelStyle(&style);
1163   base::string16 minor_text = GetMinorText();
1164 
1165   dimensions.height = child_size.height();
1166   // Adjust item content height if menu has both items with and without icons.
1167   // This way all menu items will have the same height.
1168   if (!icon_view_ && GetRootMenuItem()->has_icons()) {
1169     dimensions.height =
1170         std::max(dimensions.height, MenuConfig::instance().check_height);
1171   }
1172 
1173   // In the container case, only the child size plus margins need to be
1174   // considered.
1175   if (IsContainer()) {
1176     const gfx::Insets margins = GetContainerMargins();
1177     dimensions.height += margins.height();
1178     dimensions.children_width += margins.width();
1179     ApplyMinimumDimensions(&dimensions);
1180     return dimensions;
1181   }
1182 
1183   dimensions.height += GetBottomMargin() + GetTopMargin();
1184 
1185   // Get Icon margin overrides for this particular item.
1186   const MenuDelegate* delegate = GetDelegate();
1187   if (delegate) {
1188     delegate->GetHorizontalIconMargins(command_, icon_area_width_,
1189                                        &left_icon_margin_, &right_icon_margin_);
1190   } else {
1191     left_icon_margin_ = 0;
1192     right_icon_margin_ = 0;
1193   }
1194   int label_start = GetLabelStartForThisItem();
1195 
1196   // Determine the length of the label text.
1197   int string_width = gfx::GetStringWidth(title_, style.font_list);
1198   dimensions.standard_width = string_width + label_start + item_right_margin_;
1199   // Determine the length of the right-side text.
1200   dimensions.minor_text_width =
1201       minor_text.empty() ? 0 : gfx::GetStringWidth(minor_text, style.font_list);
1202 
1203   // Determine the height to use.
1204   dimensions.height =
1205       std::max(dimensions.height, style.font_list.GetHeight() +
1206                                       GetBottomMargin() + GetTopMargin());
1207   dimensions.height =
1208       std::max(dimensions.height, MenuConfig::instance().item_min_height);
1209 
1210   ApplyMinimumDimensions(&dimensions);
1211   return dimensions;
1212 }
1213 
ApplyMinimumDimensions(MenuItemDimensions * dims) const1214 void MenuItemView::ApplyMinimumDimensions(MenuItemDimensions* dims) const {
1215   // Don't apply minimums to menus without controllers or to comboboxes.
1216   if (!GetMenuController() || GetMenuController()->IsCombobox())
1217     return;
1218 
1219   // TODO(nicolaso): PaintBackground() doesn't cover the whole area in footnotes
1220   // when minimum height is set too high. For now, just ignore minimum height
1221   // for kHighlighted elements.
1222   if (type_ == Type::kHighlighted)
1223     return;
1224 
1225   int used =
1226       dims->standard_width + dims->children_width + dims->minor_text_width;
1227   const MenuConfig& config = MenuConfig::instance();
1228   if (used < config.minimum_menu_width)
1229     dims->standard_width += (config.minimum_menu_width - used);
1230 
1231   dims->height = std::max(dims->height,
1232                           IsContainer() ? config.minimum_container_item_height
1233                                         : config.minimum_text_item_height);
1234 }
1235 
GetLabelStartForThisItem() const1236 int MenuItemView::GetLabelStartForThisItem() const {
1237   const MenuConfig& config = MenuConfig::instance();
1238 
1239   // Touchable items with icons do not respect |label_start_|.
1240   if (GetMenuController() && GetMenuController()->use_touchable_layout() &&
1241       icon_view_) {
1242     return 2 * config.touchable_item_horizontal_padding + icon_view_->width();
1243   }
1244 
1245   int label_start = label_start_ + left_icon_margin_ + right_icon_margin_;
1246   if ((config.icons_in_label || type_ == Type::kCheckbox ||
1247        type_ == Type::kRadio) &&
1248       icon_view_) {
1249     label_start += icon_view_->size().width() + config.item_horizontal_padding;
1250   }
1251 
1252   return label_start;
1253 }
1254 
GetMinorText() const1255 base::string16 MenuItemView::GetMinorText() const {
1256   if (GetID() == kEmptyMenuItemViewID) {
1257     // Don't query the delegate for menus that represent no children.
1258     return base::string16();
1259   }
1260 
1261   base::string16 accel_text;
1262   if (MenuConfig::instance().ShouldShowAcceleratorText(this, &accel_text))
1263     return accel_text;
1264 
1265   return minor_text_;
1266 }
1267 
GetMinorIcon() const1268 ui::ThemedVectorIcon MenuItemView::GetMinorIcon() const {
1269   return minor_icon_;
1270 }
1271 
IsContainer() const1272 bool MenuItemView::IsContainer() const {
1273   // Let the first child take over |this| when we only have one child and no
1274   // title.
1275   return (NonIconChildViewsCount() == 1) && title_.empty();
1276 }
1277 
GetContainerMargins() const1278 gfx::Insets MenuItemView::GetContainerMargins() const {
1279   DCHECK(IsContainer());
1280 
1281   // Use the child's preferred margins but take the standard top and bottom
1282   // margins as minimums.
1283   const gfx::Insets* margins_prop =
1284       children().front()->GetProperty(views::kMarginsKey);
1285   gfx::Insets margins = margins_prop ? *margins_prop : gfx::Insets();
1286   margins.set_top(std::max(margins.top(), GetTopMargin()));
1287   margins.set_bottom(std::max(margins.bottom(), GetBottomMargin()));
1288   return margins;
1289 }
1290 
NonIconChildViewsCount() const1291 int MenuItemView::NonIconChildViewsCount() const {
1292   return int{children().size()} - (icon_view_ ? 1 : 0) -
1293          (radio_check_image_view_ ? 1 : 0) -
1294          (submenu_arrow_image_view_ ? 1 : 0) - (vertical_separator_ ? 1 : 0);
1295 }
1296 
GetMaxIconViewWidth() const1297 int MenuItemView::GetMaxIconViewWidth() const {
1298   DCHECK(submenu_);
1299   const auto menu_items = submenu_->GetMenuItems();
1300   if (menu_items.empty())
1301     return 0;
1302 
1303   std::vector<int> widths(menu_items.size());
1304   const auto get_width = [](MenuItemView* item) {
1305     if (item->type_ == Type::kCheckbox || item->type_ == Type::kRadio) {
1306       // If this item has a radio or checkbox, the icon will not affect
1307       // alignment of other items.
1308       return 0;
1309     }
1310     if (item->HasSubmenu())
1311       return item->GetMaxIconViewWidth();
1312     return (item->icon_view_ && !MenuConfig::instance().icons_in_label)
1313                ? item->icon_view_->GetPreferredSize().width()
1314                : 0;
1315   };
1316   std::transform(menu_items.cbegin(), menu_items.cend(), widths.begin(),
1317                  get_width);
1318   return *std::max_element(widths.cbegin(), widths.cend());
1319 }
1320 
HasChecksOrRadioButtons() const1321 bool MenuItemView::HasChecksOrRadioButtons() const {
1322   if (type_ == Type::kCheckbox || type_ == Type::kRadio)
1323     return true;
1324   if (!HasSubmenu())
1325     return false;
1326   const auto menu_items = submenu_->GetMenuItems();
1327   return std::any_of(
1328       menu_items.cbegin(), menu_items.cend(),
1329       [](const auto* item) { return item->HasChecksOrRadioButtons(); });
1330 }
1331 
1332 BEGIN_METADATA(MenuItemView)
1333 METADATA_PARENT_CLASS(View)
1334 END_METADATA()
1335 
1336 }  // namespace views
1337