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