// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/views/controls/menu/menu_item_view.h" #include #include "base/i18n/case_conversion.h" #include "base/macros.h" #include "base/stl_util.h" #include "base/strings/utf_string_conversions.h" #include "ui/accessibility/ax_view_state.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/material_design/material_design_controller.h" #include "ui/base/models/menu_model.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/image/image.h" #include "ui/gfx/text_utils.h" #include "ui/native_theme/common_theme.h" #include "ui/resources/grit/ui_resources.h" #include "ui/strings/grit/ui_strings.h" #include "ui/views/controls/button/menu_button.h" #include "ui/views/controls/image_view.h" #include "ui/views/controls/menu/menu_config.h" #include "ui/views/controls/menu/menu_controller.h" #include "ui/views/controls/menu/menu_image_util.h" #include "ui/views/controls/menu/menu_scroll_view_container.h" #include "ui/views/controls/menu/menu_separator.h" #include "ui/views/controls/menu/submenu_view.h" #include "ui/views/resources/grit/views_resources.h" #include "ui/views/widget/widget.h" namespace views { namespace { // EmptyMenuMenuItem --------------------------------------------------------- // EmptyMenuMenuItem is used when a menu has no menu items. EmptyMenuMenuItem // is itself a MenuItemView, but it uses a different ID so that it isn't // identified as a MenuItemView. class EmptyMenuMenuItem : public MenuItemView { public: explicit EmptyMenuMenuItem(MenuItemView* parent) : MenuItemView(parent, 0, EMPTY) { // Set this so that we're not identified as a normal menu item. set_id(kEmptyMenuItemViewID); SetTitle(l10n_util::GetStringUTF16(IDS_APP_MENU_EMPTY_SUBMENU)); SetEnabled(false); } bool GetTooltipText(const gfx::Point& p, base::string16* tooltip) const override { // Empty menu items shouldn't have a tooltip. return false; } private: DISALLOW_COPY_AND_ASSIGN(EmptyMenuMenuItem); }; } // namespace // Padding between child views. static const int kChildXPadding = 8; // MenuItemView --------------------------------------------------------------- // static const int MenuItemView::kMenuItemViewID = 1001; // static const int MenuItemView::kEmptyMenuItemViewID = MenuItemView::kMenuItemViewID + 1; // static int MenuItemView::icon_area_width_ = 0; // static int MenuItemView::label_start_; // static int MenuItemView::item_right_margin_; // static int MenuItemView::pref_menu_height_; // static const char MenuItemView::kViewClassName[] = "MenuItemView"; MenuItemView::MenuItemView(MenuDelegate* delegate) : delegate_(delegate), controller_(NULL), canceled_(false), parent_menu_item_(NULL), type_(SUBMENU), selected_(false), command_(0), submenu_(NULL), has_mnemonics_(false), show_mnemonics_(false), has_icons_(false), icon_view_(NULL), top_margin_(-1), bottom_margin_(-1), left_icon_margin_(0), right_icon_margin_(0), requested_menu_position_(POSITION_BEST_FIT), actual_menu_position_(requested_menu_position_), use_right_margin_(true) { // NOTE: don't check the delegate for NULL, UpdateMenuPartSizes() supplies a // NULL delegate. Init(NULL, 0, SUBMENU, delegate); } void MenuItemView::ChildPreferredSizeChanged(View* child) { invalidate_dimensions(); PreferredSizeChanged(); } bool MenuItemView::GetTooltipText(const gfx::Point& p, base::string16* tooltip) const { *tooltip = tooltip_; if (!tooltip->empty()) return true; if (GetType() == SEPARATOR) return false; const MenuController* controller = GetMenuController(); if (!controller || controller->exit_type() != MenuController::EXIT_NONE) { // Either the menu has been closed or we're in the process of closing the // menu. Don't attempt to query the delegate as it may no longer be valid. return false; } const MenuItemView* root_menu_item = GetRootMenuItem(); if (root_menu_item->canceled_) { // TODO(sky): if |canceled_| is true, controller->exit_type() should be // something other than EXIT_NONE, but crash reports seem to indicate // otherwise. Figure out why this is needed. return false; } const MenuDelegate* delegate = GetDelegate(); CHECK(delegate); gfx::Point location(p); ConvertPointToScreen(this, &location); *tooltip = delegate->GetTooltipText(command_, location); return !tooltip->empty(); } void MenuItemView::GetAccessibleState(ui::AXViewState* state) { state->role = ui::AX_ROLE_MENU_ITEM; base::string16 item_text; if (IsContainer()) { // The first child is taking over, just use its accessible name instead of // |title_|. View* child = child_at(0); ui::AXViewState state; child->GetAccessibleState(&state); item_text = state.name; } else { item_text = title_; } state->name = GetAccessibleNameForMenuItem(item_text, GetMinorText()); switch (GetType()) { case SUBMENU: state->AddStateFlag(ui::AX_STATE_HASPOPUP); break; case CHECKBOX: case RADIO: if (GetDelegate()->IsItemChecked(GetCommand())) state->AddStateFlag(ui::AX_STATE_CHECKED); break; case NORMAL: case SEPARATOR: case EMPTY: // No additional accessibility states currently for these menu states. break; } } // static bool MenuItemView::IsBubble(MenuAnchorPosition anchor) { return anchor == MENU_ANCHOR_BUBBLE_LEFT || anchor == MENU_ANCHOR_BUBBLE_RIGHT || anchor == MENU_ANCHOR_BUBBLE_ABOVE || anchor == MENU_ANCHOR_BUBBLE_BELOW; } // static base::string16 MenuItemView::GetAccessibleNameForMenuItem( const base::string16& item_text, const base::string16& minor_text) { base::string16 accessible_name = item_text; // Filter out the "&" for accessibility clients. size_t index = 0; const base::char16 amp = '&'; while ((index = accessible_name.find(amp, index)) != base::string16::npos && index + 1 < accessible_name.length()) { accessible_name.replace(index, accessible_name.length() - index, accessible_name.substr(index + 1)); // Special case for "&&" (escaped for "&"). if (accessible_name[index] == '&') ++index; } // Append subtext. if (!minor_text.empty()) { accessible_name.push_back(' '); accessible_name.append(minor_text); } return accessible_name; } void MenuItemView::Cancel() { if (controller_ && !canceled_) { canceled_ = true; controller_->Cancel(MenuController::EXIT_ALL); } } MenuItemView* MenuItemView::AddMenuItemAt( int index, int item_id, const base::string16& label, const base::string16& sublabel, const base::string16& minor_text, const gfx::ImageSkia& icon, Type type, ui::MenuSeparatorType separator_style) { DCHECK_NE(type, EMPTY); DCHECK_LE(0, index); if (!submenu_) CreateSubmenu(); DCHECK_GE(submenu_->child_count(), index); if (type == SEPARATOR) { submenu_->AddChildViewAt(new MenuSeparator(separator_style), index); return NULL; } MenuItemView* item = new MenuItemView(this, item_id, type); if (label.empty() && GetDelegate()) item->SetTitle(GetDelegate()->GetLabel(item_id)); else item->SetTitle(label); item->SetSubtitle(sublabel); item->SetMinorText(minor_text); if (!icon.isNull()) item->SetIcon(icon); if (type == SUBMENU) item->CreateSubmenu(); if (GetDelegate() && !GetDelegate()->IsCommandVisible(item_id)) item->SetVisible(false); submenu_->AddChildViewAt(item, index); return item; } void MenuItemView::RemoveMenuItemAt(int index) { DCHECK(submenu_); DCHECK_LE(0, index); DCHECK_GT(submenu_->child_count(), index); View* item = submenu_->child_at(index); DCHECK(item); submenu_->RemoveChildView(item); // RemoveChildView() does not delete the item, which is a good thing // in case a submenu is being displayed while items are being removed. // Deletion will be done by ChildrenChanged() or at destruction. removed_items_.push_back(item); } MenuItemView* MenuItemView::AppendMenuItem(int item_id, const base::string16& label, Type type) { return AppendMenuItemImpl(item_id, label, base::string16(), base::string16(), gfx::ImageSkia(), type, ui::NORMAL_SEPARATOR); } MenuItemView* MenuItemView::AppendSubMenu(int item_id, const base::string16& label) { return AppendMenuItemImpl(item_id, label, base::string16(), base::string16(), gfx::ImageSkia(), SUBMENU, ui::NORMAL_SEPARATOR); } MenuItemView* MenuItemView::AppendSubMenuWithIcon(int item_id, const base::string16& label, const gfx::ImageSkia& icon) { return AppendMenuItemImpl(item_id, label, base::string16(), base::string16(), icon, SUBMENU, ui::NORMAL_SEPARATOR); } MenuItemView* MenuItemView::AppendMenuItemWithLabel( int item_id, const base::string16& label) { return AppendMenuItem(item_id, label, NORMAL); } MenuItemView* MenuItemView::AppendDelegateMenuItem(int item_id) { return AppendMenuItem(item_id, base::string16(), NORMAL); } void MenuItemView::AppendSeparator() { AppendMenuItemImpl(0, base::string16(), base::string16(), base::string16(), gfx::ImageSkia(), SEPARATOR, ui::NORMAL_SEPARATOR); } MenuItemView* MenuItemView::AppendMenuItemWithIcon(int item_id, const base::string16& label, const gfx::ImageSkia& icon) { return AppendMenuItemImpl(item_id, label, base::string16(), base::string16(), icon, NORMAL, ui::NORMAL_SEPARATOR); } MenuItemView* MenuItemView::AppendMenuItemImpl( int item_id, const base::string16& label, const base::string16& sublabel, const base::string16& minor_text, const gfx::ImageSkia& icon, Type type, ui::MenuSeparatorType separator_style) { const int index = submenu_ ? submenu_->child_count() : 0; return AddMenuItemAt(index, item_id, label, sublabel, minor_text, icon, type, separator_style); } SubmenuView* MenuItemView::CreateSubmenu() { if (!submenu_) submenu_ = new SubmenuView(this); return submenu_; } bool MenuItemView::HasSubmenu() const { return (submenu_ != NULL); } SubmenuView* MenuItemView::GetSubmenu() const { return submenu_; } void MenuItemView::SetTitle(const base::string16& title) { title_ = title; invalidate_dimensions(); // Triggers preferred size recalculation. } void MenuItemView::SetSubtitle(const base::string16& subtitle) { subtitle_ = subtitle; invalidate_dimensions(); // Triggers preferred size recalculation. } void MenuItemView::SetMinorText(const base::string16& minor_text) { minor_text_ = minor_text; invalidate_dimensions(); // Triggers preferred size recalculation. } void MenuItemView::SetSelected(bool selected) { selected_ = selected; SchedulePaint(); } void MenuItemView::SetTooltip(const base::string16& tooltip, int item_id) { MenuItemView* item = GetMenuItemByID(item_id); DCHECK(item); item->tooltip_ = tooltip; } void MenuItemView::SetIcon(const gfx::ImageSkia& icon, int item_id) { MenuItemView* item = GetMenuItemByID(item_id); DCHECK(item); item->SetIcon(icon); } void MenuItemView::SetIcon(const gfx::ImageSkia& icon) { if (icon.isNull()) { SetIconView(NULL); return; } ImageView* icon_view = new ImageView(); icon_view->SetImage(&icon); SetIconView(icon_view); } void MenuItemView::SetIconView(View* icon_view) { if (icon_view_) { RemoveChildView(icon_view_); delete icon_view_; icon_view_ = NULL; } if (icon_view) { AddChildView(icon_view); icon_view_ = icon_view; } Layout(); SchedulePaint(); } void MenuItemView::OnPaint(gfx::Canvas* canvas) { PaintButton(canvas, PB_NORMAL); } gfx::Size MenuItemView::GetPreferredSize() const { const MenuItemDimensions& dimensions(GetDimensions()); return gfx::Size(dimensions.standard_width + dimensions.children_width, dimensions.height); } int MenuItemView::GetHeightForWidth(int width) const { // If this isn't a container, we can just use the preferred size's height. if (!IsContainer()) return GetPreferredSize().height(); int height = child_at(0)->GetHeightForWidth(width); if (!icon_view_ && GetRootMenuItem()->has_icons()) height = std::max(height, MenuConfig::instance().check_height); height += GetBottomMargin() + GetTopMargin(); return height; } const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() const { if (!is_dimensions_valid()) dimensions_ = CalculateDimensions(); DCHECK(is_dimensions_valid()); return dimensions_; } MenuController* MenuItemView::GetMenuController() { return GetRootMenuItem()->controller_; } const MenuController* MenuItemView::GetMenuController() const { return GetRootMenuItem()->controller_; } MenuDelegate* MenuItemView::GetDelegate() { return GetRootMenuItem()->delegate_; } const MenuDelegate* MenuItemView::GetDelegate() const { return GetRootMenuItem()->delegate_; } MenuItemView* MenuItemView::GetRootMenuItem() { return const_cast( static_cast(this)->GetRootMenuItem()); } const MenuItemView* MenuItemView::GetRootMenuItem() const { const MenuItemView* item = this; for (const MenuItemView* parent = GetParentMenuItem(); parent; parent = item->GetParentMenuItem()) item = parent; return item; } base::char16 MenuItemView::GetMnemonic() { if (!GetRootMenuItem()->has_mnemonics_) return 0; size_t index = 0; do { index = title_.find('&', index); if (index != base::string16::npos) { if (index + 1 != title_.size() && title_[index + 1] != '&') { base::char16 char_array[] = { title_[index + 1], 0 }; // TODO(jshin): What about Turkish locale? See http://crbug.com/81719. // If the mnemonic is capital I and the UI language is Turkish, // lowercasing it results in 'small dotless i', which is different // from a 'dotted i'. Similar issues may exist for az and lt locales. return base::i18n::ToLower(char_array)[0]; } index++; } } while (index != base::string16::npos); return 0; } MenuItemView* MenuItemView::GetMenuItemByID(int id) { if (GetCommand() == id) return this; if (!HasSubmenu()) return NULL; for (int i = 0; i < GetSubmenu()->child_count(); ++i) { View* child = GetSubmenu()->child_at(i); if (child->id() == MenuItemView::kMenuItemViewID) { MenuItemView* result = static_cast(child)-> GetMenuItemByID(id); if (result) return result; } } return NULL; } void MenuItemView::ChildrenChanged() { MenuController* controller = GetMenuController(); if (controller) { // Handles the case where we were empty and are no longer empty. RemoveEmptyMenus(); // Handles the case where we were not empty, but now are. AddEmptyMenus(); controller->MenuChildrenChanged(this); if (submenu_) { // Force a paint and layout. This handles the case of the top // level window's size remaining the same, resulting in no // change to the submenu's size and no layout. submenu_->Layout(); submenu_->SchedulePaint(); // Update the menu selection after layout. controller->UpdateSubmenuSelection(submenu_); } } STLDeleteElements(&removed_items_); } void MenuItemView::Layout() { if (!has_children()) return; if (IsContainer()) { View* child = child_at(0); gfx::Size size = child->GetPreferredSize(); child->SetBounds(0, GetTopMargin(), size.width(), size.height()); } else { // Child views are laid out right aligned and given the full height. To // right align start with the last view and progress to the first. int x = width() - (use_right_margin_ ? item_right_margin_ : 0); for (int i = child_count() - 1; i >= 0; --i) { View* child = child_at(i); if (icon_view_ && (icon_view_ == child)) continue; int width = child->GetPreferredSize().width(); child->SetBounds(x - width, 0, width, height()); x -= width - kChildXPadding; } // Position |icon_view|. const MenuConfig& config = MenuConfig::instance(); if (icon_view_) { icon_view_->SizeToPreferredSize(); gfx::Size size = icon_view_->GetPreferredSize(); int x = config.item_left_margin + left_icon_margin_ + (icon_area_width_ - size.width()) / 2; if (config.icons_in_label || type_ == CHECKBOX || type_ == RADIO) x = label_start_; int y = (height() + GetTopMargin() - GetBottomMargin() - size.height()) / 2; icon_view_->SetPosition(gfx::Point(x, y)); } } } void MenuItemView::SetMargins(int top_margin, int bottom_margin) { top_margin_ = top_margin; bottom_margin_ = bottom_margin; invalidate_dimensions(); } MenuItemView::MenuItemView(MenuItemView* parent, int command, MenuItemView::Type type) : delegate_(NULL), controller_(NULL), canceled_(false), parent_menu_item_(parent), type_(type), selected_(false), command_(command), submenu_(NULL), has_mnemonics_(false), show_mnemonics_(false), has_icons_(false), icon_view_(NULL), top_margin_(-1), bottom_margin_(-1), left_icon_margin_(0), right_icon_margin_(0), requested_menu_position_(POSITION_BEST_FIT), actual_menu_position_(requested_menu_position_), use_right_margin_(true) { Init(parent, command, type, NULL); } MenuItemView::~MenuItemView() { delete submenu_; STLDeleteElements(&removed_items_); } const char* MenuItemView::GetClassName() const { return kViewClassName; } // Calculates all sizes that we can from the OS. // // This is invoked prior to Running a menu. void MenuItemView::UpdateMenuPartSizes() { const MenuConfig& config = MenuConfig::instance(); item_right_margin_ = config.label_to_arrow_padding + config.arrow_width + config.arrow_to_edge_padding; icon_area_width_ = config.check_width; if (has_icons_) icon_area_width_ = std::max(icon_area_width_, GetMaxIconViewWidth()); label_start_ = config.item_left_margin + icon_area_width_; int padding = 0; if (config.always_use_icon_to_label_padding) { padding = config.icon_to_label_padding; } else if (!config.icons_in_label) { padding = (has_icons_ || HasChecksOrRadioButtons()) ? config.icon_to_label_padding : 0; } label_start_ += padding; EmptyMenuMenuItem menu_item(this); menu_item.set_controller(GetMenuController()); pref_menu_height_ = menu_item.GetPreferredSize().height(); } void MenuItemView::Init(MenuItemView* parent, int command, MenuItemView::Type type, MenuDelegate* delegate) { delegate_ = delegate; controller_ = NULL; canceled_ = false; parent_menu_item_ = parent; type_ = type; selected_ = false; command_ = command; submenu_ = NULL; show_mnemonics_ = false; // Assign our ID, this allows SubmenuItemView to find MenuItemViews. set_id(kMenuItemViewID); has_icons_ = false; // Don't request enabled status from the root menu item as it is just // a container for real items. EMPTY items will be disabled. MenuDelegate* root_delegate = GetDelegate(); if (parent && type != EMPTY && root_delegate) SetEnabled(root_delegate->IsCommandEnabled(command)); } void MenuItemView::PrepareForRun(bool is_first_menu, bool has_mnemonics, bool show_mnemonics) { // Currently we only support showing the root. DCHECK(!parent_menu_item_); // Force us to have a submenu. CreateSubmenu(); actual_menu_position_ = requested_menu_position_; canceled_ = false; has_mnemonics_ = has_mnemonics; show_mnemonics_ = has_mnemonics && show_mnemonics; AddEmptyMenus(); if (is_first_menu) { // Only update the menu size if there are no menus showing, otherwise // things may shift around. UpdateMenuPartSizes(); } } int MenuItemView::GetDrawStringFlags() { int flags = 0; if (base::i18n::IsRTL()) flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; else flags |= gfx::Canvas::TEXT_ALIGN_LEFT; if (GetRootMenuItem()->has_mnemonics_) { if (MenuConfig::instance().show_mnemonics || GetRootMenuItem()->show_mnemonics_) { flags |= gfx::Canvas::SHOW_PREFIX; } else { flags |= gfx::Canvas::HIDE_PREFIX; } } return flags; } const gfx::FontList& MenuItemView::GetFontList() const { const MenuDelegate* delegate = GetDelegate(); if (delegate) { const gfx::FontList* font_list = delegate->GetLabelFontList(GetCommand()); if (font_list) return *font_list; } return MenuConfig::instance().font_list; } void MenuItemView::AddEmptyMenus() { DCHECK(HasSubmenu()); if (!submenu_->has_children()) { submenu_->AddChildViewAt(new EmptyMenuMenuItem(this), 0); } else { for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count; ++i) { MenuItemView* child = submenu_->GetMenuItemAt(i); if (child->HasSubmenu()) child->AddEmptyMenus(); } } } void MenuItemView::RemoveEmptyMenus() { DCHECK(HasSubmenu()); // Iterate backwards as we may end up removing views, which alters the child // view count. for (int i = submenu_->child_count() - 1; i >= 0; --i) { View* child = submenu_->child_at(i); if (child->id() == MenuItemView::kMenuItemViewID) { MenuItemView* menu_item = static_cast(child); if (menu_item->HasSubmenu()) menu_item->RemoveEmptyMenus(); } else if (child->id() == EmptyMenuMenuItem::kEmptyMenuItemViewID) { submenu_->RemoveChildView(child); delete child; child = NULL; } } } void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const { rect->set_x(GetMirroredXForRect(*rect)); } void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { const MenuConfig& config = MenuConfig::instance(); bool render_selection = (mode == PB_NORMAL && IsSelected() && parent_menu_item_->GetSubmenu()->GetShowSelection(this) && (NonIconChildViewsCount() == 0)); MenuDelegate *delegate = GetDelegate(); // Render the background. As MenuScrollViewContainer draws the background, we // only need the background when we want it to look different, as when we're // selected. ui::NativeTheme* native_theme = GetNativeTheme(); SkColor override_color; if (delegate && delegate->GetBackgroundColor(GetCommand(), render_selection, &override_color)) { canvas->DrawColor(override_color); } else if (render_selection) { gfx::Rect item_bounds(0, 0, width(), height()); AdjustBoundsForRTLUI(&item_bounds); native_theme->Paint(canvas->sk_canvas(), ui::NativeTheme::kMenuItemBackground, ui::NativeTheme::kHovered, item_bounds, ui::NativeTheme::ExtraParams()); } const int icon_x = config.item_left_margin + left_icon_margin_; const int top_margin = GetTopMargin(); const int bottom_margin = GetBottomMargin(); const int available_height = height() - top_margin - bottom_margin; // Calculate some colors. ui::NativeTheme::ColorId color_id; if (enabled()) { color_id = render_selection ? ui::NativeTheme::kColorId_SelectedMenuItemForegroundColor: ui::NativeTheme::kColorId_EnabledMenuItemForegroundColor; } else { bool emphasized = delegate && delegate->GetShouldUseDisabledEmphasizedForegroundColor( GetCommand()); color_id = emphasized ? ui::NativeTheme::kColorId_DisabledEmphasizedMenuItemForegroundColor : ui::NativeTheme::kColorId_DisabledMenuItemForegroundColor; } SkColor fg_color = native_theme->GetSystemColor(color_id); SkColor override_foreground_color; if (delegate && delegate->GetForegroundColor(GetCommand(), render_selection, &override_foreground_color)) { fg_color = override_foreground_color; } SkColor icon_color = render_selection && !ui::MaterialDesignController::IsModeMaterial() ? fg_color : color_utils::DeriveDefaultIconColor(fg_color); // Render the check. if (type_ == CHECKBOX && delegate->IsItemChecked(GetCommand())) { gfx::ImageSkia check = GetMenuCheckImage(icon_color); // Don't use config.check_width here as it's padded // to force more padding (AURA). gfx::Rect check_bounds(icon_x, top_margin + (available_height - check.height()) / 2, check.width(), check.height()); AdjustBoundsForRTLUI(&check_bounds); canvas->DrawImageInt(check, check_bounds.x(), check_bounds.y()); } else if (type_ == RADIO) { gfx::ImageSkia image = GetRadioButtonImage( delegate->IsItemChecked(GetCommand()), render_selection, icon_color); gfx::Rect radio_bounds(icon_x, top_margin + (available_height - image.height()) / 2, image.width(), image.height()); AdjustBoundsForRTLUI(&radio_bounds); canvas->DrawImageInt(image, radio_bounds.x(), radio_bounds.y()); } // Render the foreground. const gfx::FontList& font_list = GetFontList(); int accel_width = parent_menu_item_->GetSubmenu()->max_minor_text_width(); int label_start = GetLabelStartForThisItem(); int width = this->width() - label_start - accel_width - (!delegate || delegate->ShouldReserveSpaceForSubmenuIndicator() ? item_right_margin_ : config.arrow_to_edge_padding); gfx::Rect text_bounds(label_start, top_margin, width, subtitle_.empty() ? available_height : available_height / 2); text_bounds.set_x(GetMirroredXForRect(text_bounds)); int flags = GetDrawStringFlags(); if (mode == PB_FOR_DRAG) flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING; canvas->DrawStringRectWithFlags(title(), font_list, fg_color, text_bounds, flags); if (!subtitle_.empty()) { canvas->DrawStringRectWithFlags( subtitle_, font_list, GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_ButtonDisabledColor), text_bounds + gfx::Vector2d(0, font_list.GetHeight()), flags); } PaintMinorText(canvas, render_selection); // Render the submenu indicator (arrow). if (HasSubmenu()) { gfx::ImageSkia arrow = GetSubmenuArrowImage(icon_color); gfx::Rect arrow_bounds(this->width() - config.arrow_width - config.arrow_to_edge_padding, top_margin + (available_height - arrow.height()) / 2, config.arrow_width, arrow.height()); AdjustBoundsForRTLUI(&arrow_bounds); canvas->DrawImageInt(arrow, arrow_bounds.x(), arrow_bounds.y()); } } void MenuItemView::PaintMinorText(gfx::Canvas* canvas, bool render_selection) { base::string16 minor_text = GetMinorText(); if (minor_text.empty()) return; int available_height = height() - GetTopMargin() - GetBottomMargin(); int max_accel_width = parent_menu_item_->GetSubmenu()->max_minor_text_width(); const MenuConfig& config = MenuConfig::instance(); int accel_right_margin = config.align_arrow_and_shortcut ? config.arrow_to_edge_padding : item_right_margin_; gfx::Rect accel_bounds(width() - accel_right_margin - max_accel_width, GetTopMargin(), max_accel_width, available_height); accel_bounds.set_x(GetMirroredXForRect(accel_bounds)); int flags = GetDrawStringFlags(); flags &= ~(gfx::Canvas::TEXT_ALIGN_RIGHT | gfx::Canvas::TEXT_ALIGN_LEFT); if (base::i18n::IsRTL()) flags |= gfx::Canvas::TEXT_ALIGN_LEFT; else flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; canvas->DrawStringRectWithFlags( minor_text, GetFontList(), GetNativeTheme()->GetSystemColor(render_selection ? ui::NativeTheme::kColorId_SelectedMenuItemForegroundColor : ui::NativeTheme::kColorId_ButtonDisabledColor), accel_bounds, flags); } void MenuItemView::DestroyAllMenuHosts() { if (!HasSubmenu()) return; submenu_->Close(); for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count; ++i) { submenu_->GetMenuItemAt(i)->DestroyAllMenuHosts(); } } int MenuItemView::GetTopMargin() const { if (top_margin_ >= 0) return top_margin_; const MenuItemView* root = GetRootMenuItem(); return root && root->has_icons_ ? MenuConfig::instance().item_top_margin : MenuConfig::instance().item_no_icon_top_margin; } int MenuItemView::GetBottomMargin() const { if (bottom_margin_ >= 0) return bottom_margin_; const MenuItemView* root = GetRootMenuItem(); return root && root->has_icons_ ? MenuConfig::instance().item_bottom_margin : MenuConfig::instance().item_no_icon_bottom_margin; } gfx::Size MenuItemView::GetChildPreferredSize() const { if (!has_children()) return gfx::Size(); if (IsContainer()) return child_at(0)->GetPreferredSize(); int width = 0; for (int i = 0; i < child_count(); ++i) { const View* child = child_at(i); if (icon_view_ && (icon_view_ == child)) continue; if (i) width += kChildXPadding; width += child->GetPreferredSize().width(); } int height = 0; if (icon_view_) height = icon_view_->GetPreferredSize().height(); // If there is no icon view it returns a height of 0 to indicate that // we should use the title height instead. return gfx::Size(width, height); } MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const { gfx::Size child_size = GetChildPreferredSize(); MenuItemDimensions dimensions; // Get the container height. dimensions.children_width = child_size.width(); dimensions.height = child_size.height(); // Adjust item content height if menu has both items with and without icons. // This way all menu items will have the same height. if (!icon_view_ && GetRootMenuItem()->has_icons()) { dimensions.height = std::max(dimensions.height, MenuConfig::instance().check_height); } dimensions.height += GetBottomMargin() + GetTopMargin(); // In case of a container, only the container size needs to be filled. if (IsContainer()) return dimensions; // Determine the length of the label text. const gfx::FontList& font_list = GetFontList(); // Get Icon margin overrides for this particular item. const MenuDelegate* delegate = GetDelegate(); if (delegate) { delegate->GetHorizontalIconMargins(command_, icon_area_width_, &left_icon_margin_, &right_icon_margin_); } else { left_icon_margin_ = 0; right_icon_margin_ = 0; } int label_start = GetLabelStartForThisItem(); int string_width = gfx::GetStringWidth(title_, font_list); if (!subtitle_.empty()) { string_width = std::max(string_width, gfx::GetStringWidth(subtitle_, font_list)); } dimensions.standard_width = string_width + label_start + item_right_margin_; // Determine the length of the right-side text. base::string16 minor_text = GetMinorText(); dimensions.minor_text_width = minor_text.empty() ? 0 : gfx::GetStringWidth(minor_text, font_list); // Determine the height to use. dimensions.height = std::max(dimensions.height, (subtitle_.empty() ? 0 : font_list.GetHeight()) + font_list.GetHeight() + GetBottomMargin() + GetTopMargin()); dimensions.height = std::max(dimensions.height, MenuConfig::instance().item_min_height); return dimensions; } int MenuItemView::GetLabelStartForThisItem() const { const MenuConfig& config = MenuConfig::instance(); int label_start = label_start_ + left_icon_margin_ + right_icon_margin_; if ((config.icons_in_label || type_ == CHECKBOX || type_ == RADIO) && icon_view_) label_start += icon_view_->size().width() + config.icon_to_label_padding; return label_start; } base::string16 MenuItemView::GetMinorText() const { if (id() == kEmptyMenuItemViewID) { // Don't query the delegate for menus that represent no children. return base::string16(); } ui::Accelerator accelerator; if (MenuConfig::instance().show_accelerators && GetDelegate() && GetCommand() && GetDelegate()->GetAccelerator(GetCommand(), &accelerator)) { return accelerator.GetShortcutText(); } return minor_text_; } bool MenuItemView::IsContainer() const { // Let the first child take over |this| when we only have one child and no // title. return (NonIconChildViewsCount() == 1) && title_.empty(); } int MenuItemView::NonIconChildViewsCount() const { // Note that what child_count() returns is the number of children, // not the number of menu items. return child_count() - (icon_view_ ? 1 : 0); } int MenuItemView::GetMaxIconViewWidth() const { int width = 0; for (int i = 0; i < submenu_->GetMenuItemCount(); ++i) { MenuItemView* menu_item = submenu_->GetMenuItemAt(i); int temp_width = 0; if (menu_item->GetType() == CHECKBOX || menu_item->GetType() == RADIO) { // If this item has a radio or checkbox, the icon will not affect // alignment of other items. continue; } else if (menu_item->HasSubmenu()) { temp_width = menu_item->GetMaxIconViewWidth(); } else if (menu_item->icon_view() && !MenuConfig::instance().icons_in_label) { temp_width = menu_item->icon_view()->GetPreferredSize().width(); } width = std::max(width, temp_width); } return width; } bool MenuItemView::HasChecksOrRadioButtons() const { for (int i = 0; i < submenu_->GetMenuItemCount(); ++i) { MenuItemView* menu_item = submenu_->GetMenuItemAt(i); if (menu_item->HasSubmenu()) { if (menu_item->HasChecksOrRadioButtons()) return true; } else { const Type& type = menu_item->GetType(); if (type == CHECKBOX || type == RADIO) return true; } } return false; } } // namespace views