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