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 gfx::ImageSkia& icon, 231 Type type, 232 ui::MenuSeparatorType separator_style) { 233 DCHECK_NE(type, EMPTY); 234 DCHECK_LE(0, index); 235 if (!submenu_) 236 CreateSubmenu(); 237 DCHECK_GE(submenu_->child_count(), index); 238 if (type == SEPARATOR) { 239 submenu_->AddChildViewAt(new MenuSeparator(this, separator_style), index); 240 return NULL; 241 } 242 MenuItemView* item = new MenuItemView(this, item_id, type); 243 if (label.empty() && GetDelegate()) 244 item->SetTitle(GetDelegate()->GetLabel(item_id)); 245 else 246 item->SetTitle(label); 247 item->SetSubtitle(sublabel); 248 if (!icon.isNull()) 249 item->SetIcon(icon); 250 if (type == SUBMENU) 251 item->CreateSubmenu(); 252 submenu_->AddChildViewAt(item, index); 253 return item; 254 } 255 256 void MenuItemView::RemoveMenuItemAt(int index) { 257 DCHECK(submenu_); 258 DCHECK_LE(0, index); 259 DCHECK_GT(submenu_->child_count(), index); 260 261 View* item = submenu_->child_at(index); 262 DCHECK(item); 263 submenu_->RemoveChildView(item); 264 265 // RemoveChildView() does not delete the item, which is a good thing 266 // in case a submenu is being displayed while items are being removed. 267 // Deletion will be done by ChildrenChanged() or at destruction. 268 removed_items_.push_back(item); 269 } 270 271 MenuItemView* MenuItemView::AppendMenuItem(int item_id, 272 const string16& label, 273 Type type) { 274 return AppendMenuItemImpl(item_id, label, string16(), gfx::ImageSkia(), type, 275 ui::NORMAL_SEPARATOR); 276 } 277 278 MenuItemView* MenuItemView::AppendSubMenu(int item_id, 279 const string16& label) { 280 return AppendMenuItemImpl(item_id, label, string16(), gfx::ImageSkia(), 281 SUBMENU, ui::NORMAL_SEPARATOR); 282 } 283 284 MenuItemView* MenuItemView::AppendSubMenuWithIcon(int item_id, 285 const string16& label, 286 const gfx::ImageSkia& icon) { 287 return AppendMenuItemImpl( 288 item_id, label, string16(), icon, SUBMENU, ui::NORMAL_SEPARATOR); 289 } 290 291 MenuItemView* MenuItemView::AppendMenuItemWithLabel(int item_id, 292 const string16& label) { 293 return AppendMenuItem(item_id, label, NORMAL); 294 } 295 296 MenuItemView* MenuItemView::AppendDelegateMenuItem(int item_id) { 297 return AppendMenuItem(item_id, string16(), NORMAL); 298 } 299 300 void MenuItemView::AppendSeparator() { 301 AppendMenuItemImpl(0, string16(), string16(), gfx::ImageSkia(), SEPARATOR, 302 ui::NORMAL_SEPARATOR); 303 } 304 305 MenuItemView* MenuItemView::AppendMenuItemWithIcon(int item_id, 306 const string16& label, 307 const gfx::ImageSkia& icon) { 308 return AppendMenuItemImpl( 309 item_id, label, string16(), icon, NORMAL, ui::NORMAL_SEPARATOR); 310 } 311 312 MenuItemView* MenuItemView::AppendMenuItemFromModel(ui::MenuModel* model, 313 int index, 314 int id) { 315 gfx::Image icon; 316 model->GetIconAt(index, &icon); 317 string16 label, sublabel; 318 ui::MenuSeparatorType separator_style = ui::NORMAL_SEPARATOR; 319 MenuItemView::Type type; 320 ui::MenuModel::ItemType menu_type = model->GetTypeAt(index); 321 switch (menu_type) { 322 case ui::MenuModel::TYPE_COMMAND: 323 type = MenuItemView::NORMAL; 324 label = model->GetLabelAt(index); 325 sublabel = model->GetSublabelAt(index); 326 break; 327 case ui::MenuModel::TYPE_CHECK: 328 type = MenuItemView::CHECKBOX; 329 label = model->GetLabelAt(index); 330 sublabel = model->GetSublabelAt(index); 331 break; 332 case ui::MenuModel::TYPE_RADIO: 333 type = MenuItemView::RADIO; 334 label = model->GetLabelAt(index); 335 sublabel = model->GetSublabelAt(index); 336 break; 337 case ui::MenuModel::TYPE_SEPARATOR: 338 icon = gfx::Image(); 339 type = MenuItemView::SEPARATOR; 340 separator_style = model->GetSeparatorTypeAt(index); 341 break; 342 case ui::MenuModel::TYPE_SUBMENU: 343 type = MenuItemView::SUBMENU; 344 label = model->GetLabelAt(index); 345 sublabel = model->GetSublabelAt(index); 346 break; 347 default: 348 NOTREACHED(); 349 type = MenuItemView::NORMAL; 350 break; 351 } 352 353 return AppendMenuItemImpl(id, 354 label, 355 sublabel, 356 icon.IsEmpty() ? gfx::ImageSkia() : *icon.ToImageSkia(), 357 type, 358 separator_style); 359 } 360 361 MenuItemView* MenuItemView::AppendMenuItemImpl( 362 int item_id, 363 const string16& label, 364 const string16& sublabel, 365 const gfx::ImageSkia& icon, 366 Type type, 367 ui::MenuSeparatorType separator_style) { 368 const int index = submenu_ ? submenu_->child_count() : 0; 369 return AddMenuItemAt(index, item_id, label, sublabel, icon, type, 370 separator_style); 371 } 372 373 SubmenuView* MenuItemView::CreateSubmenu() { 374 if (!submenu_) 375 submenu_ = new SubmenuView(this); 376 return submenu_; 377 } 378 379 bool MenuItemView::HasSubmenu() const { 380 return (submenu_ != NULL); 381 } 382 383 SubmenuView* MenuItemView::GetSubmenu() const { 384 return submenu_; 385 } 386 387 void MenuItemView::SetTitle(const string16& title) { 388 title_ = title; 389 invalidate_dimensions(); // Triggers preferred size recalculation. 390 } 391 392 void MenuItemView::SetSubtitle(const string16& subtitle) { 393 subtitle_ = subtitle; 394 invalidate_dimensions(); // Triggers preferred size recalculation. 395 } 396 397 void MenuItemView::SetSelected(bool selected) { 398 selected_ = selected; 399 SchedulePaint(); 400 } 401 402 void MenuItemView::SetTooltip(const string16& tooltip, int item_id) { 403 MenuItemView* item = GetMenuItemByID(item_id); 404 DCHECK(item); 405 item->tooltip_ = tooltip; 406 } 407 408 void MenuItemView::SetIcon(const gfx::ImageSkia& icon, int item_id) { 409 MenuItemView* item = GetMenuItemByID(item_id); 410 DCHECK(item); 411 item->SetIcon(icon); 412 } 413 414 void MenuItemView::SetIcon(const gfx::ImageSkia& icon) { 415 if (icon.isNull()) { 416 SetIconView(NULL); 417 return; 418 } 419 420 ImageView* icon_view = new ImageView(); 421 icon_view->SetImage(&icon); 422 SetIconView(icon_view); 423 } 424 425 void MenuItemView::SetIconView(View* icon_view) { 426 if (icon_view_) { 427 RemoveChildView(icon_view_); 428 delete icon_view_; 429 icon_view_ = NULL; 430 } 431 if (icon_view) { 432 AddChildView(icon_view); 433 icon_view_ = icon_view; 434 } 435 Layout(); 436 SchedulePaint(); 437 } 438 439 void MenuItemView::OnPaint(gfx::Canvas* canvas) { 440 PaintButton(canvas, PB_NORMAL); 441 } 442 443 gfx::Size MenuItemView::GetPreferredSize() { 444 const MenuItemDimensions& dimensions(GetDimensions()); 445 return gfx::Size(dimensions.standard_width + dimensions.children_width, 446 dimensions.height); 447 } 448 449 const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() { 450 if (!is_dimensions_valid()) 451 dimensions_ = CalculateDimensions(); 452 DCHECK(is_dimensions_valid()); 453 return dimensions_; 454 } 455 456 MenuController* MenuItemView::GetMenuController() { 457 return GetRootMenuItem()->controller_; 458 } 459 460 const MenuController* MenuItemView::GetMenuController() const { 461 return GetRootMenuItem()->controller_; 462 } 463 464 MenuDelegate* MenuItemView::GetDelegate() { 465 return GetRootMenuItem()->delegate_; 466 } 467 468 const MenuDelegate* MenuItemView::GetDelegate() const { 469 return GetRootMenuItem()->delegate_; 470 } 471 472 MenuItemView* MenuItemView::GetRootMenuItem() { 473 return const_cast<MenuItemView*>( 474 static_cast<const MenuItemView*>(this)->GetRootMenuItem()); 475 } 476 477 const MenuItemView* MenuItemView::GetRootMenuItem() const { 478 const MenuItemView* item = this; 479 for (const MenuItemView* parent = GetParentMenuItem(); parent; 480 parent = item->GetParentMenuItem()) 481 item = parent; 482 return item; 483 } 484 485 char16 MenuItemView::GetMnemonic() { 486 if (!GetRootMenuItem()->has_mnemonics_) 487 return 0; 488 489 size_t index = 0; 490 do { 491 index = title_.find('&', index); 492 if (index != string16::npos) { 493 if (index + 1 != title_.size() && title_[index + 1] != '&') { 494 char16 char_array[] = { title_[index + 1], 0 }; 495 // TODO(jshin): What about Turkish locale? See http://crbug.com/81719. 496 // If the mnemonic is capital I and the UI language is Turkish, 497 // lowercasing it results in 'small dotless i', which is different 498 // from a 'dotted i'. Similar issues may exist for az and lt locales. 499 return base::i18n::ToLower(char_array)[0]; 500 } 501 index++; 502 } 503 } while (index != string16::npos); 504 return 0; 505 } 506 507 MenuItemView* MenuItemView::GetMenuItemByID(int id) { 508 if (GetCommand() == id) 509 return this; 510 if (!HasSubmenu()) 511 return NULL; 512 for (int i = 0; i < GetSubmenu()->child_count(); ++i) { 513 View* child = GetSubmenu()->child_at(i); 514 if (child->id() == MenuItemView::kMenuItemViewID) { 515 MenuItemView* result = static_cast<MenuItemView*>(child)-> 516 GetMenuItemByID(id); 517 if (result) 518 return result; 519 } 520 } 521 return NULL; 522 } 523 524 void MenuItemView::ChildrenChanged() { 525 MenuController* controller = GetMenuController(); 526 if (controller) { 527 // Handles the case where we were empty and are no longer empty. 528 RemoveEmptyMenus(); 529 530 // Handles the case where we were not empty, but now are. 531 AddEmptyMenus(); 532 533 controller->MenuChildrenChanged(this); 534 535 if (submenu_) { 536 // Force a paint and layout. This handles the case of the top 537 // level window's size remaining the same, resulting in no 538 // change to the submenu's size and no layout. 539 submenu_->Layout(); 540 submenu_->SchedulePaint(); 541 // Update the menu selection after layout. 542 controller->UpdateSubmenuSelection(submenu_); 543 } 544 } 545 546 STLDeleteElements(&removed_items_); 547 } 548 549 void MenuItemView::Layout() { 550 if (!has_children()) 551 return; 552 553 if (IsContainer()) { 554 View* child = child_at(0); 555 gfx::Size size = child->GetPreferredSize(); 556 child->SetBounds(0, GetTopMargin(), size.width(), size.height()); 557 } else { 558 // Child views are laid out right aligned and given the full height. To 559 // right align start with the last view and progress to the first. 560 int x = width() - (use_right_margin_ ? item_right_margin_ : 0); 561 for (int i = child_count() - 1; i >= 0; --i) { 562 View* child = child_at(i); 563 if (icon_view_ && (icon_view_ == child)) 564 continue; 565 int width = child->GetPreferredSize().width(); 566 child->SetBounds(x - width, 0, width, height()); 567 x -= width - kChildXPadding; 568 } 569 // Position |icon_view|. 570 const MenuConfig& config = GetMenuConfig(); 571 if (icon_view_) { 572 icon_view_->SizeToPreferredSize(); 573 gfx::Size size = icon_view_->GetPreferredSize(); 574 int x = config.item_left_margin + left_icon_margin_ + 575 (icon_area_width_ - size.width()) / 2; 576 if (type_ == CHECKBOX || type_ == RADIO) 577 x = label_start_; 578 int y = 579 (height() + GetTopMargin() - GetBottomMargin() - size.height()) / 2; 580 icon_view_->SetPosition(gfx::Point(x, y)); 581 } 582 } 583 } 584 585 void MenuItemView::SetMargins(int top_margin, int bottom_margin) { 586 top_margin_ = top_margin; 587 bottom_margin_ = bottom_margin; 588 589 invalidate_dimensions(); 590 } 591 592 const MenuConfig& MenuItemView::GetMenuConfig() const { 593 const MenuController* controller = GetMenuController(); 594 if (controller) 595 return controller->menu_config_; 596 return MenuConfig::instance(NULL); 597 } 598 599 MenuItemView::MenuItemView(MenuItemView* parent, 600 int command, 601 MenuItemView::Type type) 602 : delegate_(NULL), 603 controller_(NULL), 604 canceled_(false), 605 parent_menu_item_(parent), 606 type_(type), 607 selected_(false), 608 command_(command), 609 submenu_(NULL), 610 has_mnemonics_(false), 611 show_mnemonics_(false), 612 has_icons_(false), 613 icon_view_(NULL), 614 top_margin_(-1), 615 bottom_margin_(-1), 616 left_icon_margin_(0), 617 right_icon_margin_(0), 618 requested_menu_position_(POSITION_BEST_FIT), 619 actual_menu_position_(requested_menu_position_), 620 use_right_margin_(true) { 621 Init(parent, command, type, NULL); 622 } 623 624 MenuItemView::~MenuItemView() { 625 delete submenu_; 626 STLDeleteElements(&removed_items_); 627 } 628 629 const char* MenuItemView::GetClassName() const { 630 return kViewClassName; 631 } 632 633 // Calculates all sizes that we can from the OS. 634 // 635 // This is invoked prior to Running a menu. 636 void MenuItemView::UpdateMenuPartSizes() { 637 const MenuConfig& config = GetMenuConfig(); 638 639 item_right_margin_ = config.label_to_arrow_padding + config.arrow_width + 640 config.arrow_to_edge_padding; 641 icon_area_width_ = config.check_width; 642 if (has_icons_) 643 icon_area_width_ = std::max(icon_area_width_, GetMaxIconViewWidth()); 644 645 label_start_ = config.item_left_margin + icon_area_width_; 646 int padding = 0; 647 if (config.always_use_icon_to_label_padding) { 648 padding = config.icon_to_label_padding; 649 } else if (config.render_gutter) { 650 padding = config.item_left_margin; 651 } else { 652 padding = (has_icons_ || HasChecksOrRadioButtons()) ? 653 config.icon_to_label_padding : 0; 654 } 655 label_start_ += padding; 656 657 if (config.render_gutter) 658 label_start_ += config.gutter_width + config.gutter_to_label; 659 660 EmptyMenuMenuItem menu_item(this); 661 menu_item.set_controller(GetMenuController()); 662 pref_menu_height_ = menu_item.GetPreferredSize().height(); 663 } 664 665 void MenuItemView::Init(MenuItemView* parent, 666 int command, 667 MenuItemView::Type type, 668 MenuDelegate* delegate) { 669 delegate_ = delegate; 670 controller_ = NULL; 671 canceled_ = false; 672 parent_menu_item_ = parent; 673 type_ = type; 674 selected_ = false; 675 command_ = command; 676 submenu_ = NULL; 677 show_mnemonics_ = false; 678 // Assign our ID, this allows SubmenuItemView to find MenuItemViews. 679 set_id(kMenuItemViewID); 680 has_icons_ = false; 681 682 // Don't request enabled status from the root menu item as it is just 683 // a container for real items. EMPTY items will be disabled. 684 MenuDelegate* root_delegate = GetDelegate(); 685 if (parent && type != EMPTY && root_delegate) 686 SetEnabled(root_delegate->IsCommandEnabled(command)); 687 } 688 689 void MenuItemView::PrepareForRun(bool is_first_menu, 690 bool has_mnemonics, 691 bool show_mnemonics) { 692 // Currently we only support showing the root. 693 DCHECK(!parent_menu_item_); 694 695 // Force us to have a submenu. 696 CreateSubmenu(); 697 actual_menu_position_ = requested_menu_position_; 698 canceled_ = false; 699 700 has_mnemonics_ = has_mnemonics; 701 show_mnemonics_ = has_mnemonics && show_mnemonics; 702 703 AddEmptyMenus(); 704 705 if (is_first_menu) { 706 // Only update the menu size if there are no menus showing, otherwise 707 // things may shift around. 708 UpdateMenuPartSizes(); 709 } 710 } 711 712 int MenuItemView::GetDrawStringFlags() { 713 int flags = 0; 714 if (base::i18n::IsRTL()) 715 flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; 716 else 717 flags |= gfx::Canvas::TEXT_ALIGN_LEFT; 718 719 if (GetRootMenuItem()->has_mnemonics_) { 720 if (GetMenuConfig().show_mnemonics || GetRootMenuItem()->show_mnemonics_) { 721 flags |= gfx::Canvas::SHOW_PREFIX; 722 } else { 723 flags |= gfx::Canvas::HIDE_PREFIX; 724 } 725 } 726 return flags; 727 } 728 729 const gfx::Font& MenuItemView::GetFont() { 730 const MenuDelegate* delegate = GetDelegate(); 731 if (delegate) { 732 const gfx::Font* font = delegate->GetLabelFont(GetCommand()); 733 if (font) 734 return *font; 735 } 736 return GetMenuConfig().font; 737 } 738 739 void MenuItemView::AddEmptyMenus() { 740 DCHECK(HasSubmenu()); 741 if (!submenu_->has_children()) { 742 submenu_->AddChildViewAt(new EmptyMenuMenuItem(this), 0); 743 } else { 744 for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count; 745 ++i) { 746 MenuItemView* child = submenu_->GetMenuItemAt(i); 747 if (child->HasSubmenu()) 748 child->AddEmptyMenus(); 749 } 750 } 751 } 752 753 void MenuItemView::RemoveEmptyMenus() { 754 DCHECK(HasSubmenu()); 755 // Iterate backwards as we may end up removing views, which alters the child 756 // view count. 757 for (int i = submenu_->child_count() - 1; i >= 0; --i) { 758 View* child = submenu_->child_at(i); 759 if (child->id() == MenuItemView::kMenuItemViewID) { 760 MenuItemView* menu_item = static_cast<MenuItemView*>(child); 761 if (menu_item->HasSubmenu()) 762 menu_item->RemoveEmptyMenus(); 763 } else if (child->id() == EmptyMenuMenuItem::kEmptyMenuItemViewID) { 764 submenu_->RemoveChildView(child); 765 delete child; 766 child = NULL; 767 } 768 } 769 } 770 771 void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const { 772 rect->set_x(GetMirroredXForRect(*rect)); 773 } 774 775 void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { 776 const MenuConfig& config = GetMenuConfig(); 777 bool render_selection = 778 (mode == PB_NORMAL && IsSelected() && 779 parent_menu_item_->GetSubmenu()->GetShowSelection(this) && 780 (NonIconChildViewsCount() == 0)); 781 782 int icon_x = config.item_left_margin + left_icon_margin_; 783 int top_margin = GetTopMargin(); 784 int bottom_margin = GetBottomMargin(); 785 int icon_y = top_margin + (height() - config.item_top_margin - 786 bottom_margin - config.check_height) / 2; 787 int icon_height = config.check_height; 788 int available_height = height() - top_margin - bottom_margin; 789 MenuDelegate *delegate = GetDelegate(); 790 // Render the background. As MenuScrollViewContainer draws the background, we 791 // only need the background when we want it to look different, as when we're 792 // selected. 793 ui::NativeTheme* native_theme = GetNativeTheme(); 794 SkColor override_color; 795 if (delegate && delegate->GetBackgroundColor(GetCommand(), 796 render_selection, 797 &override_color)) { 798 canvas->DrawColor(override_color); 799 } else if (render_selection) { 800 gfx::Rect item_bounds(0, 0, width(), height()); 801 AdjustBoundsForRTLUI(&item_bounds); 802 803 native_theme->Paint(canvas->sk_canvas(), 804 ui::NativeTheme::kMenuItemBackground, 805 ui::NativeTheme::kHovered, 806 item_bounds, 807 ui::NativeTheme::ExtraParams()); 808 } 809 810 // Render the check. 811 if (type_ == CHECKBOX && delegate->IsItemChecked(GetCommand())) { 812 const gfx::ImageSkia* check = GetMenuCheckImage(); 813 // Don't use config.check_width here as it's padded 814 // to force more padding (AURA). 815 gfx::Rect check_bounds(icon_x, icon_y, check->width(), icon_height); 816 AdjustBoundsForRTLUI(&check_bounds); 817 canvas->DrawImageInt(*check, check_bounds.x(), check_bounds.y()); 818 } else if (type_ == RADIO) { 819 const gfx::ImageSkia* image = 820 GetRadioButtonImage(delegate->IsItemChecked(GetCommand())); 821 gfx::Rect radio_bounds(icon_x, 822 top_margin + 823 (height() - top_margin - bottom_margin - 824 image->height()) / 2, 825 image->width(), 826 image->height()); 827 AdjustBoundsForRTLUI(&radio_bounds); 828 canvas->DrawImageInt(*image, radio_bounds.x(), radio_bounds.y()); 829 } 830 831 // Render the foreground. 832 ui::NativeTheme::ColorId color_id = 833 ui::NativeTheme::kColorId_DisabledMenuItemForegroundColor; 834 if (enabled()) { 835 color_id = render_selection ? 836 ui::NativeTheme::kColorId_SelectedMenuItemForegroundColor: 837 ui::NativeTheme::kColorId_EnabledMenuItemForegroundColor; 838 } 839 SkColor fg_color = native_theme->GetSystemColor(color_id); 840 SkColor override_foreground_color; 841 if (delegate && delegate->GetForegroundColor(GetCommand(), 842 render_selection, 843 &override_foreground_color)) 844 fg_color = override_foreground_color; 845 846 const gfx::Font& font = GetFont(); 847 int accel_width = parent_menu_item_->GetSubmenu()->max_minor_text_width(); 848 int label_start = GetLabelStartForThisItem(); 849 850 int width = this->width() - label_start - accel_width - 851 (!delegate || 852 delegate->ShouldReserveSpaceForSubmenuIndicator() ? 853 item_right_margin_ : config.arrow_to_edge_padding); 854 gfx::Rect text_bounds(label_start, top_margin, width, available_height); 855 text_bounds.set_x(GetMirroredXForRect(text_bounds)); 856 int flags = GetDrawStringFlags(); 857 if (mode == PB_FOR_DRAG) 858 flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING; 859 canvas->DrawStringInt(title(), font, fg_color, 860 text_bounds.x(), text_bounds.y(), text_bounds.width(), 861 text_bounds.height(), flags); 862 863 PaintMinorText(canvas, render_selection); 864 865 // Render the submenu indicator (arrow). 866 if (HasSubmenu()) { 867 gfx::Rect arrow_bounds(this->width() - config.arrow_width - 868 config.arrow_to_edge_padding, 869 top_margin + (available_height - 870 config.arrow_width) / 2, 871 config.arrow_width, height()); 872 AdjustBoundsForRTLUI(&arrow_bounds); 873 canvas->DrawImageInt(*GetSubmenuArrowImage(), 874 arrow_bounds.x(), arrow_bounds.y()); 875 } 876 } 877 878 void MenuItemView::PaintMinorText(gfx::Canvas* canvas, 879 bool render_selection) { 880 string16 minor_text = GetMinorText(); 881 if (minor_text.empty()) 882 return; 883 884 const gfx::Font& font = GetFont(); 885 int available_height = height() - GetTopMargin() - GetBottomMargin(); 886 int max_accel_width = 887 parent_menu_item_->GetSubmenu()->max_minor_text_width(); 888 const MenuConfig& config = GetMenuConfig(); 889 int accel_right_margin = config.align_arrow_and_shortcut ? 890 config.arrow_to_edge_padding : item_right_margin_; 891 gfx::Rect accel_bounds(width() - accel_right_margin - max_accel_width, 892 GetTopMargin(), max_accel_width, available_height); 893 accel_bounds.set_x(GetMirroredXForRect(accel_bounds)); 894 int flags = GetDrawStringFlags(); 895 flags &= ~(gfx::Canvas::TEXT_ALIGN_RIGHT | gfx::Canvas::TEXT_ALIGN_LEFT); 896 if (base::i18n::IsRTL()) 897 flags |= gfx::Canvas::TEXT_ALIGN_LEFT; 898 else 899 flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; 900 canvas->DrawStringInt( 901 minor_text, 902 font, 903 GetNativeTheme()->GetSystemColor(render_selection ? 904 ui::NativeTheme::kColorId_SelectedMenuItemForegroundColor : 905 ui::NativeTheme::kColorId_ButtonDisabledColor), 906 accel_bounds.x(), 907 accel_bounds.y(), 908 accel_bounds.width(), 909 accel_bounds.height(), 910 flags); 911 } 912 913 void MenuItemView::DestroyAllMenuHosts() { 914 if (!HasSubmenu()) 915 return; 916 917 submenu_->Close(); 918 for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count; 919 ++i) { 920 submenu_->GetMenuItemAt(i)->DestroyAllMenuHosts(); 921 } 922 } 923 924 int MenuItemView::GetTopMargin() { 925 if (top_margin_ >= 0) 926 return top_margin_; 927 928 MenuItemView* root = GetRootMenuItem(); 929 return root && root->has_icons_ 930 ? GetMenuConfig().item_top_margin : 931 GetMenuConfig().item_no_icon_top_margin; 932 } 933 934 int MenuItemView::GetBottomMargin() { 935 if (bottom_margin_ >= 0) 936 return bottom_margin_; 937 938 MenuItemView* root = GetRootMenuItem(); 939 return root && root->has_icons_ 940 ? GetMenuConfig().item_bottom_margin : 941 GetMenuConfig().item_no_icon_bottom_margin; 942 } 943 944 gfx::Size MenuItemView::GetChildPreferredSize() { 945 if (!has_children()) 946 return gfx::Size(); 947 948 if (IsContainer()) { 949 View* child = child_at(0); 950 return child->GetPreferredSize(); 951 } 952 953 int width = 0; 954 for (int i = 0; i < child_count(); ++i) { 955 View* child = child_at(i); 956 if (icon_view_ && (icon_view_ == child)) 957 continue; 958 if (i) 959 width += kChildXPadding; 960 width += child->GetPreferredSize().width(); 961 } 962 int height = 0; 963 if (icon_view_) 964 height = icon_view_->GetPreferredSize().height(); 965 966 // If there is no icon view it returns a height of 0 to indicate that 967 // we should use the title height instead. 968 return gfx::Size(width, height); 969 } 970 971 MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() { 972 gfx::Size child_size = GetChildPreferredSize(); 973 974 MenuItemDimensions dimensions; 975 // Get the container height. 976 dimensions.children_width = child_size.width(); 977 dimensions.height = child_size.height(); 978 // Adjust item content height if menu has both items with and without icons. 979 // This way all menu items will have the same height. 980 if (!icon_view_ && GetRootMenuItem()->has_icons()) { 981 dimensions.height = std::max(dimensions.height, 982 GetMenuConfig().check_height); 983 } 984 dimensions.height += GetBottomMargin() + GetTopMargin(); 985 986 // In case of a container, only the container size needs to be filled. 987 if (IsContainer()) 988 return dimensions; 989 990 // Determine the length of the label text. 991 const gfx::Font& font = GetFont(); 992 993 // Get Icon margin overrides for this particular item. 994 const MenuDelegate* delegate = GetDelegate(); 995 if (delegate) { 996 delegate->GetHorizontalIconMargins(command_, 997 icon_area_width_, 998 &left_icon_margin_, 999 &right_icon_margin_); 1000 } else { 1001 left_icon_margin_ = 0; 1002 right_icon_margin_ = 0; 1003 } 1004 int label_start = GetLabelStartForThisItem(); 1005 1006 dimensions.standard_width = font.GetStringWidth(title_) + label_start + 1007 item_right_margin_; 1008 // Determine the length of the right-side text. 1009 string16 minor_text = GetMinorText(); 1010 dimensions.minor_text_width = 1011 minor_text.empty() ? 0 : GetFont().GetStringWidth(minor_text); 1012 1013 // Determine the height to use. 1014 dimensions.height = std::max(dimensions.height, 1015 font.GetHeight() + GetBottomMargin() + GetTopMargin()); 1016 dimensions.height = std::max(dimensions.height, 1017 GetMenuConfig().item_min_height); 1018 return dimensions; 1019 } 1020 1021 int MenuItemView::GetLabelStartForThisItem() { 1022 int label_start = label_start_ + left_icon_margin_ + right_icon_margin_; 1023 if ((type_ == CHECKBOX || type_ == RADIO) && icon_view_) { 1024 label_start += icon_view_->size().width() + 1025 GetMenuConfig().icon_to_label_padding; 1026 } 1027 return label_start; 1028 } 1029 1030 string16 MenuItemView::GetMinorText() { 1031 if (id() == kEmptyMenuItemViewID) { 1032 // Don't query the delegate for menus that represent no children. 1033 return string16(); 1034 } 1035 1036 ui::Accelerator accelerator; 1037 if (GetMenuConfig().show_accelerators && GetDelegate() && GetCommand() && 1038 GetDelegate()->GetAccelerator(GetCommand(), &accelerator)) { 1039 return accelerator.GetShortcutText(); 1040 } 1041 1042 return subtitle_; 1043 } 1044 1045 bool MenuItemView::IsContainer() const { 1046 // Let the first child take over |this| when we only have one child and no 1047 // title. 1048 return (NonIconChildViewsCount() == 1) && title_.empty(); 1049 } 1050 1051 int MenuItemView::NonIconChildViewsCount() const { 1052 // Note that what child_count() returns is the number of children, 1053 // not the number of menu items. 1054 return child_count() - (icon_view_ ? 1 : 0); 1055 } 1056 1057 int MenuItemView::GetMaxIconViewWidth() const { 1058 int width = 0; 1059 for (int i = 0; i < submenu_->GetMenuItemCount(); ++i) { 1060 MenuItemView* menu_item = submenu_->GetMenuItemAt(i); 1061 int temp_width = 0; 1062 if (menu_item->HasSubmenu()) { 1063 temp_width = menu_item->GetMaxIconViewWidth(); 1064 } else if (menu_item->icon_view()) { 1065 temp_width = menu_item->icon_view()->GetPreferredSize().width(); 1066 } 1067 width = std::max(width, temp_width); 1068 } 1069 return width; 1070 } 1071 1072 bool MenuItemView::HasChecksOrRadioButtons() const { 1073 for (int i = 0; i < submenu_->GetMenuItemCount(); ++i) { 1074 MenuItemView* menu_item = submenu_->GetMenuItemAt(i); 1075 if (menu_item->HasSubmenu()) { 1076 if (menu_item->HasChecksOrRadioButtons()) 1077 return true; 1078 } else { 1079 const Type& type = menu_item->GetType(); 1080 if (type == CHECKBOX || type == RADIO) 1081 return true; 1082 } 1083 } 1084 return false; 1085 } 1086 1087 } // namespace views 1088