blob: 262ea70539ab80d23a346fc217345bf76c87a095 [file] [log] [blame]
// 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 <math.h>
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <numeric>
#include <utility>
#include "base/containers/adapters.h"
#include "base/i18n/case_conversion.h"
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/menu_model.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/gfx/animation/animation.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/paint_vector_icon.h"
#include "ui/gfx/text_utils.h"
#include "ui/native_theme/native_theme.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/controls/separator.h"
#include "ui/views/style/typography.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/views_features.h"
#include "ui/views/widget/widget.h"
namespace views {
namespace {
// Difference in the font size (in pixels) between menu label font and "new"
// badge font size.
constexpr int kNewBadgeFontSizeAdjustment = -1;
// Space between primary text and "new" badge.
constexpr int kNewBadgeHorizontalMargin = 8;
// Highlight size around "new" badge.
constexpr gfx::Insets kNewBadgeInternalPadding{4};
// The corner radius of the rounded rect for the "new" badge.
constexpr int kNewBadgeCornerRadius = 3;
static_assert(kNewBadgeCornerRadius <= kNewBadgeInternalPadding.left(),
"New badge corner radius should not exceed padding.");
// Returns the horizontal space required for the "new" badge.
int GetNewBadgeRequiredWidth(const gfx::FontList& primary_font) {
const base::string16 new_text =
l10n_util::GetStringUTF16(IDS_MENU_ITEM_NEW_BADGE);
gfx::FontList badge_font =
primary_font.DeriveWithSizeDelta(kNewBadgeFontSizeAdjustment);
return gfx::GetStringWidth(new_text, badge_font) +
kNewBadgeInternalPadding.width() + 2 * kNewBadgeHorizontalMargin;
}
// Returns the highlight rect for the "new" badge given the font and text rect
// for the badge text.
gfx::Rect GetNewBadgeRectOutsetAroundText(const gfx::FontList& badge_font,
const gfx::Rect& badge_text_rect) {
gfx::Rect badge_rect = badge_text_rect;
badge_rect.Inset(
-gfx::AdjustVisualBorderForFont(badge_font, kNewBadgeInternalPadding));
return badge_rect;
}
// 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, Type::kEmpty) {
// Set this so that we're not identified as a normal menu item.
SetID(kEmptyMenuItemViewID);
SetTitle(l10n_util::GetStringUTF16(IDS_APP_MENU_EMPTY_SUBMENU));
SetEnabled(false);
}
base::string16 GetTooltipText(const gfx::Point& p) const override {
// Empty menu items shouldn't have a tooltip.
return base::string16();
}
private:
DISALLOW_COPY_AND_ASSIGN(EmptyMenuMenuItem);
};
} // namespace
// Padding between child views.
static constexpr 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_;
MenuItemView::MenuItemView(MenuDelegate* delegate) : delegate_(delegate) {
// NOTE: don't check the delegate for NULL, UpdateMenuPartSizes() supplies a
// NULL delegate.
Init(nullptr, 0, Type::kSubMenu);
}
void MenuItemView::ChildPreferredSizeChanged(View* child) {
invalidate_dimensions();
PreferredSizeChanged();
}
base::string16 MenuItemView::GetTooltipText(const gfx::Point& p) const {
if (!tooltip_.empty())
return tooltip_;
if (type_ == Type::kSeparator)
return base::string16();
const MenuController* controller = GetMenuController();
if (!controller ||
controller->exit_type() != MenuController::ExitType::kNone) {
// 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 base::string16();
}
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 ExitType::kNone, but crash reports seem to indicate
// otherwise. Figure out why this is needed.
return base::string16();
}
const MenuDelegate* delegate = GetDelegate();
CHECK(delegate);
gfx::Point location(p);
ConvertPointToScreen(this, &location);
return delegate->GetTooltipText(command_, location);
}
void MenuItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
// Set the role based on the type of menu item.
switch (type_) {
case Type::kCheckbox:
node_data->role = ax::mojom::Role::kMenuItemCheckBox;
break;
case Type::kRadio:
node_data->role = ax::mojom::Role::kMenuItemRadio;
break;
default:
node_data->role = ax::mojom::Role::kMenuItem;
break;
}
base::string16 item_text;
if (IsContainer()) {
// The first child is taking over, just use its accessible name instead of
// |title_|.
View* child = children().front();
ui::AXNodeData child_node_data;
child->GetAccessibleNodeData(&child_node_data);
item_text =
child_node_data.GetString16Attribute(ax::mojom::StringAttribute::kName);
} else {
item_text = title_;
}
node_data->SetName(GetAccessibleNameForMenuItem(item_text, GetMinorText()));
switch (type_) {
case Type::kSubMenu:
case Type::kActionableSubMenu:
node_data->SetHasPopup(ax::mojom::HasPopup::kMenu);
break;
case Type::kCheckbox:
case Type::kRadio: {
const bool is_checked = GetDelegate()->IsItemChecked(GetCommand());
node_data->SetCheckedState(is_checked ? ax::mojom::CheckedState::kTrue
: ax::mojom::CheckedState::kFalse);
} break;
case Type::kTitle:
case Type::kNormal:
case Type::kSeparator:
case Type::kEmpty:
case Type::kHighlighted:
// No additional accessibility states currently for these menu states.
break;
}
base::char16 mnemonic = GetMnemonic();
if (mnemonic != '\0') {
node_data->AddStringAttribute(
ax::mojom::StringAttribute::kKeyShortcuts,
base::UTF16ToUTF8(base::string16(1, mnemonic)));
}
}
bool MenuItemView::HandleAccessibleAction(const ui::AXActionData& action_data) {
if (action_data.action != ax::mojom::Action::kDoDefault)
return View::HandleAccessibleAction(action_data);
// kDoDefault in View would simulate a mouse click in the center of this
// MenuItemView. However, mouse events for menus are dispatched via
// Widget::SetCapture() to the MenuController rather than to MenuItemView, so
// there is no effect. VKEY_RETURN provides a better UX anyway, since it will
// move focus to a submenu.
ui::KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_RETURN, ui::DomCode::ENTER,
ui::EF_NONE, ui::DomKey::ENTER, ui::EventTimeForNow());
GetMenuController()->SetSelection(this, MenuController::SELECTION_DEFAULT);
GetMenuController()->OnWillDispatchKeyEvent(&event);
return true;
}
// static
bool MenuItemView::IsBubble(MenuAnchorPosition anchor) {
return anchor == MenuAnchorPosition::kBubbleAbove ||
anchor == MenuAnchorPosition::kBubbleLeft ||
anchor == MenuAnchorPosition::kBubbleRight;
}
// 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::ExitType::kAll);
}
}
MenuItemView* MenuItemView::AddMenuItemAt(
int index,
int item_id,
const base::string16& label,
const base::string16& secondary_label,
const base::string16& minor_text,
const ui::ThemedVectorIcon& minor_icon,
const gfx::ImageSkia& icon,
const ui::ThemedVectorIcon& vector_icon,
Type type,
ui::MenuSeparatorType separator_style) {
DCHECK_NE(type, Type::kEmpty);
DCHECK_GE(index, 0);
if (!submenu_)
CreateSubmenu();
DCHECK_LE(size_t{index}, submenu_->children().size());
if (type == Type::kSeparator) {
submenu_->AddChildViewAt(std::make_unique<MenuSeparator>(separator_style),
index);
return nullptr;
}
MenuItemView* item = new MenuItemView(this, item_id, type);
if (label.empty() && GetDelegate())
item->SetTitle(GetDelegate()->GetLabel(item_id));
else
item->SetTitle(label);
item->SetSecondaryTitle(secondary_label);
item->SetMinorText(minor_text);
item->SetMinorIcon(minor_icon);
if (!vector_icon.empty()) {
DCHECK(icon.isNull());
item->SetIcon(vector_icon);
}
if (!icon.isNull())
item->SetIcon(icon);
if (type == Type::kSubMenu || type == Type::kActionableSubMenu)
item->CreateSubmenu();
if (type == Type::kHighlighted) {
const MenuConfig& config = MenuConfig::instance();
item->SetMargins(config.footnote_vertical_margin,
config.footnote_vertical_margin);
}
if (GetDelegate() && !GetDelegate()->IsCommandVisible(item_id))
item->SetVisible(false);
return submenu_->AddChildViewAt(item, index);
}
void MenuItemView::RemoveMenuItem(View* item) {
DCHECK(item);
DCHECK(submenu_);
DCHECK_EQ(submenu_, item->parent());
removed_items_.push_back(item);
submenu_->RemoveChildView(item);
}
void MenuItemView::RemoveAllMenuItems() {
DCHECK(submenu_);
removed_items_.insert(removed_items_.end(), submenu_->children().begin(),
submenu_->children().end());
submenu_->RemoveAllChildViews(false);
}
MenuItemView* MenuItemView::AppendMenuItem(int item_id,
const base::string16& label,
const gfx::ImageSkia& icon) {
return AppendMenuItemImpl(item_id, label, icon, Type::kNormal);
}
MenuItemView* MenuItemView::AppendSubMenu(int item_id,
const base::string16& label,
const gfx::ImageSkia& icon) {
return AppendMenuItemImpl(item_id, label, icon, Type::kSubMenu);
}
void MenuItemView::AppendSeparator() {
AppendMenuItemImpl(0, base::string16(), gfx::ImageSkia(), Type::kSeparator);
}
void MenuItemView::AddSeparatorAt(int index) {
AddMenuItemAt(index, /*item_id=*/0, /*label=*/base::string16(),
/*secondary_label=*/base::string16(),
/*minor_text=*/base::string16(),
/*minor_icon=*/ui::ThemedVectorIcon(),
/*icon=*/gfx::ImageSkia(),
/*vector_icon=*/ui::ThemedVectorIcon(),
/*type=*/Type::kSeparator,
/*separator_style=*/ui::NORMAL_SEPARATOR);
}
MenuItemView* MenuItemView::AppendMenuItemImpl(int item_id,
const base::string16& label,
const gfx::ImageSkia& icon,
Type type) {
const int index = submenu_ ? int{submenu_->children().size()} : 0;
return AddMenuItemAt(index, item_id, label, base::string16(),
base::string16(), ui::ThemedVectorIcon(), icon,
ui::ThemedVectorIcon(), type, ui::NORMAL_SEPARATOR);
}
SubmenuView* MenuItemView::CreateSubmenu() {
if (!submenu_) {
submenu_ = new SubmenuView(this);
// Initialize the submenu indicator icon (arrow).
submenu_arrow_image_view_ = AddChildView(std::make_unique<ImageView>());
}
return submenu_;
}
bool MenuItemView::HasSubmenu() const {
return (submenu_ != nullptr);
}
SubmenuView* MenuItemView::GetSubmenu() const {
return submenu_;
}
bool MenuItemView::SubmenuIsShowing() const {
return HasSubmenu() && GetSubmenu()->IsShowing();
}
void MenuItemView::SetTitle(const base::string16& title) {
title_ = title;
invalidate_dimensions(); // Triggers preferred size recalculation.
}
void MenuItemView::SetSecondaryTitle(const base::string16& secondary_title) {
secondary_title_ = secondary_title;
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::SetMinorIcon(const ui::ThemedVectorIcon& minor_icon) {
minor_icon_ = minor_icon;
invalidate_dimensions(); // Triggers preferred size recalculation.
}
void MenuItemView::SetSelected(bool selected) {
selected_ = selected;
SchedulePaint();
}
void MenuItemView::SetSelectionOfActionableSubmenu(
bool submenu_area_of_actionable_submenu_selected) {
DCHECK_EQ(Type::kActionableSubMenu, type_);
if (submenu_area_of_actionable_submenu_selected_ ==
submenu_area_of_actionable_submenu_selected) {
return;
}
submenu_area_of_actionable_submenu_selected_ =
submenu_area_of_actionable_submenu_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) {
vector_icon_.clear();
if (icon.isNull()) {
SetIconView(nullptr);
return;
}
auto icon_view = std::make_unique<ImageView>();
icon_view->SetImage(&icon);
SetIconView(std::move(icon_view));
}
void MenuItemView::SetIcon(const ui::ThemedVectorIcon& icon) {
vector_icon_ = icon;
UpdateIconViewFromVectorIconAndTheme();
}
void MenuItemView::UpdateIconViewFromVectorIconAndTheme() {
if (vector_icon_.empty())
return;
auto icon_view = std::make_unique<ImageView>();
icon_view->SetImage(vector_icon_.GetImageSkia(GetNativeTheme()));
SetIconView(std::move(icon_view));
}
void MenuItemView::SetIconView(std::unique_ptr<ImageView> icon_view) {
if (icon_view_) {
RemoveChildViewT(icon_view_);
icon_view_ = nullptr;
}
if (icon_view)
icon_view_ = AddChildView(std::move(icon_view));
InvalidateLayout();
SchedulePaint();
}
void MenuItemView::OnPaint(gfx::Canvas* canvas) {
PaintButton(canvas, PaintButtonMode::kNormal);
}
gfx::Size MenuItemView::CalculatePreferredSize() 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();
const gfx::Insets margins = GetContainerMargins();
int height = children().front()->GetHeightForWidth(width - margins.width());
if (!icon_view_ && GetRootMenuItem()->has_icons())
height = std::max(height, MenuConfig::instance().check_height);
height += margins.height();
return height;
}
void MenuItemView::OnThemeChanged() {
View::OnThemeChanged();
UpdateIconViewFromVectorIconAndTheme();
}
gfx::Rect MenuItemView::GetSubmenuAreaOfActionableSubmenu() const {
DCHECK_EQ(Type::kActionableSubMenu, type_);
const MenuConfig& config = MenuConfig::instance();
return gfx::Rect(gfx::Point(vertical_separator_->bounds().right(), 0),
gfx::Size(config.actionable_submenu_width, 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_.get();
}
const MenuController* MenuItemView::GetMenuController() const {
return GetRootMenuItem()->controller_.get();
}
MenuDelegate* MenuItemView::GetDelegate() {
return GetRootMenuItem()->delegate_;
}
const MenuDelegate* MenuItemView::GetDelegate() const {
return GetRootMenuItem()->delegate_;
}
MenuItemView* MenuItemView::GetRootMenuItem() {
return const_cast<MenuItemView*>(
static_cast<const MenuItemView*>(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_ ||
!MenuConfig::instance().use_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 nullptr;
for (MenuItemView* item : GetSubmenu()->GetMenuItems()) {
MenuItemView* result = item->GetMenuItemByID(id);
if (result)
return result;
}
return nullptr;
}
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 a synchronous layout. This needs a synchronous layout
// as UpdateSubmenuSelection() looks at bounds. 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_);
}
}
for (auto* item : removed_items_)
delete item;
removed_items_.clear();
}
void MenuItemView::Layout() {
if (children().empty())
return;
if (IsContainer()) {
// This MenuItemView is acting as a thin wrapper around the sole child view,
// and the size has already been set appropriately for the child's preferred
// size and margins. The child's bounds can simply be set to the content
// bounds, less the margins.
gfx::Rect bounds = GetContentsBounds();
bounds.Inset(GetContainerMargins());
children().front()->SetBoundsRect(bounds);
} 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 child_x = width() - (use_right_margin_ ? item_right_margin_ : 0);
for (View* child : base::Reversed(children())) {
if (icon_view_ == child)
continue;
if (radio_check_image_view_ == child)
continue;
if (submenu_arrow_image_view_ == child)
continue;
if (vertical_separator_ == child)
continue;
int width = child->GetPreferredSize().width();
child->SetBounds(child_x - width, 0, width, height());
child_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_horizontal_padding + left_icon_margin_ +
(icon_area_width_ - size.width()) / 2;
if (config.icons_in_label || type_ == Type::kCheckbox ||
type_ == Type::kRadio)
x = label_start_;
if (GetMenuController() && GetMenuController()->use_touchable_layout())
x = config.touchable_item_horizontal_padding;
int y =
(height() + GetTopMargin() - GetBottomMargin() - size.height()) / 2;
icon_view_->SetPosition(gfx::Point(x, y));
}
if (radio_check_image_view_) {
int x = config.item_horizontal_padding + left_icon_margin_;
if (GetMenuController() && GetMenuController()->use_touchable_layout())
x = config.touchable_item_horizontal_padding;
int y =
(height() + GetTopMargin() - GetBottomMargin() - kMenuCheckSize) / 2;
radio_check_image_view_->SetBounds(x, y, kMenuCheckSize, kMenuCheckSize);
}
if (submenu_arrow_image_view_) {
int x = width() - config.arrow_width -
(type_ == Type::kActionableSubMenu
? config.actionable_submenu_arrow_to_edge_padding
: config.arrow_to_edge_padding);
int y =
(height() + GetTopMargin() - GetBottomMargin() - kSubmenuArrowSize) /
2;
submenu_arrow_image_view_->SetBounds(x, y, config.arrow_width,
kSubmenuArrowSize);
}
if (vertical_separator_) {
const gfx::Size preferred_size = vertical_separator_->GetPreferredSize();
int x = width() - config.actionable_submenu_width -
config.actionable_submenu_vertical_separator_width;
int y = (height() - preferred_size.height()) / 2;
vertical_separator_->SetBoundsRect(
gfx::Rect(gfx::Point(x, y), preferred_size));
}
}
}
void MenuItemView::SetMargins(int top_margin, int bottom_margin) {
top_margin_ = top_margin;
bottom_margin_ = bottom_margin;
invalidate_dimensions();
}
void MenuItemView::SetForcedVisualSelection(bool selected) {
forced_visual_selection_ = selected;
SchedulePaint();
}
void MenuItemView::SetCornerRadius(int radius) {
DCHECK_EQ(Type::kHighlighted, type_);
corner_radius_ = radius;
invalidate_dimensions(); // Triggers preferred size recalculation.
}
void MenuItemView::SetAlerted() {
is_alerted_ = true;
SchedulePaint();
}
MenuItemView::MenuItemView(MenuItemView* parent,
int command,
MenuItemView::Type type) {
Init(parent, command, type);
}
MenuItemView::~MenuItemView() {
if (GetMenuController())
GetMenuController()->OnMenuItemDestroying(this);
delete submenu_;
for (auto* item : removed_items_)
delete item;
}
// 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());
const bool use_touchable_layout =
GetMenuController() && GetMenuController()->use_touchable_layout();
label_start_ =
(use_touchable_layout ? config.touchable_item_horizontal_padding
: config.item_horizontal_padding) +
icon_area_width_;
int padding = 0;
if (config.always_use_icon_to_label_padding) {
padding = LayoutProvider::Get()->GetDistanceMetric(
DISTANCE_RELATED_LABEL_HORIZONTAL);
} else if (!config.icons_in_label) {
padding = (has_icons_ || HasChecksOrRadioButtons())
? LayoutProvider::Get()->GetDistanceMetric(
DISTANCE_RELATED_LABEL_HORIZONTAL)
: 0;
}
if (use_touchable_layout)
padding = LayoutProvider::Get()->GetDistanceMetric(
DISTANCE_RELATED_LABEL_HORIZONTAL);
label_start_ += padding;
EmptyMenuMenuItem menu_item(this);
menu_item.set_controller(GetMenuController());
pref_menu_height_ = menu_item.GetPreferredSize().height();
UpdateIconViewFromVectorIconAndTheme();
}
void MenuItemView::Init(MenuItemView* parent,
int command,
MenuItemView::Type type) {
parent_menu_item_ = parent;
type_ = type;
command_ = command;
// Assign our ID, this allows SubmenuItemView to find MenuItemViews.
SetID(kMenuItemViewID);
has_icons_ = false;
if (type_ == Type::kCheckbox || type_ == Type::kRadio) {
radio_check_image_view_ = AddChildView(std::make_unique<ImageView>());
bool show_check_radio_icon =
type_ == Type::kRadio || (type_ == Type::kCheckbox &&
GetDelegate()->IsItemChecked(GetCommand()));
radio_check_image_view_->SetVisible(show_check_radio_icon);
radio_check_image_view_->set_can_process_events_within_subtree(false);
}
if (type_ == Type::kActionableSubMenu) {
vertical_separator_ = AddChildView(std::make_unique<Separator>());
vertical_separator_->SetVisible(true);
vertical_separator_->SetFocusBehavior(FocusBehavior::NEVER);
const MenuConfig& config = MenuConfig::instance();
vertical_separator_->SetColor(GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_MenuSeparatorColor));
vertical_separator_->SetPreferredSize(
gfx::Size(config.actionable_submenu_vertical_separator_width,
config.actionable_submenu_vertical_separator_height));
vertical_separator_->set_can_process_events_within_subtree(false);
}
if (submenu_arrow_image_view_)
submenu_arrow_image_view_->SetVisible(HasSubmenu());
// Don't request enabled status from the root menu item as it is just
// a container for real items. kEmpty items will be disabled.
MenuDelegate* root_delegate = GetDelegate();
if (parent && type != Type::kEmpty && 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;
}
void MenuItemView::GetLabelStyle(MenuDelegate::LabelStyle* style) const {
// Start with the default font:
style->font_list = MenuConfig::instance().font_list;
// Replace it with the touchable font in touchable menus:
if (GetMenuController() && GetMenuController()->use_touchable_layout()) {
style->font_list =
style::GetFont(style::CONTEXT_TOUCH_MENU, style::STYLE_PRIMARY);
}
// Then let the delegate replace any part of |style|.
const MenuDelegate* delegate = GetDelegate();
if (delegate)
delegate->GetLabelStyle(GetCommand(), style);
}
void MenuItemView::AddEmptyMenus() {
DCHECK(HasSubmenu());
if (!submenu_->HasVisibleChildren() && !submenu_->HasEmptyMenuItemView()) {
submenu_->AddChildViewAt(std::make_unique<EmptyMenuMenuItem>(this), 0);
} else {
for (MenuItemView* item : submenu_->GetMenuItems()) {
if (item->HasSubmenu())
item->AddEmptyMenus();
}
}
}
void MenuItemView::RemoveEmptyMenus() {
DCHECK(HasSubmenu());
// Copy the children, since we may mutate them as we go.
const Views children = submenu_->children();
for (View* child : children) {
if (child->GetID() == MenuItemView::kMenuItemViewID) {
MenuItemView* menu_item = static_cast<MenuItemView*>(child);
if (menu_item->HasSubmenu())
menu_item->RemoveEmptyMenus();
} else if (child->GetID() == EmptyMenuMenuItem::kEmptyMenuItemViewID) {
submenu_->RemoveChildView(child);
delete child;
}
}
}
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 == PaintButtonMode::kNormal && IsSelected() &&
parent_menu_item_->GetSubmenu()->GetShowSelection(this) &&
(NonIconChildViewsCount() == 0));
if (forced_visual_selection_.has_value())
render_selection = *forced_visual_selection_;
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.
PaintBackground(canvas, mode, render_selection);
// Calculate some colors.
MenuDelegate::LabelStyle style;
style.foreground = GetTextColor(/*minor=*/false, render_selection);
GetLabelStyle(&style);
SkColor icon_color = color_utils::DeriveDefaultIconColor(style.foreground);
// Calculate the margins.
int top_margin = GetTopMargin();
const int bottom_margin = GetBottomMargin();
const int available_height = height() - top_margin - bottom_margin;
const int text_height = style.font_list.GetHeight();
const int total_text_height =
secondary_title().empty() ? text_height : text_height * 2;
top_margin += (available_height - total_text_height) / 2;
// Render the check.
if (type_ == Type::kCheckbox && delegate->IsItemChecked(GetCommand())) {
radio_check_image_view_->SetImage(GetMenuCheckImage(icon_color));
} else if (type_ == Type::kRadio) {
const bool toggled = delegate->IsItemChecked(GetCommand());
const gfx::VectorIcon& radio_icon =
toggled ? kMenuRadioSelectedIcon : kMenuRadioEmptyIcon;
const SkColor radio_icon_color = GetNativeTheme()->GetSystemColor(
toggled ? ui::NativeTheme::kColorId_ButtonCheckedColor
: ui::NativeTheme::kColorId_ButtonUncheckedColor);
radio_check_image_view_->SetImage(
gfx::CreateVectorIcon(radio_icon, kMenuCheckSize, radio_icon_color));
}
// Render the foreground.
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, text_height);
text_bounds.set_x(GetMirroredXForRect(text_bounds));
int flags = GetDrawStringFlags();
if (mode == PaintButtonMode::kForDrag)
flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING;
canvas->DrawStringRectWithFlags(title(), style.font_list, style.foreground,
text_bounds, flags);
// The rest should be drawn with the minor foreground color.
style.foreground = GetTextColor(/*minor=*/true, render_selection);
if (!secondary_title().empty()) {
text_bounds.set_y(text_bounds.y() + text_height);
canvas->DrawStringRectWithFlags(secondary_title(), style.font_list,
style.foreground, text_bounds, flags);
}
PaintMinorIconAndText(canvas, style);
if (ShouldShowNewBadge()) {
DrawNewBadge(
canvas,
gfx::Point(label_start + gfx::GetStringWidth(title(), style.font_list) +
kNewBadgeHorizontalMargin,
top_margin),
style.font_list, flags);
}
// Set the submenu indicator (arrow) image and color.
if (HasSubmenu())
submenu_arrow_image_view_->SetImage(GetSubmenuArrowImage(icon_color));
}
void MenuItemView::PaintBackground(gfx::Canvas* canvas,
PaintButtonMode mode,
bool render_selection) {
if (type_ == Type::kHighlighted || is_alerted_) {
SkColor color = gfx::kPlaceholderColor;
if (type_ == Type::kHighlighted) {
const ui::NativeTheme::ColorId color_id =
render_selection
? ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor
: ui::NativeTheme::kColorId_HighlightedMenuItemBackgroundColor;
color = GetNativeTheme()->GetSystemColor(color_id);
} else {
const auto* animation = GetMenuController()->GetAlertAnimation();
color = gfx::Tween::ColorValueBetween(
animation->GetCurrentValue(),
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_MenuItemInitialAlertBackgroundColor),
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_MenuItemTargetAlertBackgroundColor));
}
DCHECK_NE(color, gfx::kPlaceholderColor);
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(color);
// Draw a rounded rect that spills outside of the clipping area, so that the
// rounded corners only show in the bottom 2 corners. Note that
// |corner_radius_| should only be set when the highlighted item is at the
// end of the menu.
gfx::RectF spilling_rect(GetLocalBounds());
spilling_rect.set_y(spilling_rect.y() - corner_radius_);
spilling_rect.set_height(spilling_rect.height() + corner_radius_);
canvas->DrawRoundRect(spilling_rect, corner_radius_, flags);
} else if (render_selection) {
gfx::Rect item_bounds = GetLocalBounds();
if (type_ == Type::kActionableSubMenu) {
if (submenu_area_of_actionable_submenu_selected_) {
item_bounds = GetSubmenuAreaOfActionableSubmenu();
} else {
item_bounds.set_width(item_bounds.width() -
MenuConfig::instance().actionable_submenu_width -
1);
}
}
AdjustBoundsForRTLUI(&item_bounds);
GetNativeTheme()->Paint(
canvas->sk_canvas(), ui::NativeTheme::kMenuItemBackground,
ui::NativeTheme::kHovered, item_bounds, ui::NativeTheme::ExtraParams());
}
}
void MenuItemView::PaintMinorIconAndText(
gfx::Canvas* canvas,
const MenuDelegate::LabelStyle& style) {
base::string16 minor_text = GetMinorText();
const ui::ThemedVectorIcon minor_icon = GetMinorIcon();
if (minor_text.empty() && minor_icon.empty())
return;
int available_height = height() - GetTopMargin() - GetBottomMargin();
int max_minor_text_width =
parent_menu_item_->GetSubmenu()->max_minor_text_width();
const MenuConfig& config = MenuConfig::instance();
int minor_text_right_margin = config.align_arrow_and_shortcut
? config.arrow_to_edge_padding
: item_right_margin_;
gfx::Rect minor_text_bounds(
width() - minor_text_right_margin - max_minor_text_width, GetTopMargin(),
max_minor_text_width, available_height);
minor_text_bounds.set_x(GetMirroredXForRect(minor_text_bounds));
std::unique_ptr<gfx::RenderText> render_text =
gfx::RenderText::CreateRenderText();
if (!minor_text.empty()) {
render_text->SetText(minor_text);
render_text->SetFontList(style.font_list);
render_text->SetColor(style.foreground);
render_text->SetDisplayRect(minor_text_bounds);
render_text->SetHorizontalAlignment(base::i18n::IsRTL() ? gfx::ALIGN_LEFT
: gfx::ALIGN_RIGHT);
render_text->Draw(canvas);
}
if (!minor_icon.empty()) {
gfx::ImageSkia image = minor_icon.GetImageSkia(style.foreground);
int image_x = GetMirroredRect(minor_text_bounds).right() -
render_text->GetContentWidth() -
(minor_text.empty() ? 0 : config.item_horizontal_padding) -
image.width();
int minor_text_center_y =
minor_text_bounds.y() + minor_text_bounds.height() / 2;
int image_y = minor_text_center_y - image.height() / 2;
canvas->DrawImageInt(
image, GetMirroredXWithWidthInView(image_x, image.width()), image_y);
}
}
SkColor MenuItemView::GetTextColor(bool minor, bool render_selection) const {
style::TextContext context =
GetMenuController() && GetMenuController()->use_touchable_layout()
? style::CONTEXT_TOUCH_MENU
: style::CONTEXT_MENU;
style::TextStyle text_style = style::STYLE_PRIMARY;
if (type_ == Type::kTitle)
text_style = style::STYLE_PRIMARY;
else if (type_ == Type::kHighlighted)
text_style = style::STYLE_HIGHLIGHTED;
else if (!GetEnabled())
text_style = style::STYLE_DISABLED;
else if (render_selection)
text_style = style::STYLE_SELECTED;
else if (minor)
text_style = style::STYLE_SECONDARY;
return style::GetColor(*this, context, text_style);
}
void MenuItemView::DestroyAllMenuHosts() {
if (!HasSubmenu())
return;
submenu_->Close();
for (MenuItemView* item : submenu_->GetMenuItems())
item->DestroyAllMenuHosts();
}
int MenuItemView::GetTopMargin() const {
int margin = top_margin_;
if (margin < 0) {
const MenuItemView* root = GetRootMenuItem();
margin = root && root->has_icons_
? MenuConfig::instance().item_top_margin
: MenuConfig::instance().item_no_icon_top_margin;
}
return margin;
}
int MenuItemView::GetBottomMargin() const {
int margin = bottom_margin_;
if (margin < 0) {
const MenuItemView* root = GetRootMenuItem();
margin = root && root->has_icons_
? MenuConfig::instance().item_bottom_margin
: MenuConfig::instance().item_no_icon_bottom_margin;
}
return margin;
}
gfx::Size MenuItemView::GetChildPreferredSize() const {
if (children().empty())
return gfx::Size();
if (IsContainer())
return children().front()->GetPreferredSize();
const auto add_width = [this](int width, const View* child) {
if (child == icon_view_ || child == radio_check_image_view_ ||
child == submenu_arrow_image_view_ || child == vertical_separator_)
return width;
if (width)
width += kChildXPadding;
return width + child->GetPreferredSize().width();
};
const int width =
std::accumulate(children().cbegin(), children().cend(), 0, add_width);
// If there is no icon view it returns a height of 0 to indicate that
// we should use the title height instead.
const int height = icon_view_ ? icon_view_->GetPreferredSize().height() : 0;
return gfx::Size(width, height);
}
MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const {
gfx::Size child_size = GetChildPreferredSize();
MenuItemDimensions dimensions;
dimensions.children_width = child_size.width();
const MenuConfig& menu_config = MenuConfig::instance();
if (GetMenuController() && GetMenuController()->use_touchable_layout()) {
dimensions.height = menu_config.touchable_menu_height;
// For container MenuItemViews, the width components should only include the
// |children_width|. Setting a |standard_width| would result in additional
// width being added to the container because the total width used in layout
// is |children_width| + |standard_width|.
if (IsContainer())
return dimensions;
dimensions.standard_width = menu_config.touchable_menu_width;
if (icon_view_) {
dimensions.height = icon_view_->GetPreferredSize().height() +
2 * menu_config.vertical_touchable_menu_item_padding;
}
return dimensions;
}
MenuDelegate::LabelStyle style;
GetLabelStyle(&style);
base::string16 minor_text = GetMinorText();
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);
}
// In the container case, only the child size plus margins need to be
// considered.
if (IsContainer()) {
const gfx::Insets margins = GetContainerMargins();
dimensions.height += margins.height();
dimensions.children_width += margins.width();
ApplyMinimumDimensions(&dimensions);
return dimensions;
}
dimensions.height += GetBottomMargin() + GetTopMargin();
// 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();
// Determine the length of the label text.
int string_width = gfx::GetStringWidth(title_, style.font_list);
dimensions.standard_width = string_width + label_start + item_right_margin_;
// Determine the length of the right-side text.
dimensions.minor_text_width =
(minor_text.empty() ? 0
: gfx::GetStringWidth(minor_text, style.font_list));
if (ShouldShowNewBadge())
dimensions.minor_text_width += GetNewBadgeRequiredWidth(style.font_list);
// Determine the height to use.
int label_text_height = secondary_title().empty()
? style.font_list.GetHeight()
: style.font_list.GetHeight() * 2;
dimensions.height =
std::max(dimensions.height,
label_text_height + GetBottomMargin() + GetTopMargin());
dimensions.height =
std::max(dimensions.height, MenuConfig::instance().item_min_height);
ApplyMinimumDimensions(&dimensions);
return dimensions;
}
void MenuItemView::ApplyMinimumDimensions(MenuItemDimensions* dims) const {
// Don't apply minimums to menus without controllers or to comboboxes.
if (!GetMenuController() || GetMenuController()->IsCombobox())
return;
// TODO(nicolaso): PaintBackground() doesn't cover the whole area in footnotes
// when minimum height is set too high. For now, just ignore minimum height
// for kHighlighted elements.
if (type_ == Type::kHighlighted)
return;
int used =
dims->standard_width + dims->children_width + dims->minor_text_width;
const MenuConfig& config = MenuConfig::instance();
if (used < config.minimum_menu_width)
dims->standard_width += (config.minimum_menu_width - used);
dims->height = std::max(dims->height,
IsContainer() ? config.minimum_container_item_height
: config.minimum_text_item_height);
}
int MenuItemView::GetLabelStartForThisItem() const {
const MenuConfig& config = MenuConfig::instance();
// Touchable items with icons do not respect |label_start_|.
if (GetMenuController() && GetMenuController()->use_touchable_layout() &&
icon_view_) {
return 2 * config.touchable_item_horizontal_padding + icon_view_->width();
}
int label_start = label_start_ + left_icon_margin_ + right_icon_margin_;
if ((config.icons_in_label || type_ == Type::kCheckbox ||
type_ == Type::kRadio) &&
icon_view_) {
label_start +=
icon_view_->size().width() + LayoutProvider::Get()->GetDistanceMetric(
DISTANCE_RELATED_LABEL_HORIZONTAL);
}
return label_start;
}
void MenuItemView::DrawNewBadge(gfx::Canvas* canvas,
const gfx::Point& unmirrored_badge_start,
const gfx::FontList& primary_font,
int text_render_flags) {
gfx::FontList badge_font =
primary_font.DeriveWithSizeDelta(kNewBadgeFontSizeAdjustment);
const base::string16 new_text =
l10n_util::GetStringUTF16(IDS_MENU_ITEM_NEW_BADGE);
// Calculate bounding box for badge text.
gfx::Rect badge_text_bounds(unmirrored_badge_start,
gfx::GetStringSize(new_text, badge_font));
badge_text_bounds.Offset(
kNewBadgeInternalPadding.left(),
gfx::GetFontCapHeightCenterOffset(primary_font, badge_font));
if (base::i18n::IsRTL())
badge_text_bounds.set_x(GetMirroredXForRect(badge_text_bounds));
// Render the badge itself.
cc::PaintFlags new_flags;
const SkColor background_color = GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_ProminentButtonColor);
new_flags.setColor(background_color);
canvas->DrawRoundRect(
GetNewBadgeRectOutsetAroundText(badge_font, badge_text_bounds),
kNewBadgeCornerRadius, new_flags);
// Render the badge text.
const SkColor foreground_color = GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_TextOnProminentButtonColor);
canvas->DrawStringRectWithFlags(new_text, badge_font, foreground_color,
badge_text_bounds, text_render_flags);
}
base::string16 MenuItemView::GetMinorText() const {
if (GetID() == kEmptyMenuItemViewID) {
// Don't query the delegate for menus that represent no children.
return base::string16();
}
base::string16 accel_text;
if (MenuConfig::instance().ShouldShowAcceleratorText(this, &accel_text))
return accel_text;
return minor_text_;
}
ui::ThemedVectorIcon MenuItemView::GetMinorIcon() const {
return minor_icon_;
}
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();
}
gfx::Insets MenuItemView::GetContainerMargins() const {
DCHECK(IsContainer());
// Use the child's preferred margins but take the standard top and bottom
// margins as minimums.
const gfx::Insets* margins_prop =
children().front()->GetProperty(views::kMarginsKey);
gfx::Insets margins = margins_prop ? *margins_prop : gfx::Insets();
margins.set_top(std::max(margins.top(), GetTopMargin()));
margins.set_bottom(std::max(margins.bottom(), GetBottomMargin()));
return margins;
}
int MenuItemView::NonIconChildViewsCount() const {
return int{children().size()} - (icon_view_ ? 1 : 0) -
(radio_check_image_view_ ? 1 : 0) -
(submenu_arrow_image_view_ ? 1 : 0) - (vertical_separator_ ? 1 : 0);
}
int MenuItemView::GetMaxIconViewWidth() const {
DCHECK(submenu_);
const auto menu_items = submenu_->GetMenuItems();
if (menu_items.empty())
return 0;
std::vector<int> widths(menu_items.size());
const auto get_width = [](MenuItemView* item) {
if (item->type_ == Type::kCheckbox || item->type_ == Type::kRadio) {
// If this item has a radio or checkbox, the icon will not affect
// alignment of other items.
return 0;
}
if (item->HasSubmenu())
return item->GetMaxIconViewWidth();
return (item->icon_view_ && !MenuConfig::instance().icons_in_label)
? item->icon_view_->GetPreferredSize().width()
: 0;
};
std::transform(menu_items.cbegin(), menu_items.cend(), widths.begin(),
get_width);
return *std::max_element(widths.cbegin(), widths.cend());
}
bool MenuItemView::HasChecksOrRadioButtons() const {
if (type_ == Type::kCheckbox || type_ == Type::kRadio)
return true;
if (!HasSubmenu())
return false;
const auto menu_items = submenu_->GetMenuItems();
return std::any_of(
menu_items.cbegin(), menu_items.cend(),
[](const auto* item) { return item->HasChecksOrRadioButtons(); });
}
bool MenuItemView::ShouldShowNewBadge() const {
static const bool feature_enabled =
base::FeatureList::IsEnabled(features::kEnableNewBadgeOnMenuItems);
return feature_enabled && is_new_;
}
BEGIN_METADATA(MenuItemView)
METADATA_PARENT_CLASS(View)
END_METADATA()
} // namespace views