summaryrefslogtreecommitdiff
path: root/chromium/ui/views/controls/menu
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-12 14:27:29 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-13 09:35:20 +0000
commitc30a6232df03e1efbd9f3b226777b07e087a1122 (patch)
treee992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/ui/views/controls/menu
parent7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff)
downloadqtwebengine-chromium-85-based.tar.gz
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/ui/views/controls/menu')
-rw-r--r--chromium/ui/views/controls/menu/menu_controller.cc33
-rw-r--r--chromium/ui/views/controls/menu/menu_controller.h10
-rw-r--r--chromium/ui/views/controls/menu/menu_controller_unittest.cc21
-rw-r--r--chromium/ui/views/controls/menu/menu_delegate.h1
-rw-r--r--chromium/ui/views/controls/menu/menu_host.cc1
-rw-r--r--chromium/ui/views/controls/menu/menu_item_view.cc161
-rw-r--r--chromium/ui/views/controls/menu/menu_item_view.h32
-rw-r--r--chromium/ui/views/controls/menu/menu_item_view_unittest.cc33
-rw-r--r--chromium/ui/views/controls/menu/menu_model_adapter.cc17
-rw-r--r--chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc22
-rw-r--r--chromium/ui/views/controls/menu/submenu_view.cc33
11 files changed, 298 insertions, 66 deletions
diff --git a/chromium/ui/views/controls/menu/menu_controller.cc b/chromium/ui/views/controls/menu/menu_controller.cc
index d9938916a53..87f9291b67d 100644
--- a/chromium/ui/views/controls/menu/menu_controller.cc
+++ b/chromium/ui/views/controls/menu/menu_controller.cc
@@ -621,6 +621,11 @@ bool MenuController::IsContextMenu() const {
return state_.context_menu;
}
+void MenuController::SelectItemAndOpenSubmenu(MenuItemView* item) {
+ DCHECK(item);
+ SetSelection(item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY);
+}
+
bool MenuController::OnMousePressed(SubmenuView* source,
const ui::MouseEvent& event) {
// We should either have no current_mouse_event_target_, or should have a
@@ -1379,6 +1384,11 @@ void MenuController::SetSelection(MenuItemView* menu_item,
menu_item->GetType() != MenuItemView::Type::kSubMenu ||
(menu_item->GetType() == MenuItemView::Type::kActionableSubMenu &&
(selection_types & SELECTION_OPEN_SUBMENU) == 0))) {
+ // Before firing the selection event, ensure that focus appears to be
+ // within the popup. This is helpful for ATs on some platforms,
+ // specifically on Windows, where selection events in a list are mapped
+ // to focus events. Without this call, the focus appears to be elsewhere.
+ menu_item->GetViewAccessibility().SetPopupFocusOverride();
menu_item->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
// Notify an accessibility selected children changed event on the parent
// submenu.
@@ -1635,6 +1645,7 @@ MenuController::~MenuController() {
active_instance_ = nullptr;
StopShowTimer();
StopCancelAllTimer();
+ CHECK(!IsInObserverList());
}
bool MenuController::SendAcceleratorToHotTrackedView() {
@@ -3104,20 +3115,18 @@ void MenuController::SetNextHotTrackedView(
}
void MenuController::SetHotTrackedButton(Button* hot_button) {
- if (hot_button == hot_button_) {
- // Hot-tracked state may change outside of the MenuController. Correct it.
- if (hot_button && !hot_button->IsHotTracked()) {
- hot_button->SetHotTracked(true);
- hot_button->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
- }
- return;
- }
- if (hot_button_)
+ // If we're providing a new hot-tracked button, first remove the existing one.
+ if (hot_button_ && hot_button_ != hot_button) {
hot_button_->SetHotTracked(false);
+ hot_button_->GetViewAccessibility().EndPopupFocusOverride();
+ }
+
+ // Then set the new one.
hot_button_ = hot_button;
- if (hot_button) {
- hot_button->SetHotTracked(true);
- hot_button->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
+ if (hot_button_ && !hot_button_->IsHotTracked()) {
+ hot_button_->GetViewAccessibility().SetPopupFocusOverride();
+ hot_button_->SetHotTracked(true);
+ hot_button_->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
}
}
diff --git a/chromium/ui/views/controls/menu/menu_controller.h b/chromium/ui/views/controls/menu/menu_controller.h
index 5fab7290f4b..2b3625c5b5a 100644
--- a/chromium/ui/views/controls/menu/menu_controller.h
+++ b/chromium/ui/views/controls/menu/menu_controller.h
@@ -125,7 +125,15 @@ class VIEWS_EXPORT MenuController
// WARNING: this may be NULL.
Widget* owner() { return owner_; }
- // Get the anchor position which is used to show this menu.
+ // Gets the most-current selected menu item, if any, including if the
+ // selection has not been committed yet.
+ views::MenuItemView* GetSelectedMenuItem() { return pending_state_.item; }
+
+ // Selects a menu-item and opens its sub-menu (if one exists) if not already
+ // so. Clears any selections within the submenu if it is already open.
+ void SelectItemAndOpenSubmenu(MenuItemView* item);
+
+ // Gets the anchor position which is used to show this menu.
MenuAnchorPosition GetAnchorPosition() { return state_.anchor; }
// Cancels the current Run. See ExitType for a description of what happens
diff --git a/chromium/ui/views/controls/menu/menu_controller_unittest.cc b/chromium/ui/views/controls/menu/menu_controller_unittest.cc
index 3176d01e445..67168197f0a 100644
--- a/chromium/ui/views/controls/menu/menu_controller_unittest.cc
+++ b/chromium/ui/views/controls/menu/menu_controller_unittest.cc
@@ -1125,6 +1125,27 @@ TEST_F(MenuControllerTest, PreviousSelectedItem) {
ResetSelection();
}
+// Tests that the APIs related to the current selected item work correctly.
+TEST_F(MenuControllerTest, CurrentSelectedItem) {
+ SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0));
+ EXPECT_EQ(1, pending_state_item()->GetCommand());
+
+ // Select the first menu-item.
+ DispatchKey(ui::VKEY_HOME);
+ EXPECT_EQ(pending_state_item(), menu_controller()->GetSelectedMenuItem());
+
+ // The API should let the submenu stay open if already so, but clear any
+ // selections within it.
+ EXPECT_TRUE(IsShowing());
+ EXPECT_EQ(1, pending_state_item()->GetCommand());
+ menu_controller()->SelectItemAndOpenSubmenu(menu_item());
+ EXPECT_TRUE(IsShowing());
+ EXPECT_EQ(0, pending_state_item()->GetCommand());
+
+ // Clear references in menu controller to the menu item that is going away.
+ ResetSelection();
+}
+
// Tests that opening menu and calling SelectByChar works correctly.
TEST_F(MenuControllerTest, SelectByChar) {
SetComboboxType(MenuController::ComboboxType::kReadonly);
diff --git a/chromium/ui/views/controls/menu/menu_delegate.h b/chromium/ui/views/controls/menu/menu_delegate.h
index 2a03ff43f4a..158724b4752 100644
--- a/chromium/ui/views/controls/menu/menu_delegate.h
+++ b/chromium/ui/views/controls/menu/menu_delegate.h
@@ -8,7 +8,6 @@
#include <set>
#include <string>
-#include "base/logging.h"
#include "base/strings/string16.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/dragdrop/drag_drop_types.h"
diff --git a/chromium/ui/views/controls/menu/menu_host.cc b/chromium/ui/views/controls/menu/menu_host.cc
index 0b9230bda55..fc94bfc7f17 100644
--- a/chromium/ui/views/controls/menu/menu_host.cc
+++ b/chromium/ui/views/controls/menu/menu_host.cc
@@ -103,6 +103,7 @@ MenuHost::MenuHost(SubmenuView* submenu)
MenuHost::~MenuHost() {
if (owner_)
owner_->RemoveObserver(this);
+ CHECK(!IsInObserverList());
}
void MenuHost::InitMenuHost(Widget* parent,
diff --git a/chromium/ui/views/controls/menu/menu_item_view.cc b/chromium/ui/views/controls/menu/menu_item_view.cc
index d00618f37c3..262ea70539a 100644
--- a/chromium/ui/views/controls/menu/menu_item_view.cc
+++ b/chromium/ui/views/controls/menu/menu_item_view.cc
@@ -10,6 +10,7 @@
#include <algorithm>
#include <memory>
#include <numeric>
+#include <utility>
#include "base/containers/adapters.h"
#include "base/i18n/case_conversion.h"
@@ -45,12 +46,48 @@
#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
@@ -261,6 +298,7 @@ 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,
@@ -282,6 +320,7 @@ MenuItemView* MenuItemView::AddMenuItemAt(
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()) {
@@ -337,6 +376,7 @@ void MenuItemView::AppendSeparator() {
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(),
@@ -351,8 +391,8 @@ MenuItemView* MenuItemView::AppendMenuItemImpl(int item_id,
Type type) {
const int index = submenu_ ? int{submenu_->children().size()} : 0;
return AddMenuItemAt(index, item_id, label, base::string16(),
- ui::ThemedVectorIcon(), icon, ui::ThemedVectorIcon(),
- type, ui::NORMAL_SEPARATOR);
+ base::string16(), ui::ThemedVectorIcon(), icon,
+ ui::ThemedVectorIcon(), type, ui::NORMAL_SEPARATOR);
}
SubmenuView* MenuItemView::CreateSubmenu() {
@@ -383,6 +423,11 @@ void MenuItemView::SetTitle(const base::string16& 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.
@@ -438,19 +483,16 @@ void MenuItemView::SetIcon(const gfx::ImageSkia& icon) {
void MenuItemView::SetIcon(const ui::ThemedVectorIcon& icon) {
vector_icon_ = icon;
+ UpdateIconViewFromVectorIconAndTheme();
}
void MenuItemView::UpdateIconViewFromVectorIconAndTheme() {
if (vector_icon_.empty())
return;
- if (!icon_view_)
- SetIconView(std::make_unique<ImageView>());
-
- const bool use_touchable_layout =
- GetMenuController() && GetMenuController()->use_touchable_layout();
- const int icon_size = use_touchable_layout ? 20 : 16;
- icon_view_->SetImage(vector_icon_.GetImageSkia(GetNativeTheme(), icon_size));
+ 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) {
@@ -740,14 +782,17 @@ void MenuItemView::UpdateMenuPartSizes() {
icon_area_width_;
int padding = 0;
if (config.always_use_icon_to_label_padding) {
- padding = config.item_horizontal_padding;
+ padding = LayoutProvider::Get()->GetDistanceMetric(
+ DISTANCE_RELATED_LABEL_HORIZONTAL);
} else if (!config.icons_in_label) {
padding = (has_icons_ || HasChecksOrRadioButtons())
- ? config.item_horizontal_padding
+ ? LayoutProvider::Get()->GetDistanceMetric(
+ DISTANCE_RELATED_LABEL_HORIZONTAL)
: 0;
}
if (use_touchable_layout)
- padding = config.touchable_item_horizontal_padding;
+ padding = LayoutProvider::Get()->GetDistanceMetric(
+ DISTANCE_RELATED_LABEL_HORIZONTAL);
label_start_ += padding;
@@ -904,17 +949,22 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) {
// selected.
PaintBackground(canvas, mode, render_selection);
- const int top_margin = GetTopMargin();
- const int bottom_margin = GetBottomMargin();
- const int available_height = height() - top_margin - bottom_margin;
-
// Calculate some colors.
MenuDelegate::LabelStyle style;
- style.foreground = GetTextColor(false, render_selection);
+ 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));
@@ -937,15 +987,33 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) {
(!delegate || delegate->ShouldReserveSpaceForSubmenuIndicator()
? item_right_margin_
: config.arrow_to_edge_padding);
- gfx::Rect text_bounds(label_start, top_margin, width, available_height);
+ 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));
@@ -1198,12 +1266,19 @@ MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const {
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);
+ (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, style.font_list.GetHeight() +
- GetBottomMargin() + GetTopMargin());
+ std::max(dimensions.height,
+ label_text_height + GetBottomMargin() + GetTopMargin());
dimensions.height =
std::max(dimensions.height, MenuConfig::instance().item_min_height);
@@ -1246,12 +1321,48 @@ int MenuItemView::GetLabelStartForThisItem() const {
if ((config.icons_in_label || type_ == Type::kCheckbox ||
type_ == Type::kRadio) &&
icon_view_) {
- label_start += icon_view_->size().width() + config.item_horizontal_padding;
+ 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.
@@ -1329,6 +1440,12 @@ bool MenuItemView::HasChecksOrRadioButtons() const {
[](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()
diff --git a/chromium/ui/views/controls/menu/menu_item_view.h b/chromium/ui/views/controls/menu/menu_item_view.h
index b38a048c22e..01b797549ac 100644
--- a/chromium/ui/views/controls/menu/menu_item_view.h
+++ b/chromium/ui/views/controls/menu/menu_item_view.h
@@ -5,11 +5,11 @@
#ifndef UI_VIEWS_CONTROLS_MENU_MENU_ITEM_VIEW_H_
#define UI_VIEWS_CONTROLS_MENU_MENU_ITEM_VIEW_H_
+#include <memory>
#include <string>
#include <vector>
#include "base/compiler_specific.h"
-#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
@@ -148,6 +148,7 @@ class VIEWS_EXPORT MenuItemView : public View {
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,
@@ -214,6 +215,11 @@ class VIEWS_EXPORT MenuItemView : public View {
void SetTitle(const base::string16& title);
const base::string16& title() const { return title_; }
+ // Sets/Gets the secondary title. When not empty, they are shown in the line
+ // below the title.
+ void SetSecondaryTitle(const base::string16& secondary_title);
+ const base::string16& secondary_title() const { return secondary_title_; }
+
// Sets the minor text.
void SetMinorText(const base::string16& minor_text);
@@ -264,6 +270,9 @@ class VIEWS_EXPORT MenuItemView : public View {
// Returns the command id of this item.
int GetCommand() const { return command_; }
+ void set_is_new(bool is_new) { is_new_ = is_new; }
+ bool is_new() const { return is_new_; }
+
// Paints the menu item.
void OnPaint(gfx::Canvas* canvas) override;
@@ -444,6 +453,13 @@ class VIEWS_EXPORT MenuItemView : public View {
// Get the horizontal position at which to draw the menu item's label.
int GetLabelStartForThisItem() const;
+ // Draws the "new" badge on |canvas|. |unmirrored_badge_start| is the
+ // upper-left corner of the badge, not mirrored for RTL.
+ void DrawNewBadge(gfx::Canvas* canvas,
+ const gfx::Point& unmirrored_badge_start,
+ const gfx::FontList& primary_font,
+ int text_render_flags);
+
// Used by MenuController to cache the menu position in use by the
// active menu.
MenuPosition actual_menu_position() const { return actual_menu_position_; }
@@ -475,6 +491,10 @@ class VIEWS_EXPORT MenuItemView : public View {
// Returns true if the menu has items with a checkbox or a radio button.
bool HasChecksOrRadioButtons() const;
+ // Returns whether or not a "new" badge should be shown on this menu item.
+ // Takes into account whether the badging feature is enabled.
+ bool ShouldShowNewBadge() const;
+
void invalidate_dimensions() { dimensions_.height = 0; }
bool is_dimensions_valid() const { return dimensions_.height > 0; }
@@ -505,16 +525,16 @@ class VIEWS_EXPORT MenuItemView : public View {
// Command id.
int command_ = 0;
+ // Whether the menu item should be badged as "New" (if badging is enabled) as
+ // a way to highlight a new feature for users.
+ bool is_new_ = false;
+
// Submenu, created via CreateSubmenu.
SubmenuView* submenu_ = nullptr;
- // Title.
base::string16 title_;
-
- // Minor text.
+ base::string16 secondary_title_;
base::string16 minor_text_;
-
- // Minor icon.
ui::ThemedVectorIcon minor_icon_;
// The icon used for |icon_view_| when a vector icon has been set instead of a
diff --git a/chromium/ui/views/controls/menu/menu_item_view_unittest.cc b/chromium/ui/views/controls/menu/menu_item_view_unittest.cc
index 119aa4a49c3..9c132d932b8 100644
--- a/chromium/ui/views/controls/menu/menu_item_view_unittest.cc
+++ b/chromium/ui/views/controls/menu/menu_item_view_unittest.cc
@@ -320,20 +320,37 @@ class MenuItemViewPaintUnitTest : public ViewsTestBase {
DISALLOW_COPY_AND_ASSIGN(MenuItemViewPaintUnitTest);
};
-// Provides assertion coverage for painting minor text and icons.
+// Provides assertion coverage for painting, secondary label, minor text and
+// icons.
TEST_F(MenuItemViewPaintUnitTest, MinorTextAndIconAssertionCoverage) {
- auto AddItem = [this](auto label, auto minor_label, auto minor_icon) {
+ auto AddItem = [this](auto label, auto secondary_label, auto minor_label,
+ auto minor_icon) {
menu_item_view()->AddMenuItemAt(
- 0, 1000, base::ASCIIToUTF16(label), minor_label, minor_icon,
- gfx::ImageSkia(), ui::ThemedVectorIcon(),
+ 0, 1000, base::ASCIIToUTF16(label), secondary_label, minor_label,
+ minor_icon, gfx::ImageSkia(), ui::ThemedVectorIcon(),
views::MenuItemView::Type::kNormal, ui::NORMAL_SEPARATOR);
};
- AddItem("No minor content", base::string16(), ui::ThemedVectorIcon());
- AddItem("Minor text only", base::ASCIIToUTF16("minor text"),
+ AddItem("No secondary label, no minor content", base::string16(),
+ base::string16(), ui::ThemedVectorIcon());
+ AddItem("No secondary label, minor text only", base::string16(),
+ base::ASCIIToUTF16("minor text"), ui::ThemedVectorIcon());
+ AddItem("No secondary label, minor icon only", base::string16(),
+ base::string16(), ui::ThemedVectorIcon(&views::kMenuCheckIcon));
+ AddItem("No secondary label, minor text and icon", base::string16(),
+ base::ASCIIToUTF16("minor text"),
+ ui::ThemedVectorIcon(&views::kMenuCheckIcon));
+ AddItem("Secondary label, no minor content",
+ base::ASCIIToUTF16("secondary label"), base::string16(),
ui::ThemedVectorIcon());
- AddItem("Minor icon only", base::string16(),
+ AddItem("Secondary label, minor text only",
+ base::ASCIIToUTF16("secondary label"),
+ base::ASCIIToUTF16("minor text"), ui::ThemedVectorIcon());
+ AddItem("Secondary label, minor icon only",
+ base::ASCIIToUTF16("secondary label"), base::string16(),
ui::ThemedVectorIcon(&views::kMenuCheckIcon));
- AddItem("Minor text and icon", base::ASCIIToUTF16("minor text"),
+ AddItem("Secondary label, minor text and icon",
+ base::ASCIIToUTF16("secondary label"),
+ base::ASCIIToUTF16("minor text"),
ui::ThemedVectorIcon(&views::kMenuCheckIcon));
menu_runner()->RunMenuAt(widget(), nullptr, gfx::Rect(),
diff --git a/chromium/ui/views/controls/menu/menu_model_adapter.cc b/chromium/ui/views/controls/menu/menu_model_adapter.cc
index 79af9195b11..b0b1cf9a7a7 100644
--- a/chromium/ui/views/controls/menu/menu_model_adapter.cc
+++ b/chromium/ui/views/controls/menu/menu_model_adapter.cc
@@ -99,16 +99,17 @@ MenuItemView* MenuModelAdapter::AddMenuItemFromModelAt(ui::MenuModel* model,
}
if (*type == MenuItemView::Type::kSeparator) {
- return menu->AddMenuItemAt(menu_index, item_id, base::string16(),
- base::string16(), ui::ThemedVectorIcon(),
- gfx::ImageSkia(), ui::ThemedVectorIcon(), *type,
- model->GetSeparatorTypeAt(model_index));
+ return menu->AddMenuItemAt(
+ menu_index, item_id, base::string16(), base::string16(),
+ base::string16(), ui::ThemedVectorIcon(), gfx::ImageSkia(),
+ ui::ThemedVectorIcon(), *type, model->GetSeparatorTypeAt(model_index));
}
ui::ImageModel icon = model->GetIconAt(model_index);
ui::ImageModel minor_icon = model->GetMinorIconAt(model_index);
- return menu->AddMenuItemAt(
+ auto* menu_item_view = menu->AddMenuItemAt(
menu_index, item_id, model->GetLabelAt(model_index),
+ model->GetSecondaryLabelAt(model_index),
model->GetMinorTextAt(model_index),
minor_icon.IsVectorIcon()
? ui::ThemedVectorIcon(minor_icon.GetVectorIcon())
@@ -117,6 +118,12 @@ MenuItemView* MenuModelAdapter::AddMenuItemFromModelAt(ui::MenuModel* model,
icon.IsVectorIcon() ? ui::ThemedVectorIcon(icon.GetVectorIcon())
: ui::ThemedVectorIcon(),
*type, ui::NORMAL_SEPARATOR);
+
+ if (model->IsAlertedAt(model_index))
+ menu_item_view->SetAlerted();
+ menu_item_view->set_is_new(model->IsNewFeatureAt(model_index));
+
+ return menu_item_view;
}
// Static.
diff --git a/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc b/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc
index 6241a9d7916..111b070d725 100644
--- a/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc
+++ b/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc
@@ -78,6 +78,12 @@ class MenuModelBase : public ui::MenuModel {
bool IsVisibleAt(int index) const override { return items_[index].visible; }
+ bool IsAlertedAt(int index) const override { return items_[index].alerted; }
+
+ bool IsNewFeatureAt(int index) const override {
+ return items_[index].new_feature;
+ }
+
MenuModel* GetSubmenuModelAt(int index) const override {
return items_[index].submenu;
}
@@ -117,6 +123,8 @@ class MenuModelBase : public ui::MenuModel {
ui::MenuModel* submenu;
bool enabled;
bool visible;
+ bool alerted = false;
+ bool new_feature = false;
};
const Item& GetItemDefinition(size_t index) { return items_[index]; }
@@ -142,6 +150,7 @@ class SubmenuModel : public MenuModelBase {
SubmenuModel() : MenuModelBase(kSubmenuIdBase) {
items_.emplace_back(TYPE_COMMAND, "submenu item 0", nullptr, false, true);
items_.emplace_back(TYPE_COMMAND, "submenu item 1", nullptr);
+ items_[1].alerted = true;
}
~SubmenuModel() override = default;
@@ -155,6 +164,7 @@ class ActionableSubmenuModel : public MenuModelBase {
ActionableSubmenuModel() : MenuModelBase(kActionableSubmenuIdBase) {
items_.emplace_back(TYPE_COMMAND, "actionable submenu item 0", nullptr);
items_.emplace_back(TYPE_COMMAND, "actionable submenu item 1", nullptr);
+ items_[1].new_feature = true;
}
~ActionableSubmenuModel() override = default;
@@ -246,6 +256,12 @@ void CheckSubmenu(const RootModel& model,
// Check visibility.
EXPECT_EQ(model_item.visible, item->GetVisible());
+ // Check alert state.
+ EXPECT_EQ(model_item.alerted, item->is_alerted());
+
+ // Check new feature flag.
+ EXPECT_EQ(model_item.new_feature, item->is_new());
+
// Check activation.
static_cast<views::MenuDelegate*>(delegate)->ExecuteCommand(id);
EXPECT_EQ(i, size_t{submodel->last_activation()});
@@ -324,6 +340,12 @@ TEST_F(MenuModelAdapterTest, BasicTest) {
// Check visibility.
EXPECT_EQ(model_item.visible, item->GetVisible());
+ // Check alert state.
+ EXPECT_EQ(model_item.alerted, item->is_alerted());
+
+ // Check new feature flag.
+ EXPECT_EQ(model_item.new_feature, item->is_new());
+
// Check activation.
static_cast<views::MenuDelegate*>(&delegate)->ExecuteCommand(id);
EXPECT_EQ(i, size_t{model.last_activation()});
diff --git a/chromium/ui/views/controls/menu/submenu_view.cc b/chromium/ui/views/controls/menu/submenu_view.cc
index 413a1de18f9..28a9c908988 100644
--- a/chromium/ui/views/controls/menu/submenu_view.cc
+++ b/chromium/ui/views/controls/menu/submenu_view.cc
@@ -16,6 +16,7 @@
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/safe_integer_conversions.h"
+#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_host.h"
@@ -396,8 +397,12 @@ void SubmenuView::ShowAt(Widget* parent,
host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture);
}
- GetScrollViewContainer()->NotifyAccessibilityEvent(
- ax::mojom::Event::kMenuStart, true);
+ // Only fire kMenuStart for the top level menu, not for each submenu.
+ if (!GetMenuItem()->GetParentMenuItem()) {
+ GetScrollViewContainer()->NotifyAccessibilityEvent(
+ ax::mojom::Event::kMenuStart, true);
+ }
+ // Fire kMenuPopupStart for each menu/submenu that is shown.
NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupStart, true);
}
@@ -408,14 +413,6 @@ void SubmenuView::Reposition(const gfx::Rect& bounds) {
void SubmenuView::Close() {
if (host_) {
- // We send the event to the ScrollViewContainer first because the View
- // accessibility delegate sets up a focus override when receiving the
- // kMenuStart event that we want to be disabled when we send the
- // kMenuPopupEnd event in order to access the previously focused node.
- GetScrollViewContainer()->NotifyAccessibilityEvent(
- ax::mojom::Event::kMenuEnd, true);
- NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupEnd, true);
-
host_->DestroyMenuHost();
host_ = nullptr;
}
@@ -423,8 +420,22 @@ void SubmenuView::Close() {
void SubmenuView::Hide() {
if (host_) {
+ /// -- Fire accessibility events ----
+ // Both of these must be fired before HideMenuHost().
+ // Only fire kMenuStart for as top levels menu closes, not for each submenu.
+ // This is sent before kMenuPopupEnd to allow ViewAXPlatformNodeDelegate to
+ // remove its focus override before AXPlatformNodeAuraLinux needs to access
+ // the previously-focused node while handling kMenuPopupEnd.
+ if (!GetMenuItem()->GetParentMenuItem()) {
+ GetScrollViewContainer()->NotifyAccessibilityEvent(
+ ax::mojom::Event::kMenuEnd, true);
+ GetViewAccessibility().EndPopupFocusOverride();
+ }
+ // Fire these kMenuPopupEnd for each menu/submenu that closes/hides.
+ if (host_->IsVisible())
+ NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupEnd, true);
+
host_->HideMenuHost();
- NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupHide, true);
}
if (scroll_animator_->is_scrolling())