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: 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 52 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 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 116 void MenuItemView::ChildPreferredSizeChanged(View* child) { 117 invalidate_dimensions(); 118 PreferredSizeChanged(); 119 } 120 121 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 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 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 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 221 void MenuItemView::Cancel() { 222 if (controller_ && !canceled_) { 223 canceled_ = true; 224 controller_->Cancel(MenuController::EXIT_ALL); 225 } 226 } 227 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 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 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 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 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 298 MenuItemView* MenuItemView::AppendMenuItemWithLabel( 299 int item_id, 300 const base::string16& label) { 301 return AppendMenuItem(item_id, label, NORMAL); 302 } 303 304 MenuItemView* MenuItemView::AppendDelegateMenuItem(int item_id) { 305 return AppendMenuItem(item_id, base::string16(), NORMAL); 306 } 307 308 void MenuItemView::AppendSeparator() { 309 AppendMenuItemImpl(0, base::string16(), base::string16(), base::string16(), 310 gfx::ImageSkia(), SEPARATOR, ui::NORMAL_SEPARATOR); 311 } 312 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 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 333 SubmenuView* MenuItemView::CreateSubmenu() { 334 if (!submenu_) 335 submenu_ = new SubmenuView(this); 336 return submenu_; 337 } 338 339 bool MenuItemView::HasSubmenu() const { 340 return (submenu_ != NULL); 341 } 342 343 SubmenuView* MenuItemView::GetSubmenu() const { 344 return submenu_; 345 } 346 347 void MenuItemView::SetTitle(const base::string16& title) { 348 title_ = title; 349 invalidate_dimensions(); // Triggers preferred size recalculation. 350 } 351 352 void MenuItemView::SetSubtitle(const base::string16& subtitle) { 353 subtitle_ = subtitle; 354 invalidate_dimensions(); // Triggers preferred size recalculation. 355 } 356 357 void MenuItemView::SetMinorText(const base::string16& minor_text) { 358 minor_text_ = minor_text; 359 invalidate_dimensions(); // Triggers preferred size recalculation. 360 } 361 362 void MenuItemView::SetSelected(bool selected) { 363 selected_ = selected; 364 SchedulePaint(); 365 } 366 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 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 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 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 404 void MenuItemView::OnPaint(gfx::Canvas* canvas) { 405 PaintButton(canvas, PB_NORMAL); 406 } 407 408 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 414 const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() const { 415 if (!is_dimensions_valid()) 416 dimensions_ = CalculateDimensions(); 417 DCHECK(is_dimensions_valid()); 418 return dimensions_; 419 } 420 421 MenuController* MenuItemView::GetMenuController() { 422 return GetRootMenuItem()->controller_; 423 } 424 425 const MenuController* MenuItemView::GetMenuController() const { 426 return GetRootMenuItem()->controller_; 427 } 428 429 MenuDelegate* MenuItemView::GetDelegate() { 430 return GetRootMenuItem()->delegate_; 431 } 432 433 const MenuDelegate* MenuItemView::GetDelegate() const { 434 return GetRootMenuItem()->delegate_; 435 } 436 437 MenuItemView* MenuItemView::GetRootMenuItem() { 438 return const_cast<MenuItemView*>( 439 static_cast<const MenuItemView*>(this)->GetRootMenuItem()); 440 } 441 442 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 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 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 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 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 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 557 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 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 589 MenuItemView::~MenuItemView() { 590 delete submenu_; 591 STLDeleteElements(&removed_items_); 592 } 593 594 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. 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 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 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 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 694 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 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 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 736 void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const { 737 rect->set_x(GetMirroredXForRect(*rect)); 738 } 739 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 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 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 900 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 910 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 920 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 945 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 1003 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 1012 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 1027 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 1033 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 1039 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 1059 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