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