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: 41 explicit EmptyMenuMenuItem(MenuItemView* parent) 42 : MenuItemView(parent, 0, EMPTY) { 43 // Set this so that we're not identified as a normal menu item. 44 set_id(kEmptyMenuItemViewID); 45 SetTitle(l10n_util::GetStringUTF16(IDS_APP_MENU_EMPTY_SUBMENU)); 46 SetEnabled(false); 47 } 48 49 virtual bool GetTooltipText(const gfx::Point& p, 50 string16* tooltip) const OVERRIDE { 51 // Empty menu items shouldn't have a tooltip. 52 return false; 53 } 54 55 private: 56 DISALLOW_COPY_AND_ASSIGN(EmptyMenuMenuItem); 57 }; 58 59 } // namespace 60 61 // Padding between child views. 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 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 113 void MenuItemView::ChildPreferredSizeChanged(View* child) { 114 invalidate_dimensions(); 115 PreferredSizeChanged(); 116 } 117 118 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 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 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 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 218 void MenuItemView::Cancel() { 219 if (controller_ && !canceled_) { 220 canceled_ = true; 221 controller_->Cancel(MenuController::EXIT_ALL); 222 } 223 } 224 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 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 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 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 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 293 MenuItemView* MenuItemView::AppendMenuItemWithLabel(int item_id, 294 const string16& label) { 295 return AppendMenuItem(item_id, label, NORMAL); 296 } 297 298 MenuItemView* MenuItemView::AppendDelegateMenuItem(int item_id) { 299 return AppendMenuItem(item_id, string16(), NORMAL); 300 } 301 302 void MenuItemView::AppendSeparator() { 303 AppendMenuItemImpl(0, string16(), string16(), string16(), gfx::ImageSkia(), 304 SEPARATOR, ui::NORMAL_SEPARATOR); 305 } 306 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 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 327 SubmenuView* MenuItemView::CreateSubmenu() { 328 if (!submenu_) 329 submenu_ = new SubmenuView(this); 330 return submenu_; 331 } 332 333 bool MenuItemView::HasSubmenu() const { 334 return (submenu_ != NULL); 335 } 336 337 SubmenuView* MenuItemView::GetSubmenu() const { 338 return submenu_; 339 } 340 341 void MenuItemView::SetTitle(const string16& title) { 342 title_ = title; 343 invalidate_dimensions(); // Triggers preferred size recalculation. 344 } 345 346 void MenuItemView::SetSubtitle(const string16& subtitle) { 347 subtitle_ = subtitle; 348 invalidate_dimensions(); // Triggers preferred size recalculation. 349 } 350 351 void MenuItemView::SetMinorText(const string16& minor_text) { 352 minor_text_ = minor_text; 353 invalidate_dimensions(); // Triggers preferred size recalculation. 354 } 355 356 void MenuItemView::SetSelected(bool selected) { 357 selected_ = selected; 358 SchedulePaint(); 359 } 360 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 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 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 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 398 void MenuItemView::OnPaint(gfx::Canvas* canvas) { 399 PaintButton(canvas, PB_NORMAL); 400 } 401 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 408 const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() { 409 if (!is_dimensions_valid()) 410 dimensions_ = CalculateDimensions(); 411 DCHECK(is_dimensions_valid()); 412 return dimensions_; 413 } 414 415 MenuController* MenuItemView::GetMenuController() { 416 return GetRootMenuItem()->controller_; 417 } 418 419 const MenuController* MenuItemView::GetMenuController() const { 420 return GetRootMenuItem()->controller_; 421 } 422 423 MenuDelegate* MenuItemView::GetDelegate() { 424 return GetRootMenuItem()->delegate_; 425 } 426 427 const MenuDelegate* MenuItemView::GetDelegate() const { 428 return GetRootMenuItem()->delegate_; 429 } 430 431 MenuItemView* MenuItemView::GetRootMenuItem() { 432 return const_cast<MenuItemView*>( 433 static_cast<const MenuItemView*>(this)->GetRootMenuItem()); 434 } 435 436 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 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 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 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 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 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 551 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 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 583 MenuItemView::~MenuItemView() { 584 delete submenu_; 585 STLDeleteElements(&removed_items_); 586 } 587 588 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. 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 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 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 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 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 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 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 730 void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const { 731 rect->set_x(GetMirroredXForRect(*rect)); 732 } 733 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 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 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 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 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 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 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 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 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 1023 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 1029 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 1035 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 1055 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