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