• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 "base/i18n/case_conversion.h"
8 #include "base/stl_util.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "ui/accessibility/ax_view_state.h"
11 #include "ui/base/l10n/l10n_util.h"
12 #include "ui/base/models/menu_model.h"
13 #include "ui/gfx/canvas.h"
14 #include "ui/gfx/geometry/rect.h"
15 #include "ui/gfx/geometry/vector2d.h"
16 #include "ui/gfx/image/image.h"
17 #include "ui/gfx/text_utils.h"
18 #include "ui/native_theme/common_theme.h"
19 #include "ui/resources/grit/ui_resources.h"
20 #include "ui/strings/grit/ui_strings.h"
21 #include "ui/views/controls/button/menu_button.h"
22 #include "ui/views/controls/image_view.h"
23 #include "ui/views/controls/menu/menu_config.h"
24 #include "ui/views/controls/menu/menu_controller.h"
25 #include "ui/views/controls/menu/menu_image_util.h"
26 #include "ui/views/controls/menu/menu_scroll_view_container.h"
27 #include "ui/views/controls/menu/menu_separator.h"
28 #include "ui/views/controls/menu/submenu_view.h"
29 #include "ui/views/widget/widget.h"
30 
31 namespace views {
32 
33 namespace {
34 
35 // EmptyMenuMenuItem ---------------------------------------------------------
36 
37 // EmptyMenuMenuItem is used when a menu has no menu items. EmptyMenuMenuItem
38 // is itself a MenuItemView, but it uses a different ID so that it isn't
39 // identified as a MenuItemView.
40 
41 class EmptyMenuMenuItem : public MenuItemView {
42  public:
EmptyMenuMenuItem(MenuItemView * parent)43   explicit EmptyMenuMenuItem(MenuItemView* parent)
44       : MenuItemView(parent, 0, EMPTY) {
45     // Set this so that we're not identified as a normal menu item.
46     set_id(kEmptyMenuItemViewID);
47     SetTitle(l10n_util::GetStringUTF16(IDS_APP_MENU_EMPTY_SUBMENU));
48     SetEnabled(false);
49   }
50 
GetTooltipText(const gfx::Point & p,base::string16 * tooltip) const51   virtual bool GetTooltipText(const gfx::Point& p,
52                               base::string16* tooltip) const OVERRIDE {
53     // Empty menu items shouldn't have a tooltip.
54     return false;
55   }
56 
57  private:
58   DISALLOW_COPY_AND_ASSIGN(EmptyMenuMenuItem);
59 };
60 
61 }  // namespace
62 
63 // Padding between child views.
64 static const int kChildXPadding = 8;
65 
66 // MenuItemView ---------------------------------------------------------------
67 
68 // static
69 const int MenuItemView::kMenuItemViewID = 1001;
70 
71 // static
72 const int MenuItemView::kEmptyMenuItemViewID =
73     MenuItemView::kMenuItemViewID + 1;
74 
75 // static
76 int MenuItemView::icon_area_width_ = 0;
77 
78 // static
79 int MenuItemView::label_start_;
80 
81 // static
82 int MenuItemView::item_right_margin_;
83 
84 // static
85 int MenuItemView::pref_menu_height_;
86 
87 // static
88 const char MenuItemView::kViewClassName[] = "MenuItemView";
89 
MenuItemView(MenuDelegate * delegate)90 MenuItemView::MenuItemView(MenuDelegate* delegate)
91     : delegate_(delegate),
92       controller_(NULL),
93       canceled_(false),
94       parent_menu_item_(NULL),
95       type_(SUBMENU),
96       selected_(false),
97       command_(0),
98       submenu_(NULL),
99       has_mnemonics_(false),
100       show_mnemonics_(false),
101       has_icons_(false),
102       icon_view_(NULL),
103       top_margin_(-1),
104       bottom_margin_(-1),
105       left_icon_margin_(0),
106       right_icon_margin_(0),
107       requested_menu_position_(POSITION_BEST_FIT),
108       actual_menu_position_(requested_menu_position_),
109       use_right_margin_(true) {
110   // NOTE: don't check the delegate for NULL, UpdateMenuPartSizes() supplies a
111   // NULL delegate.
112   Init(NULL, 0, SUBMENU, delegate);
113 }
114 
ChildPreferredSizeChanged(View * child)115 void MenuItemView::ChildPreferredSizeChanged(View* child) {
116   invalidate_dimensions();
117   PreferredSizeChanged();
118 }
119 
GetTooltipText(const gfx::Point & p,base::string16 * tooltip) const120 bool MenuItemView::GetTooltipText(const gfx::Point& p,
121                                   base::string16* tooltip) const {
122   *tooltip = tooltip_;
123   if (!tooltip->empty())
124     return true;
125 
126   if (GetType() == SEPARATOR)
127     return false;
128 
129   const MenuController* controller = GetMenuController();
130   if (!controller || controller->exit_type() != MenuController::EXIT_NONE) {
131     // Either the menu has been closed or we're in the process of closing the
132     // menu. Don't attempt to query the delegate as it may no longer be valid.
133     return false;
134   }
135 
136   const MenuItemView* root_menu_item = GetRootMenuItem();
137   if (root_menu_item->canceled_) {
138     // TODO(sky): if |canceled_| is true, controller->exit_type() should be
139     // something other than EXIT_NONE, but crash reports seem to indicate
140     // otherwise. Figure out why this is needed.
141     return false;
142   }
143 
144   const MenuDelegate* delegate = GetDelegate();
145   CHECK(delegate);
146   gfx::Point location(p);
147   ConvertPointToScreen(this, &location);
148   *tooltip = delegate->GetTooltipText(command_, location);
149   return !tooltip->empty();
150 }
151 
GetAccessibleState(ui::AXViewState * state)152 void MenuItemView::GetAccessibleState(ui::AXViewState* state) {
153   state->role = ui::AX_ROLE_MENU_ITEM;
154 
155   base::string16 item_text;
156   if (IsContainer()) {
157     // The first child is taking over, just use its accessible name instead of
158     // |title_|.
159     View* child = child_at(0);
160     ui::AXViewState state;
161     child->GetAccessibleState(&state);
162     item_text = state.name;
163   } else {
164     item_text = title_;
165   }
166   state->name = GetAccessibleNameForMenuItem(item_text, GetMinorText());
167 
168   switch (GetType()) {
169     case SUBMENU:
170       state->AddStateFlag(ui::AX_STATE_HASPOPUP);
171       break;
172     case CHECKBOX:
173     case RADIO:
174       if (GetDelegate()->IsItemChecked(GetCommand()))
175         state->AddStateFlag(ui::AX_STATE_CHECKED);
176       break;
177     case NORMAL:
178     case SEPARATOR:
179     case EMPTY:
180       // No additional accessibility states currently for these menu states.
181       break;
182   }
183 }
184 
185 // static
IsBubble(MenuAnchorPosition anchor)186 bool MenuItemView::IsBubble(MenuAnchorPosition anchor) {
187   return anchor == MENU_ANCHOR_BUBBLE_LEFT ||
188          anchor == MENU_ANCHOR_BUBBLE_RIGHT ||
189          anchor == MENU_ANCHOR_BUBBLE_ABOVE ||
190          anchor == MENU_ANCHOR_BUBBLE_BELOW;
191 }
192 
193 // static
GetAccessibleNameForMenuItem(const base::string16 & item_text,const base::string16 & minor_text)194 base::string16 MenuItemView::GetAccessibleNameForMenuItem(
195       const base::string16& item_text, const base::string16& minor_text) {
196   base::string16 accessible_name = item_text;
197 
198   // Filter out the "&" for accessibility clients.
199   size_t index = 0;
200   const base::char16 amp = '&';
201   while ((index = accessible_name.find(amp, index)) != base::string16::npos &&
202          index + 1 < accessible_name.length()) {
203     accessible_name.replace(index, accessible_name.length() - index,
204                             accessible_name.substr(index + 1));
205 
206     // Special case for "&&" (escaped for "&").
207     if (accessible_name[index] == '&')
208       ++index;
209   }
210 
211   // Append subtext.
212   if (!minor_text.empty()) {
213     accessible_name.push_back(' ');
214     accessible_name.append(minor_text);
215   }
216 
217   return accessible_name;
218 }
219 
Cancel()220 void MenuItemView::Cancel() {
221   if (controller_ && !canceled_) {
222     canceled_ = true;
223     controller_->Cancel(MenuController::EXIT_ALL);
224   }
225 }
226 
AddMenuItemAt(int index,int item_id,const base::string16 & label,const base::string16 & sublabel,const base::string16 & minor_text,const gfx::ImageSkia & icon,Type type,ui::MenuSeparatorType separator_style)227 MenuItemView* MenuItemView::AddMenuItemAt(
228     int index,
229     int item_id,
230     const base::string16& label,
231     const base::string16& sublabel,
232     const base::string16& minor_text,
233     const gfx::ImageSkia& icon,
234     Type type,
235     ui::MenuSeparatorType separator_style) {
236   DCHECK_NE(type, EMPTY);
237   DCHECK_LE(0, index);
238   if (!submenu_)
239     CreateSubmenu();
240   DCHECK_GE(submenu_->child_count(), index);
241   if (type == SEPARATOR) {
242     submenu_->AddChildViewAt(new MenuSeparator(this, separator_style), index);
243     return NULL;
244   }
245   MenuItemView* item = new MenuItemView(this, item_id, type);
246   if (label.empty() && GetDelegate())
247     item->SetTitle(GetDelegate()->GetLabel(item_id));
248   else
249     item->SetTitle(label);
250   item->SetSubtitle(sublabel);
251   item->SetMinorText(minor_text);
252   if (!icon.isNull())
253     item->SetIcon(icon);
254   if (type == SUBMENU)
255     item->CreateSubmenu();
256   if (GetDelegate() && !GetDelegate()->IsCommandVisible(item_id))
257     item->SetVisible(false);
258   submenu_->AddChildViewAt(item, index);
259   return item;
260 }
261 
RemoveMenuItemAt(int index)262 void MenuItemView::RemoveMenuItemAt(int index) {
263   DCHECK(submenu_);
264   DCHECK_LE(0, index);
265   DCHECK_GT(submenu_->child_count(), index);
266 
267   View* item = submenu_->child_at(index);
268   DCHECK(item);
269   submenu_->RemoveChildView(item);
270 
271   // RemoveChildView() does not delete the item, which is a good thing
272   // in case a submenu is being displayed while items are being removed.
273   // Deletion will be done by ChildrenChanged() or at destruction.
274   removed_items_.push_back(item);
275 }
276 
AppendMenuItem(int item_id,const base::string16 & label,Type type)277 MenuItemView* MenuItemView::AppendMenuItem(int item_id,
278                                            const base::string16& label,
279                                            Type type) {
280   return AppendMenuItemImpl(item_id, label, base::string16(), base::string16(),
281       gfx::ImageSkia(), type, ui::NORMAL_SEPARATOR);
282 }
283 
AppendSubMenu(int item_id,const base::string16 & label)284 MenuItemView* MenuItemView::AppendSubMenu(int item_id,
285                                           const base::string16& label) {
286   return AppendMenuItemImpl(item_id, label, base::string16(), base::string16(),
287       gfx::ImageSkia(), SUBMENU, ui::NORMAL_SEPARATOR);
288 }
289 
AppendSubMenuWithIcon(int item_id,const base::string16 & label,const gfx::ImageSkia & icon)290 MenuItemView* MenuItemView::AppendSubMenuWithIcon(int item_id,
291                                                   const base::string16& label,
292                                                   const gfx::ImageSkia& icon) {
293   return AppendMenuItemImpl(item_id, label, base::string16(), base::string16(),
294                             icon, SUBMENU, ui::NORMAL_SEPARATOR);
295 }
296 
AppendMenuItemWithLabel(int item_id,const base::string16 & label)297 MenuItemView* MenuItemView::AppendMenuItemWithLabel(
298     int item_id,
299     const base::string16& label) {
300   return AppendMenuItem(item_id, label, NORMAL);
301 }
302 
AppendDelegateMenuItem(int item_id)303 MenuItemView* MenuItemView::AppendDelegateMenuItem(int item_id) {
304   return AppendMenuItem(item_id, base::string16(), NORMAL);
305 }
306 
AppendSeparator()307 void MenuItemView::AppendSeparator() {
308   AppendMenuItemImpl(0, base::string16(), base::string16(), base::string16(),
309                      gfx::ImageSkia(), SEPARATOR, ui::NORMAL_SEPARATOR);
310 }
311 
AppendMenuItemWithIcon(int item_id,const base::string16 & label,const gfx::ImageSkia & icon)312 MenuItemView* MenuItemView::AppendMenuItemWithIcon(int item_id,
313                                                    const base::string16& label,
314                                                    const gfx::ImageSkia& icon) {
315   return AppendMenuItemImpl(item_id, label, base::string16(), base::string16(),
316                             icon, NORMAL, ui::NORMAL_SEPARATOR);
317 }
318 
AppendMenuItemImpl(int item_id,const base::string16 & label,const base::string16 & sublabel,const base::string16 & minor_text,const gfx::ImageSkia & icon,Type type,ui::MenuSeparatorType separator_style)319 MenuItemView* MenuItemView::AppendMenuItemImpl(
320     int item_id,
321     const base::string16& label,
322     const base::string16& sublabel,
323     const base::string16& minor_text,
324     const gfx::ImageSkia& icon,
325     Type type,
326     ui::MenuSeparatorType separator_style) {
327   const int index = submenu_ ? submenu_->child_count() : 0;
328   return AddMenuItemAt(index, item_id, label, sublabel, minor_text, icon, type,
329                        separator_style);
330 }
331 
CreateSubmenu()332 SubmenuView* MenuItemView::CreateSubmenu() {
333   if (!submenu_)
334     submenu_ = new SubmenuView(this);
335   return submenu_;
336 }
337 
HasSubmenu() const338 bool MenuItemView::HasSubmenu() const {
339   return (submenu_ != NULL);
340 }
341 
GetSubmenu() const342 SubmenuView* MenuItemView::GetSubmenu() const {
343   return submenu_;
344 }
345 
SetTitle(const base::string16 & title)346 void MenuItemView::SetTitle(const base::string16& title) {
347   title_ = title;
348   invalidate_dimensions();  // Triggers preferred size recalculation.
349 }
350 
SetSubtitle(const base::string16 & subtitle)351 void MenuItemView::SetSubtitle(const base::string16& subtitle) {
352   subtitle_ = subtitle;
353   invalidate_dimensions();  // Triggers preferred size recalculation.
354 }
355 
SetMinorText(const base::string16 & minor_text)356 void MenuItemView::SetMinorText(const base::string16& minor_text) {
357   minor_text_ = minor_text;
358   invalidate_dimensions();  // Triggers preferred size recalculation.
359 }
360 
SetSelected(bool selected)361 void MenuItemView::SetSelected(bool selected) {
362   selected_ = selected;
363   SchedulePaint();
364 }
365 
SetTooltip(const base::string16 & tooltip,int item_id)366 void MenuItemView::SetTooltip(const base::string16& tooltip, int item_id) {
367   MenuItemView* item = GetMenuItemByID(item_id);
368   DCHECK(item);
369   item->tooltip_ = tooltip;
370 }
371 
SetIcon(const gfx::ImageSkia & icon,int item_id)372 void MenuItemView::SetIcon(const gfx::ImageSkia& icon, int item_id) {
373   MenuItemView* item = GetMenuItemByID(item_id);
374   DCHECK(item);
375   item->SetIcon(icon);
376 }
377 
SetIcon(const gfx::ImageSkia & icon)378 void MenuItemView::SetIcon(const gfx::ImageSkia& icon) {
379   if (icon.isNull()) {
380     SetIconView(NULL);
381     return;
382   }
383 
384   ImageView* icon_view = new ImageView();
385   icon_view->SetImage(&icon);
386   SetIconView(icon_view);
387 }
388 
SetIconView(View * icon_view)389 void MenuItemView::SetIconView(View* icon_view) {
390   if (icon_view_) {
391     RemoveChildView(icon_view_);
392     delete icon_view_;
393     icon_view_ = NULL;
394   }
395   if (icon_view) {
396     AddChildView(icon_view);
397     icon_view_ = icon_view;
398   }
399   Layout();
400   SchedulePaint();
401 }
402 
OnPaint(gfx::Canvas * canvas)403 void MenuItemView::OnPaint(gfx::Canvas* canvas) {
404   PaintButton(canvas, PB_NORMAL);
405 }
406 
GetPreferredSize() const407 gfx::Size MenuItemView::GetPreferredSize() const {
408   const MenuItemDimensions& dimensions(GetDimensions());
409   return gfx::Size(dimensions.standard_width + dimensions.children_width,
410                    dimensions.height);
411 }
412 
GetHeightForWidth(int width) const413 int MenuItemView::GetHeightForWidth(int width) const {
414   // If this isn't a container, we can just use the preferred size's height.
415   if (!IsContainer())
416     return GetPreferredSize().height();
417 
418   int height = child_at(0)->GetHeightForWidth(width);
419   if (!icon_view_ && GetRootMenuItem()->has_icons())
420     height = std::max(height, GetMenuConfig().check_height);
421   height += GetBottomMargin() + GetTopMargin();
422 
423   return height;
424 }
425 
GetDimensions() const426 const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() const {
427   if (!is_dimensions_valid())
428     dimensions_ = CalculateDimensions();
429   DCHECK(is_dimensions_valid());
430   return dimensions_;
431 }
432 
GetMenuController()433 MenuController* MenuItemView::GetMenuController() {
434   return GetRootMenuItem()->controller_;
435 }
436 
GetMenuController() const437 const MenuController* MenuItemView::GetMenuController() const {
438   return GetRootMenuItem()->controller_;
439 }
440 
GetDelegate()441 MenuDelegate* MenuItemView::GetDelegate() {
442   return GetRootMenuItem()->delegate_;
443 }
444 
GetDelegate() const445 const MenuDelegate* MenuItemView::GetDelegate() const {
446   return GetRootMenuItem()->delegate_;
447 }
448 
GetRootMenuItem()449 MenuItemView* MenuItemView::GetRootMenuItem() {
450   return const_cast<MenuItemView*>(
451       static_cast<const MenuItemView*>(this)->GetRootMenuItem());
452 }
453 
GetRootMenuItem() const454 const MenuItemView* MenuItemView::GetRootMenuItem() const {
455   const MenuItemView* item = this;
456   for (const MenuItemView* parent = GetParentMenuItem(); parent;
457        parent = item->GetParentMenuItem())
458     item = parent;
459   return item;
460 }
461 
GetMnemonic()462 base::char16 MenuItemView::GetMnemonic() {
463   if (!GetRootMenuItem()->has_mnemonics_)
464     return 0;
465 
466   size_t index = 0;
467   do {
468     index = title_.find('&', index);
469     if (index != base::string16::npos) {
470       if (index + 1 != title_.size() && title_[index + 1] != '&') {
471         base::char16 char_array[] = { title_[index + 1], 0 };
472         // TODO(jshin): What about Turkish locale? See http://crbug.com/81719.
473         // If the mnemonic is capital I and the UI language is Turkish,
474         // lowercasing it results in 'small dotless i', which is different
475         // from a 'dotted i'. Similar issues may exist for az and lt locales.
476         return base::i18n::ToLower(char_array)[0];
477       }
478       index++;
479     }
480   } while (index != base::string16::npos);
481   return 0;
482 }
483 
GetMenuItemByID(int id)484 MenuItemView* MenuItemView::GetMenuItemByID(int id) {
485   if (GetCommand() == id)
486     return this;
487   if (!HasSubmenu())
488     return NULL;
489   for (int i = 0; i < GetSubmenu()->child_count(); ++i) {
490     View* child = GetSubmenu()->child_at(i);
491     if (child->id() == MenuItemView::kMenuItemViewID) {
492       MenuItemView* result = static_cast<MenuItemView*>(child)->
493           GetMenuItemByID(id);
494       if (result)
495         return result;
496     }
497   }
498   return NULL;
499 }
500 
ChildrenChanged()501 void MenuItemView::ChildrenChanged() {
502   MenuController* controller = GetMenuController();
503   if (controller) {
504     // Handles the case where we were empty and are no longer empty.
505     RemoveEmptyMenus();
506 
507     // Handles the case where we were not empty, but now are.
508     AddEmptyMenus();
509 
510     controller->MenuChildrenChanged(this);
511 
512     if (submenu_) {
513       // Force a paint and layout. This handles the case of the top
514       // level window's size remaining the same, resulting in no
515       // change to the submenu's size and no layout.
516       submenu_->Layout();
517       submenu_->SchedulePaint();
518       // Update the menu selection after layout.
519       controller->UpdateSubmenuSelection(submenu_);
520     }
521   }
522 
523   STLDeleteElements(&removed_items_);
524 }
525 
Layout()526 void MenuItemView::Layout() {
527   if (!has_children())
528     return;
529 
530   if (IsContainer()) {
531     View* child = child_at(0);
532     gfx::Size size = child->GetPreferredSize();
533     child->SetBounds(0, GetTopMargin(), size.width(), size.height());
534   } else {
535     // Child views are laid out right aligned and given the full height. To
536     // right align start with the last view and progress to the first.
537     int x = width() - (use_right_margin_ ? item_right_margin_ : 0);
538     for (int i = child_count() - 1; i >= 0; --i) {
539       View* child = child_at(i);
540       if (icon_view_ && (icon_view_ == child))
541         continue;
542       int width = child->GetPreferredSize().width();
543       child->SetBounds(x - width, 0, width, height());
544       x -= width - kChildXPadding;
545     }
546     // Position |icon_view|.
547     const MenuConfig& config = GetMenuConfig();
548     if (icon_view_) {
549       icon_view_->SizeToPreferredSize();
550       gfx::Size size = icon_view_->GetPreferredSize();
551       int x = config.item_left_margin + left_icon_margin_ +
552               (icon_area_width_ - size.width()) / 2;
553       if (type_ == CHECKBOX || type_ == RADIO)
554         x = label_start_;
555       int y =
556           (height() + GetTopMargin() - GetBottomMargin() - size.height()) / 2;
557       icon_view_->SetPosition(gfx::Point(x, y));
558     }
559   }
560 }
561 
SetMargins(int top_margin,int bottom_margin)562 void MenuItemView::SetMargins(int top_margin, int bottom_margin) {
563   top_margin_ = top_margin;
564   bottom_margin_ = bottom_margin;
565 
566   invalidate_dimensions();
567 }
568 
GetMenuConfig() const569 const MenuConfig& MenuItemView::GetMenuConfig() const {
570   const MenuController* controller = GetMenuController();
571   if (controller)
572     return controller->menu_config_;
573   return MenuConfig::instance(NULL);
574 }
575 
MenuItemView(MenuItemView * parent,int command,MenuItemView::Type type)576 MenuItemView::MenuItemView(MenuItemView* parent,
577                            int command,
578                            MenuItemView::Type type)
579     : delegate_(NULL),
580       controller_(NULL),
581       canceled_(false),
582       parent_menu_item_(parent),
583       type_(type),
584       selected_(false),
585       command_(command),
586       submenu_(NULL),
587       has_mnemonics_(false),
588       show_mnemonics_(false),
589       has_icons_(false),
590       icon_view_(NULL),
591       top_margin_(-1),
592       bottom_margin_(-1),
593       left_icon_margin_(0),
594       right_icon_margin_(0),
595       requested_menu_position_(POSITION_BEST_FIT),
596       actual_menu_position_(requested_menu_position_),
597       use_right_margin_(true) {
598   Init(parent, command, type, NULL);
599 }
600 
~MenuItemView()601 MenuItemView::~MenuItemView() {
602   delete submenu_;
603   STLDeleteElements(&removed_items_);
604 }
605 
GetClassName() const606 const char* MenuItemView::GetClassName() const {
607   return kViewClassName;
608 }
609 
610 // Calculates all sizes that we can from the OS.
611 //
612 // This is invoked prior to Running a menu.
UpdateMenuPartSizes()613 void MenuItemView::UpdateMenuPartSizes() {
614   const MenuConfig& config = GetMenuConfig();
615 
616   item_right_margin_ = config.label_to_arrow_padding + config.arrow_width +
617                        config.arrow_to_edge_padding;
618   icon_area_width_ = config.check_width;
619   if (has_icons_)
620     icon_area_width_ = std::max(icon_area_width_, GetMaxIconViewWidth());
621 
622   label_start_ = config.item_left_margin + icon_area_width_;
623   int padding = 0;
624   if (config.always_use_icon_to_label_padding) {
625     padding = config.icon_to_label_padding;
626   } else if (config.render_gutter) {
627     padding = config.item_left_margin;
628   } else {
629     padding = (has_icons_ || HasChecksOrRadioButtons()) ?
630         config.icon_to_label_padding : 0;
631   }
632   label_start_ += padding;
633 
634   if (config.render_gutter)
635     label_start_ += config.gutter_width + config.gutter_to_label;
636 
637   EmptyMenuMenuItem menu_item(this);
638   menu_item.set_controller(GetMenuController());
639   pref_menu_height_ = menu_item.GetPreferredSize().height();
640 }
641 
Init(MenuItemView * parent,int command,MenuItemView::Type type,MenuDelegate * delegate)642 void MenuItemView::Init(MenuItemView* parent,
643                         int command,
644                         MenuItemView::Type type,
645                         MenuDelegate* delegate) {
646   delegate_ = delegate;
647   controller_ = NULL;
648   canceled_ = false;
649   parent_menu_item_ = parent;
650   type_ = type;
651   selected_ = false;
652   command_ = command;
653   submenu_ = NULL;
654   show_mnemonics_ = false;
655   // Assign our ID, this allows SubmenuItemView to find MenuItemViews.
656   set_id(kMenuItemViewID);
657   has_icons_ = false;
658 
659   // Don't request enabled status from the root menu item as it is just
660   // a container for real items.  EMPTY items will be disabled.
661   MenuDelegate* root_delegate = GetDelegate();
662   if (parent && type != EMPTY && root_delegate)
663     SetEnabled(root_delegate->IsCommandEnabled(command));
664 }
665 
PrepareForRun(bool is_first_menu,bool has_mnemonics,bool show_mnemonics)666 void MenuItemView::PrepareForRun(bool is_first_menu,
667                                  bool has_mnemonics,
668                                  bool show_mnemonics) {
669   // Currently we only support showing the root.
670   DCHECK(!parent_menu_item_);
671 
672   // Force us to have a submenu.
673   CreateSubmenu();
674   actual_menu_position_ = requested_menu_position_;
675   canceled_ = false;
676 
677   has_mnemonics_ = has_mnemonics;
678   show_mnemonics_ = has_mnemonics && show_mnemonics;
679 
680   AddEmptyMenus();
681 
682   if (is_first_menu) {
683     // Only update the menu size if there are no menus showing, otherwise
684     // things may shift around.
685     UpdateMenuPartSizes();
686   }
687 }
688 
GetDrawStringFlags()689 int MenuItemView::GetDrawStringFlags() {
690   int flags = 0;
691   if (base::i18n::IsRTL())
692     flags |= gfx::Canvas::TEXT_ALIGN_RIGHT;
693   else
694     flags |= gfx::Canvas::TEXT_ALIGN_LEFT;
695 
696   if (GetRootMenuItem()->has_mnemonics_) {
697     if (GetMenuConfig().show_mnemonics || GetRootMenuItem()->show_mnemonics_) {
698       flags |= gfx::Canvas::SHOW_PREFIX;
699     } else {
700       flags |= gfx::Canvas::HIDE_PREFIX;
701     }
702   }
703   return flags;
704 }
705 
GetFontList() const706 const gfx::FontList& MenuItemView::GetFontList() const {
707   const MenuDelegate* delegate = GetDelegate();
708   if (delegate) {
709     const gfx::FontList* font_list = delegate->GetLabelFontList(GetCommand());
710     if (font_list)
711       return *font_list;
712   }
713   return GetMenuConfig().font_list;
714 }
715 
AddEmptyMenus()716 void MenuItemView::AddEmptyMenus() {
717   DCHECK(HasSubmenu());
718   if (!submenu_->has_children()) {
719     submenu_->AddChildViewAt(new EmptyMenuMenuItem(this), 0);
720   } else {
721     for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count;
722          ++i) {
723       MenuItemView* child = submenu_->GetMenuItemAt(i);
724       if (child->HasSubmenu())
725         child->AddEmptyMenus();
726     }
727   }
728 }
729 
RemoveEmptyMenus()730 void MenuItemView::RemoveEmptyMenus() {
731   DCHECK(HasSubmenu());
732   // Iterate backwards as we may end up removing views, which alters the child
733   // view count.
734   for (int i = submenu_->child_count() - 1; i >= 0; --i) {
735     View* child = submenu_->child_at(i);
736     if (child->id() == MenuItemView::kMenuItemViewID) {
737       MenuItemView* menu_item = static_cast<MenuItemView*>(child);
738       if (menu_item->HasSubmenu())
739         menu_item->RemoveEmptyMenus();
740     } else if (child->id() == EmptyMenuMenuItem::kEmptyMenuItemViewID) {
741       submenu_->RemoveChildView(child);
742       delete child;
743       child = NULL;
744     }
745   }
746 }
747 
AdjustBoundsForRTLUI(gfx::Rect * rect) const748 void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
749   rect->set_x(GetMirroredXForRect(*rect));
750 }
751 
PaintButton(gfx::Canvas * canvas,PaintButtonMode mode)752 void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) {
753   const MenuConfig& config = GetMenuConfig();
754   bool render_selection =
755       (mode == PB_NORMAL && IsSelected() &&
756        parent_menu_item_->GetSubmenu()->GetShowSelection(this) &&
757        (NonIconChildViewsCount() == 0));
758 
759   MenuDelegate *delegate = GetDelegate();
760   // Render the background. As MenuScrollViewContainer draws the background, we
761   // only need the background when we want it to look different, as when we're
762   // selected.
763   ui::NativeTheme* native_theme = GetNativeTheme();
764   SkColor override_color;
765   if (delegate && delegate->GetBackgroundColor(GetCommand(),
766                                                render_selection,
767                                                &override_color)) {
768     canvas->DrawColor(override_color);
769   } else if (render_selection) {
770     gfx::Rect item_bounds(0, 0, width(), height());
771     AdjustBoundsForRTLUI(&item_bounds);
772 
773     native_theme->Paint(canvas->sk_canvas(),
774                         ui::NativeTheme::kMenuItemBackground,
775                         ui::NativeTheme::kHovered,
776                         item_bounds,
777                         ui::NativeTheme::ExtraParams());
778   }
779 
780   const int icon_x = config.item_left_margin + left_icon_margin_;
781   const int top_margin = GetTopMargin();
782   const int bottom_margin = GetBottomMargin();
783   const int available_height = height() - top_margin - bottom_margin;
784 
785   // Render the check.
786   if (type_ == CHECKBOX && delegate->IsItemChecked(GetCommand())) {
787     gfx::ImageSkia check = GetMenuCheckImage(render_selection);
788     // Don't use config.check_width here as it's padded
789     // to force more padding (AURA).
790     gfx::Rect check_bounds(icon_x,
791                            top_margin + (available_height - check.height()) / 2,
792                            check.width(),
793                            check.height());
794     AdjustBoundsForRTLUI(&check_bounds);
795     canvas->DrawImageInt(check, check_bounds.x(), check_bounds.y());
796   } else if (type_ == RADIO) {
797     gfx::ImageSkia image =
798         GetRadioButtonImage(delegate->IsItemChecked(GetCommand()));
799     gfx::Rect radio_bounds(icon_x,
800                            top_margin + (available_height - image.height()) / 2,
801                            image.width(),
802                            image.height());
803     AdjustBoundsForRTLUI(&radio_bounds);
804     canvas->DrawImageInt(image, radio_bounds.x(), radio_bounds.y());
805   }
806 
807   // Render the foreground.
808   ui::NativeTheme::ColorId color_id;
809   if (enabled()) {
810     color_id = render_selection ?
811         ui::NativeTheme::kColorId_SelectedMenuItemForegroundColor:
812         ui::NativeTheme::kColorId_EnabledMenuItemForegroundColor;
813   } else {
814     bool emphasized = delegate &&
815                       delegate->GetShouldUseDisabledEmphasizedForegroundColor(
816                           GetCommand());
817     color_id = emphasized ?
818         ui::NativeTheme::kColorId_DisabledEmphasizedMenuItemForegroundColor :
819         ui::NativeTheme::kColorId_DisabledMenuItemForegroundColor;
820   }
821   SkColor fg_color = native_theme->GetSystemColor(color_id);
822   SkColor override_foreground_color;
823   if (delegate && delegate->GetForegroundColor(GetCommand(),
824                                                render_selection,
825                                                &override_foreground_color))
826     fg_color = override_foreground_color;
827 
828   const gfx::FontList& font_list = GetFontList();
829   int accel_width = parent_menu_item_->GetSubmenu()->max_minor_text_width();
830   int label_start = GetLabelStartForThisItem();
831 
832   int width = this->width() - label_start - accel_width -
833       (!delegate ||
834        delegate->ShouldReserveSpaceForSubmenuIndicator() ?
835            item_right_margin_ : config.arrow_to_edge_padding);
836   gfx::Rect text_bounds(label_start, top_margin, width,
837                         subtitle_.empty() ? available_height
838                                           : available_height / 2);
839   text_bounds.set_x(GetMirroredXForRect(text_bounds));
840   int flags = GetDrawStringFlags();
841   if (mode == PB_FOR_DRAG)
842     flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING;
843   canvas->DrawStringRectWithFlags(title(), font_list, fg_color, text_bounds,
844                                   flags);
845   if (!subtitle_.empty()) {
846     canvas->DrawStringRectWithFlags(
847         subtitle_,
848         font_list,
849         GetNativeTheme()->GetSystemColor(
850             ui::NativeTheme::kColorId_ButtonDisabledColor),
851         text_bounds + gfx::Vector2d(0, font_list.GetHeight()),
852         flags);
853   }
854 
855   PaintMinorText(canvas, render_selection);
856 
857   // Render the submenu indicator (arrow).
858   if (HasSubmenu()) {
859     gfx::ImageSkia arrow = GetSubmenuArrowImage(render_selection);
860     gfx::Rect arrow_bounds(this->width() - config.arrow_width -
861                                config.arrow_to_edge_padding,
862                            top_margin + (available_height - arrow.height()) / 2,
863                            config.arrow_width,
864                            arrow.height());
865     AdjustBoundsForRTLUI(&arrow_bounds);
866     canvas->DrawImageInt(arrow, arrow_bounds.x(), arrow_bounds.y());
867   }
868 }
869 
PaintMinorText(gfx::Canvas * canvas,bool render_selection)870 void MenuItemView::PaintMinorText(gfx::Canvas* canvas,
871                                   bool render_selection) {
872   base::string16 minor_text = GetMinorText();
873   if (minor_text.empty())
874     return;
875 
876   int available_height = height() - GetTopMargin() - GetBottomMargin();
877   int max_accel_width =
878       parent_menu_item_->GetSubmenu()->max_minor_text_width();
879   const MenuConfig& config = GetMenuConfig();
880   int accel_right_margin = config.align_arrow_and_shortcut ?
881                            config.arrow_to_edge_padding :  item_right_margin_;
882   gfx::Rect accel_bounds(width() - accel_right_margin - max_accel_width,
883                          GetTopMargin(), max_accel_width, available_height);
884   accel_bounds.set_x(GetMirroredXForRect(accel_bounds));
885   int flags = GetDrawStringFlags();
886   flags &= ~(gfx::Canvas::TEXT_ALIGN_RIGHT | gfx::Canvas::TEXT_ALIGN_LEFT);
887   if (base::i18n::IsRTL())
888     flags |= gfx::Canvas::TEXT_ALIGN_LEFT;
889   else
890     flags |= gfx::Canvas::TEXT_ALIGN_RIGHT;
891   canvas->DrawStringRectWithFlags(
892       minor_text,
893       GetFontList(),
894       GetNativeTheme()->GetSystemColor(render_selection ?
895           ui::NativeTheme::kColorId_SelectedMenuItemForegroundColor :
896           ui::NativeTheme::kColorId_ButtonDisabledColor),
897       accel_bounds,
898       flags);
899 }
900 
DestroyAllMenuHosts()901 void MenuItemView::DestroyAllMenuHosts() {
902   if (!HasSubmenu())
903     return;
904 
905   submenu_->Close();
906   for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count;
907        ++i) {
908     submenu_->GetMenuItemAt(i)->DestroyAllMenuHosts();
909   }
910 }
911 
GetTopMargin() const912 int MenuItemView::GetTopMargin() const {
913   if (top_margin_ >= 0)
914     return top_margin_;
915 
916   const MenuItemView* root = GetRootMenuItem();
917   return root && root->has_icons_
918       ? GetMenuConfig().item_top_margin :
919         GetMenuConfig().item_no_icon_top_margin;
920 }
921 
GetBottomMargin() const922 int MenuItemView::GetBottomMargin() const {
923   if (bottom_margin_ >= 0)
924     return bottom_margin_;
925 
926   const MenuItemView* root = GetRootMenuItem();
927   return root && root->has_icons_
928       ? GetMenuConfig().item_bottom_margin :
929         GetMenuConfig().item_no_icon_bottom_margin;
930 }
931 
GetChildPreferredSize() const932 gfx::Size MenuItemView::GetChildPreferredSize() const {
933   if (!has_children())
934     return gfx::Size();
935 
936   if (IsContainer())
937     return child_at(0)->GetPreferredSize();
938 
939   int width = 0;
940   for (int i = 0; i < child_count(); ++i) {
941     const View* child = child_at(i);
942     if (icon_view_ && (icon_view_ == child))
943       continue;
944     if (i)
945       width += kChildXPadding;
946     width += child->GetPreferredSize().width();
947   }
948   int height = 0;
949   if (icon_view_)
950     height = icon_view_->GetPreferredSize().height();
951 
952   // If there is no icon view it returns a height of 0 to indicate that
953   // we should use the title height instead.
954   return gfx::Size(width, height);
955 }
956 
CalculateDimensions() const957 MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const {
958   gfx::Size child_size = GetChildPreferredSize();
959 
960   MenuItemDimensions dimensions;
961   // Get the container height.
962   dimensions.children_width = child_size.width();
963   dimensions.height = child_size.height();
964   // Adjust item content height if menu has both items with and without icons.
965   // This way all menu items will have the same height.
966   if (!icon_view_ && GetRootMenuItem()->has_icons()) {
967     dimensions.height = std::max(dimensions.height,
968                                  GetMenuConfig().check_height);
969   }
970   dimensions.height += GetBottomMargin() + GetTopMargin();
971 
972   // In case of a container, only the container size needs to be filled.
973   if (IsContainer())
974     return dimensions;
975 
976   // Determine the length of the label text.
977   const gfx::FontList& font_list = GetFontList();
978 
979   // Get Icon margin overrides for this particular item.
980   const MenuDelegate* delegate = GetDelegate();
981   if (delegate) {
982     delegate->GetHorizontalIconMargins(command_,
983                                        icon_area_width_,
984                                        &left_icon_margin_,
985                                        &right_icon_margin_);
986   } else {
987     left_icon_margin_ = 0;
988     right_icon_margin_ = 0;
989   }
990   int label_start = GetLabelStartForThisItem();
991 
992   int string_width = gfx::GetStringWidth(title_, font_list);
993   if (!subtitle_.empty()) {
994     string_width = std::max(string_width,
995                             gfx::GetStringWidth(subtitle_, font_list));
996   }
997 
998   dimensions.standard_width = string_width + label_start +
999       item_right_margin_;
1000   // Determine the length of the right-side text.
1001   base::string16 minor_text = GetMinorText();
1002   dimensions.minor_text_width =
1003       minor_text.empty() ? 0 : gfx::GetStringWidth(minor_text, font_list);
1004 
1005   // Determine the height to use.
1006   dimensions.height =
1007       std::max(dimensions.height,
1008                (subtitle_.empty() ? 0 : font_list.GetHeight()) +
1009                font_list.GetHeight() + GetBottomMargin() + GetTopMargin());
1010   dimensions.height = std::max(dimensions.height,
1011                                GetMenuConfig().item_min_height);
1012   return dimensions;
1013 }
1014 
GetLabelStartForThisItem() const1015 int MenuItemView::GetLabelStartForThisItem() const {
1016   int label_start = label_start_ + left_icon_margin_ + right_icon_margin_;
1017   if ((type_ == CHECKBOX || type_ == RADIO) && icon_view_) {
1018     label_start += icon_view_->size().width() +
1019         GetMenuConfig().icon_to_label_padding;
1020   }
1021   return label_start;
1022 }
1023 
GetMinorText() const1024 base::string16 MenuItemView::GetMinorText() const {
1025   if (id() == kEmptyMenuItemViewID) {
1026     // Don't query the delegate for menus that represent no children.
1027     return base::string16();
1028   }
1029 
1030   ui::Accelerator accelerator;
1031   if (GetMenuConfig().show_accelerators && GetDelegate() && GetCommand() &&
1032           GetDelegate()->GetAccelerator(GetCommand(), &accelerator)) {
1033     return accelerator.GetShortcutText();
1034   }
1035 
1036   return minor_text_;
1037 }
1038 
IsContainer() const1039 bool MenuItemView::IsContainer() const {
1040   // Let the first child take over |this| when we only have one child and no
1041   // title.
1042   return (NonIconChildViewsCount() == 1) && title_.empty();
1043 }
1044 
NonIconChildViewsCount() const1045 int MenuItemView::NonIconChildViewsCount() const {
1046   // Note that what child_count() returns is the number of children,
1047   // not the number of menu items.
1048   return child_count() - (icon_view_ ? 1 : 0);
1049 }
1050 
GetMaxIconViewWidth() const1051 int MenuItemView::GetMaxIconViewWidth() const {
1052   int width = 0;
1053   for (int i = 0; i < submenu_->GetMenuItemCount(); ++i) {
1054     MenuItemView* menu_item = submenu_->GetMenuItemAt(i);
1055     int temp_width = 0;
1056     if (menu_item->GetType() == CHECKBOX ||
1057         menu_item->GetType() == RADIO) {
1058       // If this item has a radio or checkbox, the icon will not affect
1059       // alignment of other items.
1060       continue;
1061     } else if (menu_item->HasSubmenu()) {
1062       temp_width = menu_item->GetMaxIconViewWidth();
1063     } else if (menu_item->icon_view()) {
1064       temp_width = menu_item->icon_view()->GetPreferredSize().width();
1065     }
1066     width = std::max(width, temp_width);
1067   }
1068   return width;
1069 }
1070 
HasChecksOrRadioButtons() const1071 bool MenuItemView::HasChecksOrRadioButtons() const {
1072   for (int i = 0; i < submenu_->GetMenuItemCount(); ++i) {
1073     MenuItemView* menu_item = submenu_->GetMenuItemAt(i);
1074     if (menu_item->HasSubmenu()) {
1075       if (menu_item->HasChecksOrRadioButtons())
1076         return true;
1077     } else {
1078       const Type& type = menu_item->GetType();
1079       if (type == CHECKBOX || type == RADIO)
1080         return true;
1081     }
1082   }
1083   return false;
1084 }
1085 
1086 }  // namespace views
1087