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 | |
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')
61 files changed, 1406 insertions, 757 deletions
diff --git a/chromium/ui/views/controls/button/button.cc b/chromium/ui/views/controls/button/button.cc index d74e05dc34c..5bb670ce30a 100644 --- a/chromium/ui/views/controls/button/button.cc +++ b/chromium/ui/views/controls/button/button.cc @@ -56,6 +56,7 @@ Button::WidgetObserverButtonBridge::WidgetObserverButtonBridge(Button* button) Button::WidgetObserverButtonBridge::~WidgetObserverButtonBridge() { if (owner_) owner_->GetWidget()->RemoveObserver(this); + CHECK(!IsInObserverList()); } void Button::WidgetObserverButtonBridge::OnWidgetPaintAsActiveChanged( @@ -240,10 +241,12 @@ void Button::SetAnimationDuration(base::TimeDelta duration) { } void Button::SetInstallFocusRingOnFocus(bool install) { - if (install) + if (focus_ring_ && !install) { + RemoveChildViewT(focus_ring_); + focus_ring_ = nullptr; + } else if (!focus_ring_ && install) { focus_ring_ = FocusRing::Install(this); - else - focus_ring_.reset(); + } } void Button::SetHotTracked(bool is_hot_tracked) { diff --git a/chromium/ui/views/controls/button/button.h b/chromium/ui/views/controls/button/button.h index 79bab1f6034..6411eb3514b 100644 --- a/chromium/ui/views/controls/button/button.h +++ b/chromium/ui/views/controls/button/button.h @@ -292,7 +292,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, return hover_animation_; } - FocusRing* focus_ring() { return focus_ring_.get(); } + FocusRing* focus_ring() { return focus_ring_; } // The button's listener. Notified when clicked. ButtonListener* listener_; @@ -368,7 +368,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, SkColor ink_drop_base_color_; // The focus ring for this Button. - std::unique_ptr<FocusRing> focus_ring_; + FocusRing* focus_ring_ = nullptr; std::unique_ptr<Painter> focus_painter_; diff --git a/chromium/ui/views/controls/button/button_unittest.cc b/chromium/ui/views/controls/button/button_unittest.cc index 3dfeba259fd..5850b215f25 100644 --- a/chromium/ui/views/controls/button/button_unittest.cc +++ b/chromium/ui/views/controls/button/button_unittest.cc @@ -35,6 +35,7 @@ #include "ui/views/controls/link.h" #include "ui/views/controls/textfield/textfield.h" #include "ui/views/style/platform_style.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" #include "ui/views/widget/widget_utils.h" @@ -178,15 +179,6 @@ TestInkDrop* AddTestInkDrop(TestButton* button) { return ink_drop; } -// TODO(tluk): remove when the appropriate ownership APIs have been added for -// Widget's SetContentsView(). -template <typename T> -T* AddContentsView(Widget* widget, std::unique_ptr<T> view) { - T* view_ptr = view.get(); - widget->SetContentsView(view.release()); - return view_ptr; -} - } // namespace class ButtonTest : public ViewsTestBase { @@ -207,7 +199,7 @@ class ButtonTest : public ViewsTestBase { widget_->Init(std::move(params)); widget_->Show(); - button_ = AddContentsView(widget(), std::make_unique<TestButton>(false)); + button_ = widget()->SetContentsView(std::make_unique<TestButton>(false)); event_generator_ = std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget())); @@ -225,24 +217,21 @@ class ButtonTest : public ViewsTestBase { } TestInkDrop* CreateButtonWithInkDrop(bool has_ink_drop_action_on_click) { - button_ = AddContentsView( - widget(), std::make_unique<TestButton>(has_ink_drop_action_on_click)); - widget_->SetContentsView(button_); + button_ = widget()->SetContentsView( + std::make_unique<TestButton>(has_ink_drop_action_on_click)); return AddTestInkDrop(button_); } void CreateButtonWithRealInkDrop() { - button_ = AddContentsView(widget(), std::make_unique<TestButton>(false)); + button_ = widget()->SetContentsView(std::make_unique<TestButton>(false)); InkDropHostViewTestApi(button_).SetInkDrop( std::make_unique<InkDropImpl>(button_, button_->size())); - widget_->SetContentsView(button_); } void CreateButtonWithObserver() { - button_ = AddContentsView(widget(), std::make_unique<TestButton>(false)); + button_ = widget()->SetContentsView(std::make_unique<TestButton>(false)); button_observer_ = std::make_unique<TestButtonObserver>(); button_->AddButtonObserver(button_observer_.get()); - widget_->SetContentsView(button_); } protected: @@ -263,7 +252,12 @@ class ButtonTest : public ViewsTestBase { DISALLOW_COPY_AND_ASSIGN(ButtonTest); }; -// Tests that hover state changes correctly when visiblity/enableness changes. +// Iterate through the metadata for Button to ensure it all works. +TEST_F(ButtonTest, MetadataTest) { + test::TestViewMetadata(button()); +} + +// Tests that hover state changes correctly when visibility/enableness changes. TEST_F(ButtonTest, HoverStateOnVisibilityChange) { event_generator()->MoveMouseTo(button()->GetBoundsInScreen().CenterPoint()); event_generator()->PressLeftButton(); @@ -599,7 +593,7 @@ TEST_F(ButtonTest, InkDropAfterTryingToShowContextMenu) { } TEST_F(ButtonTest, HideInkDropHighlightWhenRemoved) { - View* contents_view = AddContentsView(widget(), std::make_unique<View>()); + View* contents_view = widget()->SetContentsView(std::make_unique<View>()); TestButton* button = contents_view->AddChildView(std::make_unique<TestButton>(false)); @@ -779,7 +773,7 @@ class VisibilityTestButton : public TestButton { // changed visibility states. TEST_F(ButtonTest, NoLayerAddedForWidgetVisibilityChanges) { VisibilityTestButton* button = - AddContentsView(widget(), std::make_unique<VisibilityTestButton>()); + widget()->SetContentsView(std::make_unique<VisibilityTestButton>()); // Ensure no layers are created during construction. EXPECT_TRUE(button->GetVisible()); diff --git a/chromium/ui/views/controls/button/checkbox_unittest.cc b/chromium/ui/views/controls/button/checkbox_unittest.cc index 3b3e5d83d76..a468b3d7212 100644 --- a/chromium/ui/views/controls/button/checkbox_unittest.cc +++ b/chromium/ui/views/controls/button/checkbox_unittest.cc @@ -32,8 +32,8 @@ class CheckboxTest : public ViewsTestBase { widget_->Init(std::move(params)); widget_->Show(); - checkbox_ = new Checkbox(base::string16()); - widget_->SetContentsView(checkbox_); + checkbox_ = + widget_->SetContentsView(std::make_unique<Checkbox>(base::string16())); } void TearDown() override { diff --git a/chromium/ui/views/controls/button/image_button_factory.cc b/chromium/ui/views/controls/button/image_button_factory.cc index 5ac49262cfe..7d7d6a9a520 100644 --- a/chromium/ui/views/controls/button/image_button_factory.cc +++ b/chromium/ui/views/controls/button/image_button_factory.cc @@ -110,21 +110,11 @@ void SetImageFromVectorIconWithColor(ImageButton* button, button->set_ink_drop_base_color(icon_color); } -void SetToggledImageFromVectorIcon(ToggleImageButton* button, - const gfx::VectorIcon& icon, - int dip_size, - SkColor related_text_color) { - const SkColor icon_color = - color_utils::DeriveDefaultIconColor(related_text_color); - SetToggledImageFromVectorIconWithColor(button, icon, dip_size, icon_color); -} - void SetToggledImageFromVectorIconWithColor(ToggleImageButton* button, const gfx::VectorIcon& icon, int dip_size, - SkColor icon_color) { - const SkColor disabled_color = - SkColorSetA(icon_color, gfx::kDisabledControlAlpha); + SkColor icon_color, + SkColor disabled_color) { const gfx::ImageSkia normal_image = gfx::CreateVectorIcon(icon, dip_size, icon_color); const gfx::ImageSkia disabled_image = diff --git a/chromium/ui/views/controls/button/image_button_factory.h b/chromium/ui/views/controls/button/image_button_factory.h index 38d7c70ad52..de2d57501b2 100644 --- a/chromium/ui/views/controls/button/image_button_factory.h +++ b/chromium/ui/views/controls/button/image_button_factory.h @@ -32,8 +32,7 @@ VIEWS_EXPORT std::unique_ptr<ImageButton> CreateVectorImageButton( ButtonListener* listener); // Creates a ToggleImageButton with an ink drop and a centered image in -// preperation for applying a vector icon from SetImageFromVectorIcon and -// SetToggledImageFromVectorIcon below. +// preparation for applying a vector icon from SetImageFromVectorIcon below. VIEWS_EXPORT std::unique_ptr<ToggleImageButton> CreateVectorToggleImageButton( ButtonListener* listener); @@ -73,7 +72,8 @@ VIEWS_EXPORT void SetToggledImageFromVectorIconWithColor( ToggleImageButton* button, const gfx::VectorIcon& icon, int dip_size, - SkColor icon_color); + SkColor icon_color, + SkColor disabled_color); } // namespace views diff --git a/chromium/ui/views/controls/button/image_button_factory_unittest.cc b/chromium/ui/views/controls/button/image_button_factory_unittest.cc index 5b8a5e4aee6..2197b35a527 100644 --- a/chromium/ui/views/controls/button/image_button_factory_unittest.cc +++ b/chromium/ui/views/controls/button/image_button_factory_unittest.cc @@ -56,24 +56,22 @@ class ImageButtonFactoryWidgetTest : public ViewsTestBase { } void TearDown() override { - button_.reset(); widget_.reset(); ViewsTestBase::TearDown(); } ImageButton* AddImageButton(std::unique_ptr<ImageButton> button) { - button_ = std::move(button); - widget_->SetContentsView(button_.get()); - return button_.get(); + button_ = widget_->SetContentsView(std::move(button)); + return button_; } protected: Widget* widget() { return widget_.get(); } - ImageButton* button() { return button_.get(); } + ImageButton* button() { return button_; } private: std::unique_ptr<Widget> widget_; - std::unique_ptr<ImageButton> button_; + ImageButton* button_ = nullptr; // owned by |widget_|. DISALLOW_COPY_AND_ASSIGN(ImageButtonFactoryWidgetTest); }; diff --git a/chromium/ui/views/controls/button/label_button.cc b/chromium/ui/views/controls/button/label_button.cc index bb9105f1a18..3aef54f32cd 100644 --- a/chromium/ui/views/controls/button/label_button.cc +++ b/chromium/ui/views/controls/button/label_button.cc @@ -281,20 +281,21 @@ int LabelButton::GetHeightForWidth(int width) const { } void LabelButton::Layout() { - gfx::Rect child_area = GetLocalBounds(); + gfx::Rect image_area = GetLocalBounds(); - ink_drop_container_->SetBoundsRect(child_area); - // The space that the label can use. Its actual bounds may be smaller if the - // label is short. - gfx::Rect label_area = child_area; + ink_drop_container_->SetBoundsRect(image_area); gfx::Insets insets = GetInsets(); - child_area.Inset(insets); - // Labels can paint over the vertical component of the border insets. - label_area.Inset(insets.left(), 0, insets.right(), 0); + // If the button have a limited space to fit in, the image and the label + // may overlap with the border, which often times contains a lot of empty + // padding. + image_area.Inset(insets.left(), 0, insets.right(), 0); + // The space that the label can use. Labels truncate horizontally, so there + // is no need to allow the label to take up the complete horizontal space. + gfx::Rect label_area = image_area; gfx::Size image_size = image_->GetPreferredSize(); - image_size.SetToMin(child_area.size()); + image_size.SetToMin(image_area.size()); const auto horizontal_alignment = GetHorizontalAlignment(); if (!image_size.IsEmpty()) { @@ -309,22 +310,28 @@ void LabelButton::Layout() { std::min(label_area.width(), label_->GetPreferredSize().width()), label_area.height()); - gfx::Point image_origin = child_area.origin(); + gfx::Point image_origin = image_area.origin(); if (label_->GetMultiLine() && !image_centered_) { - image_origin.Offset( - 0, std::max( - 0, (label_->font_list().GetHeight() - image_size.height()) / 2)); + // This code assumes the text is vertically centered. + DCHECK_EQ(gfx::ALIGN_MIDDLE, label_->GetVerticalAlignment()); + int label_height = label_->GetHeightForWidth(label_size.width()); + int first_line_y = + label_area.y() + (label_area.height() - label_height) / 2; + int image_origin_y = + first_line_y + + (label_->font_list().GetHeight() - image_size.height()) / 2; + image_origin.Offset(0, std::max(0, image_origin_y)); } else { - image_origin.Offset(0, (child_area.height() - image_size.height()) / 2); + image_origin.Offset(0, (image_area.height() - image_size.height()) / 2); } if (horizontal_alignment == gfx::ALIGN_CENTER) { const int spacing = (image_size.width() > 0 && label_size.width() > 0) ? GetImageLabelSpacing() : 0; const int total_width = image_size.width() + label_size.width() + spacing; - image_origin.Offset((child_area.width() - total_width) / 2, 0); + image_origin.Offset((image_area.width() - total_width) / 2, 0); } else if (horizontal_alignment == gfx::ALIGN_RIGHT) { - image_origin.Offset(child_area.width() - image_size.width(), 0); + image_origin.Offset(image_area.width() - image_size.width(), 0); } image_->SetBoundsRect(gfx::Rect(image_origin, image_size)); diff --git a/chromium/ui/views/controls/button/label_button_label_unittest.cc b/chromium/ui/views/controls/button/label_button_label_unittest.cc index 505c2d5ed6c..8e880de4eaf 100644 --- a/chromium/ui/views/controls/button/label_button_label_unittest.cc +++ b/chromium/ui/views/controls/button/label_button_label_unittest.cc @@ -62,12 +62,22 @@ class LabelButtonLabelTest : public ViewsTestBase { void SetUp() override { ViewsTestBase::SetUp(); - label_ = std::make_unique<TestLabel>(&last_color_); + + widget_ = CreateTestWidget(); + label_ = + widget_->SetContentsView(std::make_unique<TestLabel>(&last_color_)); + label_->SetAutoColorReadabilityEnabled(false); + } + + void TearDown() override { + widget_.reset(); + ViewsTestBase::TearDown(); } protected: - SkColor last_color_ = SK_ColorCYAN; - std::unique_ptr<TestLabel> label_; + SkColor last_color_ = gfx::kPlaceholderColor; + std::unique_ptr<views::Widget> widget_; + TestLabel* label_; TestNativeTheme theme1_; TestNativeTheme theme2_; @@ -77,16 +87,6 @@ class LabelButtonLabelTest : public ViewsTestBase { // Test that LabelButtonLabel reacts properly to themed and overridden colors. TEST_F(LabelButtonLabelTest, Colors) { - // The OnDidSchedulePaint() override won't be called while the base - // class is initialized. Not much we can do about that, so give it the first - // for free. - EXPECT_EQ(SK_ColorCYAN, last_color_); // Sanity check. - - // At the same time we can check that changing the auto color readability - // schedules a paint. Currently it does. Although it technically doesn't need - // to since the color isn't actually changing. - label_->SetAutoColorReadabilityEnabled(false); - // First one comes from the default theme. This check ensures the SK_ColorRED // placeholder initializers were replaced. SkColor default_theme_enabled_color = diff --git a/chromium/ui/views/controls/button/label_button_unittest.cc b/chromium/ui/views/controls/button/label_button_unittest.cc index cd33dbe4c0a..38443891fff 100644 --- a/chromium/ui/views/controls/button/label_button_unittest.cc +++ b/chromium/ui/views/controls/button/label_button_unittest.cc @@ -627,8 +627,8 @@ TEST_F(LabelButtonTest, HighlightedButtonStyle) { // Ensure the label resets the enabled color after LabelButton::OnThemeChanged() // is invoked. TEST_F(LabelButtonTest, OnThemeChanged) { - ASSERT_NE(button_->GetNativeTheme()->GetHighContrastColorScheme(), - ui::NativeTheme::HighContrastColorScheme::kDark); + ASSERT_NE(button_->GetNativeTheme()->GetPlatformHighContrastColorScheme(), + ui::NativeTheme::PlatformHighContrastColorScheme::kDark); ASSERT_NE(button_->label()->GetBackgroundColor(), SK_ColorBLACK); EXPECT_EQ(themed_normal_text_color_, button_->label()->GetEnabledColor()); @@ -667,6 +667,25 @@ TEST_F(LabelButtonTest, SetEnabledTextColorsResetsToThemeColors) { EXPECT_EQ(TestNativeTheme::kSystemColor, button_->label()->GetEnabledColor()); } +TEST_F(LabelButtonTest, ImageOrLabelGetClipped) { + const base::string16 text(ASCIIToUTF16("abc")); + button_->SetText(text); + + const gfx::FontList font_list = button_->label()->font_list(); + const int image_size = font_list.GetHeight(); + button_->SetImage(Button::STATE_NORMAL, + CreateTestImage(image_size, image_size)); + + button_->SetBoundsRect(gfx::Rect(button_->GetPreferredSize())); + // The border size + the content height is more than button's preferred size. + button_->SetBorder(CreateEmptyBorder(image_size / 2, 0, image_size / 2, 0)); + button_->Layout(); + + // Ensure that content (image and label) doesn't get clipped by the border. + EXPECT_GE(button_->image()->height(), image_size); + EXPECT_GE(button_->label()->height(), image_size); +} + // Test fixture for a LabelButton that has an ink drop configured. class InkDropLabelButtonTest : public ViewsTestBase { public: diff --git a/chromium/ui/views/controls/button/md_text_button.cc b/chromium/ui/views/controls/button/md_text_button.cc index b9905fc6359..79c42e7b9ac 100644 --- a/chromium/ui/views/controls/button/md_text_button.cc +++ b/chromium/ui/views/controls/button/md_text_button.cc @@ -142,6 +142,11 @@ void MdTextButton::SetEnabledTextColors(base::Optional<SkColor> color) { UpdateColors(); } +void MdTextButton::SetCustomPadding(const gfx::Insets& padding) { + custom_padding_ = padding; + UpdatePadding(); +} + void MdTextButton::SetText(const base::string16& text) { LabelButton::SetText(text); UpdatePadding(); @@ -183,6 +188,11 @@ void MdTextButton::UpdatePadding() { return; } + SetBorder( + CreateEmptyBorder(custom_padding_.value_or(CalculateDefaultPadding()))); +} + +gfx::Insets MdTextButton::CalculateDefaultPadding() const { // Text buttons default to 28dp in height on all platforms when the base font // is in use, but should grow or shrink if the font size is adjusted up or // down. When the system font size has been adjusted, the base font will be @@ -213,8 +223,8 @@ void MdTextButton::UpdatePadding() { // we apply the MD treatment to all buttons, even GTK buttons? const int horizontal_padding = LayoutProvider::Get()->GetDistanceMetric( DISTANCE_BUTTON_HORIZONTAL_PADDING); - SetBorder(CreateEmptyBorder(top_padding, horizontal_padding, bottom_padding, - horizontal_padding)); + return gfx::Insets(top_padding, horizontal_padding, bottom_padding, + horizontal_padding); } void MdTextButton::UpdateColors() { diff --git a/chromium/ui/views/controls/button/md_text_button.h b/chromium/ui/views/controls/button/md_text_button.h index 760ab2a80ae..1d293a70166 100644 --- a/chromium/ui/views/controls/button/md_text_button.h +++ b/chromium/ui/views/controls/button/md_text_button.h @@ -39,6 +39,9 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { void SetCornerRadius(float radius); float GetCornerRadius() const; + // See |custom_padding_|. + void SetCustomPadding(const gfx::Insets& padding); + // LabelButton: void OnThemeChanged() override; std::unique_ptr<views::InkDropHighlight> CreateInkDropHighlight() @@ -59,6 +62,7 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { private: void UpdatePadding(); void UpdateColors(); + gfx::Insets CalculateDefaultPadding() const; // True if this button uses prominent styling (blue fill, etc.). bool is_prominent_ = false; @@ -68,6 +72,9 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { float corner_radius_ = 0.0f; + // Used to override default padding. + base::Optional<gfx::Insets> custom_padding_; + DISALLOW_COPY_AND_ASSIGN(MdTextButton); }; diff --git a/chromium/ui/views/controls/button/md_text_button_unittest.cc b/chromium/ui/views/controls/button/md_text_button_unittest.cc new file mode 100644 index 00000000000..5fe5d9c1bf1 --- /dev/null +++ b/chromium/ui/views/controls/button/md_text_button_unittest.cc @@ -0,0 +1,25 @@ +// Copyright 2020 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/button/md_text_button.h" + +#include "ui/views/test/views_test_base.h" + +namespace views { + +using MdTextButtonTest = ViewsTestBase; + +TEST_F(MdTextButtonTest, CustomPadding) { + const base::string16 text = base::ASCIIToUTF16("abc"); + std::unique_ptr<MdTextButton> button = + MdTextButton::Create(nullptr, text, views::style::CONTEXT_BUTTON_MD); + + const gfx::Insets custom_padding(10, 20); + ASSERT_NE(button->GetInsets(), custom_padding); + + button->SetCustomPadding(custom_padding); + EXPECT_EQ(button->GetInsets(), custom_padding); +} + +} // namespace views diff --git a/chromium/ui/views/controls/button/radio_button_unittest.cc b/chromium/ui/views/controls/button/radio_button_unittest.cc index 3d25994df2a..8d22480097e 100644 --- a/chromium/ui/views/controls/button/radio_button_unittest.cc +++ b/chromium/ui/views/controls/button/radio_button_unittest.cc @@ -34,8 +34,7 @@ class RadioButtonTest : public ViewsTestBase { widget_->Init(std::move(params)); widget_->Show(); - button_container_ = new View(); - widget_->SetContentsView(button_container_); + button_container_ = widget_->SetContentsView(std::make_unique<View>()); } void TearDown() override { diff --git a/chromium/ui/views/controls/combobox/combobox.cc b/chromium/ui/views/controls/combobox/combobox.cc index b0364bcdb3e..72903d758de 100644 --- a/chromium/ui/views/controls/combobox/combobox.cc +++ b/chromium/ui/views/controls/combobox/combobox.cc @@ -17,12 +17,14 @@ #include "ui/base/ime/input_method.h" #include "ui/base/models/image_model.h" #include "ui/base/models/menu_model.h" +#include "ui/base/ui_base_types.h" #include "ui/events/event.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/scoped_canvas.h" #include "ui/gfx/text_utils.h" #include "ui/native_theme/native_theme.h" +#include "ui/native_theme/themed_vector_icon.h" #include "ui/views/animation/flood_fill_ink_drop_ripple.h" #include "ui/views/animation/ink_drop_impl.h" #include "ui/views/background.h" @@ -54,6 +56,15 @@ SkColor GetTextColorForEnableState(const Combobox& combobox, bool enabled) { return style::GetColor(combobox, style::CONTEXT_TEXTFIELD, style); } +gfx::ImageSkia GetImageSkiaFromImageModel(const ui::ImageModel* model, + const ui::NativeTheme* native_theme) { + DCHECK(model); + DCHECK(!model->IsEmpty()); + return model->IsImage() ? model->GetImage().AsImageSkia() + : ui::ThemedVectorIcon(model->GetVectorIcon()) + .GetImageSkia(native_theme); +} + // The transparent button which holds a button state but is not rendered. class TransparentButton : public Button { public: @@ -132,7 +143,13 @@ class Combobox::ComboboxMenuModel : public ui::MenuModel { } // Overridden from MenuModel: - bool HasIcons() const override { return false; } + bool HasIcons() const override { + for (int i = 0; i < GetItemCount(); ++i) { + if (!GetIconAt(i).IsEmpty()) + return true; + } + return false; + } int GetItemCount() const override { return model_->GetItemCount(); } @@ -155,7 +172,13 @@ class Combobox::ComboboxMenuModel : public ui::MenuModel { base::string16 GetLabelAt(int index) const override { // Inserting the Unicode formatting characters if necessary so that the // text is displayed correctly in right-to-left UIs. - base::string16 text = model_->GetItemAt(index); + base::string16 text = model_->GetDropDownTextAt(index); + base::i18n::AdjustStringForLocaleDirection(&text); + return text; + } + + base::string16 GetSecondaryLabelAt(int index) const override { + base::string16 text = model_->GetDropDownSecondaryTextAt(index); base::i18n::AdjustStringForLocaleDirection(&text); return text; } @@ -178,7 +201,7 @@ class Combobox::ComboboxMenuModel : public ui::MenuModel { int GetGroupIdAt(int index) const override { return -1; } ui::ImageModel GetIconAt(int index) const override { - return ui::ImageModel(); + return model_->GetDropDownIconAt(index); } ui::ButtonMenuItemModel* GetButtonMenuItemAt(int index) const override { @@ -443,7 +466,7 @@ bool Combobox::OnKeyPressed(const ui::KeyEvent& e) { void Combobox::OnPaint(gfx::Canvas* canvas) { OnPaintBackground(canvas); - PaintText(canvas); + PaintIconAndText(canvas); OnPaintBorder(canvas); } @@ -504,16 +527,8 @@ void Combobox::ButtonPressed(Button* sender, const ui::Event& event) { // TODO(hajimehoshi): Fix the problem that the arrow button blinks when // cliking this while the dropdown menu is opened. - const base::TimeDelta delta = base::TimeTicks::Now() - closed_time_; - if (delta <= kMinimumTimeBetweenButtonClicks) - return; - - ui::MenuSourceType source_type = ui::MENU_SOURCE_MOUSE; - if (event.IsKeyEvent()) - source_type = ui::MENU_SOURCE_KEYBOARD; - else if (event.IsGestureEvent() || event.IsTouchEvent()) - source_type = ui::MENU_SOURCE_TOUCH; - ShowDropDownMenu(source_type); + if ((base::TimeTicks::Now() - closed_time_) > kMinimumTimeBetweenButtonClicks) + ShowDropDownMenu(ui::GetMenuSourceTypeForEvent(event)); } void Combobox::OnComboboxModelChanged(ui::ComboboxModel* model) { @@ -543,7 +558,7 @@ void Combobox::AdjustBoundsForRTLUI(gfx::Rect* rect) const { rect->set_x(GetMirroredXForRect(*rect)); } -void Combobox::PaintText(gfx::Canvas* canvas) { +void Combobox::PaintIconAndText(gfx::Canvas* canvas) { gfx::Insets insets = GetInsets(); insets += gfx::Insets(0, LayoutProvider::Get()->GetDistanceMetric( DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING)); @@ -553,7 +568,20 @@ void Combobox::PaintText(gfx::Canvas* canvas) { int x = insets.left(); int y = insets.top(); - int text_height = height() - insets.height(); + int contents_height = height() - insets.height(); + + // Draw the icon. + ui::ImageModel icon = model()->GetIconAt(selected_index_); + if (!icon.IsEmpty()) { + gfx::ImageSkia icon_skia = + GetImageSkiaFromImageModel(&icon, GetNativeTheme()); + int icon_y = y + (contents_height - icon_skia.height()) / 2; + canvas->DrawImageInt(icon_skia, x, icon_y); + x += icon_skia.width() + LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_LABEL_HORIZONTAL); + } + + // Draw the text. SkColor text_color = GetTextColorForEnableState(*this, GetEnabled()); DCHECK_GE(selected_index_, 0); DCHECK_LT(selected_index_, model()->GetItemCount()); @@ -565,10 +593,10 @@ void Combobox::PaintText(gfx::Canvas* canvas) { const gfx::FontList& font_list = GetFontList(); int text_width = gfx::GetStringWidth(text, font_list); - if ((text_width + insets.width()) > disclosure_arrow_offset) - text_width = disclosure_arrow_offset - insets.width(); + text_width = + std::min(text_width, disclosure_arrow_offset - insets.right() - x); - gfx::Rect text_bounds(x, y, text_width, text_height); + gfx::Rect text_bounds(x, y, text_width, contents_height); AdjustBoundsForRTLUI(&text_bounds); canvas->DrawStringRect(text, font_list, text_color, text_bounds); @@ -581,21 +609,15 @@ void Combobox::PaintText(gfx::Canvas* canvas) { } void Combobox::ShowDropDownMenu(ui::MenuSourceType source_type) { - // Menu border widths. - constexpr int kMenuBorderWidthLeft = 1; constexpr int kMenuBorderWidthTop = 1; - constexpr int kMenuBorderWidthRight = 1; - + // Menu's requested position's width should be the same as local bounds so the + // border of the menu lines up with the border of the combobox. The y + // coordinate however should be shifted to the bottom with the border with not + // to overlap with the combobox border. gfx::Rect lb = GetLocalBounds(); gfx::Point menu_position(lb.origin()); - - // Inset the menu's requested position so the border of the menu lines up - // with the border of the combobox. - menu_position.set_x(menu_position.x() + kMenuBorderWidthLeft); menu_position.set_y(menu_position.y() + kMenuBorderWidthTop); - lb.set_width(lb.width() - (kMenuBorderWidthLeft + kMenuBorderWidthRight)); - View::ConvertPointToScreen(this, &menu_position); gfx::Rect bounds(menu_position, lb.size()); @@ -633,18 +655,27 @@ void Combobox::OnPerformAction() { gfx::Size Combobox::GetContentSize() const { const gfx::FontList& font_list = GetFontList(); - + int height = font_list.GetHeight(); int width = 0; for (int i = 0; i < model()->GetItemCount(); ++i) { if (model_->IsItemSeparatorAt(i)) continue; if (size_to_largest_label_ || i == selected_index_) { - width = std::max( - width, gfx::GetStringWidth(menu_model_->GetLabelAt(i), font_list)); + int item_width = gfx::GetStringWidth(model()->GetItemAt(i), font_list); + ui::ImageModel icon = model()->GetIconAt(i); + if (!icon.IsEmpty()) { + gfx::ImageSkia icon_skia = + GetImageSkiaFromImageModel(&icon, GetNativeTheme()); + item_width += + icon_skia.width() + LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_LABEL_HORIZONTAL); + height = std::max(height, icon_skia.height()); + } + width = std::max(width, item_width); } } - return gfx::Size(width, font_list.GetHeight()); + return gfx::Size(width, height); } PrefixSelector* Combobox::GetPrefixSelector() { diff --git a/chromium/ui/views/controls/combobox/combobox.h b/chromium/ui/views/controls/combobox/combobox.h index d6da07f7122..91ad60c9a14 100644 --- a/chromium/ui/views/controls/combobox/combobox.h +++ b/chromium/ui/views/controls/combobox/combobox.h @@ -126,7 +126,7 @@ class VIEWS_EXPORT Combobox : public View, void AdjustBoundsForRTLUI(gfx::Rect* rect) const; // Draws the selected value of the drop down list - void PaintText(gfx::Canvas* canvas); + void PaintIconAndText(gfx::Canvas* canvas); // Show the drop down list void ShowDropDownMenu(ui::MenuSourceType source_type); @@ -207,7 +207,7 @@ class VIEWS_EXPORT Combobox : public View, bool size_to_largest_label_; // The focus ring for this Combobox. - std::unique_ptr<FocusRing> focus_ring_; + FocusRing* focus_ring_ = nullptr; ScopedObserver<ui::ComboboxModel, ui::ComboboxModelObserver> observer_{this}; diff --git a/chromium/ui/views/controls/combobox/combobox_unittest.cc b/chromium/ui/views/controls/combobox/combobox_unittest.cc index 9f0698b0d0e..b081a0b7408 100644 --- a/chromium/ui/views/controls/combobox/combobox_unittest.cc +++ b/chromium/ui/views/controls/combobox/combobox_unittest.cc @@ -30,7 +30,9 @@ #include "ui/views/controls/combobox/combobox_listener.h" #include "ui/views/style/platform_style.h" #include "ui/views/test/combobox_test_api.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" +#include "ui/views/widget/unique_widget_ptr.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_utils.h" @@ -42,36 +44,7 @@ using test::ComboboxTestApi; namespace { -// A wrapper of Combobox to intercept the result of OnKeyPressed() and -// OnKeyReleased() methods. -class TestCombobox : public Combobox { - public: - explicit TestCombobox(ui::ComboboxModel* model) - : Combobox(model), key_handled_(false), key_received_(false) {} - - bool OnKeyPressed(const ui::KeyEvent& e) override { - key_received_ = true; - key_handled_ = Combobox::OnKeyPressed(e); - return key_handled_; - } - - bool OnKeyReleased(const ui::KeyEvent& e) override { - key_received_ = true; - key_handled_ = Combobox::OnKeyReleased(e); - return key_handled_; - } - - bool key_handled() const { return key_handled_; } - bool key_received() const { return key_received_; } - - void clear() { key_received_ = key_handled_ = false; } - - private: - bool key_handled_; - bool key_received_; - - DISALLOW_COPY_AND_ASSIGN(TestCombobox); -}; +using TestCombobox = Combobox; // A concrete class is needed to test the combobox. class TestComboboxModel : public ui::ComboboxModel { @@ -83,14 +56,14 @@ class TestComboboxModel : public ui::ComboboxModel { // ui::ComboboxModel: int GetItemCount() const override { return item_count_; } - base::string16 GetItemAt(int index) override { + base::string16 GetItemAt(int index) const override { if (IsItemSeparatorAt(index)) { NOTREACHED(); return ASCIIToUTF16("SEPARATOR"); } return ASCIIToUTF16(index % 2 == 0 ? "PEANUT BUTTER" : "JELLY"); } - bool IsItemSeparatorAt(int index) override { + bool IsItemSeparatorAt(int index) const override { return separators_.find(index) != separators_.end(); } @@ -147,10 +120,10 @@ class VectorComboboxModel : public ui::ComboboxModel { int GetItemCount() const override { return static_cast<int>(values_->size()); } - base::string16 GetItemAt(int index) override { + base::string16 GetItemAt(int index) const override { return ASCIIToUTF16(values_->at(index)); } - bool IsItemSeparatorAt(int index) override { return false; } + bool IsItemSeparatorAt(int index) const override { return false; } int GetDefaultIndex() const override { return default_index_; } void AddObserver(ui::ComboboxModelObserver* observer) override { observers_.AddObserver(observer); @@ -222,8 +195,7 @@ class ComboboxTest : public ViewsTestBase { ComboboxTest() = default; void TearDown() override { - if (widget_) - widget_->Close(); + widget_.reset(); ViewsTestBase::TearDown(); } @@ -234,26 +206,25 @@ class ComboboxTest : public ViewsTestBase { model_->SetSeparators(*separators); ASSERT_FALSE(combobox_); - combobox_ = new TestCombobox(model_.get()); - test_api_ = std::make_unique<ComboboxTestApi>(combobox_); + auto combobox = std::make_unique<TestCombobox>(model_.get()); + test_api_ = std::make_unique<ComboboxTestApi>(combobox.get()); test_api_->InstallTestMenuRunner(&menu_show_count_); - combobox_->SetID(1); + combobox->SetID(1); - widget_ = new Widget; + widget_ = std::make_unique<Widget>(); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.bounds = gfx::Rect(200, 200, 200, 200); widget_->Init(std::move(params)); - View* container = new View(); - widget_->SetContentsView(container); - container->AddChildView(combobox_); + View* container = widget_->SetContentsView(std::make_unique<View>()); + combobox_ = container->AddChildView(std::move(combobox)); widget_->Show(); combobox_->RequestFocus(); combobox_->SizeToPreferredSize(); - event_generator_ = - std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_)); + event_generator_ = std::make_unique<ui::test::EventGenerator>( + GetRootWindow(widget_.get())); event_generator_->set_target(ui::test::EventGenerator::Target::WINDOW); } @@ -291,7 +262,7 @@ class ComboboxTest : public ViewsTestBase { } // We need widget to populate wrapper class. - Widget* widget_ = nullptr; + UniqueWidgetPtr widget_; // |combobox_| will be allocated InitCombobox() and then owned by |widget_|. TestCombobox* combobox_ = nullptr; @@ -356,23 +327,28 @@ TEST_F(ComboboxTest, KeyTestMac) { } #endif +// Iterate through all the metadata and test each property. +TEST_F(ComboboxTest, MetadataTest) { + InitCombobox(nullptr); + test::TestViewMetadata(combobox_); +} + // Check that if a combobox is disabled before it has a native wrapper, then the // native wrapper inherits the disabled state when it gets created. TEST_F(ComboboxTest, DisabilityTest) { model_ = std::make_unique<TestComboboxModel>(); ASSERT_FALSE(combobox_); - combobox_ = new TestCombobox(model_.get()); - combobox_->SetEnabled(false); + auto combobox = std::make_unique<TestCombobox>(model_.get()); + combobox->SetEnabled(false); - widget_ = new Widget; + widget_ = std::make_unique<Widget>(); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.bounds = gfx::Rect(100, 100, 100, 100); widget_->Init(std::move(params)); - View* container = new View(); - widget_->SetContentsView(container); - container->AddChildView(combobox_); + View* container = widget_->SetContentsView(std::make_unique<View>()); + combobox_ = container->AddChildView(std::move(combobox)); EXPECT_FALSE(combobox_->GetEnabled()); } @@ -559,7 +535,7 @@ TEST_F(ComboboxTest, ListenerHandlesDelete) { // |combobox| will be deleted on change. TestCombobox* combobox = new TestCombobox(&model); - std::unique_ptr<EvilListener> evil_listener(new EvilListener()); + auto evil_listener = std::make_unique<EvilListener>(); combobox->set_listener(evil_listener.get()); ASSERT_NO_FATAL_FAILURE(ComboboxTestApi(combobox).PerformActionAt(2)); EXPECT_TRUE(evil_listener->deleted()); diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox.cc b/chromium/ui/views/controls/editable_combobox/editable_combobox.cc index 5f76ed0db20..b4c14a33505 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox.cc +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox.cc @@ -37,7 +37,6 @@ #include "ui/gfx/range/range.h" #include "ui/gfx/render_text.h" #include "ui/gfx/scoped_canvas.h" -#include "ui/native_theme/native_theme.h" #include "ui/views/animation/flood_fill_ink_drop_ripple.h" #include "ui/views/animation/ink_drop.h" #include "ui/views/animation/ink_drop_host_view.h" @@ -65,8 +64,7 @@ namespace { class Arrow : public Button { public: - Arrow(const SkColor color, ButtonListener* listener) - : Button(listener), color_(color) { + explicit Arrow(ButtonListener* listener) : Button(listener) { // Similar to Combobox's TransparentButton. SetFocusBehavior(FocusBehavior::NEVER); button_controller()->set_notify_action( @@ -93,8 +91,7 @@ class Arrow : public Button { std::unique_ptr<InkDropRipple> CreateInkDropRipple() const override { return std::make_unique<views::FloodFillInkDropRipple>( size(), GetInkDropCenterBasedOnLastEvent(), - GetNativeTheme()->GetSystemColor( - ui::NativeTheme::kColorId_LabelEnabledColor), + style::GetColor(*this, style::CONTEXT_TEXTFIELD, style::STYLE_PRIMARY), ink_drop_visible_opacity()); } @@ -104,7 +101,11 @@ class Arrow : public Button { canvas->ClipRect(GetContentsBounds()); gfx::Rect arrow_bounds = GetLocalBounds(); arrow_bounds.ClampToCenteredSize(ComboboxArrowSize()); - PaintComboboxArrow(color_, arrow_bounds, canvas); + // Make sure the arrow use the same color as the text in the combobox. + PaintComboboxArrow(style::GetColor(*this, style::CONTEXT_TEXTFIELD, + GetEnabled() ? style::STYLE_PRIMARY + : style::STYLE_DISABLED), + arrow_bounds, canvas); } void GetAccessibleNodeData(ui::AXNodeData* node_data) override { @@ -115,8 +116,6 @@ class Arrow : public Button { node_data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kOpen); } - const SkColor color_; - DISALLOW_COPY_AND_ASSIGN(Arrow); }; @@ -176,15 +175,10 @@ class EditableCombobox::EditableComboboxMenuModel gfx::RenderText::kPasswordReplacementChar); } - ////////////////////////////////////////////////////////////////////////////// - // Overridden from ComboboxModelObserver: void OnComboboxModelChanged(ui::ComboboxModel* model) override { UpdateItemsShown(); } - ////////////////////////////////////////////////////////////////////////////// - // Overridden from MenuModel: - int GetItemCount() const override { return items_shown_.size(); } private: @@ -309,8 +303,6 @@ class EditableCombobox::EditableComboboxPreTargetHandler DISALLOW_COPY_AND_ASSIGN(EditableComboboxPreTargetHandler); }; -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, public, non-overridden methods: EditableCombobox::EditableCombobox( std::unique_ptr<ui::ComboboxModel> combobox_model, const bool filter_on_edit, @@ -341,8 +333,7 @@ EditableCombobox::EditableCombobox( textfield_->SetExtraInsets(gfx::Insets( /*top=*/0, /*left=*/0, /*bottom=*/0, /*right=*/kComboboxArrowContainerWidth - kComboboxArrowPaddingWidth)); - arrow_ = new Arrow(textfield_->GetTextColor(), this); - AddChildView(arrow_); + arrow_ = AddChildView(std::make_unique<Arrow>(this)); } SetLayoutManager(std::make_unique<views::FillLayout>()); } @@ -399,9 +390,6 @@ base::string16 EditableCombobox::GetItemForTest(int index) { return menu_model_->GetItemTextAt(index, showing_password_text_); } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, View overrides: - void EditableCombobox::Layout() { View::Layout(); if (arrow_) { @@ -411,11 +399,6 @@ void EditableCombobox::Layout() { } } -void EditableCombobox::OnThemeChanged() { - View::OnThemeChanged(); - textfield_->OnThemeChanged(); -} - void EditableCombobox::GetAccessibleNodeData(ui::AXNodeData* node_data) { node_data->role = ax::mojom::Role::kComboBoxGrouping; @@ -435,9 +418,6 @@ void EditableCombobox::OnVisibleBoundsChanged() { CloseMenu(); } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, TextfieldController overrides: - void EditableCombobox::ContentsChanged(Textfield* sender, const base::string16& new_contents) { HandleNewContent(new_contents); @@ -455,32 +435,25 @@ bool EditableCombobox::HandleKeyEvent(Textfield* sender, return false; } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, View overrides: - void EditableCombobox::OnViewBlurred(View* observed_view) { CloseMenu(); } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, ButtonListener overrides: - void EditableCombobox::ButtonPressed(Button* sender, const ui::Event& event) { textfield_->RequestFocus(); - if (menu_runner_ && menu_runner_->IsRunning()) { + if (menu_runner_ && menu_runner_->IsRunning()) CloseMenu(); - return; - } - ui::MenuSourceType source_type = ui::MENU_SOURCE_MOUSE; - if (event.IsKeyEvent()) - source_type = ui::MENU_SOURCE_KEYBOARD; - else if (event.IsGestureEvent() || event.IsTouchEvent()) - source_type = ui::MENU_SOURCE_TOUCH; - ShowDropDownMenu(source_type); + else + ShowDropDownMenu(ui::GetMenuSourceTypeForEvent(event)); } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, Private methods: +void EditableCombobox::OnLayoutIsAnimatingChanged( + views::AnimatingLayoutManager* source, + bool is_animating) { + dropdown_blocked_for_animation_ = is_animating; + if (dropdown_blocked_for_animation_) + CloseMenu(); +} void EditableCombobox::CloseMenu() { menu_runner_.reset(); @@ -517,9 +490,10 @@ void EditableCombobox::HandleNewContent(const base::string16& new_content) { } void EditableCombobox::ShowDropDownMenu(ui::MenuSourceType source_type) { - constexpr int kMenuBorderWidthLeft = 1; constexpr int kMenuBorderWidthTop = 1; - constexpr int kMenuBorderWidthRight = 1; + + if (dropdown_blocked_for_animation_) + return; if (!menu_model_->GetItemCount()) { CloseMenu(); @@ -540,13 +514,13 @@ void EditableCombobox::ShowDropDownMenu(ui::MenuSourceType source_type) { this, GetWidget()->GetRootView()); gfx::Rect local_bounds = textfield_->GetLocalBounds(); + + // Menu's requested position's width should be the same as local bounds so the + // border of the menu lines up with the border of the combobox. The y + // coordinate however should be shifted to the bottom with the border width + // not to overlap with the combobox border. gfx::Point menu_position(local_bounds.origin()); - // Inset the menu's requested position so the border of the menu lines up - // with the border of the textfield. - menu_position.set_x(menu_position.x() + kMenuBorderWidthLeft); menu_position.set_y(menu_position.y() + kMenuBorderWidthTop); - local_bounds.set_width(local_bounds.width() - - (kMenuBorderWidthLeft + kMenuBorderWidthRight)); View::ConvertPointToScreen(this, &menu_position); gfx::Rect bounds(menu_position, local_bounds.size()); diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox.h b/chromium/ui/views/controls/editable_combobox/editable_combobox.h index c0f6b26fad8..e4a0d3c3698 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox.h +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox.h @@ -14,6 +14,7 @@ #include "ui/base/ui_base_types.h" #include "ui/views/controls/button/button.h" #include "ui/views/controls/textfield/textfield_controller.h" +#include "ui/views/layout/animating_layout_manager.h" #include "ui/views/style/typography.h" #include "ui/views/view.h" #include "ui/views/view_observer.h" @@ -37,10 +38,12 @@ class MenuRunner; class Textfield; // Textfield that also shows a drop-down list with suggestions. -class VIEWS_EXPORT EditableCombobox : public View, - public TextfieldController, - public ViewObserver, - public ButtonListener { +class VIEWS_EXPORT EditableCombobox + : public View, + public TextfieldController, + public ViewObserver, + public ButtonListener, + public views::AnimatingLayoutManager::Observer { public: METADATA_HEADER(EditableCombobox); @@ -121,7 +124,6 @@ class VIEWS_EXPORT EditableCombobox : public View, // Overridden from View: void Layout() override; - void OnThemeChanged() override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; void RequestFocus() override; bool GetNeedsNotificationWhenVisibleBoundsChange() const override; @@ -139,6 +141,10 @@ class VIEWS_EXPORT EditableCombobox : public View, // Overridden from ButtonListener: void ButtonPressed(Button* sender, const ui::Event& event) override; + // Overridden from views::AnimatingLayoutManager::Observer: + void OnLayoutIsAnimatingChanged(views::AnimatingLayoutManager* source, + bool is_animating) override; + Textfield* textfield_; Button* arrow_ = nullptr; std::unique_ptr<ui::ComboboxModel> combobox_model_; @@ -171,6 +177,8 @@ class VIEWS_EXPORT EditableCombobox : public View, // Type::kPassword. bool showing_password_text_; + bool dropdown_blocked_for_animation_ = false; + ScopedObserver<View, ViewObserver> observer_{this}; DISALLOW_COPY_AND_ASSIGN(EditableCombobox); diff --git a/chromium/ui/views/controls/focus_ring.cc b/chromium/ui/views/controls/focus_ring.cc index 443c199003c..0ee9512f5c3 100644 --- a/chromium/ui/views/controls/focus_ring.cc +++ b/chromium/ui/views/controls/focus_ring.cc @@ -8,6 +8,8 @@ #include <utility> #include "base/memory/ptr_util.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" #include "ui/gfx/canvas.h" #include "ui/views/controls/focusable_border.h" #include "ui/views/controls/highlight_path_generator.h" @@ -53,15 +55,15 @@ SkPath GetHighlightPathInternal(const View* view) { } // namespace // static -std::unique_ptr<FocusRing> FocusRing::Install(View* parent) { +FocusRing* FocusRing::Install(View* parent) { auto ring = base::WrapUnique<FocusRing>(new FocusRing()); - ring->set_owned_by_client(); - parent->AddChildView(ring.get()); ring->InvalidateLayout(); ring->SchedulePaint(); - return ring; + return parent->AddChildView(std::move(ring)); } +FocusRing::~FocusRing() = default; + void FocusRing::SetPathGenerator( std::unique_ptr<HighlightPathGenerator> generator) { path_generator_ = std::move(generator); @@ -102,14 +104,14 @@ void FocusRing::ViewHierarchyChanged( if (details.is_add) { // Need to start observing the parent. - details.parent->AddObserver(this); - } else { + view_observer_.Add(details.parent); + RefreshLayer(); + } else if (view_observer_.IsObserving(details.parent)) { // This view is being removed from its parent. It needs to remove itself - // from its parent's observer list. Otherwise, since its |parent_| will - // become a nullptr, it won't be able to do so in its destructor. - details.parent->RemoveObserver(this); + // from its parent's observer list in the case where the FocusView is + // removed from its parent but not deleted. + view_observer_.Remove(details.parent); } - RefreshLayer(); } void FocusRing::OnPaint(gfx::Canvas* canvas) { @@ -155,6 +157,12 @@ void FocusRing::OnPaint(gfx::Canvas* canvas) { } } +void FocusRing::GetAccessibleNodeData(ui::AXNodeData* node_data) { + // Mark the focus ring in the accessibility tree as invisible so that it will + // not be accessed by assistive technologies. + node_data->AddState(ax::mojom::State::kInvisible); +} + void FocusRing::OnViewFocused(View* view) { RefreshLayer(); } @@ -168,11 +176,6 @@ FocusRing::FocusRing() { set_can_process_events_within_subtree(false); } -FocusRing::~FocusRing() { - if (parent()) - parent()->RemoveObserver(this); -} - void FocusRing::RefreshLayer() { // TODO(pbos): This always keeps the layer alive if |has_focus_predicate_| is // set. This is done because we're not notified when the predicate might diff --git a/chromium/ui/views/controls/focus_ring.h b/chromium/ui/views/controls/focus_ring.h index 9ac2f4ebeeb..c3e42b6a4da 100644 --- a/chromium/ui/views/controls/focus_ring.h +++ b/chromium/ui/views/controls/focus_ring.h @@ -19,38 +19,25 @@ namespace views { class HighlightPathGenerator; // FocusRing is a View that is designed to act as an indicator of focus for its -// parent. It is a stand-alone view that paints to a layer which extends beyond -// the bounds of its parent view. -// -// Using FocusRing looks something like this: -// -// class MyView : public View { -// ... -// private: -// std::unique_ptr<FocusRing> focus_ring_; -// }; -// -// MyView::MyView() { -// focus_ring_ = FocusRing::Install(this); -// ... -// } -// +// parent. It is a view that paints to a layer which extends beyond the bounds +// of its parent view. // If MyView should show a rounded rectangular focus ring when it has focus and // hide the ring when it loses focus, no other configuration is necessary. In // other cases, it might be necessary to use the Set*() functions on FocusRing; // these take care of repainting it when the state changes. +// TODO(tluk): FocusRing should not be a view but instead a new concept which +// only participates in view painting ( https://crbug.com/840796 ). class VIEWS_EXPORT FocusRing : public View, public ViewObserver { public: METADATA_HEADER(FocusRing); using ViewPredicate = std::function<bool(View* view)>; - ~FocusRing() override; - // Create a FocusRing and adds it to |parent|. The returned focus ring is - // owned by the client (the code calling FocusRing::Install), *not* by - // |parent|. - static std::unique_ptr<FocusRing> Install(View* parent); + // owned by the |parent|. + static FocusRing* Install(View* parent); + + ~FocusRing() override; // Sets the HighlightPathGenerator to draw this FocusRing around. // Note: This method should only be used if the focus ring needs to differ @@ -77,6 +64,7 @@ class VIEWS_EXPORT FocusRing : public View, public ViewObserver { void ViewHierarchyChanged( const ViewHierarchyChangedDetails& details) override; void OnPaint(gfx::Canvas* canvas) override; + void GetAccessibleNodeData(ui::AXNodeData* node_data) override; // ViewObserver: void OnViewFocused(View* view) override; @@ -108,6 +96,8 @@ class VIEWS_EXPORT FocusRing : public View, public ViewObserver { // The predicate used to determine whether the parent has focus. base::Optional<ViewPredicate> has_focus_predicate_; + ScopedObserver<View, ViewObserver> view_observer_{this}; + DISALLOW_COPY_AND_ASSIGN(FocusRing); }; diff --git a/chromium/ui/views/controls/label.cc b/chromium/ui/views/controls/label.cc index ad6af149e28..febce15ff86 100644 --- a/chromium/ui/views/controls/label.cc +++ b/chromium/ui/views/controls/label.cc @@ -108,6 +108,19 @@ int Label::GetTextContext() const { return text_context_; } +int Label::GetTextStyle() const { + return text_style_; +} + +void Label::SetTextStyle(int style) { + if (style == text_style_) + return; + + text_style_ = style; + UpdateColorsFromTheme(); + OnPropertyChanged(&text_style_, kPropertyEffectsPreferredSizeChanged); +} + bool Label::GetAutoColorReadabilityEnabled() const { return auto_color_readability_enabled_; } @@ -291,6 +304,14 @@ void Label::SetAllowCharacterBreak(bool allow_character_break) { kPropertyEffectsLayout); } +size_t Label::GetTextIndexOfLine(size_t line) const { + return full_text_->GetTextIndexOfLine(line); +} + +void Label::SetTruncateLength(size_t truncate_length) { + return full_text_->set_truncate_length(truncate_length); +} + gfx::ElideBehavior Label::GetElideBehavior() const { return elide_behavior_; } @@ -950,7 +971,6 @@ void Label::Init(const base::string16& text, full_text_->SetCursorEnabled(false); full_text_->SetWordWrapBehavior(gfx::TRUNCATE_LONG_WORDS); - UpdateColorsFromTheme(); SetText(text); // Only selectable labels will get requests to show the context menu, due to @@ -1102,8 +1122,9 @@ void Label::BuildContextMenuContents() { BEGIN_METADATA(Label) METADATA_PARENT_CLASS(View) -ADD_PROPERTY_METADATA(Label, bool, AutoColorReadabilityEnabled) ADD_PROPERTY_METADATA(Label, base::string16, Text) +ADD_PROPERTY_METADATA(Label, int, TextStyle) +ADD_PROPERTY_METADATA(Label, bool, AutoColorReadabilityEnabled) ADD_PROPERTY_METADATA(Label, SkColor, EnabledColor) ADD_PROPERTY_METADATA(Label, gfx::ElideBehavior, ElideBehavior) ADD_PROPERTY_METADATA(Label, SkColor, BackgroundColor) diff --git a/chromium/ui/views/controls/label.h b/chromium/ui/views/controls/label.h index dc690b22dc2..5a45ef3cbf0 100644 --- a/chromium/ui/views/controls/label.h +++ b/chromium/ui/views/controls/label.h @@ -91,6 +91,11 @@ class VIEWS_EXPORT Label : public View, // a value from views::style::TextContext or an enum that extends it. int GetTextContext() const; + // The style of the label. This is a value from views::style::TextStyle or an + // enum that extends it. + int GetTextStyle() const; + void SetTextStyle(int style); + // Enables or disables auto-color-readability (enabled by default). If this // is enabled, then calls to set any foreground or background color will // trigger an automatic mapper that uses color_utils::BlendForMinContrast() @@ -177,6 +182,13 @@ class VIEWS_EXPORT Label : public View, bool GetAllowCharacterBreak() const; void SetAllowCharacterBreak(bool allow_character_break); + // For the provided line index, gets the corresponding rendered line and + // returns the text position of the first character of that line. + size_t GetTextIndexOfLine(size_t line) const; + + // Set the truncate length of the rendered text. + void SetTruncateLength(size_t truncate_length); + // Gets/Sets the eliding or fading behavior, applied as necessary. The default // is to elide at the end. Eliding is not well-supported for multi-line // labels. @@ -377,7 +389,7 @@ class VIEWS_EXPORT Label : public View, void BuildContextMenuContents(); const int text_context_; - const int text_style_; + int text_style_; // An un-elided and single-line RenderText object used for preferred sizing. std::unique_ptr<gfx::RenderText> full_text_; diff --git a/chromium/ui/views/controls/label_unittest.cc b/chromium/ui/views/controls/label_unittest.cc index 763a169b27e..33d969862cc 100644 --- a/chromium/ui/views/controls/label_unittest.cc +++ b/chromium/ui/views/controls/label_unittest.cc @@ -291,6 +291,11 @@ TEST_F(LabelTest, TextProperty) { EXPECT_EQ(test_text, label()->GetText()); } +TEST_F(LabelTest, TextStyleProperty) { + label()->SetTextStyle(views::style::STYLE_DISABLED); + EXPECT_EQ(views::style::STYLE_DISABLED, label()->GetTextStyle()); +} + TEST_F(LabelTest, ColorProperty) { SkColor color = SkColorSetARGB(20, 40, 10, 5); label()->SetAutoColorReadabilityEnabled(false); 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()) diff --git a/chromium/ui/views/controls/prefix_selector.cc b/chromium/ui/views/controls/prefix_selector.cc index 704855a3897..aa0d2fe8423 100644 --- a/chromium/ui/views/controls/prefix_selector.cc +++ b/chromium/ui/views/controls/prefix_selector.cc @@ -171,6 +171,15 @@ bool PrefixSelector::SetCompositionFromExistingText( } #endif +#if defined(OS_CHROMEOS) +bool PrefixSelector::SetAutocorrectRange(const base::string16& autocorrect_text, + const gfx::Range& range) { + // TODO(crbug.com/1091088) Implement setAutocorrectRange. + NOTIMPLEMENTED_LOG_ONCE(); + return false; +} +#endif + #if defined(OS_WIN) void PrefixSelector::SetActiveCompositionForAccessibility( const gfx::Range& range, diff --git a/chromium/ui/views/controls/prefix_selector.h b/chromium/ui/views/controls/prefix_selector.h index d61f6cf2ed8..12dffcc111b 100644 --- a/chromium/ui/views/controls/prefix_selector.h +++ b/chromium/ui/views/controls/prefix_selector.h @@ -82,6 +82,11 @@ class VIEWS_EXPORT PrefixSelector : public ui::TextInputClient { const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) override; #endif +#if defined(OS_CHROMEOS) + bool SetAutocorrectRange(const base::string16& autocorrect_text, + const gfx::Range& range) override; +#endif + #if defined(OS_WIN) void GetActiveTextInputControlLayoutBounds( base::Optional<gfx::Rect>* control_bounds, diff --git a/chromium/ui/views/controls/scroll_view.h b/chromium/ui/views/controls/scroll_view.h index 6df7bca1027..54b1e11e5ce 100644 --- a/chromium/ui/views/controls/scroll_view.h +++ b/chromium/ui/views/controls/scroll_view.h @@ -278,7 +278,7 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { const bool scroll_with_layers_enabled_; // The focus ring for this ScrollView. - std::unique_ptr<FocusRing> focus_ring_; + FocusRing* focus_ring_ = nullptr; DISALLOW_COPY_AND_ASSIGN(ScrollView); }; diff --git a/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.h b/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.h index 68e804b5d06..4f102e97a09 100644 --- a/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.h +++ b/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.h @@ -11,7 +11,6 @@ #import "components/remote_cocoa/app_shim/views_scrollbar_bridge.h" #include "ui/compositor/layer_animation_observer.h" #include "ui/gfx/animation/slide_animation.h" -#include "ui/gfx/mac/cocoa_scrollbar_painter.h" #include "ui/views/controls/scrollbar/scroll_bar.h" #include "ui/views/views_export.h" @@ -63,7 +62,7 @@ class VIEWS_EXPORT CocoaScrollBar : public ScrollBar, bool IsScrollbarFullyHidden() const; // Get the parameters for painting. - gfx::CocoaScrollbarPainter::Params GetPainterParams() const; + ui::NativeTheme::ExtraParams GetPainterParams() const; protected: // ScrollBar: diff --git a/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.mm b/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.mm index 1b4d5a5d129..05960d9ad3e 100644 --- a/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.mm +++ b/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.mm @@ -15,8 +15,6 @@ #include "ui/gfx/canvas.h" #include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h" -using gfx::CocoaScrollbarPainter; - namespace views { namespace { @@ -100,9 +98,14 @@ gfx::Size CocoaScrollBarThumb::CalculatePreferredSize() const { void CocoaScrollBarThumb::OnPaint(gfx::Canvas* canvas) { auto params = cocoa_scroll_bar_->GetPainterParams(); // Set the hover state based only on the thumb. - params.hovered = IsStateHovered() || IsStatePressed(); - CocoaScrollbarPainter::PaintThumb( - canvas->sk_canvas(), gfx::RectToSkIRect(GetLocalBounds()), params); + params.scrollbar_extra.is_hovering = IsStateHovered() || IsStatePressed(); + ui::NativeTheme::Part thumb_part = + params.scrollbar_extra.orientation == + ui::NativeTheme::ScrollbarOrientation::kHorizontal + ? ui::NativeTheme::kScrollbarHorizontalThumb + : ui::NativeTheme::kScrollbarVerticalThumb; + GetNativeTheme()->Paint(canvas->sk_canvas(), thumb_part, + ui::NativeTheme::kNormal, GetLocalBounds(), params); } bool CocoaScrollBarThumb::OnMousePressed(const ui::MouseEvent& event) { @@ -208,9 +211,14 @@ void CocoaScrollBar::OnPaint(gfx::Canvas* canvas) { auto params = GetPainterParams(); // Transparency of the track is handled by the View opacity, so always draw // using the non-overlay path. - params.overlay = false; - CocoaScrollbarPainter::PaintTrack( - canvas->sk_canvas(), gfx::RectToSkIRect(GetLocalBounds()), params); + params.scrollbar_extra.is_overlay = false; + ui::NativeTheme::Part track_part = + params.scrollbar_extra.orientation == + ui::NativeTheme::ScrollbarOrientation::kHorizontal + ? ui::NativeTheme::kScrollbarHorizontalTrack + : ui::NativeTheme::kScrollbarVerticalTrack; + GetNativeTheme()->Paint(canvas->sk_canvas(), track_part, + ui::NativeTheme::kNormal, GetLocalBounds(), params); } bool CocoaScrollBar::CanProcessEventsWithinSubtree() const { @@ -400,16 +408,20 @@ bool CocoaScrollBar::IsScrollbarFullyHidden() const { return layer()->opacity() == 0.0f; } -CocoaScrollbarPainter::Params CocoaScrollBar::GetPainterParams() const { - CocoaScrollbarPainter::Params params; - if (IsHorizontal()) - params.orientation = CocoaScrollbarPainter::Orientation::kHorizontal; - else if (base::i18n::IsRTL()) - params.orientation = CocoaScrollbarPainter::Orientation::kVerticalOnLeft; - else - params.orientation = CocoaScrollbarPainter::Orientation::kVerticalOnRight; - params.overlay = GetScrollerStyle() == NSScrollerStyleOverlay; - params.dark_mode = GetNativeTheme()->ShouldUseDarkColors(); +ui::NativeTheme::ExtraParams CocoaScrollBar::GetPainterParams() const { + ui::NativeTheme::ExtraParams params; + if (IsHorizontal()) { + params.scrollbar_extra.orientation = + ui::NativeTheme::ScrollbarOrientation::kHorizontal; + } else if (base::i18n::IsRTL()) { + params.scrollbar_extra.orientation = + ui::NativeTheme::ScrollbarOrientation::kVerticalOnLeft; + } else { + params.scrollbar_extra.orientation = + ui::NativeTheme::ScrollbarOrientation::kVerticalOnRight; + } + params.scrollbar_extra.is_overlay = + GetScrollerStyle() == NSScrollerStyleOverlay; return params; } diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc index 4fdef37e512..50ecb7c042b 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc @@ -510,6 +510,11 @@ TabbedPane::TabbedPane(TabbedPane::Orientation orientation, views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero, views::MaximumFlexSizeRule::kUnbounded)); contents_->SetLayoutManager(std::make_unique<views::FillLayout>()); + + // Support navigating tabs by Ctrl+Tab and Ctrl+Shift+Tab. + AddAccelerator( + ui::Accelerator(ui::VKEY_TAB, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN)); + AddAccelerator(ui::Accelerator(ui::VKEY_TAB, ui::EF_CONTROL_DOWN)); } TabbedPane::~TabbedPane() = default; @@ -600,16 +605,6 @@ bool TabbedPane::MoveSelectionBy(int delta) { return true; } -void TabbedPane::ViewHierarchyChanged( - const ViewHierarchyChangedDetails& details) { - if (details.is_add) { - // Support navigating tabs by Ctrl+Tab and Ctrl+Shift+Tab. - AddAccelerator( - ui::Accelerator(ui::VKEY_TAB, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN)); - AddAccelerator(ui::Accelerator(ui::VKEY_TAB, ui::EF_CONTROL_DOWN)); - } -} - bool TabbedPane::AcceleratorPressed(const ui::Accelerator& accelerator) { // Handle Ctrl+Tab and Ctrl+Shift+Tab navigation of pages. DCHECK(accelerator.key_code() == ui::VKEY_TAB && accelerator.IsCtrlDown()); diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.h b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.h index 4c42c238ecb..97553af580f 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.h +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.h @@ -123,8 +123,6 @@ class VIEWS_EXPORT TabbedPane : public View { bool MoveSelectionBy(int delta); // Overridden from View: - void ViewHierarchyChanged( - const ViewHierarchyChangedDetails& details) override; bool AcceleratorPressed(const ui::Accelerator& accelerator) override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc b/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc index 808afa4d777..6bb23566546 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc @@ -121,7 +121,7 @@ class TabbedPaneWithWidgetTest : public ViewsTestBase { params.bounds = gfx::Rect(0, 0, 650, 650); widget_->Init(std::move(params)); tabbed_pane_ = tabbed_pane.get(); - widget_->SetContentsView(tabbed_pane.release()); + widget_->SetContentsView(std::move(tabbed_pane)); } void TearDown() override { diff --git a/chromium/ui/views/controls/table/table_view.cc b/chromium/ui/views/controls/table/table_view.cc index 94eb7a0527c..fa791c4700c 100644 --- a/chromium/ui/views/controls/table/table_view.cc +++ b/chromium/ui/views/controls/table/table_view.cc @@ -183,6 +183,8 @@ TableView::TableView(ui::TableModel* model, SetModel(model); if (model_) UpdateVirtualAccessibilityChildren(); + + focus_ring_ = FocusRing::Install(this); } TableView::~TableView() { diff --git a/chromium/ui/views/controls/table/table_view.h b/chromium/ui/views/controls/table/table_view.h index 1a640fdf53e..30f1b107602 100644 --- a/chromium/ui/views/controls/table/table_view.h +++ b/chromium/ui/views/controls/table/table_view.h @@ -13,7 +13,6 @@ #include "ui/base/models/table_model.h" #include "ui/base/models/table_model_observer.h" #include "ui/gfx/font_list.h" -#include "ui/views/controls/focus_ring.h" #include "ui/views/view.h" #include "ui/views/views_export.h" @@ -396,7 +395,7 @@ class VIEWS_EXPORT TableView : public views::View, int active_visible_column_index_ = -1; // Used to draw a focus indicator around the active cell. - std::unique_ptr<FocusRing> focus_ring_ = FocusRing::Install(this); + FocusRing* focus_ring_ = nullptr; // The header. This is only created if more than one column is specified or // the first column has a non-empty title. diff --git a/chromium/ui/views/controls/textfield/textfield.cc b/chromium/ui/views/controls/textfield/textfield.cc index d8e8fd0ace0..d0c5e12a9d1 100644 --- a/chromium/ui/views/controls/textfield/textfield.cc +++ b/chromium/ui/views/controls/textfield/textfield.cc @@ -65,8 +65,8 @@ #endif #if defined(OS_LINUX) && !defined(OS_CHROMEOS) -#include "ui/base/ime/linux/text_edit_command_auralinux.h" -#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" +#include "ui/base/ime/linux/text_edit_command_auralinux.h" // nogncheck +#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" // nogncheck #endif #if defined(USE_X11) @@ -330,6 +330,12 @@ Textfield::Textfield() AddAccelerator(ui::Accelerator(ui::VKEY_C, ui::EF_CONTROL_DOWN)); AddAccelerator(ui::Accelerator(ui::VKEY_V, ui::EF_CONTROL_DOWN)); #endif + + // Sometimes there are additional ignored views, such as the View representing + // the cursor, inside the text field. These should always be ignored by + // accessibility since a plain text field should always be a leaf node in the + // accessibility trees of all the platforms we support. + GetViewAccessibility().OverrideIsLeaf(true); } Textfield::~Textfield() { @@ -677,8 +683,10 @@ gfx::Size Textfield::GetMinimumSize() const { void Textfield::SetBorder(std::unique_ptr<Border> b) { use_focus_ring_ = false; - if (focus_ring_) - focus_ring_.reset(); + if (focus_ring_) { + RemoveChildViewT(focus_ring_); + focus_ring_ = nullptr; + } View::SetBorder(std::move(b)); } @@ -1025,6 +1033,7 @@ void Textfield::OnDragDone() { void Textfield::GetAccessibleNodeData(ui::AXNodeData* node_data) { node_data->role = ax::mojom::Role::kTextField; + if (label_ax_id_) { node_data->AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds, {label_ax_id_}); @@ -1427,7 +1436,7 @@ bool Textfield::GetAcceleratorForCommandId(int command_id, void Textfield::ExecuteCommand(int command_id, int event_flags) { if (text_services_context_menu_ && text_services_context_menu_->SupportsCommand(command_id)) { - text_services_context_menu_->ExecuteCommand(command_id); + text_services_context_menu_->ExecuteCommand(command_id, event_flags); return; } @@ -1786,8 +1795,11 @@ ukm::SourceId Textfield::GetClientSourceForMetrics() const { } bool Textfield::ShouldDoLearning() { - // TODO(https://crbug.com/311180): Implement this method. - NOTIMPLEMENTED_LOG_ONCE(); + if (should_do_learning_.has_value()) + return should_do_learning_.value(); + + NOTIMPLEMENTED_LOG_ONCE() << "A Textfield does not support ShouldDoLearning"; + DVLOG(1) << "This Textfield instance does not support ShouldDoLearning"; return false; } @@ -1806,6 +1818,14 @@ bool Textfield::SetCompositionFromExistingText( } #endif +#if defined(OS_CHROMEOS) +bool Textfield::SetAutocorrectRange(const base::string16& autocorrect_text, + const gfx::Range& range) { + // TODO(crbug.com/1091088) Implement autocorrect range textfield handling. + return false; +} +#endif + #if defined(OS_WIN) void Textfield::GetActiveTextInputControlLayoutBounds( base::Optional<gfx::Rect>* control_bounds, diff --git a/chromium/ui/views/controls/textfield/textfield.h b/chromium/ui/views/controls/textfield/textfield.h index 825c45513cf..775ce4f9683 100644 --- a/chromium/ui/views/controls/textfield/textfield.h +++ b/chromium/ui/views/controls/textfield/textfield.h @@ -377,12 +377,20 @@ class VIEWS_EXPORT Textfield : public View, ukm::SourceId GetClientSourceForMetrics() const override; bool ShouldDoLearning() override; + // Set whether the text should be used to improve typing suggestions. + void SetShouldDoLearning(bool value) { should_do_learning_ = value; } + #if defined(OS_WIN) || defined(OS_CHROMEOS) bool SetCompositionFromExistingText( const gfx::Range& range, const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) override; #endif +#if defined(OS_CHROMEOS) + bool SetAutocorrectRange(const base::string16& autocorrect_text, + const gfx::Range& range) override; +#endif + #if defined(OS_WIN) void GetActiveTextInputControlLayoutBounds( base::Optional<gfx::Rect>* control_bounds, @@ -651,6 +659,9 @@ class VIEWS_EXPORT Textfield : public View, // True if this textfield should use a focus ring to indicate focus. bool use_focus_ring_ = true; + // Whether the text should be used to improve typing suggestions. + base::Optional<bool> should_do_learning_; + // Context menu related members. std::unique_ptr<ui::SimpleMenuModel> context_menu_contents_; std::unique_ptr<ViewsTextServicesContextMenu> text_services_context_menu_; @@ -669,7 +680,7 @@ class VIEWS_EXPORT Textfield : public View, ui::TextInputClient::FOCUS_REASON_NONE; // The focus ring for this TextField. - std::unique_ptr<FocusRing> focus_ring_; + FocusRing* focus_ring_ = nullptr; // The password char reveal index, for testing only. int password_char_reveal_index_ = -1; diff --git a/chromium/ui/views/controls/textfield/textfield_test_api.cc b/chromium/ui/views/controls/textfield/textfield_test_api.cc index 099f22e9bcf..40176fa3a73 100644 --- a/chromium/ui/views/controls/textfield/textfield_test_api.cc +++ b/chromium/ui/views/controls/textfield/textfield_test_api.cc @@ -5,7 +5,6 @@ #include "ui/views/controls/textfield/textfield_test_api.h" #include "ui/gfx/geometry/rect.h" -#include "ui/views/controls/views_text_services_context_menu.h" namespace views { @@ -34,12 +33,6 @@ void TextfieldTestApi::SetCursorViewRect(gfx::Rect bounds) { textfield_->cursor_view_->SetBoundsRect(bounds); } -bool TextfieldTestApi::IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection direction) const { - return ViewsTextServicesContextMenu::IsTextDirectionCheckedForTesting( - textfield_->text_services_context_menu_.get(), direction); -} - bool TextfieldTestApi::ShouldShowCursor() const { return textfield_->ShouldShowCursor(); } diff --git a/chromium/ui/views/controls/textfield/textfield_test_api.h b/chromium/ui/views/controls/textfield/textfield_test_api.h index 3f346da6059..78436bcb5a4 100644 --- a/chromium/ui/views/controls/textfield/textfield_test_api.h +++ b/chromium/ui/views/controls/textfield/textfield_test_api.h @@ -5,8 +5,6 @@ #ifndef UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_TEST_API_H_ #define UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_TEST_API_H_ -#include "base/i18n/rtl.h" -#include "base/macros.h" #include "ui/views/controls/textfield/textfield.h" namespace views { @@ -15,6 +13,9 @@ namespace views { class TextfieldTestApi { public: explicit TextfieldTestApi(Textfield* textfield); + TextfieldTestApi(const TextfieldTestApi&) = delete; + TextfieldTestApi& operator=(const TextfieldTestApi&) = delete; + ~TextfieldTestApi() = default; void UpdateContextMenu(); @@ -53,15 +54,10 @@ class TextfieldTestApi { return textfield_->cursor_view_->GetVisible(); } - bool IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection direction) const; - bool ShouldShowCursor() const; private: Textfield* textfield_; - - DISALLOW_COPY_AND_ASSIGN(TextfieldTestApi); }; } // namespace views diff --git a/chromium/ui/views/controls/textfield/textfield_unittest.cc b/chromium/ui/views/controls/textfield/textfield_unittest.cc index 456cecaba14..5a25faebf58 100644 --- a/chromium/ui/views/controls/textfield/textfield_unittest.cc +++ b/chromium/ui/views/controls/textfield/textfield_unittest.cc @@ -64,7 +64,7 @@ #endif #if defined(OS_LINUX) && !defined(OS_CHROMEOS) -#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" +#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" // nogncheck #endif #if defined(OS_CHROMEOS) @@ -74,6 +74,7 @@ #if defined(OS_MACOSX) #include "ui/base/cocoa/secure_password_input.h" +#include "ui/base/cocoa/text_services_context_menu.h" #endif using base::ASCIIToUTF16; @@ -459,8 +460,7 @@ class TextfieldTest : public ViewsTestBase, public TextfieldController { widget_->Init(std::move(params)); input_method_->SetDelegate( test::WidgetTest::GetInputMethodDelegateForWidget(widget_)); - View* container = new View(); - widget_->SetContentsView(container); + View* container = widget_->SetContentsView(std::make_unique<View>()); container->AddChildView(textfield_); textfield_->SetBoundsRect(params.bounds); textfield_->SetID(1); @@ -1395,6 +1395,17 @@ TEST_F(TextfieldTest, TextInputType_InsertionTest) { textfield_->GetText()); } +TEST_F(TextfieldTest, ShouldDoLearning) { + InitTextfield(); + + // Defaults to false. + EXPECT_EQ(false, textfield_->ShouldDoLearning()); + + // The value can be set. + textfield_->SetShouldDoLearning(true); + EXPECT_EQ(true, textfield_->ShouldDoLearning()); +} + TEST_F(TextfieldTest, TextInputType) { InitTextfield(); @@ -3478,13 +3489,12 @@ TEST_F(TextfieldTest, TextfieldBoundsChangeTest) { TEST_F(TextfieldTest, TextfieldInitialization) { TestTextfield* new_textfield = new TestTextfield(); new_textfield->set_controller(this); - View* container = new View(); Widget* widget(new Widget()); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.bounds = gfx::Rect(100, 100, 100, 100); widget->Init(std::move(params)); - widget->SetContentsView(container); + View* container = widget->SetContentsView(std::make_unique<View>()); container->AddChildView(new_textfield); new_textfield->SetBoundsRect(params.bounds); @@ -3624,56 +3634,23 @@ TEST_F(TextfieldTest, TextServicesContextMenuTextDirectionTest) { base::i18n::TextDirection::LEFT_TO_RIGHT); test_api_->UpdateContextMenu(); - EXPECT_FALSE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::UNKNOWN_DIRECTION)); - EXPECT_TRUE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::LEFT_TO_RIGHT)); - EXPECT_FALSE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::RIGHT_TO_LEFT)); + EXPECT_FALSE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionDefault)); + EXPECT_TRUE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionLtr)); + EXPECT_FALSE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionRtl)); textfield_->ChangeTextDirectionAndLayoutAlignment( base::i18n::TextDirection::RIGHT_TO_LEFT); test_api_->UpdateContextMenu(); - EXPECT_FALSE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::UNKNOWN_DIRECTION)); - EXPECT_FALSE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::LEFT_TO_RIGHT)); - EXPECT_TRUE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::RIGHT_TO_LEFT)); -} - -// Tests to see if the look up item is updated when the textfield's selected -// text has changed. -TEST_F(TextfieldTest, LookUpItemUpdate) { - InitTextfield(); - EXPECT_TRUE(textfield_->context_menu_controller()); - - const base::string16 kTextOne = ASCIIToUTF16("crake"); - textfield_->SetText(kTextOne); - textfield_->SelectAll(false); - - ui::MenuModel* context_menu = GetContextMenuModel(); - EXPECT_TRUE(context_menu); - EXPECT_GT(context_menu->GetItemCount(), 0); - EXPECT_EQ(context_menu->GetLabelAt(0), - l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, kTextOne)); - -#if !defined(OS_MACOSX) - // Mac context menus don't behave this way: it's not possible to update the - // text while the menu is still "open", but also the selection can't change - // while the menu is open (because the user can't interact with the rest of - // the app). - const base::string16 kTextTwo = ASCIIToUTF16("rail"); - textfield_->SetText(kTextTwo); - textfield_->SelectAll(false); - - context_menu = GetContextMenuModel(); - EXPECT_TRUE(context_menu); - EXPECT_GT(context_menu->GetItemCount(), 0); - EXPECT_EQ(context_menu->GetLabelAt(0), - l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, kTextTwo)); -#endif + EXPECT_FALSE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionDefault)); + EXPECT_FALSE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionLtr)); + EXPECT_TRUE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionRtl)); } // Tests to see if the look up item is hidden for password fields. diff --git a/chromium/ui/views/controls/tree/tree_view.cc b/chromium/ui/views/controls/tree/tree_view.cc index bd84a3f38d7..69333ebe84c 100644 --- a/chromium/ui/views/controls/tree/tree_view.cc +++ b/chromium/ui/views/controls/tree/tree_view.cc @@ -139,6 +139,7 @@ void TreeView::SetModel(TreeModel* model) { model_ = model; selected_node_ = nullptr; + active_node_ = nullptr; icons_.clear(); if (model_) { model_->AddObserver(this); @@ -154,18 +155,9 @@ void TreeView::SetModel(TreeModel* model) { root_.set_is_expanded(true); if (root_shown_) - selected_node_ = &root_; + SetSelectedNode(root_.model_node()); else if (!root_.children().empty()) - selected_node_ = root_.children().front().get(); - - if (selected_node_) { - AXVirtualView* ax_selected_view = selected_node_->accessibility_view(); - if (ax_selected_view) { - GetViewAccessibility().OverrideFocus(ax_selected_view); - ax_selected_view->NotifyAccessibilityEvent( - ax::mojom::Event::kSelection); - } - } + SetSelectedNode(root_.children().front().get()->model_node()); } DrawnNodesChanged(); @@ -252,57 +244,21 @@ TreeModelNode* TreeView::GetEditingNode() { } void TreeView::SetSelectedNode(TreeModelNode* model_node) { - if (editing_ || model_node != selected_node_) - CancelEdit(); - if (model_node && model_->GetParent(model_node)) - Expand(model_->GetParent(model_node)); - if (model_node && model_node == root_.model_node() && !root_shown_) - return; // Ignore requests to select the root when not shown. - InternalNode* node = - model_node ? GetInternalNodeForModelNode(model_node, CREATE_IF_NOT_LOADED) - : nullptr; - bool was_empty_selection = (selected_node_ == nullptr); - bool changed = (selected_node_ != node); - if (changed) { - SchedulePaintForNode(selected_node_); - selected_node_ = node; - if (selected_node_ == &root_ && !root_shown_) - selected_node_ = nullptr; - if (selected_node_ && selected_node_ != &root_) - Expand(model_->GetParent(selected_node_->model_node())); - SchedulePaintForNode(selected_node_); - } - - if (selected_node_) { - // GetForegroundBoundsForNode() returns RTL-flipped coordinates for paint. - // Un-flip before passing to ScrollRectToVisible(), which uses layout - // coordinates. - ScrollRectToVisible( - GetMirroredRect(GetForegroundBoundsForNode(selected_node_))); - } - - // Notify controller if the old selection was empty to handle the case of - // remove explicitly resetting selected_node_ before invoking this. - if (controller_ && (changed || was_empty_selection)) - controller_->OnTreeViewSelectionChanged(this); - - if (changed) { - AXVirtualView* ax_selected_view = - selected_node_ ? selected_node_->accessibility_view() : nullptr; - if (ax_selected_view) { - GetViewAccessibility().OverrideFocus(ax_selected_view); - ax_selected_view->NotifyAccessibilityEvent(ax::mojom::Event::kSelection); - } else { - GetViewAccessibility().OverrideFocus(nullptr); - NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); - } - } + UpdateSelection(model_node, kActiveAndSelected); } const TreeModelNode* TreeView::GetSelectedNode() const { return selected_node_ ? selected_node_->model_node() : nullptr; } +void TreeView::SetActiveNode(TreeModelNode* model_node) { + UpdateSelection(model_node, kActive); +} + +const TreeModelNode* TreeView::GetActiveNode() const { + return active_node_ ? active_node_->model_node() : nullptr; +} + void TreeView::Collapse(ui::TreeModelNode* model_node) { // Don't collapse the root if the root isn't shown, otherwise nothing is // displayed. @@ -315,7 +271,9 @@ void TreeView::Collapse(ui::TreeModelNode* model_node) { bool was_expanded = IsExpanded(model_node); if (node->is_expanded()) { if (selected_node_ && selected_node_->HasAncestor(node)) - SetSelectedNode(model_node); + UpdateSelection(model_node, kActiveAndSelected); + else if (active_node_ && active_node_->HasAncestor(node)) + UpdateSelection(model_node, kActive); node->set_is_expanded(false); } if (was_expanded) { @@ -392,9 +350,13 @@ void TreeView::SetRootShown(bool root_shown) { if (root_shown_ == root_shown) return; root_shown_ = root_shown; - if (!root_shown_ && selected_node_ == &root_) { + if (!root_shown_ && (selected_node_ == &root_ || active_node_ == &root_)) { const auto& children = model_->GetChildren(root_.model_node()); - SetSelectedNode(children.empty() ? nullptr : children.front()); + TreeModelNode* first_child = children.empty() ? nullptr : children.front(); + if (selected_node_ == &root_) + UpdateSelection(first_child, kActiveAndSelected); + else if (active_node_ == &root_) + UpdateSelection(first_child, kActive); } AXVirtualView* ax_view = root_.accessibility_view(); @@ -491,36 +453,53 @@ bool TreeView::HandleAccessibleAction(const ui::AXActionData& action_data) { if (!model_) return false; - switch (action_data.action) { - case ax::mojom::Action::kDoDefault: { - CommitEdit(); - RequestFocus(); - TreeModelNode* selected_model_node = GetSelectedNode(); - if (!selected_model_node) + AXVirtualView* ax_view = AXVirtualView::GetFromId(action_data.target_node_id); + InternalNode* node = + ax_view ? GetInternalNodeForVirtualView(ax_view) : nullptr; + if (!node) { + switch (action_data.action) { + case ax::mojom::Action::kFocus: + if (active_node_) + return false; + if (!HasFocus()) + RequestFocus(); return true; + case ax::mojom::Action::kBlur: + case ax::mojom::Action::kScrollToMakeVisible: + return View::HandleAccessibleAction(action_data); + default: + return false; + } + } - if (IsExpanded(selected_model_node)) - Collapse(selected_model_node); + switch (action_data.action) { + case ax::mojom::Action::kDoDefault: + SetSelectedNode(node->model_node()); + if (!HasFocus()) + RequestFocus(); + if (IsExpanded(node->model_node())) + Collapse(node->model_node()); else - Expand(selected_model_node); + Expand(node->model_node()); break; - } case ax::mojom::Action::kFocus: - RequestFocus(); + SetSelectedNode(node->model_node()); + if (!HasFocus()) + RequestFocus(); break; case ax::mojom::Action::kScrollToMakeVisible: - if (selected_node_) { - // GetForegroundBoundsForNode() returns RTL-flipped coordinates for - // paint. Un-flip before passing to ScrollRectToVisible(), which uses - // layout coordinates. - ScrollRectToVisible( - GetMirroredRect(GetForegroundBoundsForNode(selected_node_))); - } + // GetForegroundBoundsForNode() returns RTL-flipped coordinates for paint. + // Un-flip before passing to ScrollRectToVisible(), which uses layout + // coordinates. + ScrollRectToVisible(GetMirroredRect(GetForegroundBoundsForNode(node))); break; case ax::mojom::Action::kShowContextMenu: + SetSelectedNode(node->model_node()); + if (!HasFocus()) + RequestFocus(); ShowContextMenu(GetBoundsInScreen().CenterPoint(), ui::MENU_SOURCE_KEYBOARD); break; @@ -565,11 +544,14 @@ void TreeView::TreeNodesRemoved(TreeModel* model, GetInternalNodeForModelNode(parent, DONT_CREATE_IF_NOT_LOADED); if (!parent_node || !parent_node->loaded_children()) return; - bool reset_selection = false; + bool reset_selected_node = false; + bool reset_active_node = false; for (size_t i = 0; i < count; ++i) { InternalNode* child_removing = parent_node->children()[start].get(); if (selected_node_ && selected_node_->HasAncestor(child_removing)) - reset_selection = true; + reset_selected_node = true; + if (active_node_ && active_node_->HasAncestor(child_removing)) + reset_active_node = true; DCHECK(parent_node->accessibility_view()->Contains( child_removing->accessibility_view())); @@ -578,21 +560,31 @@ void TreeView::TreeNodesRemoved(TreeModel* model, child_removing->set_accessibility_view(nullptr); parent_node->Remove(start); } - if (reset_selection) { - // selected_node_ is no longer valid (at the time we enter this function - // its model_node() is likely deleted). Explicitly NULL out the field - // rather than invoking SetSelectedNode() otherwise, we'll try and use a - // deleted value. - selected_node_ = nullptr; + + if (reset_selected_node || reset_active_node) { + // selected_node_ or active_node_ or both were no longer valid (i.e. the + // model_node() was likely deleted by the time we entered this function). + // Explicitly set to nullptr before continuing; otherwise, we might try to + // use a deleted value. + if (reset_selected_node) + selected_node_ = nullptr; + if (reset_active_node) + active_node_ = nullptr; + + // Replace invalidated states with the nearest valid node. const auto& children = model_->GetChildren(parent); - TreeModelNode* to_select = nullptr; + TreeModelNode* nearest_node = nullptr; if (!children.empty()) { - to_select = children[std::min(start, children.size() - 1)]; + nearest_node = children[std::min(start, children.size() - 1)]; } else if (parent != root_.model_node() || root_shown_) { - to_select = parent; + nearest_node = parent; } - SetSelectedNode(to_select); + if (reset_selected_node) + UpdateSelection(nearest_node, kActiveAndSelected); + else if (reset_active_node) + UpdateSelection(nearest_node, kActive); } + if (IsExpanded(parent)) { NotifyAccessibilityEvent(ax::mojom::Event::kRowCountChanged, true); DrawnNodesChanged(); @@ -652,11 +644,17 @@ int TreeView::GetRowCount() { } int TreeView::GetSelectedRow() { - ui::TreeModelNode* model_node = GetSelectedNode(); + // Type-ahead searches should be relative to the active node, so return the + // row of the active node for |PrefixSelector|. + ui::TreeModelNode* model_node = GetActiveNode(); return model_node ? GetRowForNode(model_node) : -1; } void TreeView::SetSelectedRow(int row) { + // Type-ahead manipulates selection because active node is synced to selected + // node, so call SetSelectedNode() instead of SetActiveNode(). + // TODO(crbug.com/1080944): Decouple active node from selected node by adding + // new keyboard affordances. SetSelectedNode(GetNodeForRow(row)); } @@ -665,18 +663,20 @@ base::string16 TreeView::GetTextForRow(int row) { } gfx::Point TreeView::GetKeyboardContextMenuLocation() { - int y = height() / 2; - if (selected_node_) { - gfx::Rect node_bounds(GetForegroundBoundsForNode(selected_node_)); - gfx::Rect vis_bounds(GetVisibleBounds()); - if (node_bounds.y() >= vis_bounds.y() && - node_bounds.y() < vis_bounds.bottom()) { - y = node_bounds.y(); - } + gfx::Rect vis_bounds(GetVisibleBounds()); + int x = 0; + int y = 0; + if (active_node_) { + gfx::Rect node_bounds(GetForegroundBoundsForNode(active_node_)); + if (node_bounds.Intersects(vis_bounds)) + node_bounds.Intersect(vis_bounds); + gfx::Point menu_point(node_bounds.CenterPoint()); + x = base::ClampToRange(menu_point.x(), vis_bounds.x(), vis_bounds.right()); + y = base::ClampToRange(menu_point.y(), vis_bounds.y(), vis_bounds.bottom()); } - gfx::Point screen_loc(0, y); + gfx::Point screen_loc(x, y); if (base::i18n::IsRTL()) - screen_loc.set_x(width()); + screen_loc.set_x(vis_bounds.width() - screen_loc.x()); ConvertPointToScreen(this, &screen_loc); return screen_loc; } @@ -688,10 +688,10 @@ bool TreeView::OnKeyPressed(const ui::KeyEvent& event) { switch (event.key_code()) { case ui::VKEY_F2: if (!editing_) { - TreeModelNode* selected_node = GetSelectedNode(); - if (selected_node && - (!controller_ || controller_->CanEdit(this, selected_node))) { - StartEditing(selected_node); + TreeModelNode* active_node = GetActiveNode(); + if (active_node && + (!controller_ || controller_->CanEdit(this, active_node))) { + StartEditing(active_node); } } return true; @@ -762,13 +762,6 @@ void TreeView::OnFocus() { GetInputMethod()->OnCaretBoundsChanged(GetPrefixSelector()); SetHasFocusIndicator(true); - AXVirtualView* ax_selected_view = - selected_node_ ? selected_node_->accessibility_view() : nullptr; - if (ax_selected_view) { - GetViewAccessibility().OverrideFocus(ax_selected_view); - } else { - GetViewAccessibility().OverrideFocus(nullptr); - } } void TreeView::OnBlur() { @@ -780,24 +773,82 @@ void TreeView::OnBlur() { SetHasFocusIndicator(false); } +void TreeView::UpdateSelection(TreeModelNode* model_node, + SelectionType selection_type) { + CancelEdit(); + if (model_node && model_->GetParent(model_node)) + Expand(model_->GetParent(model_node)); + if (model_node && model_node == root_.model_node() && !root_shown_) + return; // Ignore requests for the root when not shown. + InternalNode* node = + model_node ? GetInternalNodeForModelNode(model_node, CREATE_IF_NOT_LOADED) + : nullptr; + + // Force update if old value was nullptr to handle case of TreeNodesRemoved + // explicitly resetting selected_node_ or active_node_ before invoking this. + bool active_changed = (!active_node_ || active_node_ != node); + bool selection_changed = (selection_type == kActiveAndSelected && + (!selected_node_ || selected_node_ != node)); + + // Update tree view states to new values. + if (active_changed) + active_node_ = node; + + if (selection_changed) { + SchedulePaintForNode(selected_node_); + selected_node_ = node; + SchedulePaintForNode(selected_node_); + } + + if (active_changed && node) { + // GetForegroundBoundsForNode() returns RTL-flipped coordinates for paint. + // Un-flip before passing to ScrollRectToVisible(), which uses layout + // coordinates. + ScrollRectToVisible(GetMirroredRect(GetForegroundBoundsForNode(node))); + } + + // Notify assistive technologies of state changes. + if (active_changed) { + // Update |ViewAccessibility| so that focus lands directly on this node when + // |FocusManager| gives focus to the tree view. This update also fires an + // accessible focus event. + GetViewAccessibility().OverrideFocus(node ? node->accessibility_view() + : nullptr); + } + + if (selection_changed) { + AXVirtualView* ax_selected_view = + node ? node->accessibility_view() : nullptr; + if (ax_selected_view) + ax_selected_view->NotifyAccessibilityEvent(ax::mojom::Event::kSelection); + else + NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); + } + + // Notify controller of state changes. + if (selection_changed && controller_) + controller_->OnTreeViewSelectionChanged(this); +} + bool TreeView::OnClickOrTap(const ui::LocatedEvent& event) { CommitEdit(); - RequestFocus(); InternalNode* node = GetNodeAtPoint(event.location()); - if (!node) - return true; - - bool hits_arrow = IsPointInExpandControl(node, event.location()); - if (!hits_arrow) - SetSelectedNode(node->model_node()); + if (node) { + bool hits_arrow = IsPointInExpandControl(node, event.location()); + if (!hits_arrow) + SetSelectedNode(node->model_node()); - if (hits_arrow || EventIsDoubleTapOrClick(event)) { - if (node->is_expanded()) - Collapse(node->model_node()); - else - Expand(node->model_node()); + if (hits_arrow || EventIsDoubleTapOrClick(event)) { + if (node->is_expanded()) + Collapse(node->model_node()); + else + Expand(node->model_node()); + } } + + if (!HasFocus()) + RequestFocus(); return true; } @@ -859,6 +910,7 @@ void TreeView::PopulateAccessibilityData(InternalNode* node, : nullptr; const bool selected = (node == selected_node); data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, selected); + data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kSelect); if (node->is_expanded()) data->AddState(ax::mojom::State::kExpanded); @@ -916,7 +968,8 @@ void TreeView::PopulateAccessibilityData(InternalNode* node, gfx::Rect node_bounds = GetBackgroundBoundsForNode(node); data->relative_bounds.bounds = gfx::RectF(node_bounds); } else { - data->AddState(ax::mojom::State::kInvisible); + data->AddState(node != &root_ || root_shown_ ? ax::mojom::State::kInvisible + : ax::mojom::State::kIgnored); } } @@ -1123,6 +1176,22 @@ TreeView::InternalNode* TreeView::GetInternalNodeForModelNode( return parent_internal_node->children()[index].get(); } +TreeView::InternalNode* TreeView::GetInternalNodeForVirtualView( + AXVirtualView* ax_view) { + if (ax_view == root_.accessibility_view()) + return &root_; + DCHECK(ax_view); + InternalNode* parent_internal_node = + GetInternalNodeForVirtualView(ax_view->virtual_parent_view()); + if (!parent_internal_node) + return nullptr; + DCHECK(parent_internal_node->loaded_children()); + AXVirtualView* parent_ax_view = parent_internal_node->accessibility_view(); + DCHECK(parent_ax_view); + size_t index = parent_ax_view->GetIndexOf(ax_view); + return parent_internal_node->children()[index].get(); +} + gfx::Rect TreeView::GetBoundsForNode(InternalNode* node) { int row, ignored_depth; row = GetRowForInternalNode(node, &ignored_depth); @@ -1248,7 +1317,7 @@ void TreeView::IncrementSelection(IncrementType type) { if (!model_) return; - if (!GetSelectedNode()) { + if (!active_node_) { // If nothing is selected select the first or last node. if (root_.children().empty()) return; @@ -1268,7 +1337,7 @@ void TreeView::IncrementSelection(IncrementType type) { int depth = 0; int delta = type == INCREMENT_PREVIOUS ? -1 : 1; - int row = GetRowForInternalNode(selected_node_, &depth); + int row = GetRowForInternalNode(active_node_, &depth); int new_row = base::ClampToRange(row + delta, 0, GetRowCount() - 1); if (new_row == row) return; // At the end/beginning. @@ -1276,20 +1345,20 @@ void TreeView::IncrementSelection(IncrementType type) { } void TreeView::CollapseOrSelectParent() { - if (selected_node_) { - if (selected_node_->is_expanded()) - Collapse(selected_node_->model_node()); - else if (selected_node_->parent()) - SetSelectedNode(selected_node_->parent()->model_node()); + if (active_node_) { + if (active_node_->is_expanded()) + Collapse(active_node_->model_node()); + else if (active_node_->parent()) + SetSelectedNode(active_node_->parent()->model_node()); } } void TreeView::ExpandOrSelectChild() { - if (selected_node_) { - if (!selected_node_->is_expanded()) - Expand(selected_node_->model_node()); - else if (!selected_node_->children().empty()) - SetSelectedNode(selected_node_->children().front()->model_node()); + if (active_node_) { + if (!active_node_->is_expanded()) + Expand(active_node_->model_node()); + else if (!active_node_->children().empty()) + SetSelectedNode(active_node_->children().front()->model_node()); } } diff --git a/chromium/ui/views/controls/tree/tree_view.h b/chromium/ui/views/controls/tree/tree_view.h index b03b860f7b2..19504e30d01 100644 --- a/chromium/ui/views/controls/tree/tree_view.h +++ b/chromium/ui/views/controls/tree/tree_view.h @@ -44,6 +44,13 @@ class TreeViewController; // can expand, collapse and edit the items. A Controller may be attached to // receive notification of selection changes and restrict editing. // +// In addition to tracking selection, TreeView also tracks the active node, +// which is the item that receives keyboard input when the tree has focus. +// Active/focus is like a pointer for keyboard navigation, and operations such +// as selection are performed at the point of focus. The active node is synced +// to the selected node. When the active node is nullptr, the TreeView itself is +// the target of keyboard input. +// // Note on implementation. This implementation doesn't scale well. In particular // it does not store any row information, but instead calculates it as // necessary. But it's more than adequate for current uses. @@ -103,6 +110,20 @@ class VIEWS_EXPORT TreeView : public View, } const ui::TreeModelNode* GetSelectedNode() const; + // Marks the specified node as active, scrolls it into view, and reports a + // keyboard focus update to ATs. Active node should be synced to the selected + // node and should be nullptr when the tree is empty. + // TODO(crbug.com/1080944): Decouple active node from selected node by adding + // new keyboard affordances. + void SetActiveNode(ui::TreeModelNode* model_node); + + // Returns the active node, or nullptr if nothing is active. + ui::TreeModelNode* GetActiveNode() { + return const_cast<ui::TreeModelNode*>( + const_cast<const TreeView*>(this)->GetActiveNode()); + } + const ui::TreeModelNode* GetActiveNode() const; + // Marks |model_node| as collapsed. This only effects the UI if node and all // its parents are expanded (IsExpanded(model_node) returns true). void Collapse(ui::TreeModelNode* model_node); @@ -193,6 +214,20 @@ class VIEWS_EXPORT TreeView : public View, private: friend class TreeViewTest; + // Enumeration of possible changes to tree view state when the UI is updated. + enum SelectionType { + // Active state is being set to a tree item. + kActive, + + // Active and selected states are being set to a tree item. + kActiveAndSelected, + }; + + // Performs active node and selected node state transitions. Updates states + // and scrolling before notifying assistive technologies and the controller. + void UpdateSelection(ui::TreeModelNode* model_node, + SelectionType selection_type); + // Selects, expands or collapses nodes in the tree. Consistent behavior for // tap gesture and click events. bool OnClickOrTap(const ui::LocatedEvent& event); @@ -354,6 +389,9 @@ class VIEWS_EXPORT TreeView : public View, ui::TreeModelNode* model_node, GetInternalNodeCreateType create_type); + // Returns the InternalNode for a virtual view. + InternalNode* GetInternalNodeForVirtualView(AXVirtualView* ax_view); + // Returns the bounds for a node. This rectangle contains the node's icon, // text, arrow, and auxiliary text (if any). All of the other bounding // rectangles computed by the functions below lie inside this rectangle. @@ -435,6 +473,9 @@ class VIEWS_EXPORT TreeView : public View, // The selected node, may be null. InternalNode* selected_node_ = nullptr; + // The current active node, may be null. + InternalNode* active_node_ = nullptr; + bool editing_ = false; // The editor; lazily created and never destroyed (well, until TreeView is diff --git a/chromium/ui/views/controls/tree/tree_view_unittest.cc b/chromium/ui/views/controls/tree/tree_view_unittest.cc index 7f56294c155..6081d640219 100644 --- a/chromium/ui/views/controls/tree/tree_view_unittest.cc +++ b/chromium/ui/views/controls/tree/tree_view_unittest.cc @@ -14,6 +14,7 @@ #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" +#include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/platform/ax_platform_node_delegate.h" @@ -24,7 +25,9 @@ #include "ui/views/controls/prefix_selector.h" #include "ui/views/controls/textfield/textfield.h" #include "ui/views/controls/tree/tree_view_controller.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" +#include "ui/views/widget/unique_widget_ptr.h" #include "ui/views/widget/widget.h" using ui::TreeModel; @@ -102,10 +105,27 @@ class TreeViewTest : public ViewsTestBase { std::string TreeViewAccessibilityContentsAsString() const; + // Gets the selected node from the tree view. The result can be compared with + // GetSelectedAccessibilityViewName() to check consistency between the tree + // view state and the accessibility data. std::string GetSelectedNodeTitle(); + // Finds the selected node via iterative depth first search over the internal + // accessibility tree, examining both ignored and unignored nodes. The result + // can be compared with GetSelectedNodeTitle() to check consistency between + // the tree view state and the accessibility data. std::string GetSelectedAccessibilityViewName() const; + // Gets the active node from the tree view. The result can be compared with + // GetSelectedAccessibilityViewName() to check consistency between the tree + // view state and the accessibility data. + std::string GetActiveNodeTitle(); + + // Gets the active node from the tree view's |ViewAccessibility|. The result + // can be compared with GetSelectedNodeTitle() to check consistency between + // the tree view internal state and the accessibility data. + std::string GetActiveAccessibilityViewName() const; + std::string GetEditingNodeTitle(); AXVirtualView* GetRootAccessibilityView() const; @@ -125,7 +145,7 @@ class TreeViewTest : public ViewsTestBase { ui::TreeNodeModel<TestNode> model_; TreeView* tree_; - Widget* widget_; + UniqueWidgetPtr widget_; private: std::string InternalNodeAsString(TreeView::InternalNode* node); @@ -141,12 +161,13 @@ class TreeViewTest : public ViewsTestBase { void TreeViewTest::SetUp() { ViewsTestBase::SetUp(); - widget_ = new Widget; + widget_ = std::make_unique<Widget>(); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); params.bounds = gfx::Rect(0, 0, 200, 200); widget_->Init(std::move(params)); - tree_ = new TreeView(); - widget_->GetContentsView()->AddChildView(tree_); + tree_ = + widget_->GetContentsView()->AddChildView(std::make_unique<TreeView>()); + tree_->RequestFocus(); ViewAccessibility::AccessibilityEventsCallback accessibility_events_callback = base::BindRepeating( @@ -164,8 +185,7 @@ void TreeViewTest::SetUp() { } void TreeViewTest::TearDown() { - if (!widget_->IsClosed()) - widget_->Close(); + widget_.reset(); ViewsTestBase::TearDown(); } @@ -233,6 +253,20 @@ std::string TreeViewTest::GetSelectedAccessibilityViewName() const { return {}; } +std::string TreeViewTest::GetActiveNodeTitle() { + TreeModelNode* model_node = tree_->GetActiveNode(); + return model_node ? base::UTF16ToASCII(model_node->GetTitle()) + : std::string(); +} + +std::string TreeViewTest::GetActiveAccessibilityViewName() const { + const AXVirtualView* ax_view = + tree_->GetViewAccessibility().FocusedVirtualChild(); + return ax_view ? ax_view->GetData().GetStringAttribute( + ax::mojom::StringAttribute::kName) + : std::string(); +} + std::string TreeViewTest::GetEditingNodeTitle() { TreeModelNode* model_node = tree_->GetEditingNode(); return model_node ? base::UTF16ToASCII(model_node->GetTitle()) @@ -336,6 +370,12 @@ std::string TreeViewTest::InternalNodeAsString(TreeView::InternalNode* node) { return result; } +// Verify properties are accessible via metadata. +TEST_F(TreeViewTest, MetadataTest) { + tree_->SetModel(&model_); + test::TestViewMetadata(tree_); +} + // Verifies setting model correctly updates internal state. TEST_F(TreeViewTest, SetModel) { tree_->SetModel(&model_); @@ -930,4 +970,225 @@ TEST_F(TreeViewTest, CommitOnFocusLost) { ax::mojom::StringAttribute::kName)); } +// Verifies that virtual accessible actions go to virtual view targets. +TEST_F(TreeViewTest, VirtualAccessibleAction) { + tree_->SetModel(&model_); + tree_->Expand(GetNodeByTitle("b1")); + EXPECT_EQ("root [a b [b1] c]", TreeViewContentsAsString()); + EXPECT_EQ("root [a b [b1] c]", TreeViewAccessibilityContentsAsString()); + EXPECT_EQ(5, GetRowCount()); + + // Set to nullptr should clear the selection. + tree_->SetSelectedNode(nullptr); + EXPECT_EQ(std::string(), GetActiveNodeTitle()); + EXPECT_EQ(std::string(), GetActiveAccessibilityViewName()); + EXPECT_EQ(std::string(), GetSelectedNodeTitle()); + EXPECT_EQ(std::string(), GetSelectedAccessibilityViewName()); + + // Test using each virtual view as target. + ui::AXActionData data; + const std::string test_cases[] = {"root", "a", "b", "b1", "c"}; + for (const std::string& name : test_cases) { + data.target_node_id = GetAccessibilityViewByName(name)->GetData().id; + data.action = ax::mojom::Action::kDoDefault; + EXPECT_TRUE(tree_->HandleAccessibleAction(data)); + EXPECT_EQ(name, GetActiveNodeTitle()); + EXPECT_EQ(name, GetActiveAccessibilityViewName()); + EXPECT_EQ(name, GetSelectedNodeTitle()); + EXPECT_EQ(name, GetSelectedAccessibilityViewName()); + } + + // Do nothing when a valid node id is not provided. This can happen if the + // actions target the owner view itself. + tree_->SetSelectedNode(GetNodeByTitle("b")); + data.target_node_id = -1; + data.action = ax::mojom::Action::kDoDefault; + EXPECT_FALSE(tree_->HandleAccessibleAction(data)); + EXPECT_EQ("b", GetActiveNodeTitle()); + EXPECT_EQ("b", GetActiveAccessibilityViewName()); + EXPECT_EQ("b", GetSelectedNodeTitle()); + EXPECT_EQ("b", GetSelectedAccessibilityViewName()); + + // Check that the active node is set if assistive technologies set focus. + tree_->SetSelectedNode(GetNodeByTitle("b")); + data.target_node_id = GetAccessibilityViewByName("a")->GetData().id; + data.action = ax::mojom::Action::kFocus; + EXPECT_TRUE(tree_->HandleAccessibleAction(data)); + EXPECT_EQ("a", GetActiveNodeTitle()); + EXPECT_EQ("a", GetActiveAccessibilityViewName()); + EXPECT_EQ("a", GetSelectedNodeTitle()); + EXPECT_EQ("a", GetSelectedAccessibilityViewName()); + + // Do not handle accessible actions when no node is selected. + tree_->SetSelectedNode(nullptr); + data.target_node_id = -1; + data.action = ax::mojom::Action::kDoDefault; + EXPECT_FALSE(tree_->HandleAccessibleAction(data)); + EXPECT_EQ(std::string(), GetActiveNodeTitle()); + EXPECT_EQ(std::string(), GetActiveAccessibilityViewName()); + EXPECT_EQ(std::string(), GetSelectedNodeTitle()); + EXPECT_EQ(std::string(), GetSelectedAccessibilityViewName()); +} + +// Verifies that accessibility focus events get fired for the correct nodes when +// the tree view is given focus. +TEST_F(TreeViewTest, OnFocusAccessibilityEvents) { + // Without keyboard focus, model changes should not fire focus events. + tree_->GetFocusManager()->ClearFocus(); + EXPECT_FALSE(tree_->HasFocus()); + tree_->SetModel(&model_); + EXPECT_EQ("root [a b c]", TreeViewContentsAsString()); + EXPECT_EQ("root [a b c]", TreeViewAccessibilityContentsAsString()); + EXPECT_EQ("root", GetSelectedNodeTitle()); + EXPECT_EQ("root", GetSelectedAccessibilityViewName()); + EXPECT_EQ(4, GetRowCount()); + EXPECT_EQ((AccessibilityEventsVector{ + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetRootAccessibilityView(), + ax::mojom::Event::kSelection)}), + accessibility_events()); + + // The initial focus should fire a focus event for the active node + // (in this case, the root node). + ClearAccessibilityEvents(); + tree_->RequestFocus(); + EXPECT_EQ((AccessibilityEventsVector{std::make_pair( + GetRootAccessibilityView(), ax::mojom::Event::kFocus)}), + accessibility_events()); + + // Focus clear and restore should fire a focus event for the active node. + ClearAccessibilityEvents(); + tree_->SetSelectedNode(GetNodeByTitle("b")); + tree_->SetActiveNode(GetNodeByTitle("a")); + EXPECT_EQ("a", GetActiveNodeTitle()); + EXPECT_EQ("a", GetActiveAccessibilityViewName()); + EXPECT_EQ("b", GetSelectedNodeTitle()); + EXPECT_EQ("b", GetSelectedAccessibilityViewName()); + tree_->GetFocusManager()->ClearFocus(); + EXPECT_FALSE(tree_->HasFocus()); + tree_->GetFocusManager()->RestoreFocusedView(); + EXPECT_TRUE(tree_->HasFocus()); + EXPECT_EQ("a", GetActiveNodeTitle()); + EXPECT_EQ("a", GetActiveAccessibilityViewName()); + EXPECT_EQ("b", GetSelectedNodeTitle()); + EXPECT_EQ("b", GetSelectedAccessibilityViewName()); + EXPECT_EQ( + (AccessibilityEventsVector{std::make_pair(GetAccessibilityViewByName("b"), + ax::mojom::Event::kFocus), + std::make_pair(GetAccessibilityViewByName("b"), + ax::mojom::Event::kSelection), + std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kFocus), + std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kFocus)}), + accessibility_events()); + + // Without keyboard focus, selection should not fire focus events. + ClearAccessibilityEvents(); + tree_->GetFocusManager()->ClearFocus(); + tree_->SetSelectedNode(GetNodeByTitle("a")); + EXPECT_FALSE(tree_->HasFocus()); + EXPECT_EQ("a", GetSelectedNodeTitle()); + EXPECT_EQ("a", GetSelectedAccessibilityViewName()); + EXPECT_EQ( + (AccessibilityEventsVector{std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kSelection)}), + accessibility_events()); + + // A direct focus action on a tree item should give focus to the tree view but + // only fire a focus event for the target node. + ui::AXActionData data; + const std::string test_cases[] = {"root", "a", "b", "c"}; + for (const std::string& name : test_cases) { + ClearAccessibilityEvents(); + tree_->GetFocusManager()->ClearFocus(); + EXPECT_FALSE(tree_->HasFocus()); + data.target_node_id = GetAccessibilityViewByName(name)->GetData().id; + data.action = ax::mojom::Action::kFocus; + EXPECT_TRUE(tree_->HandleAccessibleAction(data)); + EXPECT_TRUE(tree_->HasFocus()); + EXPECT_EQ(name, GetActiveNodeTitle()); + EXPECT_EQ(name, GetActiveAccessibilityViewName()); + EXPECT_EQ(name, GetSelectedNodeTitle()); + EXPECT_EQ(name, GetSelectedAccessibilityViewName()); + EXPECT_EQ((AccessibilityEventsVector{ + std::make_pair(GetAccessibilityViewByName(name), + ax::mojom::Event::kSelection), + std::make_pair(GetAccessibilityViewByName(name), + ax::mojom::Event::kFocus)}), + accessibility_events()); + } + + // A direct focus action on the tree view itself with an active node should + // have no effect. + ClearAccessibilityEvents(); + tree_->GetFocusManager()->ClearFocus(); + tree_->SetSelectedNode(GetNodeByTitle("b")); + data.target_node_id = -1; + data.action = ax::mojom::Action::kFocus; + EXPECT_FALSE(tree_->HandleAccessibleAction(data)); + EXPECT_FALSE(tree_->HasFocus()); + EXPECT_EQ("b", GetActiveNodeTitle()); + EXPECT_EQ("b", GetActiveAccessibilityViewName()); + EXPECT_EQ("b", GetSelectedNodeTitle()); + EXPECT_EQ("b", GetSelectedAccessibilityViewName()); + EXPECT_EQ( + (AccessibilityEventsVector{std::make_pair(GetAccessibilityViewByName("b"), + ax::mojom::Event::kSelection)}), + accessibility_events()); + + // A direct focus action on a tree view without an active node (i.e. empty + // tree) should fire a focus event for the tree view. + ClearAccessibilityEvents(); + tree_->GetFocusManager()->ClearFocus(); + ui::TreeNodeModel<TestNode> empty_model(std::make_unique<TestNode>()); + static_cast<TestNode*>(empty_model.GetRoot())->SetTitle(ASCIIToUTF16("root")); + tree_->SetModel(&empty_model); + tree_->SetRootShown(false); + data.target_node_id = -1; + data.action = ax::mojom::Action::kFocus; + EXPECT_TRUE(tree_->HandleAccessibleAction(data)); + EXPECT_TRUE(tree_->HasFocus()); + EXPECT_EQ(std::string(), GetActiveNodeTitle()); + EXPECT_EQ(std::string(), GetActiveAccessibilityViewName()); + EXPECT_EQ(std::string(), GetSelectedNodeTitle()); + EXPECT_EQ(std::string(), GetSelectedAccessibilityViewName()); + EXPECT_EQ((AccessibilityEventsVector{ + std::make_pair(GetRootAccessibilityView(), + ax::mojom::Event::kSelection), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kSelection), + std::make_pair(GetRootAccessibilityView(), + ax::mojom::Event::kStateChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kFocus)}), + accessibility_events()); + + // When a focused empty tree is populated with nodes, it should immediately + // hand off focus to one of them and select it. + ClearAccessibilityEvents(); + tree_->SetModel(&model_); + EXPECT_EQ("a", GetActiveNodeTitle()); + EXPECT_EQ("a", GetActiveAccessibilityViewName()); + EXPECT_EQ("a", GetSelectedNodeTitle()); + EXPECT_EQ("a", GetSelectedAccessibilityViewName()); + EXPECT_EQ((AccessibilityEventsVector{ + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kFocus), + std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kSelection)}), + accessibility_events()); +} + } // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu.cc b/chromium/ui/views/controls/views_text_services_context_menu.cc deleted file mode 100644 index 0cebc532c69..00000000000 --- a/chromium/ui/views/controls/views_text_services_context_menu.cc +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2018 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/views_text_services_context_menu.h" - -#include <memory> - -#include "base/notreached.h" -#include "ui/views/controls/views_text_services_context_menu_base.h" - -namespace views { - -// static -std::unique_ptr<ViewsTextServicesContextMenu> -ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, - Textfield* client) { - return std::make_unique<ViewsTextServicesContextMenuBase>(menu, client); -} - -bool ViewsTextServicesContextMenu::IsTextDirectionCheckedForTesting( - ViewsTextServicesContextMenu* menu, - base::i18n::TextDirection direction) { - NOTREACHED(); - return false; -} - -} // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu.h b/chromium/ui/views/controls/views_text_services_context_menu.h index 1b84aa43f20..ade35947552 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu.h +++ b/chromium/ui/views/controls/views_text_services_context_menu.h @@ -7,13 +7,7 @@ #include <memory> -#include "base/i18n/rtl.h" -#include "ui/base/accelerators/accelerator.h" -#include "ui/views/views_export.h" - -namespace ui { -class SimpleMenuModel; -} +#include "ui/base/models/simple_menu_model.h" namespace views { @@ -21,26 +15,15 @@ class Textfield; // This class is used to add and handle text service items in the text context // menu. -class ViewsTextServicesContextMenu : public ui::AcceleratorProvider { +class ViewsTextServicesContextMenu : public ui::SimpleMenuModel::Delegate { public: // Creates a platform-specific ViewsTextServicesContextMenu object. static std::unique_ptr<ViewsTextServicesContextMenu> Create( ui::SimpleMenuModel* menu, Textfield* textfield); - // Method for testing. Returns true if the text direction BiDi submenu item - // in |menu| should be checked. - VIEWS_EXPORT static bool IsTextDirectionCheckedForTesting( - ViewsTextServicesContextMenu* menu, - base::i18n::TextDirection direction); - // Returns true if the given |command_id| is handled by the menu. virtual bool SupportsCommand(int command_id) const = 0; - - // Methods associated with SimpleMenuModel::Delegate. - virtual bool IsCommandIdChecked(int command_id) const = 0; - virtual bool IsCommandIdEnabled(int command_id) const = 0; - virtual void ExecuteCommand(int command_id) = 0; }; } // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu_base.cc b/chromium/ui/views/controls/views_text_services_context_menu_base.cc index b1f7b1d9890..a9b53aeb9c2 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu_base.cc +++ b/chromium/ui/views/controls/views_text_services_context_menu_base.cc @@ -4,6 +4,8 @@ #include "ui/views/controls/views_text_services_context_menu_base.h" +#include <memory> + #include "base/metrics/histogram_macros.h" #include "build/build_config.h" #include "ui/base/accelerators/accelerator.h" @@ -41,10 +43,6 @@ ViewsTextServicesContextMenuBase::ViewsTextServicesContextMenuBase( ViewsTextServicesContextMenuBase::~ViewsTextServicesContextMenuBase() = default; -bool ViewsTextServicesContextMenuBase::SupportsCommand(int command_id) const { - return command_id == IDS_CONTENT_CONTEXT_EMOJI; -} - bool ViewsTextServicesContextMenuBase::GetAcceleratorForCommandId( int command_id, ui::Accelerator* accelerator) const { @@ -72,17 +70,28 @@ bool ViewsTextServicesContextMenuBase::IsCommandIdChecked( bool ViewsTextServicesContextMenuBase::IsCommandIdEnabled( int command_id) const { - if (command_id == IDS_CONTENT_CONTEXT_EMOJI) - return true; - - return false; + return command_id == IDS_CONTENT_CONTEXT_EMOJI; } -void ViewsTextServicesContextMenuBase::ExecuteCommand(int command_id) { +void ViewsTextServicesContextMenuBase::ExecuteCommand(int command_id, + int event_flags) { if (command_id == IDS_CONTENT_CONTEXT_EMOJI) { - client()->GetWidget()->ShowEmojiPanel(); + client_->GetWidget()->ShowEmojiPanel(); UMA_HISTOGRAM_BOOLEAN(kViewsTextServicesContextMenuEmoji, true); } } +bool ViewsTextServicesContextMenuBase::SupportsCommand(int command_id) const { + return command_id == IDS_CONTENT_CONTEXT_EMOJI; +} + +#if !defined(OS_MACOSX) +// static +std::unique_ptr<ViewsTextServicesContextMenu> +ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, + Textfield* client) { + return std::make_unique<ViewsTextServicesContextMenuBase>(menu, client); +} +#endif + } // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu_base.h b/chromium/ui/views/controls/views_text_services_context_menu_base.h index bbc3e19d206..3c8484778f8 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu_base.h +++ b/chromium/ui/views/controls/views_text_services_context_menu_base.h @@ -5,39 +5,40 @@ #ifndef UI_VIEWS_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_BASE_H_ #define UI_VIEWS_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_BASE_H_ -#include "base/macros.h" +#include "build/build_config.h" #include "ui/views/controls/views_text_services_context_menu.h" namespace views { -// This base class is used to add and handle text service items in the text +// This base class is used to add and handle text service items in the textfield // context menu. Specific platforms may subclass and add additional items. class ViewsTextServicesContextMenuBase : public ViewsTextServicesContextMenu { public: ViewsTextServicesContextMenuBase(ui::SimpleMenuModel* menu, Textfield* client); + ViewsTextServicesContextMenuBase(const ViewsTextServicesContextMenuBase&) = + delete; + ViewsTextServicesContextMenuBase& operator=( + const ViewsTextServicesContextMenuBase&) = delete; ~ViewsTextServicesContextMenuBase() override; - // Returns true if the given |command_id| is handled by the menu. - bool SupportsCommand(int command_id) const override; - - // ui::AcceleratorProvider: + // ViewsTextServicesContextMenu: bool GetAcceleratorForCommandId(int command_id, ui::Accelerator* accelerator) const override; - - // Methods associated with SimpleMenuModel::Delegate. bool IsCommandIdChecked(int command_id) const override; bool IsCommandIdEnabled(int command_id) const override; - void ExecuteCommand(int command_id) override; + void ExecuteCommand(int command_id, int event_flags) override; + bool SupportsCommand(int command_id) const override; protected: - Textfield* client() const { return client_; } +#if defined(OS_MACOSX) + Textfield* client() { return client_; } + const Textfield* client() const { return client_; } +#endif private: // The view associated with the menu. Weak. Owns |this|. - Textfield* client_ = nullptr; - - DISALLOW_COPY_AND_ASSIGN(ViewsTextServicesContextMenuBase); + Textfield* const client_; }; } // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu_mac.mm b/chromium/ui/views/controls/views_text_services_context_menu_mac.mm index a95d7058b80..4f7510d6a1e 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu_mac.mm +++ b/chromium/ui/views/controls/views_text_services_context_menu_mac.mm @@ -29,108 +29,118 @@ class ViewsTextServicesContextMenuMac : public ViewsTextServicesContextMenuBase, public ui::TextServicesContextMenu::Delegate { public: - ViewsTextServicesContextMenuMac(ui::SimpleMenuModel* menu, Textfield* client) - : ViewsTextServicesContextMenuBase(menu, client), - text_services_menu_(this) { - // Insert the "Look up" item in the first position. - base::string16 text = GetSelectedText(); - if (!text.empty()) { - menu->InsertSeparatorAt(0, ui::NORMAL_SEPARATOR); - menu->InsertItemAt( - 0, IDS_CONTENT_CONTEXT_LOOK_UP, - l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, text)); - } - - text_services_menu_.AppendToContextMenu(menu); - text_services_menu_.AppendEditableItems(menu); - } - + ViewsTextServicesContextMenuMac(ui::SimpleMenuModel* menu, Textfield* client); + ViewsTextServicesContextMenuMac(const ViewsTextServicesContextMenuMac&) = + delete; + ViewsTextServicesContextMenuMac& operator=( + const ViewsTextServicesContextMenuMac&) = delete; ~ViewsTextServicesContextMenuMac() override = default; // ViewsTextServicesContextMenu: - bool SupportsCommand(int command_id) const override { - return text_services_menu_.SupportsCommand(command_id) || - command_id == IDS_CONTENT_CONTEXT_LOOK_UP || - ViewsTextServicesContextMenuBase::SupportsCommand(command_id); - } + bool IsCommandIdChecked(int command_id) const override; + bool IsCommandIdEnabled(int command_id) const override; + void ExecuteCommand(int command_id, int event_flags) override; + bool SupportsCommand(int command_id) const override; - bool IsCommandIdEnabled(int command_id) const override { - if (text_services_menu_.SupportsCommand(command_id)) - return text_services_menu_.IsCommandIdEnabled(command_id); - - switch (command_id) { - case IDS_CONTENT_CONTEXT_LOOK_UP: - return true; + // TextServicesContextMenu::Delegate: + base::string16 GetSelectedText() const override; + bool IsTextDirectionEnabled( + base::i18n::TextDirection direction) const override; + bool IsTextDirectionChecked( + base::i18n::TextDirection direction) const override; + void UpdateTextDirection(base::i18n::TextDirection direction) override; - default: - return ViewsTextServicesContextMenuBase::IsCommandIdEnabled(command_id); - } - } + private: + // Handler for the "Look Up" menu item. + void LookUpInDictionary(); - void ExecuteCommand(int command_id) override { - switch (command_id) { - case IDS_CONTENT_CONTEXT_LOOK_UP: - LookUpInDictionary(); - break; + ui::TextServicesContextMenu text_services_menu_{this}; +}; - default: - ViewsTextServicesContextMenuBase::ExecuteCommand(command_id); - break; - } +ViewsTextServicesContextMenuMac::ViewsTextServicesContextMenuMac( + ui::SimpleMenuModel* menu, + Textfield* client) + : ViewsTextServicesContextMenuBase(menu, client) { + // Insert the "Look up" item in the first position. + const base::string16 text = GetSelectedText(); + if (!text.empty()) { + menu->InsertSeparatorAt(0, ui::NORMAL_SEPARATOR); + menu->InsertItemAt( + 0, IDS_CONTENT_CONTEXT_LOOK_UP, + l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, text)); } - // TextServicesContextMenu::Delegate: - base::string16 GetSelectedText() const override { - if (client()->GetTextInputType() == ui::TEXT_INPUT_TYPE_PASSWORD) - return base::string16(); + text_services_menu_.AppendToContextMenu(menu); + text_services_menu_.AppendEditableItems(menu); +} - return client()->GetSelectedText(); - } +bool ViewsTextServicesContextMenuMac::IsCommandIdChecked(int command_id) const { + return text_services_menu_.SupportsCommand(command_id) + ? text_services_menu_.IsCommandIdChecked(command_id) + : ViewsTextServicesContextMenuBase::IsCommandIdChecked(command_id); +} - bool IsTextDirectionEnabled( - base::i18n::TextDirection direction) const override { - return direction != base::i18n::UNKNOWN_DIRECTION; - } +bool ViewsTextServicesContextMenuMac::IsCommandIdEnabled(int command_id) const { + if (text_services_menu_.SupportsCommand(command_id)) + return text_services_menu_.IsCommandIdEnabled(command_id); + return (command_id == IDS_CONTENT_CONTEXT_LOOK_UP) || + ViewsTextServicesContextMenuBase::IsCommandIdEnabled(command_id); +} - bool IsTextDirectionChecked( - base::i18n::TextDirection direction) const override { - return direction != base::i18n::UNKNOWN_DIRECTION && - client()->GetTextDirection() == direction; - } +void ViewsTextServicesContextMenuMac::ExecuteCommand(int command_id, + int event_flags) { + if (text_services_menu_.SupportsCommand(command_id)) + text_services_menu_.ExecuteCommand(command_id, event_flags); + else if (command_id == IDS_CONTENT_CONTEXT_LOOK_UP) + LookUpInDictionary(); + else + ViewsTextServicesContextMenuBase::ExecuteCommand(command_id, event_flags); +} - void UpdateTextDirection(base::i18n::TextDirection direction) override { - DCHECK_NE(direction, base::i18n::UNKNOWN_DIRECTION); +bool ViewsTextServicesContextMenuMac::SupportsCommand(int command_id) const { + return text_services_menu_.SupportsCommand(command_id) || + command_id == IDS_CONTENT_CONTEXT_LOOK_UP || + ViewsTextServicesContextMenuBase::SupportsCommand(command_id); +} - base::i18n::TextDirection text_direction = - direction == base::i18n::LEFT_TO_RIGHT ? base::i18n::LEFT_TO_RIGHT - : base::i18n::RIGHT_TO_LEFT; - client()->ChangeTextDirectionAndLayoutAlignment(text_direction); - } +base::string16 ViewsTextServicesContextMenuMac::GetSelectedText() const { + return (client()->GetTextInputType() == ui::TEXT_INPUT_TYPE_PASSWORD) + ? base::string16() + : client()->GetSelectedText(); +} - private: - // Handler for the "Look Up" menu item. - void LookUpInDictionary() { - gfx::Point baseline_point; - gfx::DecoratedText text; - if (client()->GetWordLookupDataFromSelection(&text, &baseline_point)) { - Widget* widget = client()->GetWidget(); - NSView* view = widget->GetNativeView().GetNativeNSView(); - views::View::ConvertPointToTarget(client(), widget->GetRootView(), - &baseline_point); - - NSPoint lookup_point = NSMakePoint( - baseline_point.x(), NSHeight([view frame]) - baseline_point.y()); - [view showDefinitionForAttributedString: - gfx::GetAttributedStringFromDecoratedText(text) - atPoint:lookup_point]; - } - } +bool ViewsTextServicesContextMenuMac::IsTextDirectionEnabled( + base::i18n::TextDirection direction) const { + return direction != base::i18n::UNKNOWN_DIRECTION; +} - // Appends and handles the text service menu. - ui::TextServicesContextMenu text_services_menu_; +bool ViewsTextServicesContextMenuMac::IsTextDirectionChecked( + base::i18n::TextDirection direction) const { + return IsTextDirectionEnabled(direction) && + client()->GetTextDirection() == direction; +} - DISALLOW_COPY_AND_ASSIGN(ViewsTextServicesContextMenuMac); -}; +void ViewsTextServicesContextMenuMac::UpdateTextDirection( + base::i18n::TextDirection direction) { + DCHECK(IsTextDirectionEnabled(direction)); + client()->ChangeTextDirectionAndLayoutAlignment(direction); +} + +void ViewsTextServicesContextMenuMac::LookUpInDictionary() { + gfx::DecoratedText text; + gfx::Point baseline_point; + if (client()->GetWordLookupDataFromSelection(&text, &baseline_point)) { + Widget* widget = client()->GetWidget(); + views::View::ConvertPointToTarget(client(), widget->GetRootView(), + &baseline_point); + NSView* view = widget->GetNativeView().GetNativeNSView(); + NSPoint lookup_point = NSMakePoint( + baseline_point.x(), NSHeight([view frame]) - baseline_point.y()); + [view showDefinitionForAttributedString: + gfx::GetAttributedStringFromDecoratedText(text) + atPoint:lookup_point]; + } +} } // namespace @@ -141,11 +151,4 @@ ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, return std::make_unique<ViewsTextServicesContextMenuMac>(menu, client); } -// static -bool ViewsTextServicesContextMenu::IsTextDirectionCheckedForTesting( - ViewsTextServicesContextMenu* menu, - base::i18n::TextDirection direction) { - return static_cast<views::ViewsTextServicesContextMenuMac*>(menu) - ->IsTextDirectionChecked(direction); -} } // namespace views diff --git a/chromium/ui/views/controls/webview/webview.cc b/chromium/ui/views/controls/webview/webview.cc index 7bef84b8c9e..0c523877565 100644 --- a/chromium/ui/views/controls/webview/webview.cc +++ b/chromium/ui/views/controls/webview/webview.cc @@ -331,14 +331,6 @@ void WebView::DidToggleFullscreenModeForTab(bool entered_fullscreen, ReattachForFullscreenChange(entered_fullscreen); } -void WebView::DidAttachInterstitialPage() { - NotifyAccessibilityWebContentsChanged(); -} - -void WebView::DidDetachInterstitialPage() { - NotifyAccessibilityWebContentsChanged(); -} - void WebView::OnWebContentsFocused( content::RenderWidgetHost* render_widget_host) { RequestFocus(); diff --git a/chromium/ui/views/controls/webview/webview.h b/chromium/ui/views/controls/webview/webview.h index 84d5cd520f7..624dcb1a4d8 100644 --- a/chromium/ui/views/controls/webview/webview.h +++ b/chromium/ui/views/controls/webview/webview.h @@ -149,8 +149,6 @@ class WEBVIEW_EXPORT WebView : public View, void DidDestroyFullscreenWidget() override; void DidToggleFullscreenModeForTab(bool entered_fullscreen, bool will_cause_resize) override; - void DidAttachInterstitialPage() override; - void DidDetachInterstitialPage() override; // Workaround for MSVC++ linker bug/feature that requires // instantiation of the inline IPC::Listener methods in all translation units. void OnChannelConnected(int32_t peer_id) override {} diff --git a/chromium/ui/views/controls/webview/webview_unittest.cc b/chromium/ui/views/controls/webview/webview_unittest.cc index 5bfd431308d..5c28d3091f8 100644 --- a/chromium/ui/views/controls/webview/webview_unittest.cc +++ b/chromium/ui/views/controls/webview/webview_unittest.cc @@ -159,8 +159,8 @@ class WebViewUnitTest : public views::test::WidgetTest { // child. top_level_widget_ = CreateTopLevelFramelessPlatformWidget(); top_level_widget_->SetBounds(gfx::Rect(0, 10, 100, 100)); - View* const contents_view = new View(); - top_level_widget_->SetContentsView(contents_view); + View* const contents_view = + top_level_widget_->SetContentsView(std::make_unique<View>()); web_view_ = new WebView(browser_context_.get()); web_view_->SetBoundsRect(gfx::Rect(contents_view->size())); contents_view->AddChildView(web_view_); |