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