diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/ui/views/controls/menu | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-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')
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()) |