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