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