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
32namespace views {
33
34namespace {
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
42class EmptyMenuMenuItem : public MenuItemView {
43 public:
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
52  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.
65static const int kChildXPadding = 8;
66
67// MenuItemView ---------------------------------------------------------------
68
69// static
70const int MenuItemView::kMenuItemViewID = 1001;
71
72// static
73const int MenuItemView::kEmptyMenuItemViewID =
74    MenuItemView::kMenuItemViewID + 1;
75
76// static
77int MenuItemView::icon_area_width_ = 0;
78
79// static
80int MenuItemView::label_start_;
81
82// static
83int MenuItemView::item_right_margin_;
84
85// static
86int MenuItemView::pref_menu_height_;
87
88// static
89const char MenuItemView::kViewClassName[] = "MenuItemView";
90
91MenuItemView::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
116void MenuItemView::ChildPreferredSizeChanged(View* child) {
117  invalidate_dimensions();
118  PreferredSizeChanged();
119}
120
121bool 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
153void 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
187bool 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
195base::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
221void MenuItemView::Cancel() {
222  if (controller_ && !canceled_) {
223    canceled_ = true;
224    controller_->Cancel(MenuController::EXIT_ALL);
225  }
226}
227
228MenuItemView* 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
263void 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
278MenuItemView* 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
285MenuItemView* 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
291MenuItemView* 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
298MenuItemView* MenuItemView::AppendMenuItemWithLabel(
299    int item_id,
300    const base::string16& label) {
301  return AppendMenuItem(item_id, label, NORMAL);
302}
303
304MenuItemView* MenuItemView::AppendDelegateMenuItem(int item_id) {
305  return AppendMenuItem(item_id, base::string16(), NORMAL);
306}
307
308void MenuItemView::AppendSeparator() {
309  AppendMenuItemImpl(0, base::string16(), base::string16(), base::string16(),
310                     gfx::ImageSkia(), SEPARATOR, ui::NORMAL_SEPARATOR);
311}
312
313MenuItemView* 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
320MenuItemView* 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
333SubmenuView* MenuItemView::CreateSubmenu() {
334  if (!submenu_)
335    submenu_ = new SubmenuView(this);
336  return submenu_;
337}
338
339bool MenuItemView::HasSubmenu() const {
340  return (submenu_ != NULL);
341}
342
343SubmenuView* MenuItemView::GetSubmenu() const {
344  return submenu_;
345}
346
347void MenuItemView::SetTitle(const base::string16& title) {
348  title_ = title;
349  invalidate_dimensions();  // Triggers preferred size recalculation.
350}
351
352void MenuItemView::SetSubtitle(const base::string16& subtitle) {
353  subtitle_ = subtitle;
354  invalidate_dimensions();  // Triggers preferred size recalculation.
355}
356
357void MenuItemView::SetMinorText(const base::string16& minor_text) {
358  minor_text_ = minor_text;
359  invalidate_dimensions();  // Triggers preferred size recalculation.
360}
361
362void MenuItemView::SetSelected(bool selected) {
363  selected_ = selected;
364  SchedulePaint();
365}
366
367void MenuItemView::SetTooltip(const base::string16& tooltip, int item_id) {
368  MenuItemView* item = GetMenuItemByID(item_id);
369  DCHECK(item);
370  item->tooltip_ = tooltip;
371}
372
373void MenuItemView::SetIcon(const gfx::ImageSkia& icon, int item_id) {
374  MenuItemView* item = GetMenuItemByID(item_id);
375  DCHECK(item);
376  item->SetIcon(icon);
377}
378
379void 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
390void 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
404void MenuItemView::OnPaint(gfx::Canvas* canvas) {
405  PaintButton(canvas, PB_NORMAL);
406}
407
408gfx::Size MenuItemView::GetPreferredSize() const {
409  const MenuItemDimensions& dimensions(GetDimensions());
410  return gfx::Size(dimensions.standard_width + dimensions.children_width,
411                   dimensions.height);
412}
413
414const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() const {
415  if (!is_dimensions_valid())
416    dimensions_ = CalculateDimensions();
417  DCHECK(is_dimensions_valid());
418  return dimensions_;
419}
420
421MenuController* MenuItemView::GetMenuController() {
422  return GetRootMenuItem()->controller_;
423}
424
425const MenuController* MenuItemView::GetMenuController() const {
426  return GetRootMenuItem()->controller_;
427}
428
429MenuDelegate* MenuItemView::GetDelegate() {
430  return GetRootMenuItem()->delegate_;
431}
432
433const MenuDelegate* MenuItemView::GetDelegate() const {
434  return GetRootMenuItem()->delegate_;
435}
436
437MenuItemView* MenuItemView::GetRootMenuItem() {
438  return const_cast<MenuItemView*>(
439      static_cast<const MenuItemView*>(this)->GetRootMenuItem());
440}
441
442const 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
450base::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
472MenuItemView* 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
489void 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
514void 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
550void MenuItemView::SetMargins(int top_margin, int bottom_margin) {
551  top_margin_ = top_margin;
552  bottom_margin_ = bottom_margin;
553
554  invalidate_dimensions();
555}
556
557const MenuConfig& MenuItemView::GetMenuConfig() const {
558  const MenuController* controller = GetMenuController();
559  if (controller)
560    return controller->menu_config_;
561  return MenuConfig::instance(NULL);
562}
563
564MenuItemView::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
589MenuItemView::~MenuItemView() {
590  delete submenu_;
591  STLDeleteElements(&removed_items_);
592}
593
594const 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.
601void 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
630void 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
654void 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
677int 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
694const 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
704void 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
718void 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
736void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
737  rect->set_x(GetMirroredXForRect(*rect));
738}
739
740void 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
858void 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
889void 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
900int 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
910int 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
920gfx::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
945MenuItemView::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
1003int 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
1012base::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
1027bool 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
1033int 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
1039int 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
1059bool 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