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