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