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: 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 51 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 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 115 void MenuItemView::ChildPreferredSizeChanged(View* child) { 116 invalidate_dimensions(); 117 PreferredSizeChanged(); 118 } 119 120 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 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 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 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 220 void MenuItemView::Cancel() { 221 if (controller_ && !canceled_) { 222 canceled_ = true; 223 controller_->Cancel(MenuController::EXIT_ALL); 224 } 225 } 226 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 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 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 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 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 297 MenuItemView* MenuItemView::AppendMenuItemWithLabel( 298 int item_id, 299 const base::string16& label) { 300 return AppendMenuItem(item_id, label, NORMAL); 301 } 302 303 MenuItemView* MenuItemView::AppendDelegateMenuItem(int item_id) { 304 return AppendMenuItem(item_id, base::string16(), NORMAL); 305 } 306 307 void MenuItemView::AppendSeparator() { 308 AppendMenuItemImpl(0, base::string16(), base::string16(), base::string16(), 309 gfx::ImageSkia(), SEPARATOR, ui::NORMAL_SEPARATOR); 310 } 311 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 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 332 SubmenuView* MenuItemView::CreateSubmenu() { 333 if (!submenu_) 334 submenu_ = new SubmenuView(this); 335 return submenu_; 336 } 337 338 bool MenuItemView::HasSubmenu() const { 339 return (submenu_ != NULL); 340 } 341 342 SubmenuView* MenuItemView::GetSubmenu() const { 343 return submenu_; 344 } 345 346 void MenuItemView::SetTitle(const base::string16& title) { 347 title_ = title; 348 invalidate_dimensions(); // Triggers preferred size recalculation. 349 } 350 351 void MenuItemView::SetSubtitle(const base::string16& subtitle) { 352 subtitle_ = subtitle; 353 invalidate_dimensions(); // Triggers preferred size recalculation. 354 } 355 356 void MenuItemView::SetMinorText(const base::string16& minor_text) { 357 minor_text_ = minor_text; 358 invalidate_dimensions(); // Triggers preferred size recalculation. 359 } 360 361 void MenuItemView::SetSelected(bool selected) { 362 selected_ = selected; 363 SchedulePaint(); 364 } 365 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 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 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 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 403 void MenuItemView::OnPaint(gfx::Canvas* canvas) { 404 PaintButton(canvas, PB_NORMAL); 405 } 406 407 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 413 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 426 const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() const { 427 if (!is_dimensions_valid()) 428 dimensions_ = CalculateDimensions(); 429 DCHECK(is_dimensions_valid()); 430 return dimensions_; 431 } 432 433 MenuController* MenuItemView::GetMenuController() { 434 return GetRootMenuItem()->controller_; 435 } 436 437 const MenuController* MenuItemView::GetMenuController() const { 438 return GetRootMenuItem()->controller_; 439 } 440 441 MenuDelegate* MenuItemView::GetDelegate() { 442 return GetRootMenuItem()->delegate_; 443 } 444 445 const MenuDelegate* MenuItemView::GetDelegate() const { 446 return GetRootMenuItem()->delegate_; 447 } 448 449 MenuItemView* MenuItemView::GetRootMenuItem() { 450 return const_cast<MenuItemView*>( 451 static_cast<const MenuItemView*>(this)->GetRootMenuItem()); 452 } 453 454 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 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 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 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 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 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 569 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 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 601 MenuItemView::~MenuItemView() { 602 delete submenu_; 603 STLDeleteElements(&removed_items_); 604 } 605 606 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. 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 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 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 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 706 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 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 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 748 void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const { 749 rect->set_x(GetMirroredXForRect(*rect)); 750 } 751 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 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 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 912 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 922 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 932 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 957 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 1015 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 1024 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 1039 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 1045 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 1051 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 1071 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