diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2018-08-24 12:15:48 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2018-08-28 13:30:04 +0000 |
commit | b014812705fc80bff0a5c120dfcef88f349816dc (patch) | |
tree | 25a2e2d9fa285f1add86aa333389a839f81a39ae /chromium/ui/views/controls | |
parent | 9f4560b1027ae06fdb497023cdcaf91b8511fa74 (diff) | |
download | qtwebengine-chromium-b014812705fc80bff0a5c120dfcef88f349816dc.tar.gz |
BASELINE: Update Chromium to 68.0.3440.125
Change-Id: I23f19369e01f688e496f5bf179abb521ad73874f
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/ui/views/controls')
75 files changed, 2186 insertions, 1052 deletions
diff --git a/chromium/ui/views/controls/button/button.cc b/chromium/ui/views/controls/button/button.cc index 8a2e4dff278..5fa2f6f1ebb 100644 --- a/chromium/ui/views/controls/button/button.cc +++ b/chromium/ui/views/controls/button/button.cc @@ -22,6 +22,7 @@ #include "ui/views/controls/button/menu_button.h" #include "ui/views/controls/button/radio_button.h" #include "ui/views/controls/button/toggle_button.h" +#include "ui/views/controls/focus_ring.h" #include "ui/views/painter.h" #include "ui/views/style/platform_style.h" #include "ui/views/widget/widget.h" @@ -145,6 +146,13 @@ void Button::SetAnimationDuration(int duration) { hover_animation_.SetSlideDuration(duration); } +void Button::SetInstallFocusRingOnFocus(bool install) { + if (install) + focus_ring_ = FocusRing::Install(this); + else + focus_ring_.reset(); +} + void Button::SetHotTracked(bool is_hot_tracked) { if (state_ != STATE_DISABLED) SetState(is_hot_tracked ? STATE_HOVERED : STATE_NORMAL); @@ -443,7 +451,7 @@ void Button::OnBlur() { std::unique_ptr<InkDrop> Button::CreateInkDrop() { std::unique_ptr<views::InkDropImpl> ink_drop = CreateDefaultInkDropImpl(); - ink_drop->SetShowHighlightOnFocus(true); + ink_drop->SetShowHighlightOnFocus(!focus_ring_); ink_drop->SetAutoHighlightModeForPlatform(); return std::move(ink_drop); } diff --git a/chromium/ui/views/controls/button/button.h b/chromium/ui/views/controls/button/button.h index 3942d790bb4..6f0ce20627b 100644 --- a/chromium/ui/views/controls/button/button.h +++ b/chromium/ui/views/controls/button/button.h @@ -13,6 +13,7 @@ #include "ui/native_theme/native_theme.h" #include "ui/views/animation/ink_drop_host_view.h" #include "ui/views/animation/ink_drop_state.h" +#include "ui/views/controls/focus_ring.h" #include "ui/views/painter.h" namespace views { @@ -143,6 +144,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, void set_has_ink_drop_action_on_click(bool has_ink_drop_action_on_click) { has_ink_drop_action_on_click_ = has_ink_drop_action_on_click; } + void SetInstallFocusRingOnFocus(bool install_focus_ring_on_focus); void SetHotTracked(bool is_hot_tracked); bool IsHotTracked() const; @@ -291,6 +293,9 @@ class VIEWS_EXPORT Button : public InkDropHostView, // The color of the ripple and hover. SkColor ink_drop_base_color_; + // The focus ring for this Button. + std::unique_ptr<FocusRing> focus_ring_; + std::unique_ptr<Painter> focus_painter_; DISALLOW_COPY_AND_ASSIGN(Button); diff --git a/chromium/ui/views/controls/button/checkbox.cc b/chromium/ui/views/controls/button/checkbox.cc index ecdcda3d503..cfe84643274 100644 --- a/chromium/ui/views/controls/button/checkbox.cc +++ b/chromium/ui/views/controls/button/checkbox.cc @@ -30,47 +30,6 @@ namespace views { -constexpr int kFocusRingThicknessDip = 2; - -// View used to paint the focus ring around the Checkbox icon. -// The icon is painted separately. -class IconFocusRing : public View { - public: - explicit IconFocusRing(Checkbox* checkbox); - ~IconFocusRing() override = default; - - private: - // View: - void Layout() override; - void OnPaint(gfx::Canvas* canvas) override; - - Checkbox* checkbox_; - - DISALLOW_COPY_AND_ASSIGN(IconFocusRing); -}; - -IconFocusRing::IconFocusRing(Checkbox* checkbox) : checkbox_(checkbox) { - FocusRing::InitFocusRing(this); -} - -void IconFocusRing::Layout() { - gfx::Rect focus_bounds = checkbox_->image()->bounds(); - focus_bounds.Inset(-kFocusRingThicknessDip, -kFocusRingThicknessDip); - SetBoundsRect(focus_bounds); -} - -void IconFocusRing::OnPaint(gfx::Canvas* canvas) { - cc::PaintFlags focus_flags; - focus_flags.setAntiAlias(true); - focus_flags.setColor( - SkColorSetA(GetNativeTheme()->GetSystemColor( - ui::NativeTheme::kColorId_FocusedBorderColor), - 0x66)); - focus_flags.setStyle(cc::PaintFlags::kStroke_Style); - focus_flags.setStrokeWidth(2); - checkbox_->PaintFocusRing(this, canvas, focus_flags); -} - // static const char Checkbox::kViewClassName[] = "Checkbox"; @@ -88,9 +47,7 @@ Checkbox::Checkbox(const base::string16& label, bool force_md) set_request_focus_on_press(false); SetInkDropMode(InkDropMode::ON); set_has_ink_drop_action_on_click(true); - focus_ring_ = new IconFocusRing(this); - focus_ring_->SetVisible(false); - AddChildView(focus_ring_); + focus_ring_ = FocusRing::Install(this); } else { std::unique_ptr<LabelButtonBorder> button_border(new LabelButtonBorder()); // Inset the trailing side by a couple pixels for the focus border. @@ -200,16 +157,12 @@ void Checkbox::OnFocus() { LabelButton::OnFocus(); if (!UseMd()) UpdateImage(); - else - focus_ring_->SetVisible(true); } void Checkbox::OnBlur() { LabelButton::OnBlur(); if (!UseMd()) UpdateImage(); - else - focus_ring_->SetVisible(false); } void Checkbox::OnNativeThemeChanged(const ui::NativeTheme* theme) { @@ -262,6 +215,20 @@ std::unique_ptr<LabelButtonBorder> Checkbox::CreateDefaultBorder() const { return border; } +void Checkbox::Layout() { + LabelButton::Layout(); + if (focus_ring_ && !image()->bounds().IsEmpty()) + focus_ring_->SetPath(GetFocusRingPath()); +} + +SkPath Checkbox::GetFocusRingPath() const { + SkPath path; + gfx::Rect bounds = image()->bounds(); + bounds.Inset(1, 1); + path.addRect(RectToSkRect(bounds)); + return path; +} + void Checkbox::SetCustomImage(bool checked, bool focused, ButtonState for_state, @@ -272,14 +239,6 @@ void Checkbox::SetCustomImage(bool checked, UpdateImage(); } -void Checkbox::PaintFocusRing(View* view, - gfx::Canvas* canvas, - const cc::PaintFlags& flags) { - gfx::RectF bounds(view->GetLocalBounds()); - bounds.Inset(kFocusRingThicknessDip, kFocusRingThicknessDip); - canvas->DrawRoundRect(bounds, kFocusRingThicknessDip, flags); -} - const gfx::VectorIcon& Checkbox::GetVectorIcon() const { return checked() ? kCheckboxActiveIcon : kCheckboxNormalIcon; } diff --git a/chromium/ui/views/controls/button/checkbox.h b/chromium/ui/views/controls/button/checkbox.h index ca8da3cb2af..de681d69271 100644 --- a/chromium/ui/views/controls/button/checkbox.h +++ b/chromium/ui/views/controls/button/checkbox.h @@ -12,6 +12,7 @@ #include "base/strings/string16.h" #include "cc/paint/paint_flags.h" #include "ui/views/controls/button/label_button.h" +#include "ui/views/controls/focus_ring.h" namespace gfx { struct VectorIcon; @@ -63,6 +64,7 @@ class VIEWS_EXPORT Checkbox : public LabelButton { SkColor GetInkDropBaseColor() const override; gfx::ImageSkia GetImage(ButtonState for_state) const override; std::unique_ptr<LabelButtonBorder> CreateDefaultBorder() const override; + void Layout() override; // Set the image shown for each button state depending on whether it is // [checked] or [focused]. @@ -71,14 +73,12 @@ class VIEWS_EXPORT Checkbox : public LabelButton { ButtonState for_state, const gfx::ImageSkia& image); - // Paints a focus indicator for the view. Overridden in RadioButton. - virtual void PaintFocusRing(View* view, - gfx::Canvas* canvas, - const cc::PaintFlags& flags); - // Gets the vector icon to use based on the current state of |checked_|. virtual const gfx::VectorIcon& GetVectorIcon() const; + // Returns the path to draw the focus ring around for this Checkbox. + virtual SkPath GetFocusRingPath() const; + private: friend class IconFocusRing; @@ -97,15 +97,15 @@ class VIEWS_EXPORT Checkbox : public LabelButton { // True if the checkbox is checked. bool checked_; - // FocusRing used in MD mode - View* focus_ring_ = nullptr; - // The images for each button node_data. gfx::ImageSkia images_[2][2][STATE_COUNT]; // The unique id for the associated label's accessible object. int32_t label_ax_id_; + // The focus ring to use for this Checkbox. + std::unique_ptr<FocusRing> focus_ring_; + bool use_md_; DISALLOW_COPY_AND_ASSIGN(Checkbox); diff --git a/chromium/ui/views/controls/button/image_button.cc b/chromium/ui/views/controls/button/image_button.cc index 6d945e4579c..677d4dc3d84 100644 --- a/chromium/ui/views/controls/button/image_button.cc +++ b/chromium/ui/views/controls/button/image_button.cc @@ -83,6 +83,13 @@ void ImageButton::SetImageAlignment(HorizontalAlignment h_align, SchedulePaint(); } +void ImageButton::SetBackgroundImageAlignment(HorizontalAlignment h_align, + VerticalAlignment v_align) { + h_background_alignment_ = h_align; + v_background_alignment_ = v_align; + SchedulePaint(); +} + void ImageButton::SetMinimumImageSize(const gfx::Size& size) { if (minimum_image_size_ == size) return; @@ -138,10 +145,21 @@ void ImageButton::PaintButtonContents(gfx::Canvas* canvas) { canvas->Scale(-1, 1); } - gfx::Point position = ComputeImagePaintPosition(img); - if (!background_image_.isNull()) - canvas->DrawImageInt(background_image_, position.x(), position.y()); + if (!background_image_.isNull()) { + // If the background image alignment was not set, use the image + // alignment. + HorizontalAlignment h_alignment = + h_background_alignment_.value_or(h_alignment_); + VerticalAlignment v_alignment = + v_background_alignment_.value_or(v_alignment_); + gfx::Point background_position = ComputeImagePaintPosition( + background_image_, h_alignment, v_alignment); + canvas->DrawImageInt(background_image_, background_position.x(), + background_position.y()); + } + gfx::Point position = + ComputeImagePaintPosition(img, h_alignment_, v_alignment_); canvas->DrawImageInt(img, position.x(), position.y()); } } @@ -166,11 +184,13 @@ gfx::ImageSkia ImageButton::GetImageToPaint() { //////////////////////////////////////////////////////////////////////////////// // ImageButton, private: -gfx::Point ImageButton::ComputeImagePaintPosition(const gfx::ImageSkia& image) { +const gfx::Point ImageButton::ComputeImagePaintPosition( + const gfx::ImageSkia& image, + HorizontalAlignment h_alignment, + VerticalAlignment v_alignment) { int x = 0, y = 0; gfx::Rect rect = GetContentsBounds(); - HorizontalAlignment h_alignment = h_alignment_; if (draw_image_mirrored_) { if (h_alignment == ALIGN_RIGHT) h_alignment = ALIGN_LEFT; @@ -185,7 +205,7 @@ gfx::Point ImageButton::ComputeImagePaintPosition(const gfx::ImageSkia& image) { if (v_alignment_ == ALIGN_MIDDLE) y = (rect.height() - image.height()) / 2; - else if (v_alignment_ == ALIGN_BOTTOM) + else if (v_alignment == ALIGN_BOTTOM) y = rect.height() - image.height(); x += rect.x(); diff --git a/chromium/ui/views/controls/button/image_button.h b/chromium/ui/views/controls/button/image_button.h index fb73e6c1e06..bc69e6e350b 100644 --- a/chromium/ui/views/controls/button/image_button.h +++ b/chromium/ui/views/controls/button/image_button.h @@ -60,6 +60,10 @@ class VIEWS_EXPORT ImageButton : public Button { void SetImageAlignment(HorizontalAlignment h_align, VerticalAlignment v_align); + // Sets how the background is laid out within the button's bounds. + void SetBackgroundImageAlignment(HorizontalAlignment h_align, + VerticalAlignment v_align); + // The minimum size of the contents (not including the border). The contents // will be at least this size, but may be larger if the image itself is // larger. @@ -97,17 +101,25 @@ class VIEWS_EXPORT ImageButton : public Button { FRIEND_TEST_ALL_PREFIXES(ImageButtonTest, ImagePositionWithBorder); FRIEND_TEST_ALL_PREFIXES(ImageButtonTest, LeftAlignedMirrored); FRIEND_TEST_ALL_PREFIXES(ImageButtonTest, RightAlignedMirrored); + FRIEND_TEST_ALL_PREFIXES(ImageButtonTest, ImagePositionWithBackground); FRIEND_TEST_ALL_PREFIXES(ImageButtonFactoryTest, CreateVectorImageButton); // Returns the correct position of the image for painting. - gfx::Point ComputeImagePaintPosition(const gfx::ImageSkia& image); + const gfx::Point ComputeImagePaintPosition(const gfx::ImageSkia& image, + HorizontalAlignment h_alignment, + VerticalAlignment v_alignment); // Image alignment. HorizontalAlignment h_alignment_; VerticalAlignment v_alignment_; gfx::Size minimum_image_size_; + // Background alignment. If these are not set, the background image uses the + // image alignment. + base::Optional<HorizontalAlignment> h_background_alignment_; + base::Optional<VerticalAlignment> v_background_alignment_; + // Whether we draw our resources horizontally flipped. This can happen in the // linux titlebar, where image resources were designed to be flipped so a // small curved corner in the close button designed to fit into the frame 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 52728ff3516..6a2e776d07d 100644 --- a/chromium/ui/views/controls/button/image_button_factory_unittest.cc +++ b/chromium/ui/views/controls/button/image_button_factory_unittest.cc @@ -26,14 +26,14 @@ TEST_F(ImageButtonFactoryTest, CreateVectorImageButton) { TEST_F(ImageButtonFactoryTest, SetImageFromVectorIcon) { ImageButton* button = CreateVectorImageButton(nullptr); - SetImageFromVectorIcon(button, vector_icons::kClose16Icon, SK_ColorRED); + SetImageFromVectorIcon(button, vector_icons::kCloseRoundedIcon, SK_ColorRED); EXPECT_FALSE(button->GetImage(Button::STATE_NORMAL).isNull()); EXPECT_FALSE(button->GetImage(Button::STATE_DISABLED).isNull()); EXPECT_EQ(color_utils::DeriveDefaultIconColor(SK_ColorRED), button->GetInkDropBaseColor()); // Default to GoogleGrey900. - SetImageFromVectorIcon(button, vector_icons::kClose16Icon); + SetImageFromVectorIcon(button, vector_icons::kCloseRoundedIcon); EXPECT_EQ(color_utils::DeriveDefaultIconColor(gfx::kGoogleGrey900), button->GetInkDropBaseColor()); delete button; diff --git a/chromium/ui/views/controls/button/image_button_unittest.cc b/chromium/ui/views/controls/button/image_button_unittest.cc index baf9a75d18b..4c595a5d7ef 100644 --- a/chromium/ui/views/controls/button/image_button_unittest.cc +++ b/chromium/ui/views/controls/button/image_button_unittest.cc @@ -39,6 +39,13 @@ class Parent : public views::View { namespace views { +namespace { +const ImageButton::HorizontalAlignment kDefaultHorizontalAlignment = + ImageButton::ALIGN_LEFT; +const ImageButton::VerticalAlignment kDefaultVerticalAlignment = + ImageButton::ALIGN_TOP; +} // namespace + typedef ViewsTestBase ImageButtonTest; TEST_F(ImageButtonTest, Basics) { @@ -122,24 +129,39 @@ TEST_F(ImageButtonTest, ImagePositionWithBorder) { // The image should be painted at the top-left corner. EXPECT_EQ(gfx::Point().ToString(), - button.ComputeImagePaintPosition(image).ToString()); + button + .ComputeImagePaintPosition(image, kDefaultHorizontalAlignment, + kDefaultVerticalAlignment) + .ToString()); button.SetBorder(views::CreateEmptyBorder(10, 5, 0, 0)); EXPECT_EQ(gfx::Point(5, 10).ToString(), - button.ComputeImagePaintPosition(image).ToString()); + button + .ComputeImagePaintPosition(image, kDefaultHorizontalAlignment, + kDefaultVerticalAlignment) + .ToString()); button.SetBorder(NullBorder()); button.SetBounds(0, 0, 50, 50); EXPECT_EQ(gfx::Point().ToString(), - button.ComputeImagePaintPosition(image).ToString()); + button + .ComputeImagePaintPosition(image, kDefaultHorizontalAlignment, + kDefaultVerticalAlignment) + .ToString()); button.SetImageAlignment(ImageButton::ALIGN_CENTER, ImageButton::ALIGN_MIDDLE); EXPECT_EQ(gfx::Point(15, 10).ToString(), - button.ComputeImagePaintPosition(image).ToString()); + button + .ComputeImagePaintPosition(image, ImageButton::ALIGN_CENTER, + ImageButton::ALIGN_MIDDLE) + .ToString()); button.SetBorder(views::CreateEmptyBorder(10, 10, 0, 0)); EXPECT_EQ(gfx::Point(20, 15).ToString(), - button.ComputeImagePaintPosition(image).ToString()); + button + .ComputeImagePaintPosition(image, ImageButton::ALIGN_CENTER, + ImageButton::ALIGN_MIDDLE) + .ToString()); // The entire button's size should take the border into account. EXPECT_EQ(gfx::Size(30, 40).ToString(), button.GetPreferredSize().ToString()); @@ -161,7 +183,10 @@ TEST_F(ImageButtonTest, LeftAlignedMirrored) { // Because the coordinates are flipped, we should expect this to draw as if // it were ALIGN_RIGHT. EXPECT_EQ(gfx::Point(30, 0).ToString(), - button.ComputeImagePaintPosition(image).ToString()); + button + .ComputeImagePaintPosition(image, ImageButton::ALIGN_LEFT, + ImageButton::ALIGN_BOTTOM) + .ToString()); } TEST_F(ImageButtonTest, RightAlignedMirrored) { @@ -176,7 +201,10 @@ TEST_F(ImageButtonTest, RightAlignedMirrored) { // Because the coordinates are flipped, we should expect this to draw as if // it were ALIGN_LEFT. EXPECT_EQ(gfx::Point(0, 0).ToString(), - button.ComputeImagePaintPosition(image).ToString()); + button + .ComputeImagePaintPosition(image, ImageButton::ALIGN_RIGHT, + ImageButton::ALIGN_BOTTOM) + .ToString()); } TEST_F(ImageButtonTest, PreferredSizeInvalidation) { diff --git a/chromium/ui/views/controls/button/label_button.cc b/chromium/ui/views/controls/button/label_button.cc index d56f4cb2f73..60907bd19cd 100644 --- a/chromium/ui/views/controls/button/label_button.cc +++ b/chromium/ui/views/controls/button/label_button.cc @@ -398,7 +398,7 @@ std::unique_ptr<views::InkDropHighlight> LabelButton::CreateInkDropHighlight() const { return ShouldUseFloodFillInkDrop() ? std::make_unique<views::InkDropHighlight>( - size(), kInkDropSmallCornerRadius, + size(), ink_drop_small_corner_radius(), gfx::RectF(GetLocalBounds()).CenterPoint(), GetInkDropBaseColor()) : CreateDefaultInkDropHighlight( diff --git a/chromium/ui/views/controls/button/md_text_button.cc b/chromium/ui/views/controls/button/md_text_button.cc index 60f6ca0ffb3..e3c0f6e9dc2 100644 --- a/chromium/ui/views/controls/button/md_text_button.cc +++ b/chromium/ui/views/controls/button/md_text_button.cc @@ -77,6 +77,7 @@ MdTextButton* MdTextButton::Create(ButtonListener* listener, MdTextButton* button = new MdTextButton(listener, button_context); button->SetText(text); button->SetFocusForPlatform(); + return button; } @@ -95,6 +96,11 @@ void MdTextButton::SetBgColorOverride(const base::Optional<SkColor>& color) { UpdateColors(); } +void MdTextButton::set_corner_radius(float radius) { + corner_radius_ = radius; + set_ink_drop_corner_radii(corner_radius_, corner_radius_); +} + void MdTextButton::OnPaintBackground(gfx::Canvas* canvas) { LabelButton::OnPaintBackground(canvas); if (hover_animation().is_animating() || state() == STATE_HOVERED) { @@ -104,16 +110,6 @@ void MdTextButton::OnPaintBackground(gfx::Canvas* canvas) { } } -void MdTextButton::OnFocus() { - LabelButton::OnFocus(); - FocusRing::Install(this); -} - -void MdTextButton::OnBlur() { - LabelButton::OnBlur(); - FocusRing::Uninstall(this); -} - void MdTextButton::OnNativeThemeChanged(const ui::NativeTheme* theme) { LabelButton::OnNativeThemeChanged(theme); UpdateColors(); @@ -179,16 +175,17 @@ void MdTextButton::UpdateStyleToIndicateDefaultStatus() { MdTextButton::MdTextButton(ButtonListener* listener, int button_context) : LabelButton(listener, base::string16(), button_context), - is_prominent_(false), - corner_radius_(kInkDropSmallCornerRadius) { + is_prominent_(false) { SetInkDropMode(InkDropMode::ON); set_has_ink_drop_action_on_click(true); + set_corner_radius(LayoutProvider::Get()->GetCornerRadiusMetric(EMPHASIS_LOW)); SetHorizontalAlignment(gfx::ALIGN_CENTER); SetFocusForPlatform(); const int minimum_width = LayoutProvider::Get()->GetDistanceMetric( DISTANCE_DIALOG_BUTTON_MINIMUM_WIDTH); SetMinSize(gfx::Size(minimum_width, 0)); SetFocusPainter(nullptr); + SetInstallFocusRingOnFocus(true); label()->SetAutoColorReadabilityEnabled(false); set_request_focus_on_press(false); @@ -226,8 +223,9 @@ void MdTextButton::UpdatePadding() { // GetControlHeightForFont(). It can't because that only returns a correct // result with --secondary-ui-md, and MdTextButtons appear in top chrome // without that. - const int kBaseHeight = 28; - int target_height = std::max(kBaseHeight + size_delta * 2, + const int base_height = + ui::MaterialDesignController::IsNewerMaterialUi() ? 32 : 28; + int target_height = std::max(base_height + size_delta * 2, label()->font_list().GetFontSize() * 2); int label_height = label()->GetPreferredSize().height(); diff --git a/chromium/ui/views/controls/button/md_text_button.h b/chromium/ui/views/controls/button/md_text_button.h index 25f65ce5e87..eafa8a817ba 100644 --- a/chromium/ui/views/controls/button/md_text_button.h +++ b/chromium/ui/views/controls/button/md_text_button.h @@ -9,6 +9,7 @@ #include "base/optional.h" #include "ui/views/controls/button/label_button.h" +#include "ui/views/controls/focus_ring.h" #include "ui/views/style/typography.h" namespace views { @@ -36,14 +37,12 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { // Override the default corner radius of the round rect used for the // background and ink drop effects. - void set_corner_radius(float radius) { corner_radius_ = radius; } + void set_corner_radius(float radius); // View: void OnPaintBackground(gfx::Canvas* canvas) override; // LabelButton: - void OnFocus() override; - void OnBlur() override; void OnNativeThemeChanged(const ui::NativeTheme* theme) override; std::unique_ptr<InkDrop> CreateInkDrop() override; std::unique_ptr<views::InkDropRipple> CreateInkDropRipple() const override; diff --git a/chromium/ui/views/controls/button/menu_button.cc b/chromium/ui/views/controls/button/menu_button.cc index b6996aab742..7f38894e57c 100644 --- a/chromium/ui/views/controls/button/menu_button.cc +++ b/chromium/ui/views/controls/button/menu_button.cc @@ -287,7 +287,7 @@ bool MenuButton::OnKeyReleased(const ui::KeyEvent& event) { void MenuButton::GetAccessibleNodeData(ui::AXNodeData* node_data) { Button::GetAccessibleNodeData(node_data); node_data->role = ax::mojom::Role::kPopUpButton; - node_data->AddState(ax::mojom::State::kHaspopup); + node_data->SetHasPopup(ax::mojom::HasPopup::kMenu); if (enabled()) node_data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kOpen); } @@ -385,7 +385,8 @@ void MenuButton::DecrementPressedLocked() { if (should_disable_after_press_) { desired_state = STATE_DISABLED; should_disable_after_press_ = false; - } else if (ShouldEnterHoveredState()) { + } else if (GetWidget() && !GetWidget()->dragged_view() && + ShouldEnterHoveredState()) { desired_state = STATE_HOVERED; GetInkDrop()->SetHovered(true); } diff --git a/chromium/ui/views/controls/button/radio_button.cc b/chromium/ui/views/controls/button/radio_button.cc index e8499db3311..efadd78b3e9 100644 --- a/chromium/ui/views/controls/button/radio_button.cc +++ b/chromium/ui/views/controls/button/radio_button.cc @@ -157,15 +157,14 @@ void RadioButton::SetChecked(bool checked) { Checkbox::SetChecked(checked); } -void RadioButton::PaintFocusRing(View* view, - gfx::Canvas* canvas, - const cc::PaintFlags& flags) { - canvas->DrawCircle(gfx::RectF(view->GetLocalBounds()).CenterPoint(), - image()->width() / 2, flags); -} - const gfx::VectorIcon& RadioButton::GetVectorIcon() const { return checked() ? kRadioButtonActiveIcon : kRadioButtonNormalIcon; } +SkPath RadioButton::GetFocusRingPath() const { + SkPath path; + path.addOval(gfx::RectToSkRect(image()->bounds())); + return path; +} + } // namespace views diff --git a/chromium/ui/views/controls/button/radio_button.h b/chromium/ui/views/controls/button/radio_button.h index b91ed088182..02cf91dcf8d 100644 --- a/chromium/ui/views/controls/button/radio_button.h +++ b/chromium/ui/views/controls/button/radio_button.h @@ -8,6 +8,7 @@ #include "base/macros.h" #include "base/strings/string16.h" #include "ui/views/controls/button/checkbox.h" +#include "ui/views/controls/focus_ring.h" namespace views { @@ -38,10 +39,8 @@ class VIEWS_EXPORT RadioButton : public Checkbox { // Overridden from Checkbox: void SetChecked(bool checked) override; - void PaintFocusRing(View* view, - gfx::Canvas* canvas, - const cc::PaintFlags& flags) override; const gfx::VectorIcon& GetVectorIcon() const override; + SkPath GetFocusRingPath() const override; private: DISALLOW_COPY_AND_ASSIGN(RadioButton); diff --git a/chromium/ui/views/controls/combobox/combobox.cc b/chromium/ui/views/controls/combobox/combobox.cc index 67f9af2eaee..2cb22fea6dc 100644 --- a/chromium/ui/views/controls/combobox/combobox.cc +++ b/chromium/ui/views/controls/combobox/combobox.cc @@ -462,6 +462,9 @@ Combobox::Combobox(ui::ComboboxModel* model, Style style) arrow_image_ = *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( IDR_MENU_DROPARROW); } + + if (UseMd()) + focus_ring_ = FocusRing::Install(this); } Combobox::~Combobox() { @@ -533,11 +536,9 @@ void Combobox::SetInvalid(bool invalid) { invalid_ = invalid; - if (HasFocus() && UseMd()) { - FocusRing::Install(this, invalid_ - ? ui::NativeTheme::kColorId_AlertSeverityHigh - : ui::NativeTheme::kColorId_NumColors); - } + if (focus_ring_) + focus_ring_->SetInvalid(invalid); + UpdateBorder(); SchedulePaint(); } @@ -746,11 +747,6 @@ void Combobox::OnFocus() { View::OnFocus(); // Border renders differently when focused. SchedulePaint(); - if (UseMd()) { - FocusRing::Install(this, invalid_ - ? ui::NativeTheme::kColorId_AlertSeverityHigh - : ui::NativeTheme::kColorId_NumColors); - } } void Combobox::OnBlur() { @@ -761,8 +757,6 @@ void Combobox::OnBlur() { selector_->OnViewBlur(); // Border renders differently when focused. SchedulePaint(); - if (UseMd()) - FocusRing::Uninstall(this); } void Combobox::GetAccessibleNodeData(ui::AXNodeData* node_data) { diff --git a/chromium/ui/views/controls/combobox/combobox.h b/chromium/ui/views/controls/combobox/combobox.h index 7977c36feda..d6132543edd 100644 --- a/chromium/ui/views/controls/combobox/combobox.h +++ b/chromium/ui/views/controls/combobox/combobox.h @@ -11,6 +11,7 @@ #include "base/time/time.h" #include "ui/base/models/combobox_model.h" #include "ui/views/controls/button/button.h" +#include "ui/views/controls/focus_ring.h" #include "ui/views/controls/prefix_delegate.h" namespace gfx { @@ -113,7 +114,7 @@ class VIEWS_EXPORT Combobox : public View, void SetSelectedRow(int row) override; base::string16 GetTextForRow(int row) override; - // Overriden from ButtonListener: + // Overridden from ButtonListener: void ButtonPressed(Button* sender, const ui::Event& event) override; protected: @@ -165,6 +166,9 @@ class VIEWS_EXPORT Combobox : public View, // Returns the width of the combobox's arrow container. int GetArrowContainerWidth() const; + // Returns the color to use for the combobox's focus ring. + SkColor GetFocusRingColor() const; + // Optionally used to tie the lifetime of the model to this combobox. See // constructor. std::unique_ptr<ui::ComboboxModel> owned_model_; @@ -232,6 +236,9 @@ class VIEWS_EXPORT Combobox : public View, // true, the parent view must relayout in ChildPreferredSizeChanged(). bool size_to_largest_label_; + // The focus ring for this Combobox. + std::unique_ptr<FocusRing> focus_ring_; + // Used for making calbacks. base::WeakPtrFactory<Combobox> weak_ptr_factory_; diff --git a/chromium/ui/views/controls/focus_ring.cc b/chromium/ui/views/controls/focus_ring.cc index 1cf8348a308..c17bc868e2f 100644 --- a/chromium/ui/views/controls/focus_ring.cc +++ b/chromium/ui/views/controls/focus_ring.cc @@ -5,70 +5,52 @@ #include "ui/views/controls/focus_ring.h" #include "ui/gfx/canvas.h" - -namespace views { +#include "ui/views/controls/focusable_border.h" +#include "ui/views/style/platform_style.h" namespace { -// The default stroke width of the focus border in dp. -constexpr float kFocusHaloThicknessDp = 2.f; - -FocusRing* GetFocusRing(View* parent) { - for (int i = 0; i < parent->child_count(); ++i) { - if (parent->child_at(i)->GetClassName() == FocusRing::kViewClassName) - return static_cast<FocusRing*>(parent->child_at(i)); - } - return nullptr; +ui::NativeTheme::ColorId ColorIdForValidity(bool valid) { + return valid ? ui::NativeTheme::kColorId_FocusedBorderColor + : ui::NativeTheme::kColorId_AlertSeverityHigh; } } // namespace +namespace views { + const char FocusRing::kViewClassName[] = "FocusRing"; // static -FocusRing* FocusRing::Install(View* parent, - SkColor color, - float corner_radius) { - FocusRing* ring = GetFocusRing(parent); - if (!ring) { - ring = new FocusRing(color, corner_radius); - parent->AddChildView(ring); - } else { - ring->color_ = color; - ring->corner_radius_ = corner_radius; - } +std::unique_ptr<FocusRing> FocusRing::Install(View* parent) { + auto ring = base::WrapUnique<FocusRing>(new FocusRing(parent)); + ring->set_owned_by_client(); + parent->AddChildView(ring.get()); + parent->AddObserver(ring.get()); ring->Layout(); ring->SchedulePaint(); return ring; } // static -FocusRing* FocusRing::Install(View* parent, - ui::NativeTheme::ColorId override_color_id) { - SkColor ring_color = parent->GetNativeTheme()->GetSystemColor( - override_color_id == ui::NativeTheme::kColorId_NumColors - ? ui::NativeTheme::kColorId_FocusedBorderColor - : override_color_id); - FocusRing* ring = Install(parent, ring_color); - DCHECK(ring); - ring->override_color_id_ = override_color_id; - if (!ring->view_observer_.IsObserving(parent)) - ring->view_observer_.Add(parent); - return ring; +bool FocusRing::IsPathUseable(const SkPath& path) { + return path.isRect(nullptr) || path.isOval(nullptr) || path.isRRect(nullptr); } -// static -void FocusRing::Uninstall(View* parent) { - delete GetFocusRing(parent); +void FocusRing::SetPath(const SkPath& path) { + DCHECK(IsPathUseable(path)); + path_ = path; + SchedulePaint(); } -// static -void FocusRing::InitFocusRing(View* view) { - // A layer is necessary to paint beyond the parent's bounds. - view->SetPaintToLayer(); - view->layer()->SetFillsBoundsOpaquely(false); - // Don't allow the view to process events. - view->set_can_process_events_within_subtree(false); +void FocusRing::SetInvalid(bool invalid) { + invalid_ = invalid; + SchedulePaint(); +} + +void FocusRing::SetHasFocusPredicate(const ViewPredicate& predicate) { + has_focus_predicate_ = predicate; + SchedulePaint(); } const char* FocusRing::GetClassName() const { @@ -79,37 +61,87 @@ void FocusRing::Layout() { // The focus ring handles its own sizing, which is simply to fill the parent // and extend a little beyond its borders. gfx::Rect focus_bounds = parent()->GetLocalBounds(); - focus_bounds.Inset(gfx::Insets(-kFocusHaloThicknessDp)); + focus_bounds.Inset(gfx::Insets(PlatformStyle::kFocusHaloInset)); SetBoundsRect(focus_bounds); } void FocusRing::OnPaint(gfx::Canvas* canvas) { - cc::PaintFlags flags; - flags.setAntiAlias(true); - flags.setColor(SkColorSetA(color_, 0x66)); - flags.setStyle(cc::PaintFlags::kStroke_Style); - flags.setStrokeWidth(kFocusHaloThicknessDp); - gfx::RectF rect(GetLocalBounds()); - rect.Inset(gfx::InsetsF(kFocusHaloThicknessDp / 2.f)); - // The focus indicator should hug the normal border, when present (as in the - // case of text buttons). Since it's drawn outside the parent view, increase - // the rounding slightly by adding half the ring thickness. - canvas->DrawRoundRect(rect, corner_radius_ + kFocusHaloThicknessDp / 2.f, - flags); + if (!has_focus_predicate_(parent())) + return; + + SkColor base_color = + GetNativeTheme()->GetSystemColor(ColorIdForValidity(!invalid_)); + + cc::PaintFlags paint; + paint.setAntiAlias(true); + paint.setColor(SkColorSetA(base_color, 0x66)); + paint.setStyle(cc::PaintFlags::kStroke_Style); + paint.setStrokeWidth(PlatformStyle::kFocusHaloThickness); + + SkPath path = path_; + if (path.isEmpty()) + path.addRect(RectToSkRect(parent()->GetLocalBounds())); + + DCHECK(IsPathUseable(path)); + SkRect bounds; + SkRRect rbounds; + if (path.isRect(&bounds)) { + canvas->sk_canvas()->drawRRect(RingRectFromPathRect(bounds), paint); + } else if (path.isOval(&bounds)) { + gfx::RectF rect = gfx::SkRectToRectF(bounds); + View::ConvertRectToTarget(view_, this, &rect); + canvas->sk_canvas()->drawRRect(SkRRect::MakeOval(gfx::RectFToSkRect(rect)), + paint); + } else if (path.isRRect(&rbounds)) { + canvas->sk_canvas()->drawRRect(RingRectFromPathRect(rbounds), paint); + } } -void FocusRing::OnViewNativeThemeChanged(View* observed_view) { - if (override_color_id_) { - color_ = observed_view->GetNativeTheme()->GetSystemColor( - override_color_id_.value()); - } +void FocusRing::OnViewFocused(View* view) { + SchedulePaint(); } -FocusRing::FocusRing(SkColor color, float corner_radius) - : color_(color), corner_radius_(corner_radius), view_observer_(this) { - InitFocusRing(this); +void FocusRing::OnViewBlurred(View* view) { + SchedulePaint(); } -FocusRing::~FocusRing() {} +FocusRing::FocusRing(View* parent) : view_(parent) { + // A layer is necessary to paint beyond the parent's bounds. + SetPaintToLayer(); + layer()->SetFillsBoundsOpaquely(false); + // Don't allow the view to process events. + set_can_process_events_within_subtree(false); + + has_focus_predicate_ = [](View* p) -> bool { return p->HasFocus(); }; +} + +FocusRing::~FocusRing() { + if (parent()) + parent()->RemoveObserver(this); +} + +SkRRect FocusRing::RingRectFromPathRect(const SkRect& rect) const { + double thickness = PlatformStyle::kFocusHaloThickness / 2.f; + double corner_radius = FocusableBorder::kCornerRadiusDp + thickness; + return RingRectFromPathRect( + SkRRect::MakeRectXY(rect, corner_radius, corner_radius)); +} + +SkRRect FocusRing::RingRectFromPathRect(const SkRRect& rrect) const { + double thickness = PlatformStyle::kFocusHaloThickness / 2.f; + gfx::RectF r = gfx::SkRectToRectF(rrect.rect()); + View::ConvertRectToTarget(view_, this, &r); + + SkRRect skr = + rrect.makeOffset(r.x() - rrect.rect().x(), r.y() - rrect.rect().y()); + + // The focus indicator should hug the normal border, when present (as in the + // case of text buttons). Since it's drawn outside the parent view, increase + // the rounding slightly by adding half the ring thickness. + skr.inset(PlatformStyle::kFocusHaloInset, PlatformStyle::kFocusHaloInset); + skr.inset(thickness, thickness); + + return skr; +} } // namespace views diff --git a/chromium/ui/views/controls/focus_ring.h b/chromium/ui/views/controls/focus_ring.h index df1f1b330ca..1b4d99ca2b1 100644 --- a/chromium/ui/views/controls/focus_ring.h +++ b/chromium/ui/views/controls/focus_ring.h @@ -17,31 +17,55 @@ namespace views { // 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); +// ... +// } +// +// 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. class VIEWS_EXPORT FocusRing : public View, public ViewObserver { public: static const char kViewClassName[]; - // Create a FocusRing and adds it to |parent|, or updates the one that already - // exists with the given |color| and |corner_radius|. - // TODO(crbug.com/831926): Prefer using the below Install() method - this one - // should eventually be removed. - static FocusRing* Install( - View* parent, - SkColor color, - float corner_radius = FocusableBorder::kCornerRadiusDp); + using ViewPredicate = std::function<bool(View* view)>; - // Similar to FocusRing::Install(View, SkColor, float), but - // |override_color_id| will be used in place of the default coloration - // when provided. - static FocusRing* Install(View* parent, - ui::NativeTheme::ColorId override_color_id = - ui::NativeTheme::kColorId_NumColors); + ~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); + + // Returns whether this class can draw a focus ring from |path|. Not all paths + // are useable since not all paths can be easily outset. + static bool IsPathUseable(const SkPath& path); - // Removes the FocusRing from |parent|. - static void Uninstall(View* parent); + // Sets the path to draw this FocusRing around. This path is in the parent + // view's coordinate system, *not* in the FocusRing's coordinate system. + void SetPath(const SkPath& path); - // Configure |view| for painting focus ring highlights. - static void InitFocusRing(View* view); + // Sets whether the FocusRing should show an invalid state for the View it + // encloses. + void SetInvalid(bool invalid); + + // Sets the predicate function used to tell when the parent has focus. The + // parent is passed into this predicate; it should return whether the parent + // should be treated as focused. This is useful when, for example, the parent + // wraps an inner view and the inner view is the one that actually receives + // focus, but the FocusRing sits on the parent instead of the inner view. + void SetHasFocusPredicate(const ViewPredicate& predicate); // View: const char* GetClassName() const override; @@ -49,17 +73,33 @@ class VIEWS_EXPORT FocusRing : public View, public ViewObserver { void OnPaint(gfx::Canvas* canvas) override; // ViewObserver: - void OnViewNativeThemeChanged(View* observed_view) override; - - protected: - FocusRing(SkColor color, float corner_radius); - ~FocusRing() override; + void OnViewFocused(View* view) override; + void OnViewBlurred(View* view) override; private: - SkColor color_; - float corner_radius_; - base::Optional<ui::NativeTheme::ColorId> override_color_id_; - ScopedObserver<View, FocusRing> view_observer_; + explicit FocusRing(View* parent); + + // Translates the provided SkRect or SkRRect, which is in the parent's + // coordinate system, into this view's coordinate system, then insets it + // appropriately to produce the focus ring "halo" effect. If the supplied rect + // is an SkRect, it will have the default focus ring corner radius applied as + // well. + SkRRect RingRectFromPathRect(const SkRect& rect) const; + SkRRect RingRectFromPathRect(const SkRRect& rect) const; + + // The View this focus ring is installed on. + View* view_ = nullptr; + + // The path to draw this focus ring around. IsPathUseable(path_) is always + // true. + SkPath path_; + + // Whether the enclosed View is in an invalid state, which controls whether + // the focus ring shows an invalid appearance (usually a different color). + bool invalid_ = false; + + // The predicate used to determine whether the parent has focus. + ViewPredicate has_focus_predicate_; DISALLOW_COPY_AND_ASSIGN(FocusRing); }; diff --git a/chromium/ui/views/controls/glow_hover_controller.cc b/chromium/ui/views/controls/glow_hover_controller.cc deleted file mode 100644 index ef08308f7e7..00000000000 --- a/chromium/ui/views/controls/glow_hover_controller.cc +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "ui/views/controls/glow_hover_controller.h" - -#include "ui/views/view.h" - -namespace views { - -// Amount to scale the opacity. -static const double kSubtleOpacityScale = 0.45; -static const double kPronouncedOpacityScale = 1.0; - -// How long the hover state takes. -static const int kTrackHoverDurationMs = 400; - -GlowHoverController::GlowHoverController(views::View* view) - : view_(view), - animation_(this), - opacity_scale_(kSubtleOpacityScale) { - animation_.set_delegate(this); -} - -GlowHoverController::~GlowHoverController() { -} - -void GlowHoverController::SetAnimationContainer( - gfx::AnimationContainer* container) { - animation_.SetContainer(container); -} - -void GlowHoverController::SetLocation(const gfx::Point& location) { - location_ = location; - if (ShouldDraw()) - view_->SchedulePaint(); -} - -void GlowHoverController::Show(Style style) { - switch (style) { - case SUBTLE: - opacity_scale_ = kSubtleOpacityScale; - animation_.SetSlideDuration(kTrackHoverDurationMs); - animation_.SetTweenType(gfx::Tween::EASE_OUT); - animation_.Show(); - break; - case PRONOUNCED: - opacity_scale_ = kPronouncedOpacityScale; - // Force the end state to show immediately. - animation_.Show(); - animation_.End(); - break; - } -} - -void GlowHoverController::Hide() { - animation_.SetTweenType(gfx::Tween::EASE_IN); - animation_.Hide(); -} - -void GlowHoverController::HideImmediately() { - if (ShouldDraw()) - view_->SchedulePaint(); - animation_.Reset(); -} - -double GlowHoverController::GetAnimationValue() const { - return animation_.GetCurrentValue(); -} - -SkAlpha GlowHoverController::GetAlpha() const { - return static_cast<SkAlpha>(animation_.CurrentValueBetween( - 0, gfx::ToRoundedInt(255 * opacity_scale_))); -} - -bool GlowHoverController::ShouldDraw() const { - return animation_.IsShowing() || animation_.is_animating(); -} - -void GlowHoverController::AnimationEnded(const gfx::Animation* animation) { - view_->SchedulePaint(); -} - -void GlowHoverController::AnimationProgressed(const gfx::Animation* animation) { - view_->SchedulePaint(); -} - -} // namespace views diff --git a/chromium/ui/views/controls/glow_hover_controller.h b/chromium/ui/views/controls/glow_hover_controller.h deleted file mode 100644 index 49db0a8c4d9..00000000000 --- a/chromium/ui/views/controls/glow_hover_controller.h +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef UI_VIEWS_CONTROLS_GLOW_HOVER_CONTROLLER_H_ -#define UI_VIEWS_CONTROLS_GLOW_HOVER_CONTROLLER_H_ - -#include "base/macros.h" -#include "ui/gfx/animation/animation_delegate.h" -#include "ui/gfx/animation/slide_animation.h" -#include "ui/views/views_export.h" - -namespace gfx { -class Point; -} - -namespace views { - -class View; - -// GlowHoverController is responsible for drawing a hover effect as is used by -// the tabstrip. Typical usage: -// OnMouseEntered() -> invoke Show(). -// OnMouseMoved() -> invoke SetLocation(). -// OnMouseExited() -> invoke Hide(). -// OnPaint() -> if ShouldDraw() returns true invoke Draw(). -// Internally GlowHoverController uses an animation to animate the glow and -// invokes SchedulePaint() back on the View as necessary. -class VIEWS_EXPORT GlowHoverController : public gfx::AnimationDelegate { - public: - enum Style { - SUBTLE, - PRONOUNCED - }; - - explicit GlowHoverController(views::View* view); - ~GlowHoverController() override; - - // Sets the AnimationContainer used by the animation. - void SetAnimationContainer(gfx::AnimationContainer* container); - - // Sets the location of the hover, relative to the View passed to the - // constructor. - void SetLocation(const gfx::Point& location); - - const gfx::Point& location() const { return location_; } - - // Initiates showing the hover. - void Show(Style style); - - // Hides the hover. - void Hide(); - - // Hides the hover immediately. - void HideImmediately(); - - // Returns the value of the animation. - double GetAnimationValue() const; - - SkAlpha GetAlpha() const; - - // Returns true if there is something to be drawn. Use this instead of - // invoking Draw() if creating |mask_image| is expensive. - bool ShouldDraw() const; - - // gfx::AnimationDelegate overrides: - void AnimationEnded(const gfx::Animation* animation) override; - void AnimationProgressed(const gfx::Animation* animation) override; - - private: - // View we're drawing to. - views::View* view_; - - // Opacity of the glow ramps up over time. - gfx::SlideAnimation animation_; - - // Location of the glow, relative to view. - gfx::Point location_; - double opacity_scale_; - - DISALLOW_COPY_AND_ASSIGN(GlowHoverController); -}; - -} // namespace views - -#endif // UI_VIEWS_CONTROLS_GLOW_HOVER_CONTROLLER_H_ diff --git a/chromium/ui/views/controls/image_view.cc b/chromium/ui/views/controls/image_view.cc index 059a83652f7..5b7f65a5522 100644 --- a/chromium/ui/views/controls/image_view.cc +++ b/chromium/ui/views/controls/image_view.cc @@ -29,10 +29,10 @@ void* GetBitmapPixels(const gfx::ImageSkia& img, float image_scale) { const char ImageView::kViewClassName[] = "ImageView"; ImageView::ImageView() - : horiz_alignment_(CENTER), - vert_alignment_(CENTER), + : horizontal_alignment_(CENTER), + vertical_alignment_(CENTER), last_paint_scale_(0.f), - last_painted_bitmap_pixels_(NULL) {} + last_painted_bitmap_pixels_(nullptr) {} ImageView::~ImageView() {} @@ -40,7 +40,7 @@ void ImageView::SetImage(const gfx::ImageSkia& img) { if (IsImageEqual(img)) return; - last_painted_bitmap_pixels_ = NULL; + last_painted_bitmap_pixels_ = nullptr; gfx::Size pref_size(GetPreferredSize()); image_ = img; if (pref_size != GetPreferredSize()) @@ -93,31 +93,39 @@ gfx::Size ImageView::GetImageSize() const { gfx::Point ImageView::ComputeImageOrigin(const gfx::Size& image_size) const { gfx::Insets insets = GetInsets(); - int x; + int x = 0; // In order to properly handle alignment of images in RTL locales, we need // to flip the meaning of trailing and leading. For example, if the // horizontal alignment is set to trailing, then we'll use left alignment for // the image instead of right alignment if the UI layout is RTL. - Alignment actual_horiz_alignment = horiz_alignment_; - if (base::i18n::IsRTL() && (horiz_alignment_ != CENTER)) - actual_horiz_alignment = (horiz_alignment_ == LEADING) ? TRAILING : LEADING; - switch (actual_horiz_alignment) { - case LEADING: x = insets.left(); break; - case TRAILING: x = width() - insets.right() - image_size.width(); break; + Alignment actual_horizontal_alignment = horizontal_alignment_; + if (base::i18n::IsRTL() && (horizontal_alignment_ != CENTER)) { + actual_horizontal_alignment = + (horizontal_alignment_ == LEADING) ? TRAILING : LEADING; + } + switch (actual_horizontal_alignment) { + case LEADING: + x = insets.left(); + break; + case TRAILING: + x = width() - insets.right() - image_size.width(); + break; case CENTER: x = (width() - insets.width() - image_size.width()) / 2 + insets.left(); break; - default: NOTREACHED(); x = 0; break; } - int y; - switch (vert_alignment_) { - case LEADING: y = insets.top(); break; - case TRAILING: y = height() - insets.bottom() - image_size.height(); break; + int y = 0; + switch (vertical_alignment_) { + case LEADING: + y = insets.top(); + break; + case TRAILING: + y = height() - insets.bottom() - image_size.height(); + break; case CENTER: y = (height() - insets.height() - image_size.height()) / 2 + insets.top(); break; - default: NOTREACHED(); y = 0; break; } return gfx::Point(x, y); @@ -137,26 +145,26 @@ const char* ImageView::GetClassName() const { return kViewClassName; } -void ImageView::SetHorizontalAlignment(Alignment ha) { - if (ha != horiz_alignment_) { - horiz_alignment_ = ha; +void ImageView::SetHorizontalAlignment(Alignment alignment) { + if (alignment != horizontal_alignment_) { + horizontal_alignment_ = alignment; SchedulePaint(); } } ImageView::Alignment ImageView::GetHorizontalAlignment() const { - return horiz_alignment_; + return horizontal_alignment_; } -void ImageView::SetVerticalAlignment(Alignment va) { - if (va != vert_alignment_) { - vert_alignment_ = va; +void ImageView::SetVerticalAlignment(Alignment alignment) { + if (alignment != vertical_alignment_) { + vertical_alignment_ = alignment; SchedulePaint(); } } ImageView::Alignment ImageView::GetVerticalAlignment() const { - return vert_alignment_; + return vertical_alignment_; } void ImageView::SetTooltipText(const base::string16& tooltip) { @@ -184,7 +192,7 @@ gfx::Size ImageView::CalculatePreferredSize() const { views::PaintInfo::ScaleType ImageView::GetPaintScaleType() const { // ImageView contains an image which is rastered at the device scale factor. - // By default, the paint commands are recorded at a scale factor slighlty + // By default, the paint commands are recorded at a scale factor slightly // different from the device scale factor. Re-rastering the image at this // paint recording scale will result in a distorted image. Paint recording // scale might also not be uniform along the x & y axis, thus resulting in @@ -197,7 +205,7 @@ views::PaintInfo::ScaleType ImageView::GetPaintScaleType() const { void ImageView::OnPaintImage(gfx::Canvas* canvas) { last_paint_scale_ = canvas->image_scale(); - last_painted_bitmap_pixels_ = NULL; + last_painted_bitmap_pixels_ = nullptr; if (image_.isNull()) return; diff --git a/chromium/ui/views/controls/image_view.h b/chromium/ui/views/controls/image_view.h index 178c50e5087..c19a3c5ea8e 100644 --- a/chromium/ui/views/controls/image_view.h +++ b/chromium/ui/views/controls/image_view.h @@ -106,10 +106,10 @@ class VIEWS_EXPORT ImageView : public View { gfx::ImageSkia image_; // Horizontal alignment. - Alignment horiz_alignment_; + Alignment horizontal_alignment_; // Vertical alignment. - Alignment vert_alignment_; + Alignment vertical_alignment_; // The current tooltip text. base::string16 tooltip_text_; diff --git a/chromium/ui/views/controls/menu/menu_config.cc b/chromium/ui/views/controls/menu/menu_config.cc index 62fbbca1d1d..c398009a47d 100644 --- a/chromium/ui/views/controls/menu/menu_config.cc +++ b/chromium/ui/views/controls/menu/menu_config.cc @@ -5,7 +5,9 @@ #include "ui/views/controls/menu/menu_config.h" #include "base/macros.h" +#include "ui/views/controls/menu/menu_controller.h" #include "ui/views/controls/menu/menu_image_util.h" +#include "ui/views/controls/menu/menu_item_view.h" #include "ui/views/round_rect_painter.h" namespace views { @@ -18,28 +20,36 @@ MenuConfig::MenuConfig() item_bottom_margin(3), item_no_icon_top_margin(4), item_no_icon_bottom_margin(4), - fixed_text_item_height(0), - fixed_menu_width(0), + minimum_text_item_height(0), + minimum_container_item_height(0), + minimum_menu_width(0), item_left_margin(10), touchable_item_left_margin(16), label_to_arrow_padding(10), arrow_to_edge_padding(5), icon_to_label_padding(10), - touchable_icon_to_label_padding(22), + touchable_icon_to_label_padding(16), touchable_icon_size(20), - touchable_icon_color(SkColorSetA(SK_ColorBLACK, 0xDE)), + touchable_icon_color(SkColorSetRGB(0x5F, 0x63, 0x60)), check_width(kMenuCheckSize), check_height(kMenuCheckSize), arrow_width(kSubmenuArrowSize), separator_height(11), + double_separator_height(18), separator_upper_height(3), separator_lower_height(4), separator_spacing_height(3), separator_thickness(1), + double_separator_thickness(2), show_mnemonics(false), + use_mnemonics(true), scroll_arrow_height(3), label_to_minor_text_padding(10), item_min_height(0), + actionable_submenu_arrow_to_edge_padding(14), + actionable_submenu_width(37), + actionable_submenu_vertical_separator_height(18), + actionable_submenu_vertical_separator_width(1), show_accelerators(true), always_use_icon_to_label_padding(false), align_arrow_and_shortcut(false), @@ -49,17 +59,44 @@ MenuConfig::MenuConfig() check_selected_combobox_item(false), show_delay(400), corner_radius(0), + auxiliary_corner_radius(0), touchable_corner_radius(8), touchable_anchor_offset(8), touchable_menu_height(36), touchable_menu_width(256), touchable_menu_shadow_elevation(12), - vertical_touchable_menu_item_padding(8) { + vertical_touchable_menu_item_padding(8), + padded_separator_left_margin(64), + arrow_key_selection_wraps(true), + show_context_menu_accelerators(true) { Init(); } MenuConfig::~MenuConfig() {} +int MenuConfig::CornerRadiusForMenu(const MenuController* controller) const { + if (controller && controller->use_touchable_layout()) + return touchable_corner_radius; + if (controller && (controller->is_combobox() || controller->IsContextMenu())) + return auxiliary_corner_radius; + return corner_radius; +} + +bool MenuConfig::ShouldShowAcceleratorText(const MenuItemView* item, + base::string16* text) const { + if (!show_accelerators || !item->GetDelegate() || !item->GetCommand()) + return false; + ui::Accelerator accelerator; + if (!item->GetDelegate()->GetAccelerator(item->GetCommand(), &accelerator)) + return false; + if (item->GetMenuController() && item->GetMenuController()->IsContextMenu() && + !show_context_menu_accelerators) { + return false; + } + *text = accelerator.GetShortcutText(); + return true; +} + // static const MenuConfig& MenuConfig::instance() { CR_DEFINE_STATIC_LOCAL(MenuConfig, instance, ()); diff --git a/chromium/ui/views/controls/menu/menu_config.h b/chromium/ui/views/controls/menu/menu_config.h index 3fbb794646a..c07195b1be6 100644 --- a/chromium/ui/views/controls/menu/menu_config.h +++ b/chromium/ui/views/controls/menu/menu_config.h @@ -11,6 +11,9 @@ namespace views { +class MenuController; +class MenuItemView; + // Layout type information for menu items. Use the instance() method to obtain // the MenuConfig for the current platform. struct VIEWS_EXPORT MenuConfig { @@ -19,6 +22,16 @@ struct VIEWS_EXPORT MenuConfig { static const MenuConfig& instance(); + // Helper methods to simplify access to MenuConfig: + // Returns the appropriate corner radius for the menu controlled by + // |controller|, or the default corner radius if |controller| is nullptr. + int CornerRadiusForMenu(const MenuController* controller) const; + + // Returns whether |item_view| should show accelerator text. If so, returns + // the text to show. + bool ShouldShowAcceleratorText(const MenuItemView* item_view, + base::string16* text) const; + // Font list used by menus. gfx::FontList font_list; @@ -44,12 +57,12 @@ struct VIEWS_EXPORT MenuConfig { int item_no_icon_top_margin; int item_no_icon_bottom_margin; - // Fixed dimensions used for entire items. If these are nonzero, they override - // the vertical margin constants given above - the item's text and icon are - // vertically centered within these heights. - int fixed_text_item_height; - int fixed_container_item_height; - int fixed_menu_width; + // Minimum dimensions used for entire items. If these are nonzero, they + // override the vertical margin constants given above - the item's text and + // icon are vertically centered within these heights. + int minimum_text_item_height; + int minimum_container_item_height; + int minimum_menu_width; // Margins between the left of the item and the icon. int item_left_margin; @@ -87,6 +100,9 @@ struct VIEWS_EXPORT MenuConfig { // Height of a normal separator (ui::NORMAL_SEPARATOR). int separator_height; + // Height of a double separator (ui::DOUBLE_SEPARATOR). + int double_separator_height; + // Height of a ui::UPPER_SEPARATOR. int separator_upper_height; @@ -99,9 +115,15 @@ struct VIEWS_EXPORT MenuConfig { // Thickness of the drawn separator line in pixels. int separator_thickness; + // Thickness of the drawn separator line in pixels for double separator. + int double_separator_thickness; + // Are mnemonics shown? bool show_mnemonics; + // Are mnemonics used to activate items? + bool use_mnemonics; + // Height of the scroll arrow. int scroll_arrow_height; @@ -112,6 +134,18 @@ struct VIEWS_EXPORT MenuConfig { // Minimum height of menu item. int item_min_height; + // Edge padding for an actionable submenu arrow. + int actionable_submenu_arrow_to_edge_padding; + + // Width of the submenu in an actionable submenu. + int actionable_submenu_width; + + // The height of the vertical separator used in an actionable submenu. + int actionable_submenu_vertical_separator_height; + + // The width of the vertical separator used in an actionable submenu. + int actionable_submenu_vertical_separator_width; + // Whether the keyboard accelerators are visible. bool show_accelerators; @@ -140,6 +174,10 @@ struct VIEWS_EXPORT MenuConfig { // Radius of the rounded corners of the menu border. Must be >= 0. int corner_radius; + // Radius of "auxiliary" rounded corners - comboboxes and context menus. + // Must be >= 0. + int auxiliary_corner_radius; + // Radius of the rounded corners of the touchable menu border int touchable_corner_radius; @@ -158,6 +196,15 @@ struct VIEWS_EXPORT MenuConfig { // Vertical padding for touchable menus. int vertical_touchable_menu_item_padding; + // Left margin of padded separator (ui::PADDED_SEPARATOR). + int padded_separator_left_margin; + + // Whether arrow keys should wrap around the end of the menu when selecting. + bool arrow_key_selection_wraps; + + // Whether to show accelerators in context menus. + bool show_context_menu_accelerators; + private: // Configures a MenuConfig as appropriate for the current platform. void Init(); diff --git a/chromium/ui/views/controls/menu/menu_config_mac.mm b/chromium/ui/views/controls/menu/menu_config_mac.mm index e950582f43f..7854af9d373 100644 --- a/chromium/ui/views/controls/menu/menu_config_mac.mm +++ b/chromium/ui/views/controls/menu/menu_config_mac.mm @@ -17,9 +17,9 @@ void InitMaterialMenuConfig(views::MenuConfig* config) { config->menu_vertical_border_size = 8; config->menu_horizontal_border_size = 0; config->submenu_horizontal_inset = 0; - config->fixed_text_item_height = 32; - config->fixed_container_item_height = 48; - config->fixed_menu_width = 320; + config->minimum_text_item_height = 32; + config->minimum_container_item_height = 48; + config->minimum_menu_width = 320; config->item_left_margin = 8; config->label_to_arrow_padding = 0; config->arrow_to_edge_padding = 16; @@ -28,12 +28,16 @@ void InitMaterialMenuConfig(views::MenuConfig* config) { config->check_height = 16; config->arrow_width = 8; config->separator_height = 17; + config->separator_lower_height = 9; + config->separator_upper_height = 9; + config->separator_spacing_height = 9; config->separator_thickness = 1; config->label_to_minor_text_padding = 8; config->align_arrow_and_shortcut = true; config->use_outer_border = false; config->icons_in_label = true; config->corner_radius = 8; + config->auxiliary_corner_radius = 4; } } // namespace @@ -43,6 +47,9 @@ namespace views { void MenuConfig::Init() { font_list = gfx::FontList(gfx::Font([NSFont menuFontOfSize:0.0])); check_selected_combobox_item = true; + arrow_key_selection_wraps = false; + use_mnemonics = false; + show_context_menu_accelerators = false; if (ui::MaterialDesignController::IsSecondaryUiMaterial()) InitMaterialMenuConfig(this); } diff --git a/chromium/ui/views/controls/menu/menu_controller.cc b/chromium/ui/views/controls/menu/menu_controller.cc index 46713d7170a..00590f09d91 100644 --- a/chromium/ui/views/controls/menu/menu_controller.cc +++ b/chromium/ui/views/controls/menu/menu_controller.cc @@ -165,7 +165,7 @@ View* GetNextFocusableView(View* ancestor, View* start_at, bool forward) { return NULL; } -#if defined(OS_WIN) || defined(OS_CHROMEOS) +#if defined(OS_WIN) // Determines the correct coordinates and window to repost |event| to, if it is // a mouse or touch event. static void RepostEventImpl(const ui::LocatedEvent* event, @@ -252,7 +252,7 @@ static void RepostEventImpl(const ui::LocatedEvent* event, PostMessage(target_window, event_type, target, window_coords); return; } -#endif +#endif // defined(OS_WIN) #if defined(USE_AURA) if (!window) @@ -276,7 +276,7 @@ static void RepostEventImpl(const ui::LocatedEvent* event, root->GetHost()->dispatcher()->RepostEvent(located_event.get()); #endif // defined(USE_AURA) } -#endif // defined(OS_WIN) || defined(OS_CHROMEOS) +#endif // defined(OS_WIN) } // namespace @@ -442,9 +442,8 @@ void MenuController::Run(Widget* parent, // If we are already showing, this new menu is being nested. Such as context // menus on top of normal menus. if (showing_) { - // Only support nesting of blocking_run menus, nesting of - // blocking/non-blocking shouldn't be needed. - DCHECK(blocking_run_); + // Nesting (context menus) is not used for drag and drop. + DCHECK(!for_drop_); state_.hot_button = hot_button_; hot_button_ = nullptr; @@ -477,7 +476,10 @@ void MenuController::Run(Widget* parent, // Set the selection, which opens the initial menu. SetSelection(root, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); - if (!blocking_run_) { + if (button) + pressed_lock_ = std::make_unique<MenuButton::PressedLock>(button); + + if (for_drop_) { if (!is_nested_drag) { // Start the timer to hide the menu. This is needed as we get no // notification when the drag has finished. @@ -486,9 +488,6 @@ void MenuController::Run(Widget* parent, return; } - if (button) - pressed_lock_.reset(new MenuButton::PressedLock(button)); - // Make sure Chrome doesn't attempt to shut down while the menu is showing. if (ViewsDelegate::GetInstance()) ViewsDelegate::GetInstance()->AddRef(); @@ -516,7 +515,7 @@ void MenuController::Cancel(ExitType type) { // Hide windows immediately. SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); - if (!blocking_run_) { + if (for_drop_) { // If we didn't block the caller we need to notify the menu, which // triggers deleting us. DCHECK(selected); @@ -550,6 +549,10 @@ void MenuController::AddNestedDelegate( delegate_ = delegate; } +bool MenuController::IsContextMenu() const { + return state_.context_menu; +} + bool MenuController::OnMousePressed(SubmenuView* source, const ui::MouseEvent& event) { // We should either have no current_mouse_event_target_, or should have a @@ -607,7 +610,7 @@ bool MenuController::OnMouseDragged(SubmenuView* source, MenuPart part = GetMenuPart(source, event.location()); UpdateScrolling(part); - if (!blocking_run_) + if (for_drop_) return false; if (possible_drag_) { @@ -664,12 +667,11 @@ void MenuController::OnMouseReleased(SubmenuView* source, return; } - if (!blocking_run_) + if (for_drop_) return; DCHECK(state_.item); possible_drag_ = false; - DCHECK(blocking_run_); MenuPart part = GetMenuPart(source, event.location()); if (event.IsRightMouseButton() && part.type == MenuPart::MENU_ITEM) { MenuItemView* menu = part.menu; @@ -694,7 +696,7 @@ void MenuController::OnMouseReleased(SubmenuView* source, // for selected folder menu items. If it's only a left click, show the // contents of the folder. if (!part.is_scroll() && part.menu && - !(part.menu->HasSubmenu() && + !(part.should_submenu_show && part.menu->HasSubmenu() && (event.flags() & ui::EF_LEFT_MOUSE_BUTTON))) { if (active_mouse_view_tracker_->view()) { SendMouseReleaseToActiveView(source, event); @@ -819,7 +821,7 @@ void MenuController::OnGestureEvent(SubmenuView* source, } } else if (event->type() == ui::ET_GESTURE_TAP) { if (!part.is_scroll() && part.menu && - !(part.menu->HasSubmenu())) { + !(part.should_submenu_show && part.menu->HasSubmenu())) { if (part.menu->GetDelegate()->IsTriggerableEvent( part.menu, *event)) { item_selected_by_touch_ = true; @@ -1004,7 +1006,7 @@ int MenuController::OnPerformDrop(SubmenuView* source, if (drop_target->id() == MenuItemView::kEmptyMenuItemViewID) drop_target = drop_target->GetParentMenuItem(); - if (!IsBlockingRun()) { + if (for_drop_) { delegate_->OnMenuClosed( internal::MenuControllerDelegate::DONT_NOTIFY_DELEGATE, item->GetRootMenuItem(), accept_event_flags_); @@ -1165,7 +1167,14 @@ void MenuController::SetSelection(MenuItemView* menu_item, size_t current_size = current_path.size(); size_t new_size = new_path.size(); - bool pending_item_changed = pending_state_.item != menu_item; + // ACTIONABLE_SUBMENUs can change without changing the pending item, this + // occurs when selection moves from the COMMAND area to the SUBMENU area of + // the ACTIONABLE_SUBMENU. + const bool pending_item_changed = + pending_state_.item != menu_item || + pending_state_.submenu_open != + !!(selection_types & SELECTION_OPEN_SUBMENU); + if (pending_item_changed && pending_state_.item) SetHotTrackedButton(nullptr); @@ -1174,7 +1183,8 @@ void MenuController::SetSelection(MenuItemView* menu_item, current_path.empty() ? NULL : current_path.front()->GetDelegate(); for (size_t i = paths_differ_at; i < current_size; ++i) { if (current_delegate && - current_path[i]->GetType() == MenuItemView::SUBMENU) { + (current_path[i]->GetType() == MenuItemView::SUBMENU || + current_path[i]->GetType() == MenuItemView::ACTIONABLE_SUBMENU)) { current_delegate->WillHideMenu(current_path[i]); } current_path[i]->SetSelected(false); @@ -1184,6 +1194,14 @@ void MenuController::SetSelection(MenuItemView* menu_item, for (size_t i = paths_differ_at; i < new_size; ++i) { new_path[i]->ScrollRectToVisible(new_path[i]->GetLocalBounds()); new_path[i]->SetSelected(true); + if (new_path[i]->GetType() == MenuItemView::ACTIONABLE_SUBMENU) { + new_path[i]->SetSelectionOfActionableSubmenu( + (selection_types & SELECTION_OPEN_SUBMENU) != 0); + } + } + if (menu_item && menu_item->GetType() == MenuItemView::ACTIONABLE_SUBMENU) { + menu_item->SetSelectionOfActionableSubmenu( + (selection_types & SELECTION_OPEN_SUBMENU) != 0); } if (menu_item && menu_item->GetDelegate()) @@ -1206,16 +1224,17 @@ void MenuController::SetSelection(MenuItemView* menu_item, StartShowTimer(); // Notify an accessibility focus event on all menu items except for the root. - if (menu_item && - (MenuDepth(menu_item) != 1 || - menu_item->GetType() != MenuItemView::SUBMENU)) { + if (menu_item && (MenuDepth(menu_item) != 1 || + menu_item->GetType() != MenuItemView::SUBMENU || + (menu_item->GetType() == MenuItemView::ACTIONABLE_SUBMENU && + (selection_types & SELECTION_OPEN_SUBMENU) == 0))) { menu_item->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); } } void MenuController::SetSelectionOnPointerDown(SubmenuView* source, const ui::LocatedEvent* event) { - if (!blocking_run_) + if (for_drop_) return; DCHECK(!active_mouse_view_tracker_->view()); @@ -1255,7 +1274,7 @@ void MenuController::SetSelectionOnPointerDown(SubmenuView* source, possible_drag_ = true; press_pt_ = event->location(); } - if (part.menu->HasSubmenu()) + if (part.menu->HasSubmenu() && part.should_submenu_show) selection_types |= SELECTION_OPEN_SUBMENU; } SetSelection(part.menu, selection_types); @@ -1298,7 +1317,7 @@ void MenuController::StartDrag(SubmenuView* source, void MenuController::OnKeyDown(ui::KeyboardCode key_code) { // Do not process while performing drag-and-drop - if (!blocking_run_) + if (for_drop_) return; switch (key_code) { @@ -1405,9 +1424,9 @@ void MenuController::OnKeyDown(ui::KeyboardCode key_code) { } } -MenuController::MenuController(bool blocking, +MenuController::MenuController(bool for_drop, internal::MenuControllerDelegate* delegate) - : blocking_run_(blocking), + : for_drop_(for_drop), active_mouse_view_tracker_(std::make_unique<ViewTracker>()), delegate_(delegate) { delegate_stack_.push_back(delegate_); @@ -1445,11 +1464,6 @@ void MenuController::UpdateInitialLocation(const gfx::Rect& bounds, bool context_menu) { pending_state_.context_menu = context_menu; pending_state_.initial_bounds = bounds; - if (bounds.height() > 1) { - // Inset the bounds slightly, otherwise drag coordinates don't line up - // nicely and menus close prematurely. - pending_state_.initial_bounds.Inset(0, 1); - } // Reverse anchor position for RTL languages. if (base::i18n::IsRTL() && @@ -1491,7 +1505,7 @@ void MenuController::Accept(MenuItemView* item, int event_flags) { } void MenuController::ReallyAccept(MenuItemView* item, int event_flags) { - DCHECK(IsBlockingRun()); + DCHECK(!for_drop_); result_ = item; #if defined(OS_MACOSX) // Reset the closure animation since it's now finished - this also unblocks @@ -1562,9 +1576,8 @@ bool MenuController::ShowSiblingMenu(SubmenuView* source, // It is currently not possible to show a submenu recursively in a bubble. DCHECK(!MenuItemView::IsBubble(anchor)); - // Subtract 1 from the height to make the popup flush with the button border. UpdateInitialLocation(gfx::Rect(screen_menu_loc.x(), screen_menu_loc.y(), - button->width(), button->height() - 1), + button->width(), button->height()), anchor, state_.context_menu); alt_menu->PrepareForRun( false, has_mnemonics, @@ -1699,11 +1712,19 @@ bool MenuController::GetMenuPartByScreenCoordinateImpl( part->menu = GetMenuItemAt(menu, menu_loc.x(), menu_loc.y()); part->type = MenuPart::MENU_ITEM; part->submenu = menu; + part->should_submenu_show = + part->submenu && part->menu && + (part->menu->GetType() == MenuItemView::SUBMENU || + IsLocationOverSubmenuAreaOfActionableSubmenu(part->menu, screen_loc)); if (!part->menu) part->parent = menu->GetMenuItem(); return true; } + // Return false for points on touchable menu shadows, to search parent menus. + if (use_touchable_layout_) + return false; + // While the mouse isn't over a menu item or the scroll buttons of menu, it // is contained by menu and so we return true. If we didn't return true other // menus would be searched, even though they are likely obscured by us. @@ -1735,7 +1756,20 @@ bool MenuController::DoesSubmenuContainLocation(SubmenuView* submenu, gfx::Point view_loc = screen_loc; View::ConvertPointFromScreen(submenu, &view_loc); gfx::Rect vis_rect = submenu->GetVisibleBounds(); - return vis_rect.Contains(view_loc.x(), view_loc.y()); + return vis_rect.Contains(view_loc); +} + +bool MenuController::IsLocationOverSubmenuAreaOfActionableSubmenu( + MenuItemView* item, + const gfx::Point& screen_loc) const { + if (!item || item->GetType() != MenuItemView::ACTIONABLE_SUBMENU) + return false; + + gfx::Point view_loc = screen_loc; + View::ConvertPointFromScreen(item, &view_loc); + if (base::i18n::IsRTL()) + view_loc.set_x(item->GetMirroredXInView(view_loc.x())); + return item->GetSubmenuAreaOfActionableSubmenu().Contains(view_loc); } void MenuController::CommitPendingSelection() { @@ -1847,7 +1881,7 @@ void MenuController::OpenMenuImpl(MenuItemView* item, bool show) { CalculateBubbleMenuBounds(item, prefer_leading, &resulting_direction) : CalculateMenuBounds(item, prefer_leading, &resulting_direction); state_.open_leading.push_back(resulting_direction); - bool do_capture = (!did_capture_ && blocking_run_); + bool do_capture = (!did_capture_ && !for_drop_); showing_submenu_ = true; if (show) { // Menus are the only place using kGroupingPropertyKey, so any value (other @@ -2127,7 +2161,6 @@ gfx::Rect MenuController::CalculateBubbleMenuBounds(MenuItemView* item, bool prefer_leading, bool* is_leading) { DCHECK(item); - DCHECK(!item->GetParentMenuItem()); // Assume we can honor prefer_leading. *is_leading = prefer_leading; @@ -2136,103 +2169,153 @@ gfx::Rect MenuController::CalculateBubbleMenuBounds(MenuItemView* item, DCHECK(submenu); gfx::Size pref = submenu->GetScrollViewContainer()->GetPreferredSize(); - const gfx::Rect& owner_bounds = pending_state_.initial_bounds; - - // First the size gets reduced to the possible space. - if (!state_.monitor_bounds.IsEmpty()) { - int max_width = state_.monitor_bounds.width(); - int max_height = state_.monitor_bounds.height(); - // In case of bubbles, the maximum width is limited by the space - // between the display corner and the target area + the tip size. - if (state_.anchor == MENU_ANCHOR_BUBBLE_LEFT) { - max_width = owner_bounds.x() - state_.monitor_bounds.x() + - kBubbleTipSizeLeftRight; - } else if (state_.anchor == MENU_ANCHOR_BUBBLE_RIGHT) { - max_width = state_.monitor_bounds.right() - owner_bounds.right() + - kBubbleTipSizeLeftRight; - } else if (state_.anchor == MENU_ANCHOR_BUBBLE_ABOVE) { - max_height = owner_bounds.y() - state_.monitor_bounds.y() + - kBubbleTipSizeTopBottom; - } else if (state_.anchor == MENU_ANCHOR_BUBBLE_BELOW) { - max_height = state_.monitor_bounds.bottom() - owner_bounds.bottom() + - kBubbleTipSizeTopBottom; - } - // The space for the menu to cover should never get empty. - DCHECK_GE(max_width, kBubbleTipSizeLeftRight); - DCHECK_GE(max_height, kBubbleTipSizeTopBottom); - pref.set_width(std::min(pref.width(), max_width)); - pref.set_height(std::min(pref.height(), max_height)); - } - // Also make sure that the menu does not go too wide. - pref.set_width(std::min(pref.width(), - item->GetDelegate()->GetMaxWidthForMenu(item))); - + int x = 0; + int y = 0; const MenuConfig& menu_config = MenuConfig::instance(); // Shadow insets are built into MenuScrollView's preferred size so it must be // compensated for when determining the bounds of touchable menus. - gfx::Insets shadow_insets = BubbleBorder::GetBorderAndShadowInsets( - menu_config.touchable_menu_shadow_elevation); + const gfx::Insets border_and_shadow_insets = + BubbleBorder::GetBorderAndShadowInsets( + menu_config.touchable_menu_shadow_elevation); - int x, y; - if (state_.anchor == MENU_ANCHOR_BUBBLE_ABOVE || - state_.anchor == MENU_ANCHOR_BUBBLE_BELOW) { - if (state_.anchor == MENU_ANCHOR_BUBBLE_ABOVE) - y = owner_bounds.y() - pref.height() + kBubbleTipSizeTopBottom; - else - y = owner_bounds.bottom() - kBubbleTipSizeTopBottom; - - x = owner_bounds.CenterPoint().x() - pref.width() / 2; - int x_old = x; - if (x < state_.monitor_bounds.x()) { - x = state_.monitor_bounds.x(); - } else if (x + pref.width() > state_.monitor_bounds.right()) { - x = state_.monitor_bounds.right() - pref.width(); + if (!item->GetParentMenuItem()) { + // This is a top-level menu, position it relative to the anchor bounds. + const gfx::Rect& owner_bounds = pending_state_.initial_bounds; + + // First the size gets reduced to the possible space. + if (!state_.monitor_bounds.IsEmpty()) { + int max_width = state_.monitor_bounds.width(); + int max_height = state_.monitor_bounds.height(); + // In case of bubbles, the maximum width is limited by the space + // between the display corner and the target area + the tip size. + if (state_.anchor == MENU_ANCHOR_BUBBLE_LEFT) { + max_width = owner_bounds.x() - state_.monitor_bounds.x() + + kBubbleTipSizeLeftRight; + } else if (state_.anchor == MENU_ANCHOR_BUBBLE_RIGHT) { + max_width = state_.monitor_bounds.right() - owner_bounds.right() + + kBubbleTipSizeLeftRight; + } else if (state_.anchor == MENU_ANCHOR_BUBBLE_ABOVE) { + max_height = owner_bounds.y() - state_.monitor_bounds.y() + + kBubbleTipSizeTopBottom; + } else if (state_.anchor == MENU_ANCHOR_BUBBLE_BELOW) { + max_height = state_.monitor_bounds.bottom() - owner_bounds.bottom() + + kBubbleTipSizeTopBottom; + } + // The menu should always have a non-empty available area. + DCHECK_GE(max_width, kBubbleTipSizeLeftRight); + DCHECK_GE(max_height, kBubbleTipSizeTopBottom); + pref.set_width(std::min(pref.width(), max_width)); + pref.set_height(std::min(pref.height(), max_height)); } - submenu->GetScrollViewContainer()->SetBubbleArrowOffset( - pref.width() / 2 - x + x_old); - } else if (state_.anchor == MENU_ANCHOR_BUBBLE_TOUCHABLE_ABOVE) { - // Align the left edges of the menu and anchor, and the bottom of the menu - // with the top of the anchor. - x = owner_bounds.origin().x() - shadow_insets.left(); - y = owner_bounds.origin().y() - pref.height() + shadow_insets.bottom() - - menu_config.touchable_anchor_offset; - // Align the right of the container with the right of the app icon. - if (x + pref.width() > state_.monitor_bounds.width()) - x = owner_bounds.right() - pref.width() + shadow_insets.right(); - // Align the top of the menu with the bottom of the anchor. - if (y < 0) { - y = owner_bounds.bottom() - shadow_insets.top() + + // Respect the delegate's maximum width. + pref.set_width( + std::min(pref.width(), item->GetDelegate()->GetMaxWidthForMenu(item))); + + if (state_.anchor == MENU_ANCHOR_BUBBLE_ABOVE || + state_.anchor == MENU_ANCHOR_BUBBLE_BELOW) { + if (state_.anchor == MENU_ANCHOR_BUBBLE_ABOVE) + y = owner_bounds.y() - pref.height() + kBubbleTipSizeTopBottom; + else + y = owner_bounds.bottom() - kBubbleTipSizeTopBottom; + + x = owner_bounds.CenterPoint().x() - pref.width() / 2; + int x_old = x; + if (x < state_.monitor_bounds.x()) + x = state_.monitor_bounds.x(); + else if (x + pref.width() > state_.monitor_bounds.right()) + x = state_.monitor_bounds.right() - pref.width(); + submenu->GetScrollViewContainer()->SetBubbleArrowOffset(pref.width() / 2 - + x + x_old); + } else if (state_.anchor == MENU_ANCHOR_BUBBLE_TOUCHABLE_ABOVE) { + // Align the left edges of the menu and anchor, and the bottom of the menu + // with the top of the anchor. + x = owner_bounds.origin().x() - border_and_shadow_insets.left(); + y = owner_bounds.origin().y() - pref.height() + + border_and_shadow_insets.bottom() - menu_config.touchable_anchor_offset; - } - } else if (state_.anchor == MENU_ANCHOR_BUBBLE_TOUCHABLE_LEFT) { - // Align the right of the menu with the left of the anchor, and the top of - // the menu with the top of the anchor. - x = owner_bounds.origin().x() - pref.width() + shadow_insets.right() - - menu_config.touchable_anchor_offset; - y = owner_bounds.origin().y() - shadow_insets.top(); - // Align the left of the menu with the right of the anchor. - if (x < 0) { - x = owner_bounds.right() + shadow_insets.left() + + // Align the right of the container with the right of the anchor. + if (x + pref.width() > state_.monitor_bounds.width()) { + x = owner_bounds.right() - pref.width() + + border_and_shadow_insets.right(); + } + // Align the top of the menu with the bottom of the anchor. + if (y < 0) { + y = owner_bounds.bottom() - border_and_shadow_insets.top() + + menu_config.touchable_anchor_offset; + } + } else if (state_.anchor == MENU_ANCHOR_BUBBLE_TOUCHABLE_LEFT) { + // Align the right of the menu with the left of the anchor, and the top of + // the menu with the top of the anchor. + x = owner_bounds.origin().x() - pref.width() + + border_and_shadow_insets.right() - menu_config.touchable_anchor_offset; + y = owner_bounds.origin().y() - border_and_shadow_insets.top(); + // Align the left of the menu with the right of the anchor. + if (x < 0) { + x = owner_bounds.right() - border_and_shadow_insets.left() + + menu_config.touchable_anchor_offset; + } + // Align the bottom of the menu to the bottom of the anchor. + if (y + pref.height() > state_.monitor_bounds.height()) { + y = owner_bounds.bottom() - pref.height() + + border_and_shadow_insets.bottom(); + } + } else { + if (state_.anchor == MENU_ANCHOR_BUBBLE_RIGHT) + x = owner_bounds.right() - kBubbleTipSizeLeftRight; + else + x = owner_bounds.x() - pref.width() + kBubbleTipSizeLeftRight; + + y = owner_bounds.CenterPoint().y() - pref.height() / 2; + int y_old = y; + if (y < state_.monitor_bounds.y()) + y = state_.monitor_bounds.y(); + else if (y + pref.height() > state_.monitor_bounds.bottom()) + y = state_.monitor_bounds.bottom() - pref.height(); + submenu->GetScrollViewContainer()->SetBubbleArrowOffset( + pref.height() / 2 - y + y_old); } - // Align the bottom of the menu to the bottom of the anchor. - if (y + pref.height() > state_.monitor_bounds.height()) - y = owner_bounds.bottom() - pref.height() + shadow_insets.bottom(); } else { - if (state_.anchor == MENU_ANCHOR_BUBBLE_RIGHT) - x = owner_bounds.right() - kBubbleTipSizeLeftRight; - else - x = owner_bounds.x() - pref.width() + kBubbleTipSizeLeftRight; + if (!use_touchable_layout_) { + NOTIMPLEMENTED() + << "Nested bubble menus are only implemented for touchable menus."; + } - y = owner_bounds.CenterPoint().y() - pref.height() / 2; - int y_old = y; - if (y < state_.monitor_bounds.y()) { - y = state_.monitor_bounds.y(); - } else if (y + pref.height() > state_.monitor_bounds.bottom()) { - y = state_.monitor_bounds.bottom() - pref.height(); + // This is a sub-menu, position it relative to the parent menu. + const gfx::Rect item_bounds = item->GetBoundsInScreen(); + // If the layout is RTL, then a 'leading' menu is positioned to the left of + // the parent menu item and not to the right. + const bool layout_is_rtl = base::i18n::IsRTL(); + const bool create_on_the_right = (prefer_leading && !layout_is_rtl) || + (!prefer_leading && layout_is_rtl); + if (create_on_the_right) { + x = item_bounds.right() - border_and_shadow_insets.left(); + if (state_.monitor_bounds.width() != 0 && + (x + menu_config.touchable_menu_width - + border_and_shadow_insets.right() > + state_.monitor_bounds.right())) { + *is_leading = prefer_leading; + x = item_bounds.x() - menu_config.touchable_menu_width - + border_and_shadow_insets.right(); + } + } else { + x = item_bounds.x() - menu_config.touchable_menu_width - + border_and_shadow_insets.right(); + if (state_.monitor_bounds.width() != 0 && x < state_.monitor_bounds.x()) { + *is_leading = !prefer_leading; + x = item_bounds.x() + menu_config.touchable_menu_width - + border_and_shadow_insets.left(); + } } - submenu->GetScrollViewContainer()->SetBubbleArrowOffset( - pref.height() / 2 - y + y_old); + y = item_bounds.y() - border_and_shadow_insets.top() - + menu_config.vertical_touchable_menu_item_padding; + if (y + pref.height() - border_and_shadow_insets.bottom() > + state_.monitor_bounds.bottom()) { + y = state_.monitor_bounds.bottom() - pref.height() + + border_and_shadow_insets.top(); + } + if (y < state_.monitor_bounds.y()) + y = state_.monitor_bounds.y() - border_and_shadow_insets.top(); } return gfx::Rect(x, y, pref.width(), pref.height()); } @@ -2309,14 +2392,20 @@ MenuItemView* MenuController::FindNextSelectableMenuItem( // Loop through the menu items skipping any invisible menus. The loop stops // when we wrap or find a visible and enabled child. do { + if (!MenuConfig::instance().arrow_key_selection_wraps) { + if (index == 0 && direction == INCREMENT_SELECTION_UP) + return nullptr; + if (index == parent_count - 1 && direction == INCREMENT_SELECTION_DOWN) + return nullptr; + } index = (index + delta + parent_count) % parent_count; if (index == stop_index && !include_all_items) - return NULL; + return nullptr; MenuItemView* child = parent->GetSubmenu()->GetMenuItemAt(index); if (child->visible() && child->enabled()) return child; } while (index != stop_index); - return NULL; + return nullptr; } void MenuController::OpenSubmenuChangeSelectionIfCan() { @@ -2398,8 +2487,8 @@ void MenuController::AcceptOrSelect(MenuItemView* parent, } void MenuController::SelectByChar(base::char16 character) { - // Do not process while performing drag-and-drop - if (!blocking_run_) + // Do not process while performing drag-and-drop. + if (for_drop_) return; if (!character) return; @@ -2483,14 +2572,6 @@ void MenuController::RepostEventAndCancel(SubmenuView* source, exit_type = EXIT_OUTERMOST; } Cancel(exit_type); - -#if defined(OS_CHROMEOS) - // We're going to exit the menu and want to repost the event so that is - // is handled normally after the context menu has exited. We call - // RepostEvent after Cancel so that event capture has been released so - // that finding the event target is unaffected by the current capture. - RepostEventImpl(event, screen_loc, native_view, window); -#endif } void MenuController::SetDropMenuItem( @@ -2723,14 +2804,15 @@ void MenuController::HandleMouseLocation(SubmenuView* source, UpdateScrolling(part); - if (!blocking_run_) + if (for_drop_) return; if (part.type == MenuPart::NONE && ShowSiblingMenu(source, mouse_location)) return; if (part.type == MenuPart::MENU_ITEM && part.menu) { - SetSelection(part.menu, SELECTION_OPEN_SUBMENU); + SetSelection(part.menu, part.should_submenu_show ? SELECTION_OPEN_SUBMENU + : SELECTION_DEFAULT); } else if (!part.is_scroll() && pending_state_.item && pending_state_.item->GetParentMenuItem() && !pending_state_.item->SubmenuIsShowing()) { diff --git a/chromium/ui/views/controls/menu/menu_controller.h b/chromium/ui/views/controls/menu/menu_controller.h index b2080448720..67b8849bca5 100644 --- a/chromium/ui/views/controls/menu/menu_controller.h +++ b/chromium/ui/views/controls/menu/menu_controller.h @@ -95,8 +95,7 @@ class VIEWS_EXPORT MenuController bool context_menu, bool is_nested_drag); - // Whether or not Run blocks. - bool IsBlockingRun() const { return blocking_run_; } + bool for_drop() const { return for_drop_; } bool in_nested_run() const { return !menu_stack_.empty(); } @@ -144,6 +143,8 @@ class VIEWS_EXPORT MenuController void set_is_combobox(bool is_combobox) { is_combobox_ = is_combobox; } bool is_combobox() const { return is_combobox_; } + bool IsContextMenu() const; + // Various events, forwarded from the submenu. // // NOTE: the coordinates of the events are in that of the @@ -294,13 +295,11 @@ class VIEWS_EXPORT MenuController SCROLL_DOWN }; - MenuPart() : type(NONE), menu(NULL), parent(NULL), submenu(NULL) {} - // Convenience for testing type == SCROLL_DOWN or type == SCROLL_UP. bool is_scroll() const { return type == SCROLL_DOWN || type == SCROLL_UP; } // Type of part. - Type type; + Type type = NONE; // If type is MENU_ITEM, this is the menu item the mouse is over, otherwise // this is NULL. @@ -308,14 +307,17 @@ class VIEWS_EXPORT MenuController // but is over a menu (for example, the mouse is over a separator or // empty menu), this is NULL and parent is the menu the mouse was // clicked on. - MenuItemView* menu; + MenuItemView* menu = nullptr; // If type is MENU_ITEM but the mouse is not over a menu item this is the // parent of the menu item the user clicked on. Otherwise this is NULL. - MenuItemView* parent; + MenuItemView* parent = nullptr; // This is the submenu the mouse is over. - SubmenuView* submenu; + SubmenuView* submenu = nullptr; + + // Whether the controller should apply SELECTION_OPEN_SUBMENU to this item. + bool should_submenu_show = false; }; // Sets the selection to |menu_item|. A value of NULL unselects @@ -334,10 +336,8 @@ class VIEWS_EXPORT MenuController // Key processing. void OnKeyDown(ui::KeyboardCode key_code); - // Creates a MenuController. If |blocking| is true a nested run loop is - // started in |Run|. - MenuController(bool blocking, - internal::MenuControllerDelegate* delegate); + // Creates a MenuController. See |for_drop_| member for details on |for_drop|. + MenuController(bool for_drop, internal::MenuControllerDelegate* delegate); ~MenuController() override; @@ -419,6 +419,11 @@ class VIEWS_EXPORT MenuController bool DoesSubmenuContainLocation(SubmenuView* submenu, const gfx::Point& screen_loc); + // Returns whether the location is over the ACTIONABLE_SUBMENU's submenu area. + bool IsLocationOverSubmenuAreaOfActionableSubmenu( + MenuItemView* item, + const gfx::Point& screen_loc) const; + // Opens/Closes the necessary menus such that state_ matches that of // pending_state_. This is invoked if submenus are not opened immediately, // but after a delay. @@ -576,11 +581,10 @@ class VIEWS_EXPORT MenuController // The active instance. static MenuController* active_instance_; - // If true, Run blocks. If false, Run doesn't block and this is used for - // drag and drop. Note that the semantics for drag and drop are slightly - // different: cancel timer is kicked off any time the drag moves outside the - // menu, mouse events do nothing... - bool blocking_run_; + // If true the menu is shown for a drag and drop. Note that the semantics for + // drag and drop are slightly different: cancel timer is kicked off any time + // the drag moves outside the menu, mouse events do nothing... + const bool for_drop_; // If true, we're showing. bool showing_ = false; diff --git a/chromium/ui/views/controls/menu/menu_controller_unittest.cc b/chromium/ui/views/controls/menu/menu_controller_unittest.cc index 22fa8335af1..2d1c138bdc0 100644 --- a/chromium/ui/views/controls/menu/menu_controller_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_controller_unittest.cc @@ -294,6 +294,11 @@ class MenuControllerTest : public ViewsTestBase { event_generator_->PressKey(key_code, 0); } + void DispatchKey(ui::KeyboardCode key_code) { + ui::KeyEvent event(ui::EventType::ET_KEY_PRESSED, key_code, 0); + menu_controller_->OnWillDispatchKeyEvent(&event); + } + #if defined(USE_AURA) // Verifies that a non-nested menu fully closes when receiving an escape key. void TestAsyncEscapeKey() { @@ -330,8 +335,9 @@ class MenuControllerTest : public ViewsTestBase { void TestMenuControllerReplacementDuringDrag() { DestroyMenuController(); menu_item()->GetSubmenu()->Close(); + const bool for_drop = false; menu_controller_ = - new MenuController(true, menu_controller_delegate_.get()); + new MenuController(for_drop, menu_controller_delegate_.get()); menu_controller_->owner_ = owner_.get(); menu_controller_->showing_ = true; } @@ -485,7 +491,7 @@ class MenuControllerTest : public ViewsTestBase { } MenuController* menu_controller() { return menu_controller_; } const MenuItemView* pending_state_item() const { - return menu_controller_->pending_state_.item; + return menu_controller_->pending_state_.item; } MenuController::ExitType menu_exit_type() const { return menu_controller_->exit_type_; @@ -533,6 +539,10 @@ class MenuControllerTest : public ViewsTestBase { int CountOwnerOnGestureEvent() const { return owner_->gesture_count(); } + bool SelectionWraps() { + return MenuConfig::instance().arrow_key_selection_wraps; + } + private: void Init() { owner_ = std::make_unique<GestureTestWidget>(); @@ -558,8 +568,9 @@ class MenuControllerTest : public ViewsTestBase { void SetupMenuController() { menu_controller_delegate_.reset(new TestMenuControllerDelegate); + const bool for_drop = false; menu_controller_ = - new MenuController(true, menu_controller_delegate_.get()); + new MenuController(for_drop, menu_controller_delegate_.get()); menu_controller_->owner_ = owner_.get(); menu_controller_->showing_ = true; menu_controller_->SetSelection( @@ -644,8 +655,12 @@ TEST_F(MenuControllerTest, InitialSelectedItem) { // The last selectable item should be item "Four". MenuItemView* last_selectable = FindInitialSelectableMenuItemUp(menu_item()); - ASSERT_NE(nullptr, last_selectable); - EXPECT_EQ(4, last_selectable->GetCommand()); + if (SelectionWraps()) { + ASSERT_NE(nullptr, last_selectable); + EXPECT_EQ(4, last_selectable->GetCommand()); + } else { + ASSERT_EQ(nullptr, last_selectable); + } // Leave items "One" and "Two" enabled. menu_item()->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(true); @@ -658,8 +673,12 @@ TEST_F(MenuControllerTest, InitialSelectedItem) { EXPECT_EQ(1, first_selectable->GetCommand()); // The last selectable item should be item "Two". last_selectable = FindInitialSelectableMenuItemUp(menu_item()); - ASSERT_NE(nullptr, last_selectable); - EXPECT_EQ(2, last_selectable->GetCommand()); + if (SelectionWraps()) { + ASSERT_NE(nullptr, last_selectable); + EXPECT_EQ(2, last_selectable->GetCommand()); + } else { + ASSERT_EQ(nullptr, last_selectable); + } // Leave only a single item "One" enabled. menu_item()->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(true); @@ -672,8 +691,12 @@ TEST_F(MenuControllerTest, InitialSelectedItem) { EXPECT_EQ(1, first_selectable->GetCommand()); // The last selectable item should be item "One". last_selectable = FindInitialSelectableMenuItemUp(menu_item()); - ASSERT_NE(nullptr, last_selectable); - EXPECT_EQ(1, last_selectable->GetCommand()); + if (SelectionWraps()) { + ASSERT_NE(nullptr, last_selectable); + EXPECT_EQ(1, last_selectable->GetCommand()); + } else { + ASSERT_EQ(nullptr, last_selectable); + } // Leave only a single item "Three" enabled. menu_item()->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(false); @@ -686,8 +709,12 @@ TEST_F(MenuControllerTest, InitialSelectedItem) { EXPECT_EQ(3, first_selectable->GetCommand()); // The last selectable item should be item "Three". last_selectable = FindInitialSelectableMenuItemUp(menu_item()); - ASSERT_NE(nullptr, last_selectable); - EXPECT_EQ(3, last_selectable->GetCommand()); + if (SelectionWraps()) { + ASSERT_NE(nullptr, last_selectable); + EXPECT_EQ(3, last_selectable->GetCommand()); + } else { + ASSERT_EQ(nullptr, last_selectable); + } // Leave only a single item ("Two") selected. It should be the first and the // last selectable item. @@ -699,8 +726,12 @@ TEST_F(MenuControllerTest, InitialSelectedItem) { ASSERT_NE(nullptr, first_selectable); EXPECT_EQ(2, first_selectable->GetCommand()); last_selectable = FindInitialSelectableMenuItemUp(menu_item()); - ASSERT_NE(nullptr, last_selectable); - EXPECT_EQ(2, last_selectable->GetCommand()); + if (SelectionWraps()) { + ASSERT_NE(nullptr, last_selectable); + EXPECT_EQ(2, last_selectable->GetCommand()); + } else { + ASSERT_EQ(nullptr, last_selectable); + } // There should be no next or previous selectable item since there is only a // single enabled item in the menu. @@ -730,14 +761,20 @@ TEST_F(MenuControllerTest, NextSelectedItem) { IncrementSelection(); EXPECT_EQ(4, pending_state_item()->GetCommand()); - // Wrap around. - IncrementSelection(); - EXPECT_EQ(1, pending_state_item()->GetCommand()); - - // Move up in the menu. - // Wrap around. - DecrementSelection(); - EXPECT_EQ(4, pending_state_item()->GetCommand()); + if (SelectionWraps()) { + // Wrap around. + IncrementSelection(); + EXPECT_EQ(1, pending_state_item()->GetCommand()); + + // Move up in the menu. + // Wrap around. + DecrementSelection(); + EXPECT_EQ(4, pending_state_item()->GetCommand()); + } else { + // Don't wrap. + IncrementSelection(); + EXPECT_EQ(4, pending_state_item()->GetCommand()); + } // Skip disabled item. DecrementSelection(); @@ -762,7 +799,10 @@ TEST_F(MenuControllerTest, PreviousSelectedItem) { // Move up and select a previous (in our case the last enabled) item. DecrementSelection(); - EXPECT_EQ(3, pending_state_item()->GetCommand()); + if (SelectionWraps()) + EXPECT_EQ(3, pending_state_item()->GetCommand()); + else + EXPECT_EQ(0, pending_state_item()->GetCommand()); // Clear references in menu controller to the menu item that is going away. ResetSelection(); @@ -843,7 +883,10 @@ TEST_F(MenuControllerTest, SelectChildButtonView) { // Increment selection twice to wrap around. IncrementSelection(); IncrementSelection(); - EXPECT_EQ(1, pending_state_item()->GetCommand()); + if (SelectionWraps()) + EXPECT_EQ(1, pending_state_item()->GetCommand()); + else + EXPECT_EQ(5, pending_state_item()->GetCommand()); // Clear references in menu controller to the menu item that is going away. ResetSelection(); @@ -1288,6 +1331,36 @@ TEST_F(MenuControllerTest, AsynchronousGestureDeletesController) { EXPECT_EQ(1, nested_delegate->on_menu_closed_called()); } +TEST_F(MenuControllerTest, ArrowKeysAtEnds) { + menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false); + + SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0)); + EXPECT_EQ(1, pending_state_item()->GetCommand()); + + if (SelectionWraps()) { + DispatchKey(ui::VKEY_UP); + EXPECT_EQ(4, pending_state_item()->GetCommand()); + + DispatchKey(ui::VKEY_DOWN); + EXPECT_EQ(1, pending_state_item()->GetCommand()); + } else { + DispatchKey(ui::VKEY_UP); + EXPECT_EQ(1, pending_state_item()->GetCommand()); + } + + DispatchKey(ui::VKEY_DOWN); + EXPECT_EQ(2, pending_state_item()->GetCommand()); + + DispatchKey(ui::VKEY_DOWN); + EXPECT_EQ(4, pending_state_item()->GetCommand()); + + DispatchKey(ui::VKEY_DOWN); + if (SelectionWraps()) + EXPECT_EQ(1, pending_state_item()->GetCommand()); + else + EXPECT_EQ(4, pending_state_item()->GetCommand()); +} + #if defined(USE_AURA) // Tests that when an asynchronous menu receives a cancel event, that it closes. TEST_F(MenuControllerTest, AsynchronousCancelEvent) { diff --git a/chromium/ui/views/controls/menu/menu_host.cc b/chromium/ui/views/controls/menu/menu_host.cc index f1ff95b4748..f14a1c06685 100644 --- a/chromium/ui/views/controls/menu/menu_host.cc +++ b/chromium/ui/views/controls/menu/menu_host.cc @@ -116,9 +116,7 @@ void MenuHost::InitMenuHost(Widget* parent, const MenuController* menu_controller = submenu_->GetMenuItem()->GetMenuController(); const MenuConfig& menu_config = MenuConfig::instance(); - bool rounded_border = - menu_controller && (menu_controller->use_touchable_layout() || - (menu_config.corner_radius > 0)); + bool rounded_border = menu_config.CornerRadiusForMenu(menu_controller) != 0; bool bubble_border = submenu_->GetScrollViewContainer() && submenu_->GetScrollViewContainer()->HasBubbleBorder(); params.shadow_type = bubble_border ? Widget::InitParams::SHADOW_TYPE_NONE diff --git a/chromium/ui/views/controls/menu/menu_item_view.cc b/chromium/ui/views/controls/menu/menu_item_view.cc index 257f71e6eaa..22a219b4660 100644 --- a/chromium/ui/views/controls/menu/menu_item_view.cc +++ b/chromium/ui/views/controls/menu/menu_item_view.cc @@ -12,6 +12,7 @@ #include "ui/accessibility/ax_node_data.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/models/menu_model.h" +#include "ui/base/ui_base_features.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/geometry/rect.h" @@ -29,6 +30,7 @@ #include "ui/views/controls/menu/menu_scroll_view_container.h" #include "ui/views/controls/menu/menu_separator.h" #include "ui/views/controls/menu/submenu_view.h" +#include "ui/views/controls/separator.h" #include "ui/views/widget/widget.h" namespace views { @@ -171,7 +173,8 @@ void MenuItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) { switch (GetType()) { case SUBMENU: - node_data->AddState(ax::mojom::State::kHaspopup); + case ACTIONABLE_SUBMENU: + node_data->SetHasPopup(ax::mojom::HasPopup::kMenu); break; case CHECKBOX: case RADIO: { @@ -267,7 +270,7 @@ MenuItemView* MenuItemView::AddMenuItemAt( item->SetMinorIcon(minor_icon); if (!icon.isNull()) item->SetIcon(icon); - if (type == SUBMENU) + if (type == SUBMENU || type == ACTIONABLE_SUBMENU) item->CreateSubmenu(); if (GetDelegate() && !GetDelegate()->IsCommandVisible(item_id)) item->SetVisible(false); @@ -398,6 +401,19 @@ void MenuItemView::SetSelected(bool selected) { SchedulePaint(); } +void MenuItemView::SetSelectionOfActionableSubmenu( + bool submenu_area_of_actionable_submenu_selected) { + DCHECK_EQ(ACTIONABLE_SUBMENU, type_); + if (submenu_area_of_actionable_submenu_selected_ == + submenu_area_of_actionable_submenu_selected) { + return; + } + + submenu_area_of_actionable_submenu_selected_ = + submenu_area_of_actionable_submenu_selected; + SchedulePaint(); +} + void MenuItemView::SetTooltip(const base::string16& tooltip, int item_id) { MenuItemView* item = GetMenuItemByID(item_id); DCHECK(item); @@ -458,6 +474,13 @@ int MenuItemView::GetHeightForWidth(int width) const { return height; } +gfx::Rect MenuItemView::GetSubmenuAreaOfActionableSubmenu() const { + DCHECK_EQ(ACTIONABLE_SUBMENU, type_); + const MenuConfig& config = MenuConfig::instance(); + return gfx::Rect(gfx::Point(vertical_separator_->bounds().right(), 0), + gfx::Size(config.actionable_submenu_width, height())); +} + const MenuItemView::MenuItemDimensions& MenuItemView::GetDimensions() const { if (!is_dimensions_valid()) dimensions_ = CalculateDimensions(); @@ -495,8 +518,10 @@ const MenuItemView* MenuItemView::GetRootMenuItem() const { } base::char16 MenuItemView::GetMnemonic() { - if (!GetRootMenuItem()->has_mnemonics_) + if (!GetRootMenuItem()->has_mnemonics_ || + !MenuConfig::instance().use_mnemonics) { return 0; + } size_t index = 0; do { @@ -580,6 +605,8 @@ void MenuItemView::Layout() { continue; if (submenu_arrow_image_view_ == child) continue; + if (vertical_separator_ == child) + continue; int width = child->GetPreferredSize().width(); child->SetBounds(x - width, 0, width, height()); x -= width + kChildXPadding; @@ -611,13 +638,25 @@ void MenuItemView::Layout() { } if (submenu_arrow_image_view_) { - int x = width() - config.arrow_width - config.arrow_to_edge_padding; + int x = width() - config.arrow_width - + (type_ == ACTIONABLE_SUBMENU + ? config.actionable_submenu_arrow_to_edge_padding + : config.arrow_to_edge_padding); int y = (height() + GetTopMargin() - GetBottomMargin() - kSubmenuArrowSize) / 2; submenu_arrow_image_view_->SetBounds(x, y, config.arrow_width, kSubmenuArrowSize); } + + if (vertical_separator_) { + const gfx::Size preferred_size = vertical_separator_->GetPreferredSize(); + int x = width() - config.actionable_submenu_width - + config.actionable_submenu_vertical_separator_width; + int y = (height() - preferred_size.height()) / 2; + vertical_separator_->SetBoundsRect( + gfx::Rect(gfx::Point(x, y), preferred_size)); + } } } @@ -682,7 +721,11 @@ void MenuItemView::UpdateMenuPartSizes() { if (has_icons_) icon_area_width_ = std::max(icon_area_width_, GetMaxIconViewWidth()); - label_start_ = config.item_left_margin + icon_area_width_; + const bool use_touchable_layout = + GetMenuController() && GetMenuController()->use_touchable_layout(); + label_start_ = (use_touchable_layout ? config.touchable_item_left_margin + : config.item_left_margin) + + icon_area_width_; int padding = 0; if (config.always_use_icon_to_label_padding) { padding = config.icon_to_label_padding; @@ -690,7 +733,7 @@ void MenuItemView::UpdateMenuPartSizes() { padding = (has_icons_ || HasChecksOrRadioButtons()) ? config.icon_to_label_padding : 0; } - if (GetMenuController() && GetMenuController()->use_touchable_layout()) + if (use_touchable_layout) padding = config.touchable_icon_to_label_padding; label_start_ += padding; @@ -705,15 +748,16 @@ void MenuItemView::Init(MenuItemView* parent, MenuItemView::Type type, MenuDelegate* delegate) { delegate_ = delegate; - controller_ = NULL; + controller_ = nullptr; canceled_ = false; parent_menu_item_ = parent; type_ = type; selected_ = false; command_ = command; - submenu_ = NULL; + submenu_ = nullptr; radio_check_image_view_ = nullptr; submenu_arrow_image_view_ = nullptr; + vertical_separator_ = nullptr; show_mnemonics_ = false; // Assign our ID, this allows SubmenuItemView to find MenuItemViews. set_id(kMenuItemViewID); @@ -729,6 +773,20 @@ void MenuItemView::Init(MenuItemView* parent, AddChildView(radio_check_image_view_); } + if (type_ == ACTIONABLE_SUBMENU) { + vertical_separator_ = new Separator(); + vertical_separator_->SetVisible(true); + vertical_separator_->SetFocusBehavior(FocusBehavior::NEVER); + const MenuConfig& config = MenuConfig::instance(); + vertical_separator_->SetColor(GetNativeTheme()->GetSystemColor( + ui::NativeTheme::kColorId_ActionableSubmenuVerticalSeparatorColor)); + vertical_separator_->SetPreferredSize( + gfx::Size(config.actionable_submenu_vertical_separator_width, + config.actionable_submenu_vertical_separator_height)); + vertical_separator_->set_can_process_events_within_subtree(false); + AddChildView(vertical_separator_); + } + if (submenu_arrow_image_view_) submenu_arrow_image_view_->SetVisible(HasSubmenu()); @@ -844,6 +902,15 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { ui::NativeTheme* native_theme = GetNativeTheme(); if (render_selection) { gfx::Rect item_bounds(0, 0, width(), height()); + if (type_ == ACTIONABLE_SUBMENU) { + if (submenu_area_of_actionable_submenu_selected_) { + item_bounds = GetSubmenuAreaOfActionableSubmenu(); + } else { + item_bounds = gfx::Rect(gfx::Size( + width() - MenuConfig::instance().actionable_submenu_width - 1, + height())); + } + } AdjustBoundsForRTLUI(&item_bounds); native_theme->Paint(canvas->sk_canvas(), @@ -962,6 +1029,10 @@ SkColor MenuItemView::GetTextColor(bool minor, if (!emphasized) color_id = ui::NativeTheme::kColorId_DisabledMenuItemForegroundColor; } + + if (GetMenuController() && GetMenuController()->use_touchable_layout()) + color_id = ui::NativeTheme::kColorId_TouchableMenuItemLabelColor; + return GetNativeTheme()->GetSystemColor(color_id); } @@ -1012,6 +1083,8 @@ gfx::Size MenuItemView::GetChildPreferredSize() const { continue; if (submenu_arrow_image_view_ == child) continue; + if (vertical_separator_ == child) + continue; if (i) width += kChildXPadding; width += child->GetPreferredSize().width(); @@ -1034,30 +1107,18 @@ MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const { const MenuConfig& menu_config = MenuConfig::instance(); if (GetMenuController() && GetMenuController()->use_touchable_layout()) { - // MenuItemViews that use the touchable layout have fixed height and width. + // Touchable layout uses a fixed size, but adjusts the height for icons. dimensions.height = menu_config.touchable_menu_height; + if (icon_view_) { + dimensions.height = icon_view_->height() + + 2 * menu_config.vertical_touchable_menu_item_padding; + } dimensions.standard_width = menu_config.touchable_menu_width; return dimensions; } const gfx::FontList& font_list = GetFontList(); base::string16 minor_text = GetMinorText(); - if (menu_config.fixed_text_item_height && - menu_config.fixed_container_item_height && menu_config.fixed_menu_width && - GetMenuController() && !GetMenuController()->is_combobox()) { - bool has_children = NonIconChildViewsCount() > 0; - dimensions.height = has_children ? menu_config.fixed_container_item_height - : menu_config.fixed_text_item_height; - dimensions.children_width = 0; - dimensions.minor_text_width = - minor_text.empty() ? 0 : gfx::GetStringWidth(minor_text, font_list); - int leave_for_minor = dimensions.minor_text_width - ? dimensions.minor_text_width + - menu_config.label_to_minor_text_padding - : 0; - dimensions.standard_width = menu_config.fixed_menu_width - leave_for_minor; - return dimensions; - } dimensions.height = child_size.height(); // Adjust item content height if menu has both items with and without icons. @@ -1069,8 +1130,10 @@ MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const { dimensions.height += GetBottomMargin() + GetTopMargin(); // In case of a container, only the container size needs to be filled. - if (IsContainer()) + if (IsContainer()) { + ApplyMinimumDimensions(&dimensions); return dimensions; + } // Get Icon margin overrides for this particular item. const MenuDelegate* delegate = GetDelegate(); @@ -1105,11 +1168,37 @@ MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const { font_list.GetHeight() + GetBottomMargin() + GetTopMargin()); dimensions.height = std::max(dimensions.height, MenuConfig::instance().item_min_height); + + ApplyMinimumDimensions(&dimensions); return dimensions; } +void MenuItemView::ApplyMinimumDimensions(MenuItemDimensions* dims) const { + // Don't apply minimums to menus without controllers or to comboboxes. + if (!GetMenuController() || GetMenuController()->is_combobox()) + return; + + int used = + dims->standard_width + dims->children_width + dims->minor_text_width; + const MenuConfig& config = MenuConfig::instance(); + if (used < config.minimum_menu_width) + dims->standard_width += (config.minimum_menu_width - used); + + dims->height = std::max(dims->height, + IsContainer() ? config.minimum_container_item_height + : config.minimum_text_item_height); +} + int MenuItemView::GetLabelStartForThisItem() const { const MenuConfig& config = MenuConfig::instance(); + + // Touchable items with icons do not respect |label_start_|. + if (GetMenuController() && GetMenuController()->use_touchable_layout() && + icon_view_) { + return config.touchable_item_left_margin + icon_view_->width() + + config.touchable_icon_to_label_padding; + } + int label_start = label_start_ + left_icon_margin_ + right_icon_margin_; if ((config.icons_in_label || type_ == CHECKBOX || type_ == RADIO) && icon_view_) @@ -1124,12 +1213,9 @@ base::string16 MenuItemView::GetMinorText() const { return base::string16(); } - ui::Accelerator accelerator; - if (MenuConfig::instance().show_accelerators && GetDelegate() && - GetCommand() && - GetDelegate()->GetAccelerator(GetCommand(), &accelerator)) { - return accelerator.GetShortcutText(); - } + base::string16 accel_text; + if (MenuConfig::instance().ShouldShowAcceleratorText(this, &accel_text)) + return accel_text; return minor_text_; } @@ -1149,7 +1235,7 @@ int MenuItemView::NonIconChildViewsCount() const { // not the number of menu items. return child_count() - (icon_view_ ? 1 : 0) - (radio_check_image_view_ ? 1 : 0) - - (submenu_arrow_image_view_ ? 1 : 0); + (submenu_arrow_image_view_ ? 1 : 0) - (vertical_separator_ ? 1 : 0); } int MenuItemView::GetMaxIconViewWidth() const { diff --git a/chromium/ui/views/controls/menu/menu_item_view.h b/chromium/ui/views/controls/menu/menu_item_view.h index be24fb73cf7..6276932ceaf 100644 --- a/chromium/ui/views/controls/menu/menu_item_view.h +++ b/chromium/ui/views/controls/menu/menu_item_view.h @@ -17,7 +17,6 @@ #include "build/build_config.h" #include "ui/base/models/menu_separator_types.h" #include "ui/gfx/image/image_skia.h" -#include "ui/views/controls/menu/menu_config.h" #include "ui/views/controls/menu/menu_controller.h" #include "ui/views/controls/menu/menu_types.h" #include "ui/views/view.h" @@ -45,6 +44,7 @@ class TestMenuItemViewShown; class MenuController; class MenuDelegate; +class Separator; class TestMenuItemView; class SubmenuView; @@ -83,15 +83,16 @@ class VIEWS_EXPORT MenuItemView : public View { // ID used to identify empty menu items. static const int kEmptyMenuItemViewID; - // Different types of menu items. EMPTY is a special type for empty - // menus that is only used internally. + // Different types of menu items. enum Type { - NORMAL, - SUBMENU, - CHECKBOX, - RADIO, - SEPARATOR, - EMPTY + NORMAL, // Performs an action when selected. + SUBMENU, // Presents a submenu within another menu. + ACTIONABLE_SUBMENU, // A SUBMENU that is also a COMMAND. + CHECKBOX, // Can be selected/checked to toggle a boolean state. + RADIO, // Can be selected/checked among a group of choices. + SEPARATOR, // Shows a horizontal line separator. + EMPTY, // EMPTY is a special type for empty menus that is only used + // internally. }; // Where the menu should be drawn, above or below the bounds (when @@ -257,6 +258,15 @@ class VIEWS_EXPORT MenuItemView : public View { // Returns true if the item is selected. bool IsSelected() const { return selected_; } + // Sets whether the submenu area of an ACTIONABLE_SUBMENU is selected. + void SetSelectionOfActionableSubmenu( + bool submenu_area_of_actionable_submenu_selected); + + // Whether the submenu area of an ACTIONABLE_SUBMENU is selected. + bool IsSubmenuAreaOfActionableSubmenuSelected() const { + return submenu_area_of_actionable_submenu_selected_; + } + // Sets the |tooltip| for a menu item view with |item_id| identifier. void SetTooltip(const base::string16& tooltip, int item_id); @@ -288,6 +298,9 @@ class VIEWS_EXPORT MenuItemView : public View { // dimensions. int GetHeightForWidth(int width) const override; + // Returns the bounds of the submenu part of the ACTIONABLE_SUBMENU. + gfx::Rect GetSubmenuAreaOfActionableSubmenu() const; + // Return the preferred dimensions of the item in pixel. const MenuItemDimensions& GetDimensions() const; @@ -430,6 +443,14 @@ class VIEWS_EXPORT MenuItemView : public View { // Calculates and returns the MenuItemDimensions. MenuItemDimensions CalculateDimensions() const; + // Imposes MenuConfig's minimum sizes, if any, on the supplied + // dimensions and returns the new dimensions. It is guaranteed that: + // ApplyMinimumDimensions(x).standard_width >= x.standard_width + // ApplyMinimumDimensions(x).children_width == x.children_width + // ApplyMinimumDimensions(x).minor_text_width == x.minor_text_width + // ApplyMinimumDimensions(x).height >= x.height + void ApplyMinimumDimensions(MenuItemDimensions* dims) const; + // Get the horizontal position at which to draw the menu item's label. int GetLabelStartForThisItem() const; @@ -484,6 +505,9 @@ class VIEWS_EXPORT MenuItemView : public View { // Whether we're selected. bool selected_; + // Whether the submenu area of an ACTIONABLE_SUBMENU is selected. + bool submenu_area_of_actionable_submenu_selected_; + // Command id. int command_; @@ -565,6 +589,10 @@ class VIEWS_EXPORT MenuItemView : public View { // The forced visual selection state of this item, if any. base::Optional<bool> forced_visual_selection_; + // The vertical separator that separates the actionable and submenu regions of + // an ACTIONABLE_SUBMENU. + Separator* vertical_separator_; + DISALLOW_COPY_AND_ASSIGN(MenuItemView); }; 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 bdc22a6b784..07273f5de1a 100644 --- a/chromium/ui/views/controls/menu/menu_item_view_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_item_view_unittest.cc @@ -41,6 +41,8 @@ class TestMenuItemView : public MenuItemView { void AddEmptyMenus() { MenuItemView::AddEmptyMenus(); } + void SetHasMnemonics(bool has_mnemonics) { has_mnemonics_ = has_mnemonics; } + private: DISALLOW_COPY_AND_ASSIGN(TestMenuItemView); }; @@ -70,15 +72,14 @@ TEST(MenuItemViewUnitTest, TestMenuItemViewWithFlexibleWidthChild) { ASSERT_EQ(flexible_view, submenu->GetMenuItemAt(1)); gfx::Size flexible_size = flexible_view->GetPreferredSize(); - // The flexible view's "preferred size" should be 1x1... - EXPECT_EQ(flexible_size, gfx::Size(1, 1)); + EXPECT_EQ(1, flexible_size.width()); // ...but it should use whatever space is available to make a square. int flex_height = flexible_view->GetHeightForWidth(label_size.width()); EXPECT_EQ(label_size.width(), flex_height); - // The submenu should be tall enough to allow for both menu items at the given - // width. + // The submenu should be tall enough to allow for both menu items at the + // given width. EXPECT_EQ(label_size.height() + flex_height, submenu->GetPreferredSize().height()); } @@ -147,6 +148,24 @@ TEST(MenuItemViewUnitTest, TestEmptySubmenuWhenAllChildItemsAreHidden) { empty_item->title()); } +TEST(MenuItemViewUnitTest, UseMnemonicOnPlatform) { + TestMenuItemView root_menu; + views::MenuItemView* item1 = + root_menu.AppendMenuItemWithLabel(1, base::ASCIIToUTF16("&Item 1")); + views::MenuItemView* item2 = + root_menu.AppendMenuItemWithLabel(2, base::ASCIIToUTF16("I&tem 2")); + + root_menu.SetHasMnemonics(true); + + if (MenuConfig::instance().use_mnemonics) { + EXPECT_EQ('i', item1->GetMnemonic()); + EXPECT_EQ('t', item2->GetMnemonic()); + } else { + EXPECT_EQ(0, item1->GetMnemonic()); + EXPECT_EQ(0, item2->GetMnemonic()); + } +} + class MenuItemViewPaintUnitTest : public ViewsTestBase { public: MenuItemViewPaintUnitTest() {} diff --git a/chromium/ui/views/controls/menu/menu_model_adapter.cc b/chromium/ui/views/controls/menu/menu_model_adapter.cc index e1a2339bddb..e9cfe1c466c 100644 --- a/chromium/ui/views/controls/menu/menu_model_adapter.cc +++ b/chromium/ui/views/controls/menu/menu_model_adapter.cc @@ -80,6 +80,9 @@ MenuItemView* MenuModelAdapter::AddMenuItemFromModelAt(ui::MenuModel* model, case ui::MenuModel::TYPE_SUBMENU: type = MenuItemView::SUBMENU; break; + case ui::MenuModel::TYPE_ACTIONABLE_SUBMENU: + type = MenuItemView::ACTIONABLE_SUBMENU; + break; } if (*type == MenuItemView::SEPARATOR) { @@ -267,9 +270,11 @@ void MenuModelAdapter::BuildMenuImpl(MenuItemView* menu, ui::MenuModel* model) { for (int i = 0; i < item_count; ++i) { MenuItemView* item = AppendMenuItem(menu, model, i); - if (model->GetTypeAt(i) == ui::MenuModel::TYPE_SUBMENU) { + if (model->GetTypeAt(i) == ui::MenuModel::TYPE_SUBMENU || + model->GetTypeAt(i) == ui::MenuModel::TYPE_ACTIONABLE_SUBMENU) { DCHECK(item); - DCHECK_EQ(MenuItemView::SUBMENU, item->GetType()); + DCHECK(item->GetType() == MenuItemView::SUBMENU || + item->GetType() == MenuItemView::ACTIONABLE_SUBMENU); ui::MenuModel* submodel = model->GetSubmenuModelAt(i); DCHECK(submodel); BuildMenuImpl(item, submodel); 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 c5df9547874..df3bb3ef251 100644 --- a/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc @@ -16,8 +16,9 @@ namespace { // Base command id for test menu and its submenu. -const int kRootIdBase = 100; -const int kSubmenuIdBase = 200; +constexpr int kRootIdBase = 100; +constexpr int kSubmenuIdBase = 200; +constexpr int kActionableSubmenuIdBase = 300; class MenuModelBase : public ui::MenuModel { public: @@ -139,61 +140,68 @@ class SubmenuModel : public MenuModelBase { DISALLOW_COPY_AND_ASSIGN(SubmenuModel); }; +class ActionableSubmenuModel : public MenuModelBase { + public: + ActionableSubmenuModel() : MenuModelBase(kActionableSubmenuIdBase) { + items_.push_back(Item(TYPE_COMMAND, "actionable submenu item 0", NULL)); + items_.push_back(Item(TYPE_COMMAND, "actionable submenu item 1", NULL)); + } + ~ActionableSubmenuModel() override = default; + + private: + DISALLOW_COPY_AND_ASSIGN(ActionableSubmenuModel); +}; + class RootModel : public MenuModelBase { public: RootModel() : MenuModelBase(kRootIdBase) { - submenu_model_.reset(new SubmenuModel); + submenu_model_ = std::make_unique<SubmenuModel>(); + actionable_submenu_model_ = std::make_unique<ActionableSubmenuModel>(); items_.push_back(Item(TYPE_COMMAND, "command 0", NULL)); items_.push_back(Item(TYPE_CHECK, "check 1", NULL)); items_.push_back(Item(TYPE_SEPARATOR, "", NULL)); items_.push_back(Item(TYPE_SUBMENU, "submenu 3", submenu_model_.get())); items_.push_back(Item(TYPE_RADIO, "radio 4", NULL)); + items_.push_back(Item(TYPE_ACTIONABLE_SUBMENU, "actionable 5", + actionable_submenu_model_.get())); } ~RootModel() override {} private: std::unique_ptr<MenuModel> submenu_model_; + std::unique_ptr<MenuModel> actionable_submenu_model_; DISALLOW_COPY_AND_ASSIGN(RootModel); }; -} // namespace - -namespace views { - -typedef ViewsTestBase MenuModelAdapterTest; - -TEST_F(MenuModelAdapterTest, BasicTest) { - // Build model and adapter. - RootModel model; - views::MenuModelAdapter delegate(&model); - - // Create menu. Build menu twice to check that rebuilding works properly. - MenuItemView* menu = new views::MenuItemView(&delegate); - // MenuRunner takes ownership of menu. - std::unique_ptr<MenuRunner> menu_runner(new MenuRunner(menu, 0)); - delegate.BuildMenu(menu); - delegate.BuildMenu(menu); - EXPECT_TRUE(menu->HasSubmenu()); +void CheckSubmenu(const RootModel& model, + views::MenuItemView* menu, + views::MenuModelAdapter* delegate, + int submenu_id, + int expected_children, + int submenu_model_index, + int id_base) { + views::MenuItemView* submenu = menu->GetMenuItemByID(submenu_id); + views::SubmenuView* subitem_container = submenu->GetSubmenu(); + EXPECT_EQ(expected_children, subitem_container->child_count()); - // Check top level menu items. - views::SubmenuView* item_container = menu->GetSubmenu(); - EXPECT_EQ(5, item_container->child_count()); + for (int i = 0; i < subitem_container->child_count(); ++i) { + MenuModelBase* submodel = static_cast<MenuModelBase*>( + model.GetSubmenuModelAt(submenu_model_index)); + EXPECT_TRUE(submodel); - for (int i = 0; i < item_container->child_count(); ++i) { - const MenuModelBase::Item& model_item = model.GetItemDefinition(i); + const MenuModelBase::Item& model_item = submodel->GetItemDefinition(i); - const int id = i + kRootIdBase; - MenuItemView* item = menu->GetMenuItemByID(id); + const int id = i + id_base; + views::MenuItemView* item = menu->GetMenuItemByID(id); if (!item) { EXPECT_EQ(ui::MenuModel::TYPE_SEPARATOR, model_item.type); continue; } - // Check placement. - EXPECT_EQ(i, menu->GetSubmenu()->GetIndexOf(item)); + EXPECT_EQ(i, submenu->GetSubmenu()->GetIndexOf(item)); // Check type. switch (model_item.type) { @@ -212,27 +220,45 @@ TEST_F(MenuModelAdapterTest, BasicTest) { case ui::MenuModel::TYPE_SUBMENU: EXPECT_EQ(views::MenuItemView::SUBMENU, item->GetType()); break; + case ui::MenuModel::TYPE_ACTIONABLE_SUBMENU: + EXPECT_EQ(views::MenuItemView::ACTIONABLE_SUBMENU, item->GetType()); + break; } // Check activation. - static_cast<views::MenuDelegate*>(&delegate)->ExecuteCommand(id); - EXPECT_EQ(i, model.last_activation()); - model.set_last_activation(-1); + static_cast<views::MenuDelegate*>(delegate)->ExecuteCommand(id); + EXPECT_EQ(i, submodel->last_activation()); + submodel->set_last_activation(-1); } +} - // Check submenu items. - views::MenuItemView* submenu = menu->GetMenuItemByID(103); - views::SubmenuView* subitem_container = submenu->GetSubmenu(); - EXPECT_EQ(2, subitem_container->child_count()); +} // namespace - for (int i = 0; i < subitem_container->child_count(); ++i) { - MenuModelBase* submodel = static_cast<MenuModelBase*>( - model.GetSubmenuModelAt(3)); - EXPECT_TRUE(submodel); +namespace views { - const MenuModelBase::Item& model_item = submodel->GetItemDefinition(i); +typedef ViewsTestBase MenuModelAdapterTest; + +TEST_F(MenuModelAdapterTest, BasicTest) { + // Build model and adapter. + RootModel model; + views::MenuModelAdapter delegate(&model); - const int id = i + kSubmenuIdBase; + // Create menu. Build menu twice to check that rebuilding works properly. + MenuItemView* menu = new views::MenuItemView(&delegate); + // MenuRunner takes ownership of menu. + std::unique_ptr<MenuRunner> menu_runner(new MenuRunner(menu, 0)); + delegate.BuildMenu(menu); + delegate.BuildMenu(menu); + EXPECT_TRUE(menu->HasSubmenu()); + + // Check top level menu items. + views::SubmenuView* item_container = menu->GetSubmenu(); + EXPECT_EQ(6, item_container->child_count()); + + for (int i = 0; i < item_container->child_count(); ++i) { + const MenuModelBase::Item& model_item = model.GetItemDefinition(i); + + const int id = i + kRootIdBase; MenuItemView* item = menu->GetMenuItemByID(id); if (!item) { EXPECT_EQ(ui::MenuModel::TYPE_SEPARATOR, model_item.type); @@ -240,7 +266,7 @@ TEST_F(MenuModelAdapterTest, BasicTest) { } // Check placement. - EXPECT_EQ(i, submenu->GetSubmenu()->GetIndexOf(item)); + EXPECT_EQ(i, menu->GetSubmenu()->GetIndexOf(item)); // Check type. switch (model_item.type) { @@ -259,14 +285,27 @@ TEST_F(MenuModelAdapterTest, BasicTest) { case ui::MenuModel::TYPE_SUBMENU: EXPECT_EQ(views::MenuItemView::SUBMENU, item->GetType()); break; + case ui::MenuModel::TYPE_ACTIONABLE_SUBMENU: + EXPECT_EQ(views::MenuItemView::ACTIONABLE_SUBMENU, item->GetType()); + break; } // Check activation. static_cast<views::MenuDelegate*>(&delegate)->ExecuteCommand(id); - EXPECT_EQ(i, submodel->last_activation()); - submodel->set_last_activation(-1); + EXPECT_EQ(i, model.last_activation()); + model.set_last_activation(-1); } + // Check the submenu. + const int submenu_index = 3; + CheckSubmenu(model, menu, &delegate, kRootIdBase + submenu_index, 2, + submenu_index, kSubmenuIdBase); + + // Check the actionable submenu. + const int actionable_submenu_index = 5; + CheckSubmenu(model, menu, &delegate, kRootIdBase + actionable_submenu_index, + 2, actionable_submenu_index, kActionableSubmenuIdBase); + // Check that selecting the root item is safe. The MenuModel does // not care about the root so MenuModelAdapter should do nothing // (not hit the NOTREACHED check) when the root is selected. diff --git a/chromium/ui/views/controls/menu/menu_runner_cocoa_unittest.mm b/chromium/ui/views/controls/menu/menu_runner_cocoa_unittest.mm index 8bd58e67d17..ae0c8bafd05 100644 --- a/chromium/ui/views/controls/menu/menu_runner_cocoa_unittest.mm +++ b/chromium/ui/views/controls/menu/menu_runner_cocoa_unittest.mm @@ -12,8 +12,10 @@ #include "base/test/test_timeouts.h" #include "base/threading/thread_task_runner_handle.h" #import "testing/gtest_mac.h" +#include "ui/base/accelerators/accelerator.h" #include "ui/base/models/simple_menu_model.h" #include "ui/events/event_utils.h" +#import "ui/events/test/cocoa_test_event_utils.h" #include "ui/views/controls/menu/menu_runner_impl_adapter.h" #include "ui/views/test/views_test_base.h" @@ -21,14 +23,16 @@ namespace views { namespace test { namespace { +constexpr int kTestCommandId = 0; + class TestModel : public ui::SimpleMenuModel { public: TestModel() : ui::SimpleMenuModel(&delegate_), delegate_(this) {} void set_checked_command(int command) { checked_command_ = command; } - void set_menu_open_callback(const base::Closure& callback) { - menu_open_callback_ = callback; + void set_menu_open_callback(base::OnceClosure callback) { + menu_open_callback_ = std::move(callback); } private: @@ -43,7 +47,17 @@ class TestModel : public ui::SimpleMenuModel { void MenuWillShow(SimpleMenuModel* source) override { if (!model_->menu_open_callback_.is_null()) - model_->menu_open_callback_.Run(); + std::move(model_->menu_open_callback_).Run(); + } + + bool GetAcceleratorForCommandId( + int command_id, + ui::Accelerator* accelerator) const override { + if (command_id == kTestCommandId) { + *accelerator = ui::Accelerator(ui::VKEY_E, ui::EF_CONTROL_DOWN); + return true; + } + return false; } private: @@ -55,7 +69,7 @@ class TestModel : public ui::SimpleMenuModel { private: int checked_command_ = -1; Delegate delegate_; - base::Closure menu_open_callback_; + base::OnceClosure menu_open_callback_; DISALLOW_COPY_AND_ASSIGN(TestModel); }; @@ -84,7 +98,7 @@ class MenuRunnerCocoaTest : public ViewsTestBase, ViewsTestBase::SetUp(); menu_.reset(new TestModel()); - menu_->AddCheckItem(0, base::ASCIIToUTF16("Menu Item")); + menu_->AddCheckItem(kTestCommandId, base::ASCIIToUTF16("Menu Item")); parent_ = new views::Widget(); parent_->Init(CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS)); @@ -114,16 +128,17 @@ class MenuRunnerCocoaTest : public ViewsTestBase, int IsAsync() const { return GetParam() == MenuType::VIEWS; } // Runs the menu after registering |callback| as the menu open callback. - void RunMenu(const base::Closure& callback) { + void RunMenu(base::OnceClosure callback) { if (IsAsync()) { // Cancelling an async menu under MenuControllerCocoa::OpenMenuImpl() // (which invokes WillShowMenu()) will cause a UAF when that same function // tries to show the menu. So post a task instead. - base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, callback); + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + std::move(callback)); } else { menu_->set_menu_open_callback( - base::Bind(&MenuRunnerCocoaTest::RunMenuWrapperCallback, - base::Unretained(this), callback)); + base::BindOnce(&MenuRunnerCocoaTest::RunMenuWrapperCallback, + base::Unretained(this), std::move(callback))); } runner_->RunMenuAt(parent_, nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT, @@ -140,13 +155,15 @@ class MenuRunnerCocoaTest : public ViewsTestBase, // go up by one (the anchor view) while the menu is shown. EXPECT_EQ(1u, [[parent_->GetNativeView() subviews] count]); - base::Closure callback = - base::Bind(&MenuRunnerCocoaTest::ComboboxRunMenuAtCallback, - base::Unretained(this)); - if (IsAsync()) - base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, callback); - else - menu_->set_menu_open_callback(callback); + base::OnceClosure callback = + base::BindOnce(&MenuRunnerCocoaTest::ComboboxRunMenuAtCallback, + base::Unretained(this)); + if (IsAsync()) { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + std::move(callback)); + } else { + menu_->set_menu_open_callback(std::move(callback)); + } runner_->RunMenuAt(parent_, nullptr, anchor, MENU_ANCHOR_TOPLEFT, MenuRunner::COMBOBOX); @@ -178,6 +195,44 @@ class MenuRunnerCocoaTest : public ViewsTestBase, QuitAsyncRunLoop(); } + NSMenu* GetNativeNSMenu() { + if (GetParam() == MenuType::VIEWS) + return nil; + + internal::MenuRunnerImplCocoa* cocoa_runner = + static_cast<internal::MenuRunnerImplCocoa*>(runner_); + return [cocoa_runner->menu_controller_ menu]; + } + + void ModelDeleteThenSelectItemCallback() { + // AppKit may retain a reference to the NSMenu. + base::scoped_nsobject<NSMenu> native_menu(GetNativeNSMenu(), + base::scoped_policy::RETAIN); + + // A View showing a menu typically owns a MenuRunner unique_ptr, which will + // will be destroyed (releasing the MenuRunnerImpl) alongside the MenuModel. + runner_->Release(); + runner_ = nullptr; + menu_ = nullptr; + + // The menu is closing (yet "alive"), but the model is destroyed. The user + // may have already made an event to select an item in the menu. This + // doesn't bother views menus (see MenuRunnerImpl::empty_delegate_) but + // Cocoa menu items are refcounted and have access to a raw weak pointer in + // the MenuController. + if (GetParam() == MenuType::VIEWS) { + QuitAsyncRunLoop(); + return; + } + + EXPECT_TRUE(native_menu.get()); + + // Simulate clicking the item using its accelerator. + NSEvent* accelerator = cocoa_test_event_utils::KeyEventWithKeyCode( + 'e', 'e', NSKeyDown, NSCommandKeyMask); + [native_menu performKeyEquivalent:accelerator]; + } + void MenuCancelAndDeleteCallback() { runner_->Cancel(); runner_->Release(); @@ -192,9 +247,9 @@ class MenuRunnerCocoaTest : public ViewsTestBase, int menu_close_count_ = 0; private: - void RunMenuWrapperCallback(const base::Closure& callback) { + void RunMenuWrapperCallback(base::OnceClosure callback) { EXPECT_TRUE(runner_->IsRunning()); - callback.Run(); + std::move(callback).Run(); } void ComboboxRunMenuAtCallback() { @@ -248,8 +303,8 @@ class MenuRunnerCocoaTest : public ViewsTestBase, TEST_P(MenuRunnerCocoaTest, RunMenuAndCancel) { base::TimeTicks min_time = ui::EventTimeForNow(); - RunMenu(base::Bind(&MenuRunnerCocoaTest::MenuCancelCallback, - base::Unretained(this))); + RunMenu(base::BindOnce(&MenuRunnerCocoaTest::MenuCancelCallback, + base::Unretained(this))); EXPECT_EQ(1, menu_close_count_); EXPECT_FALSE(runner_->IsRunning()); @@ -271,17 +326,26 @@ TEST_P(MenuRunnerCocoaTest, RunMenuAndCancel) { } TEST_P(MenuRunnerCocoaTest, RunMenuAndDelete) { - RunMenu(base::Bind(&MenuRunnerCocoaTest::MenuDeleteCallback, - base::Unretained(this))); + RunMenu(base::BindOnce(&MenuRunnerCocoaTest::MenuDeleteCallback, + base::Unretained(this))); // Note the close callback is NOT invoked for deleted menus. EXPECT_EQ(0, menu_close_count_); } +// Tests a potential lifetime issue using the Cocoa MenuController, which has a +// weak reference to the model. +TEST_P(MenuRunnerCocoaTest, RunMenuAndDeleteThenSelectItem) { + RunMenu( + base::BindOnce(&MenuRunnerCocoaTest::ModelDeleteThenSelectItemCallback, + base::Unretained(this))); + EXPECT_EQ(0, menu_close_count_); +} + // Ensure a menu can be safely released immediately after a call to Cancel() in // the same run loop iteration. TEST_P(MenuRunnerCocoaTest, DestroyAfterCanceling) { - RunMenu(base::Bind(&MenuRunnerCocoaTest::MenuCancelAndDeleteCallback, - base::Unretained(this))); + RunMenu(base::BindOnce(&MenuRunnerCocoaTest::MenuCancelAndDeleteCallback, + base::Unretained(this))); if (IsAsync()) { EXPECT_EQ(1, menu_close_count_); @@ -294,8 +358,8 @@ TEST_P(MenuRunnerCocoaTest, DestroyAfterCanceling) { TEST_P(MenuRunnerCocoaTest, RunMenuTwice) { for (int i = 0; i < 2; ++i) { - RunMenu(base::Bind(&MenuRunnerCocoaTest::MenuCancelCallback, - base::Unretained(this))); + RunMenu(base::BindOnce(&MenuRunnerCocoaTest::MenuCancelCallback, + base::Unretained(this))); EXPECT_FALSE(runner_->IsRunning()); EXPECT_EQ(i + 1, menu_close_count_); } @@ -339,7 +403,7 @@ TEST_P(MenuRunnerCocoaTest, ComboboxAnchoring) { combobox_rect.width(), 0), last_anchor_frame_); - menu_->set_checked_command(0); + menu_->set_checked_command(kTestCommandId); RunMenuAt(anchor_rect); // Native constant used by MenuRunnerImplCocoa. diff --git a/chromium/ui/views/controls/menu/menu_runner_impl.cc b/chromium/ui/views/controls/menu/menu_runner_impl.cc index cd9ac6df3ff..1c7b5c4c72a 100644 --- a/chromium/ui/views/controls/menu/menu_runner_impl.cc +++ b/chromium/ui/views/controls/menu/menu_runner_impl.cc @@ -92,9 +92,9 @@ void MenuRunnerImpl::RunMenuAt(Widget* parent, MenuController* controller = MenuController::GetActiveInstance(); if (controller) { if ((run_types & MenuRunner::IS_NESTED) != 0) { - if (!controller->IsBlockingRun()) { + if (controller->for_drop()) { controller->CancelAll(); - controller = NULL; + controller = nullptr; } else { // Only nest the delegate when not cancelling drag-and-drop. When // cancelling this will become the root delegate of the new @@ -112,7 +112,7 @@ void MenuRunnerImpl::RunMenuAt(Widget* parent, } // Drop menus don't block the message loop, so it's ok to create a new // MenuController. - controller = NULL; + controller = nullptr; } } @@ -122,7 +122,7 @@ void MenuRunnerImpl::RunMenuAt(Widget* parent, owns_controller_ = false; if (!controller) { // No menus are showing, show one. - controller = new MenuController(!for_drop_, this); + controller = new MenuController(for_drop_, this); owns_controller_ = true; } controller->set_is_combobox((run_types & MenuRunner::COMBOBOX) != 0); @@ -204,6 +204,8 @@ bool MenuRunnerImpl::ShouldShowMnemonics(MenuButton* button) { show_mnemonics |= ui::win::IsAltPressed(); #elif defined(USE_X11) show_mnemonics |= ui::IsAltPressed(); +#elif defined(OS_MACOSX) + show_mnemonics = false; #endif return show_mnemonics; } diff --git a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.h b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.h index c89e9cee9c7..599000dd285 100644 --- a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.h +++ b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.h @@ -16,6 +16,9 @@ @class MenuControllerCocoa; namespace views { +namespace test { +class MenuRunnerCocoaTest; +} namespace internal { // A menu runner implementation that uses NSMenu to show a context menu. @@ -35,6 +38,8 @@ class VIEWS_EXPORT MenuRunnerImplCocoa : public MenuRunnerImplInterface { base::TimeTicks GetClosingEventTime() const override; private: + friend class views::test::MenuRunnerCocoaTest; + ~MenuRunnerImplCocoa() override; // The Cocoa menu controller that this instance is bridging. diff --git a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm index e68a76f83ed..8ccd3382da9 100644 --- a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm +++ b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm @@ -148,7 +148,12 @@ void MenuRunnerImplCocoa::Release() { return; // We already canceled. delete_after_run_ = true; - [menu_controller_ cancel]; + + // Reset |menu_controller_| to ensure it clears itself as a delegate to + // prevent NSMenu attempting to access the weak pointer to the ui::MenuModel + // it holds (which is not owned by |this|). Toolkit-views menus use + // MenuRunnerImpl::empty_delegate_ to handle this case. + menu_controller_.reset(); } else { delete this; } diff --git a/chromium/ui/views/controls/menu/menu_runner_unittest.cc b/chromium/ui/views/controls/menu/menu_runner_unittest.cc index 79875e46f84..4c6c320cd93 100644 --- a/chromium/ui/views/controls/menu/menu_runner_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_runner_unittest.cc @@ -10,6 +10,7 @@ #include "base/macros.h" #include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" #include "ui/base/ui_base_types.h" #include "ui/events/test/event_generator.h" #include "ui/views/controls/menu/menu_controller.h" @@ -179,8 +180,9 @@ TEST_F(MenuRunnerTest, LatinMnemonic) { EXPECT_NE(nullptr, delegate->on_menu_closed_menu()); } +#if !defined(OS_WIN) // Tests that a key press on a non-US keyboard layout activates the correct menu -// item. +// item. Disabled on Windows because a WM_CHAR event does not activate an item. TEST_F(MenuRunnerTest, NonLatinMnemonic) { // TODO: test uses GetContext(), which is not applicable to aura-mus. // http://crbug.com/663809. @@ -204,6 +206,7 @@ TEST_F(MenuRunnerTest, NonLatinMnemonic) { EXPECT_EQ(1, delegate->on_menu_closed_called()); EXPECT_NE(nullptr, delegate->on_menu_closed_menu()); } +#endif // !defined(OS_WIN) // Tests that attempting to nest a menu within a drag-and-drop menu does not // cause a crash. Instead the drag and drop action should be canceled, and the diff --git a/chromium/ui/views/controls/menu/menu_scroll_view_container.cc b/chromium/ui/views/controls/menu/menu_scroll_view_container.cc index 2e2e6b2aa26..abcc8e28e7e 100644 --- a/chromium/ui/views/controls/menu/menu_scroll_view_container.cc +++ b/chromium/ui/views/controls/menu/menu_scroll_view_container.cc @@ -250,10 +250,8 @@ void MenuScrollViewContainer::OnPaintBackground(gfx::Canvas* canvas) { gfx::Rect bounds(0, 0, width(), height()); NativeTheme::ExtraParams extra; const MenuConfig& menu_config = MenuConfig::instance(); - extra.menu_background.corner_radius = - content_view_->GetMenuItem()->GetMenuController()->use_touchable_layout() - ? menu_config.touchable_corner_radius - : menu_config.corner_radius; + extra.menu_background.corner_radius = menu_config.CornerRadiusForMenu( + content_view_->GetMenuItem()->GetMenuController()); GetNativeTheme()->Paint(canvas->sk_canvas(), NativeTheme::kMenuPopupBackground, NativeTheme::kNormal, bounds, extra); } @@ -277,8 +275,14 @@ void MenuScrollViewContainer::CreateDefaultBorder() { bubble_border_ = nullptr; const MenuConfig& menu_config = MenuConfig::instance(); - - int padding = menu_config.use_outer_border && menu_config.corner_radius > 0 + const ui::NativeTheme* native_theme = GetNativeTheme(); + MenuController* controller = + content_view_->GetMenuItem()->GetMenuController(); + bool use_outer_border = + menu_config.use_outer_border || + (native_theme && native_theme->UsesHighContrastColors()); + int corner_radius = menu_config.CornerRadiusForMenu(controller); + int padding = use_outer_border && corner_radius > 0 ? kBorderPaddingDueToRoundedCorners : 0; @@ -286,14 +290,13 @@ void MenuScrollViewContainer::CreateDefaultBorder() { const int horizontal_inset = menu_config.menu_horizontal_border_size + padding; - if (menu_config.use_outer_border) { + if (use_outer_border) { SkColor color = GetNativeTheme() ? GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_MenuBorderColor) : gfx::kPlaceholderColor; SetBorder(views::CreateBorderPainter( - std::make_unique<views::RoundRectPainter>(color, - menu_config.corner_radius), + std::make_unique<views::RoundRectPainter>(color, corner_radius), gfx::Insets(vertical_inset, horizontal_inset))); } else { SetBorder(CreateEmptyBorder(vertical_inset, horizontal_inset, diff --git a/chromium/ui/views/controls/menu/menu_separator.cc b/chromium/ui/views/controls/menu/menu_separator.cc index 55bdcf82428..aa27d3c260b 100644 --- a/chromium/ui/views/controls/menu/menu_separator.cc +++ b/chromium/ui/views/controls/menu/menu_separator.cc @@ -23,6 +23,8 @@ void MenuSeparator::OnPaint(gfx::Canvas* canvas) { const MenuConfig& menu_config = MenuConfig::instance(); int pos = 0; int separator_thickness = menu_config.separator_thickness; + if (type_ == ui::DOUBLE_SEPARATOR) + separator_thickness = menu_config.double_separator_thickness; switch (type_) { case ui::LOWER_SEPARATOR: pos = height() - separator_thickness; @@ -30,12 +32,14 @@ void MenuSeparator::OnPaint(gfx::Canvas* canvas) { case ui::UPPER_SEPARATOR: break; default: - pos = height() / 2; + pos = (height() - separator_thickness) / 2; break; } gfx::Rect paint_rect(0, pos, width(), separator_thickness); - if (menu_config.use_outer_border) + if (type_ == ui::PADDED_SEPARATOR) + paint_rect.Inset(menu_config.padded_separator_left_margin, 0, 0, 0); + else if (menu_config.use_outer_border) paint_rect.Inset(1, 0); #if defined(OS_WIN) @@ -70,6 +74,12 @@ gfx::Size MenuSeparator::CalculatePreferredSize() const { case ui::UPPER_SEPARATOR: height = menu_config.separator_upper_height; break; + case ui::DOUBLE_SEPARATOR: + height = menu_config.double_separator_height; + break; + case ui::PADDED_SEPARATOR: + height = menu_config.separator_thickness; + break; default: height = menu_config.separator_height; break; diff --git a/chromium/ui/views/controls/menu/menu_types.h b/chromium/ui/views/controls/menu/menu_types.h index d95ce0b33c0..03a0c72a5db 100644 --- a/chromium/ui/views/controls/menu/menu_types.h +++ b/chromium/ui/views/controls/menu/menu_types.h @@ -9,8 +9,7 @@ namespace views { // Where a popup menu should be anchored to for non-RTL languages. The opposite // position will be used if base::i18n:IsRTL() is true. The BUBBLE flags are -// used when the menu should get enclosed by a bubble. Note that BUBBLE flags -// should only be used with menus which have no children. The Fixed flags are +// used when the menu should get enclosed by a bubble. The Fixed flags are // used for the menus that have a fixed anchor position. enum MenuAnchorPosition { MENU_ANCHOR_TOPLEFT, diff --git a/chromium/ui/views/controls/menu/submenu_view.cc b/chromium/ui/views/controls/menu/submenu_view.cc index 9b244c9e9ea..b69a318e69f 100644 --- a/chromium/ui/views/controls/menu/submenu_view.cc +++ b/chromium/ui/views/controls/menu/submenu_view.cc @@ -182,8 +182,9 @@ gfx::Size SubmenuView::CalculatePreferredSize() const { minimum_preferred_width_ - 2 * insets.width())); if (GetMenuItem()->GetMenuController() && - GetMenuItem()->GetMenuController()->use_touchable_layout()) + GetMenuItem()->GetMenuController()->use_touchable_layout()) { width = std::max(touchable_minimum_width, width); + } // Then, the height for that width. int height = 0; diff --git a/chromium/ui/views/controls/message_box_view.cc b/chromium/ui/views/controls/message_box_view.cc index 27dc783cf4f..36db5787075 100644 --- a/chromium/ui/views/controls/message_box_view.cc +++ b/chromium/ui/views/controls/message_box_view.cc @@ -7,7 +7,6 @@ #include <stddef.h> #include "base/i18n/rtl.h" -#include "base/message_loop/message_loop.h" #include "base/strings/string_split.h" #include "base/strings/utf_string_conversions.h" #include "ui/accessibility/ax_node_data.h" diff --git a/chromium/ui/views/controls/native/native_view_host_aura.cc b/chromium/ui/views/controls/native/native_view_host_aura.cc index 28033ad99d5..097bcd4879b 100644 --- a/chromium/ui/views/controls/native/native_view_host_aura.cc +++ b/chromium/ui/views/controls/native/native_view_host_aura.cc @@ -150,7 +150,8 @@ void NativeViewHostAura::RemovedFromWidget() { bool NativeViewHostAura::SetCornerRadius(int corner_radius) { #if defined(OS_WIN) - // Layer masks don't work on Windows. See crbug.com/713359 + // TODO(crbug/843250): On Aura, layer masks don't play with HiDPI. Fix this + // and enable this on Windows. return false; #else mask_ = views::Painter::CreatePaintedLayer( diff --git a/chromium/ui/views/controls/native/native_view_host_mac.mm b/chromium/ui/views/controls/native/native_view_host_mac.mm index e6f31f85f3d..0ca2c4a6a41 100644 --- a/chromium/ui/views/controls/native/native_view_host_mac.mm +++ b/chromium/ui/views/controls/native/native_view_host_mac.mm @@ -12,6 +12,13 @@ #include "ui/views/widget/native_widget_mac.h" #include "ui/views/widget/widget.h" +// NSViews that can be drawn as a ui::Layer directly will implement this +// interface. Calling cr_setParentLayer will embed the ui::Layer of the NSView +// under |parentUiLayer|. +@interface NSView (UICompositor) +- (void)cr_setParentUiLayer:(ui::Layer*)parentUiLayer; +@end + namespace views { namespace { @@ -32,6 +39,8 @@ void EnsureNativeViewHasNoChildWidgets(NSView* native_view) { } // namespace NativeViewHostMac::NativeViewHostMac(NativeViewHost* host) : host_(host) { + // Ensure that |host_| have its own ui::Layer and that it draw nothing. + host_->SetPaintToLayer(ui::LAYER_NOT_DRAWN); } NativeViewHostMac::~NativeViewHostMac() { @@ -45,6 +54,9 @@ void NativeViewHostMac::AttachNativeView() { DCHECK(!native_view_); native_view_.reset([host_->native_view() retain]); + if ([native_view_ respondsToSelector:@selector(cr_setParentUiLayer:)]) + [native_view_ cr_setParentUiLayer:host_->layer()]; + EnsureNativeViewHasNoChildWidgets(native_view_); BridgedNativeWidget* bridge = NativeWidgetMac::GetBridgeForNativeWindow( host_->GetWidget()->GetNativeWindow()); @@ -71,6 +83,9 @@ void NativeViewHostMac::NativeViewDetaching(bool destroyed) { [host_->native_view() setHidden:YES]; [host_->native_view() removeFromSuperview]; + if ([native_view_ respondsToSelector:@selector(cr_setParentUiLayer:)]) + [native_view_ cr_setParentUiLayer:nullptr]; + EnsureNativeViewHasNoChildWidgets(host_->native_view()); BridgedNativeWidget* bridge = NativeWidgetMac::GetBridgeForNativeWindow( host_->GetWidget()->GetNativeWindow()); diff --git a/chromium/ui/views/controls/prefix_selector.cc b/chromium/ui/views/controls/prefix_selector.cc index b1195d51c67..6590a8e0b05 100644 --- a/chromium/ui/views/controls/prefix_selector.cc +++ b/chromium/ui/views/controls/prefix_selector.cc @@ -89,6 +89,12 @@ bool PrefixSelector::HasCompositionText() const { return false; } +ui::TextInputClient::FocusReason PrefixSelector::GetFocusReason() const { + // TODO(https://crbug.com/824604): Implement this correctly. + NOTIMPLEMENTED_LOG_ONCE(); + return ui::TextInputClient::FOCUS_REASON_OTHER; +} + bool PrefixSelector::GetTextRange(gfx::Range* range) const { *range = gfx::Range(); return false; @@ -145,6 +151,12 @@ const std::string& PrefixSelector::GetClientSourceInfo() const { return base::EmptyString(); } +bool PrefixSelector::ShouldDoLearning() { + // TODO(https://crbug.com/311180): Implement this method. + NOTIMPLEMENTED_LOG_ONCE(); + return false; +} + void PrefixSelector::OnTextInput(const base::string16& text) { // Small hack to filter out 'tab' and 'enter' input, as the expectation is // that they are control characters and will not affect the currently-active diff --git a/chromium/ui/views/controls/prefix_selector.h b/chromium/ui/views/controls/prefix_selector.h index 1e70d1ad3ed..e3654eebbb3 100644 --- a/chromium/ui/views/controls/prefix_selector.h +++ b/chromium/ui/views/controls/prefix_selector.h @@ -44,6 +44,7 @@ class VIEWS_EXPORT PrefixSelector : public ui::TextInputClient { bool GetCompositionCharacterBounds(uint32_t index, gfx::Rect* rect) const override; bool HasCompositionText() const override; + FocusReason GetFocusReason() const override; bool GetTextRange(gfx::Range* range) const override; bool GetCompositionTextRange(gfx::Range* range) const override; bool GetSelectionRange(gfx::Range* range) const override; @@ -60,6 +61,7 @@ class VIEWS_EXPORT PrefixSelector : public ui::TextInputClient { bool IsTextEditCommandEnabled(ui::TextEditCommand command) const override; void SetTextEditCommandForNextKeyEvent(ui::TextEditCommand command) override; const std::string& GetClientSourceInfo() const override; + bool ShouldDoLearning() override; private: // Invoked when text is typed. Tries to change the selection appropriately. diff --git a/chromium/ui/views/controls/scroll_view.cc b/chromium/ui/views/controls/scroll_view.cc index 06a8b1625c7..e58ef7d3cdc 100644 --- a/chromium/ui/views/controls/scroll_view.cc +++ b/chromium/ui/views/controls/scroll_view.cc @@ -8,6 +8,8 @@ #include "base/logging.h" #include "base/macros.h" #include "ui/base/material_design/material_design_controller.h" +#include "ui/base/ui_base_features.h" +#include "ui/compositor/overscroll/scroll_input_handler.h" #include "ui/events/event.h" #include "ui/gfx/canvas.h" #include "ui/native_theme/native_theme.h" @@ -24,15 +26,6 @@ const char ScrollView::kViewClassName[] = "ScrollView"; namespace { -const base::Feature kToolkitViewsScrollWithLayers { - "ToolkitViewsScrollWithLayers", -#if defined(OS_MACOSX) - base::FEATURE_ENABLED_BY_DEFAULT -#else - base::FEATURE_DISABLED_BY_DEFAULT -#endif -}; - class ScrollCornerView : public View { public: ScrollCornerView() {} @@ -187,8 +180,8 @@ ScrollView::ScrollView() min_height_(-1), max_height_(-1), hide_horizontal_scrollbar_(false), - scroll_with_layers_enabled_( - base::FeatureList::IsEnabled(kToolkitViewsScrollWithLayers)) { + scroll_with_layers_enabled_(base::FeatureList::IsEnabled( + features::kUiCompositorScrollWithLayers)) { set_notify_enter_exit_on_child(true); AddChildView(contents_viewport_); @@ -221,6 +214,14 @@ ScrollView::ScrollView() more_content_bottom_->SetPaintToLayer(); } UpdateBackground(); + + if (ui::MaterialDesignController::IsSecondaryUiMaterial()) { + focus_ring_ = FocusRing::Install(this); + focus_ring_->SetHasFocusPredicate([](View* view) -> bool { + auto* v = static_cast<ScrollView*>(view); + return v->draw_focus_indicator_; + }); + } } ScrollView::~ScrollView() { @@ -334,17 +335,10 @@ void ScrollView::SetHasFocusIndicator(bool has_focus_indicator) { return; draw_focus_indicator_ = has_focus_indicator; - if (ui::MaterialDesignController::IsSecondaryUiMaterial()) { - DCHECK_EQ(draw_focus_indicator_, !focus_ring_); - if (has_focus_indicator) { - focus_ring_ = FocusRing::Install(this); - } else { - FocusRing::Uninstall(this); - focus_ring_ = nullptr; - } - } else { + if (ui::MaterialDesignController::IsSecondaryUiMaterial()) + focus_ring_->SchedulePaint(); + else UpdateBorder(); - } SchedulePaint(); } @@ -519,6 +513,30 @@ void ScrollView::Layout() { container_size.SetToMax(viewport_bounds.size()); contents_->SetBoundsRect(gfx::Rect(container_size)); contents_->layer()->SetScrollable(viewport_bounds.size()); + + // Flip the viewport with layer transforms under RTL. Note the net effect is + // to flip twice, so the text is not mirrored. This is necessary because + // compositor scrolling is not RTL-aware. So although a toolkit-views layout + // will flip, increasing a horizontal gfx::ScrollOffset will move content to + // the left, regardless of RTL. A gfx::ScrollOffset must be positive, so to + // move (unscrolled) content to the right, we need to flip the viewport + // layer. That would flip all the content as well, so flip (and translate) + // the content layer. Compensating in this way allows the scrolling/offset + // logic to remain the same when scrolling via layers or bounds offsets. + if (base::i18n::IsRTL()) { + gfx::Transform flip; + flip.Translate(viewport_bounds.width(), 0); + flip.Scale(-1, 1); + contents_viewport_->layer()->SetTransform(flip); + + // Add `contents_->width() - viewport_width` to the translation step. This + // is to prevent the top-left of the (flipped) contents aligning to the + // top-left of the viewport. Instead, the top-right should align in RTL. + gfx::Transform shift; + shift.Translate(2 * contents_->width() - viewport_bounds.width(), 0); + shift.Scale(-1, 1); + contents_->layer()->SetTransform(shift); + } } header_viewport_->SetBounds(contents_x, contents_y, @@ -552,6 +570,7 @@ bool ScrollView::OnKeyPressed(const ui::KeyEvent& event) { bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent& e) { bool processed = false; + // TODO(https://crbug.com/615948): Use composited scrolling. if (vert_sb_->visible()) processed = vert_sb_->OnMouseWheel(e); @@ -562,13 +581,18 @@ bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent& e) { } void ScrollView::OnScrollEvent(ui::ScrollEvent* event) { -#if defined(OS_MACOSX) if (!contents_) return; - // TODO(tapted): Send |event| to a cc::InputHandler. For now, there's nothing - // to do because Widget::OnScrollEvent() will automatically process an - // unhandled ScrollEvent as a MouseWheelEvent. + ui::ScrollInputHandler* compositor_scroller = + GetWidget()->GetCompositor()->scroll_input_handler(); + if (compositor_scroller) { + DCHECK(scroll_with_layers_enabled_); + if (compositor_scroller->OnScrollEvent(*event, contents_->layer())) { + event->SetHandled(); + event->StopPropagation(); + } + } // A direction might not be known when the event stream starts, notify both // scrollbars that they may be about scroll, or that they may need to cancel @@ -577,7 +601,6 @@ void ScrollView::OnScrollEvent(ui::ScrollEvent* event) { horiz_sb_->ObserveScrollEvent(*event); if (vert_sb_) vert_sb_->ObserveScrollEvent(*event); -#endif } void ScrollView::OnGestureEvent(ui::GestureEvent* event) { @@ -589,6 +612,7 @@ void ScrollView::OnGestureEvent(ui::GestureEvent* event) { event->type() == ui::ET_GESTURE_SCROLL_END || event->type() == ui::ET_SCROLL_FLING_START; + // TODO(https://crbug.com/615948): Use composited scrolling. if (vert_sb_->visible()) { if (vert_sb_->bounds().Contains(event->location()) || scroll_event) vert_sb_->OnGestureEvent(event); diff --git a/chromium/ui/views/controls/scroll_view.h b/chromium/ui/views/controls/scroll_view.h index ff7f71b19c3..e0e279ce914 100644 --- a/chromium/ui/views/controls/scroll_view.h +++ b/chromium/ui/views/controls/scroll_view.h @@ -11,6 +11,7 @@ #include "base/gtest_prod_util.h" #include "base/macros.h" #include "ui/native_theme/native_theme.h" +#include "ui/views/controls/focus_ring.h" #include "ui/views/controls/scrollbar/scroll_bar.h" namespace gfx { @@ -246,12 +247,12 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { // it overflows. bool draw_overflow_indicator_ = true; - // Focus ring, if one is installed. - View* focus_ring_ = nullptr; - // Set to true if the scroll with layers feature is enabled. const bool scroll_with_layers_enabled_; + // The focus ring for this ScrollView. + std::unique_ptr<FocusRing> focus_ring_; + DISALLOW_COPY_AND_ASSIGN(ScrollView); }; diff --git a/chromium/ui/views/controls/scroll_view_unittest.cc b/chromium/ui/views/controls/scroll_view_unittest.cc index 6b818c25420..d2596d71109 100644 --- a/chromium/ui/views/controls/scroll_view_unittest.cc +++ b/chromium/ui/views/controls/scroll_view_unittest.cc @@ -6,9 +6,13 @@ #include "base/macros.h" #include "base/run_loop.h" +#include "base/test/icu_test_util.h" +#include "base/test/scoped_feature_list.h" #include "base/test/test_timeouts.h" #include "base/threading/thread_task_runner_handle.h" +#include "build/build_config.h" #include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/ui_base_features.h" #include "ui/compositor/scoped_animation_duration_scale_mode.h" #include "ui/events/test/event_generator.h" #include "ui/views/border.h" @@ -229,8 +233,8 @@ class ScrollViewTest : public ViewsTestBase { class WidgetScrollViewTest : public test::WidgetTest, public ui::CompositorObserver { public: - static const int kDefaultHeight = 100; - static const int kDefaultWidth = 100; + static constexpr int kDefaultHeight = 100; + static constexpr int kDefaultWidth = 100; WidgetScrollViewTest() {} @@ -303,6 +307,9 @@ class WidgetScrollViewTest : public test::WidgetTest, WidgetTest::TearDown(); } + protected: + Widget* widget() const { return widget_; } + private: // ui::CompositorObserver: void OnCompositingDidCommit(ui::Compositor* compositor) override { @@ -330,8 +337,74 @@ class WidgetScrollViewTest : public test::WidgetTest, DISALLOW_COPY_AND_ASSIGN(WidgetScrollViewTest); }; -const int WidgetScrollViewTest::kDefaultHeight; -const int WidgetScrollViewTest::kDefaultWidth; +constexpr int WidgetScrollViewTest::kDefaultHeight; +constexpr int WidgetScrollViewTest::kDefaultWidth; + +// A gtest parameter to permute over whether ScrollView uses a left-to-right or +// right-to-left locale, or whether it uses ui::Layers or View bounds offsets to +// position contents (i.e. features::kUiCompositorScrollWithLayers). +enum class UiConfig { kLtr, kLtrWithLayers, kRtl, kRtlWithLayers }; + +class WidgetScrollViewTestRTLAndLayers + : public WidgetScrollViewTest, + public ::testing::WithParamInterface<UiConfig> { + public: + WidgetScrollViewTestRTLAndLayers() : locale_(IsTestingRtl() ? "he" : "en") { + if (IsTestingLayers()) { + layer_config_.InitAndEnableFeature( + features::kUiCompositorScrollWithLayers); + } else { + layer_config_.InitAndDisableFeature( + features::kUiCompositorScrollWithLayers); + } + } + + bool IsTestingRtl() const { + return GetParam() == UiConfig::kRtl || + GetParam() == UiConfig::kRtlWithLayers; + } + + bool IsTestingLayers() const { + return GetParam() == UiConfig::kLtrWithLayers || + GetParam() == UiConfig::kRtlWithLayers; + } + + // Returns a point in the coordinate space of |target| by translating a point + // inset one pixel from the top of the Widget and one pixel on the leading + // side of the Widget. There should be no scroll bar on this side. If + // |flip_result| is true, automatically account for flipped coordinates in + // RTL. + gfx::Point HitTestInCorner(View* target, bool flip_result = true) const { + const gfx::Point test_mouse_point_in_root = + IsTestingRtl() ? gfx::Point(kDefaultWidth - 1, 1) : gfx::Point(1, 1); + gfx::Point point = test_mouse_point_in_root; + View::ConvertPointToTarget(widget()->GetRootView(), target, &point); + if (flip_result) + return gfx::Point(target->GetMirroredXInView(point.x()), point.y()); + return point; + } + + private: + base::test::ScopedRestoreICUDefaultLocale locale_; + base::test::ScopedFeatureList layer_config_; + + DISALLOW_COPY_AND_ASSIGN(WidgetScrollViewTestRTLAndLayers); +}; + +std::string UiConfigToString(const testing::TestParamInfo<UiConfig>& info) { + switch (info.param) { + case UiConfig::kLtr: + return "LTR"; + case UiConfig::kLtrWithLayers: + return "LTR_LAYERS"; + case UiConfig::kRtl: + return "RTL"; + case UiConfig::kRtlWithLayers: + return "RTL_LAYERS"; + } + NOTREACHED(); + return std::string(); +} // Verifies the viewport is sized to fit the available space. TEST_F(ScrollViewTest, ViewportSizedToFit) { @@ -1436,26 +1509,136 @@ TEST_F(WidgetScrollViewTest, EventLocation) { contents->last_location()); } +// Ensure behavior of ScrollRectToVisible() is consistent when scrolling with +// and without layers, and under LTR and RTL. +TEST_P(WidgetScrollViewTestRTLAndLayers, ScrollOffsetWithoutLayers) { + // Set up with both scrollers. And a nested view hierarchy like: + // +-------------+ + // |XX | + // | +----------| + // | | | + // | | +-------| + // | | | | + // | | | etc. | + // | | | | + // +-------------+ + // Note that "XX" indicates the size of the viewport. + constexpr int kNesting = 5; + constexpr int kCellWidth = kDefaultWidth; + constexpr int kCellHeight = kDefaultHeight; + constexpr gfx::Size kContentSize(kCellWidth * kNesting, + kCellHeight * kNesting); + ScrollView* scroll_view = AddScrollViewWithContentSize(kContentSize, false); + ScrollViewTestApi test_api(scroll_view); + EXPECT_EQ(gfx::ScrollOffset(0, 0), test_api.CurrentOffset()); + + // Sanity check that the contents has a layer iff testing layers. + EXPECT_EQ(IsTestingLayers(), !!scroll_view->contents()->layer()); + + if (IsTestingRtl()) { + // Sanity check the hit-testing logic on the root view. That is, verify that + // coordinates really do flip in RTL. The difference inside the viewport is + // that the flipping should occur consistently in the entire contents (not + // just the visible contents), and take into account the scroll offset. + EXPECT_EQ(gfx::Point(kDefaultWidth - 1, 1), + HitTestInCorner(scroll_view->GetWidget()->GetRootView(), false)); + EXPECT_EQ(gfx::Point(kContentSize.width() - 1, 1), + HitTestInCorner(scroll_view->contents(), false)); + } else { + EXPECT_EQ(gfx::Point(1, 1), + HitTestInCorner(scroll_view->GetWidget()->GetRootView(), false)); + EXPECT_EQ(gfx::Point(1, 1), + HitTestInCorner(scroll_view->contents(), false)); + } + + // Test vertical scrolling using coordinates on the contents canvas. + gfx::Rect offset(0, kCellHeight * 2, kCellWidth, kCellHeight); + scroll_view->contents()->ScrollRectToVisible(offset); + EXPECT_EQ(gfx::ScrollOffset(0, offset.y()), test_api.CurrentOffset()); + + // Rely on auto-flipping for this and future HitTestInCorner() calls. + EXPECT_EQ(gfx::Point(1, kCellHeight * 2 + 1), + HitTestInCorner(scroll_view->contents())); + + // Test horizontal scrolling. + offset.set_x(kCellWidth * 2); + scroll_view->contents()->ScrollRectToVisible(offset); + EXPECT_EQ(gfx::ScrollOffset(offset.x(), offset.y()), + test_api.CurrentOffset()); + EXPECT_EQ(gfx::Point(kCellWidth * 2 + 1, kCellHeight * 2 + 1), + HitTestInCorner(scroll_view->contents())); + + // Reset the scrolling. + scroll_view->contents()->ScrollRectToVisible(gfx::Rect(0, 0, 1, 1)); + + // Test transformations through a nested view hierarchy. + View* deepest_view = scroll_view->contents(); + constexpr gfx::Rect kCellRect(kCellWidth, kCellHeight, kContentSize.width(), + kContentSize.height()); + for (int i = 1; i < kNesting; ++i) { + SCOPED_TRACE(testing::Message("Nesting = ") << i); + View* child = new View; + child->SetBoundsRect(kCellRect); + deepest_view->AddChildView(child); + deepest_view = child; + + // Add a view in one quadrant. Scrolling just this view should only scroll + // far enough for it to become visible. That is, it should be positioned at + // the bottom right of the viewport, not the top-left. But since there are + // scroll bars, the scroll offset needs to go "a bit more". + View* partial_view = new View; + partial_view->SetSize(gfx::Size(kCellWidth / 3, kCellHeight / 3)); + deepest_view->AddChildView(partial_view); + partial_view->ScrollViewToVisible(); + int x_offset_in_cell = kCellWidth - partial_view->width(); + if (!scroll_view->horizontal_scroll_bar()->OverlapsContent()) + x_offset_in_cell -= scroll_view->horizontal_scroll_bar()->GetThickness(); + int y_offset_in_cell = kCellHeight - partial_view->height(); + if (!scroll_view->vertical_scroll_bar()->OverlapsContent()) + y_offset_in_cell -= scroll_view->vertical_scroll_bar()->GetThickness(); + EXPECT_EQ(gfx::ScrollOffset(kCellWidth * i - x_offset_in_cell, + kCellHeight * i - y_offset_in_cell), + test_api.CurrentOffset()); + + // Now scroll the rest. + deepest_view->ScrollViewToVisible(); + EXPECT_EQ(gfx::ScrollOffset(kCellWidth * i, kCellHeight * i), + test_api.CurrentOffset()); + + // The partial view should now be at the top-left of the viewport (top-right + // in RTL). + EXPECT_EQ(gfx::Point(1, 1), HitTestInCorner(partial_view)); + + gfx::Point origin; + View::ConvertPointToWidget(partial_view, &origin); + constexpr gfx::Point kTestPointRTL(kDefaultWidth - kCellWidth / 3, 0); + EXPECT_EQ(IsTestingRtl() ? kTestPointRTL : gfx::Point(), origin); + } + + // Scrolling to the deepest view should have moved the viewport so that the + // (kNesting - 1) parent views are all off-screen. + EXPECT_EQ(gfx::ScrollOffset(kCellWidth * (kNesting - 1), + kCellHeight * (kNesting - 1)), + test_api.CurrentOffset()); +} + // Test that views scroll offsets are in sync with the layer scroll offsets. -TEST_F(WidgetScrollViewTest, ScrollOffsetUsingLayers) { - // Set up with a vertical scroller, but don't commit the layer changes yet. - ScrollView* scroll_view = - AddScrollViewWithContentSize(gfx::Size(10, kDefaultHeight * 5), false); +TEST_P(WidgetScrollViewTestRTLAndLayers, ScrollOffsetUsingLayers) { + // Set up with both scrollers, but don't commit the layer changes yet. + ScrollView* scroll_view = AddScrollViewWithContentSize( + gfx::Size(kDefaultWidth * 5, kDefaultHeight * 5), false); ScrollViewTestApi test_api(scroll_view); EXPECT_EQ(gfx::ScrollOffset(0, 0), test_api.CurrentOffset()); // UI code may request a scroll before layer changes are committed. - gfx::Rect offset(0, kDefaultHeight * 2, 1, kDefaultHeight); + gfx::Rect offset(0, kDefaultHeight * 2, kDefaultWidth, kDefaultHeight); scroll_view->contents()->ScrollRectToVisible(offset); EXPECT_EQ(gfx::ScrollOffset(0, offset.y()), test_api.CurrentOffset()); // The following only makes sense when layered scrolling is enabled. View* container = scroll_view->contents(); -#if defined(OS_MACOSX) - // Sanity check: Mac should always scroll with layers. - EXPECT_TRUE(container->layer()); -#endif + EXPECT_EQ(IsTestingLayers(), !!container->layer()); if (!container->layer()) return; @@ -1493,6 +1676,60 @@ TEST_F(WidgetScrollViewTest, ScrollOffsetUsingLayers) { EXPECT_TRUE(compositor->GetScrollOffsetForLayer(layer_id, &impl_offset)); EXPECT_EQ(gfx::ScrollOffset(0, offset.y()), impl_offset); + + // Test horizontal scrolling. + offset.set_x(kDefaultWidth * 2); + scroll_view->contents()->ScrollRectToVisible(offset); + EXPECT_EQ(gfx::ScrollOffset(offset.x(), offset.y()), + test_api.CurrentOffset()); + + EXPECT_TRUE(compositor->GetScrollOffsetForLayer(layer_id, &impl_offset)); + EXPECT_EQ(gfx::ScrollOffset(offset.x(), offset.y()), impl_offset); } +// Tests to see the scroll events are handled correctly in composited and +// non-composited scrolling. +TEST_F(WidgetScrollViewTest, CompositedScrollEvents) { + // Set up with a vertical scroll bar. + ScrollView* scroll_view = + AddScrollViewWithContentSize(gfx::Size(10, kDefaultHeight * 5)); + ScrollViewTestApi test_api(scroll_view); + + // Create a fake scroll event and send it to the scroll view. + ui::ScrollEvent scroll(ui::ET_SCROLL, gfx::Point(), base::TimeTicks::Now(), 0, + 0, -10, 0, -10, 3); + EXPECT_FALSE(scroll.handled()); + EXPECT_FALSE(scroll.stopped_propagation()); + scroll_view->OnScrollEvent(&scroll); + + // Check to see if the scroll event is handled by the scroll view. + if (base::FeatureList::IsEnabled(features::kUiCompositorScrollWithLayers)) { + // If UiCompositorScrollWithLayers is enabled, the event is set handled + // and its propagation is stopped. + EXPECT_TRUE(scroll.handled()); + EXPECT_TRUE(scroll.stopped_propagation()); + } else { + // If UiCompositorScrollWithLayers is disabled, the event isn't handled. + // This informs Widget::OnScrollEvent() to convert to a MouseWheel event + // and dispatch again. Simulate that. + EXPECT_FALSE(scroll.handled()); + EXPECT_FALSE(scroll.stopped_propagation()); + EXPECT_EQ(gfx::ScrollOffset(), test_api.CurrentOffset()); + + ui::MouseWheelEvent wheel(scroll); + scroll_view->OnMouseEvent(&wheel); + } + + // Check if the scroll view has been offset. + EXPECT_EQ(gfx::ScrollOffset(0, 10), test_api.CurrentOffset()); +} + +INSTANTIATE_TEST_CASE_P(, + WidgetScrollViewTestRTLAndLayers, + ::testing::Values(UiConfig::kLtr, + UiConfig::kRtl, + UiConfig::kLtrWithLayers, + UiConfig::kRtlWithLayers), + &UiConfigToString); + } // namespace views diff --git a/chromium/ui/views/controls/scrollbar/base_scroll_bar.cc b/chromium/ui/views/controls/scrollbar/base_scroll_bar.cc index 9add6bb5776..bf652746430 100644 --- a/chromium/ui/views/controls/scrollbar/base_scroll_bar.cc +++ b/chromium/ui/views/controls/scrollbar/base_scroll_bar.cc @@ -8,7 +8,6 @@ #include "base/bind_helpers.h" #include "base/callback.h" #include "base/compiler_specific.h" -#include "base/message_loop/message_loop.h" #include "base/strings/string16.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" diff --git a/chromium/ui/views/controls/slider.cc b/chromium/ui/views/controls/slider.cc index 015c695e102..cf767aee4c2 100644 --- a/chromium/ui/views/controls/slider.cc +++ b/chromium/ui/views/controls/slider.cc @@ -7,7 +7,7 @@ #include <algorithm> #include "base/logging.h" -#include "base/message_loop/message_loop.h" +#include "base/message_loop/message_loop_current.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "cc/paint/paint_flags.h" @@ -138,7 +138,7 @@ void Slider::SetValueInternal(float value, SliderChangeReason reason) { if (listener_) listener_->SliderValueChanged(this, value_, old_value, reason); - if (old_value_valid && base::MessageLoop::current()) { + if (old_value_valid && base::MessageLoopCurrent::Get()) { // Do not animate when setting the value of the slider for the first time. // There is no message-loop when running tests. So we cannot animate then. if (!move_animation_) { diff --git a/chromium/ui/views/controls/styled_label.cc b/chromium/ui/views/controls/styled_label.cc index 5e7982dcdb9..1f2d6fbcd4e 100644 --- a/chromium/ui/views/controls/styled_label.cc +++ b/chromium/ui/views/controls/styled_label.cc @@ -123,9 +123,7 @@ StyledLabel::StyledLabel(const base::string16& text, width_at_last_layout_(0), displayed_on_background_color_(SkColorSetRGB(0xFF, 0xFF, 0xFF)), displayed_on_background_color_set_(false), - auto_color_readability_enabled_(true), - horizontal_alignment_(base::i18n::IsRTL() ? gfx::ALIGN_RIGHT - : gfx::ALIGN_LEFT) { + auto_color_readability_enabled_(true) { base::TrimWhitespace(text, base::TRIM_TRAILING, &text_); } @@ -271,7 +269,7 @@ void StyledLabel::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { if (horizontal_alignment_ == alignment) return; horizontal_alignment_ = alignment; - SchedulePaint(); + PreferredSizeChanged(); } void StyledLabel::ClearStyleRanges() { diff --git a/chromium/ui/views/controls/styled_label.h b/chromium/ui/views/controls/styled_label.h index 3268ad50e2c..ae5bac5511a 100644 --- a/chromium/ui/views/controls/styled_label.h +++ b/chromium/ui/views/controls/styled_label.h @@ -219,7 +219,7 @@ class VIEWS_EXPORT StyledLabel : public View, public LinkListener { // The horizontal alignment. This value is flipped for RTL. The default // behavior is to align left in LTR UI and right in RTL UI. - gfx::HorizontalAlignment horizontal_alignment_; + gfx::HorizontalAlignment horizontal_alignment_ = gfx::ALIGN_LEFT; DISALLOW_COPY_AND_ASSIGN(StyledLabel); }; diff --git a/chromium/ui/views/controls/styled_label_unittest.cc b/chromium/ui/views/controls/styled_label_unittest.cc index 7aa780d2c8f..4f18b6880c4 100644 --- a/chromium/ui/views/controls/styled_label_unittest.cc +++ b/chromium/ui/views/controls/styled_label_unittest.cc @@ -9,8 +9,10 @@ #include <memory> #include <string> +#include "base/i18n/base_i18n_switches.h" #include "base/macros.h" #include "base/strings/utf_string_conversions.h" +#include "base/test/icu_test_util.h" #include "base/test/scoped_feature_list.h" #include "build/build_config.h" #include "testing/gtest/include/gtest/gtest.h" @@ -707,48 +709,64 @@ TEST_F(StyledLabelTest, LineWrapperWithCustomView) { styled()->GetHeightForWidth(100)); } -TEST_F(StyledLabelTest, LeftAlignment) { - const std::string multiline_text("one\ntwo\nthree"); - InitStyledLabel(multiline_text); - styled()->SetHorizontalAlignment(gfx::ALIGN_LEFT); +TEST_F(StyledLabelTest, AlignmentInLTR) { + const std::string text("text"); + InitStyledLabel(text); styled()->SetBounds(0, 0, 1000, 1000); styled()->Layout(); - ASSERT_EQ(3, styled()->child_count()); + ASSERT_EQ(1, styled()->child_count()); + + // Test the default alignment puts the text on the leading side (left). EXPECT_EQ(0, styled()->child_at(0)->bounds().x()); - EXPECT_EQ(0, styled()->child_at(1)->bounds().x()); - EXPECT_EQ(0, styled()->child_at(2)->bounds().x()); -} -TEST_F(StyledLabelTest, RightAlignment) { - const std::string multiline_text("one\ntwo\nthree"); - InitStyledLabel(multiline_text); styled()->SetHorizontalAlignment(gfx::ALIGN_RIGHT); - styled()->SetBounds(0, 0, 1000, 1000); styled()->Layout(); - ASSERT_EQ(3, styled()->child_count()); EXPECT_EQ(1000, styled()->child_at(0)->bounds().right()); - EXPECT_EQ(1000, styled()->child_at(1)->bounds().right()); - EXPECT_EQ(1000, styled()->child_at(2)->bounds().right()); -} -TEST_F(StyledLabelTest, CenterAlignment) { - const std::string multiline_text("one\ntwo\nthree"); - InitStyledLabel(multiline_text); + styled()->SetHorizontalAlignment(gfx::ALIGN_LEFT); + styled()->Layout(); + EXPECT_EQ(0, styled()->child_at(0)->bounds().x()); + styled()->SetHorizontalAlignment(gfx::ALIGN_CENTER); + styled()->Layout(); + Label label(ASCIIToUTF16(text)); + EXPECT_EQ((1000 - label.GetPreferredSize().width()) / 2, + styled()->child_at(0)->bounds().x()); +} + +TEST_F(StyledLabelTest, AlignmentInRTL) { + // |g_icu_text_direction| is cached to prevent reading new commandline switch. + // Set |g_icu_text_direction| to |UNKNOWN_DIRECTION| in order to read the new + // commandline switch. + base::test::ScopedRestoreICUDefaultLocale scoped_locale("en_US"); + base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( + switches::kForceUIDirection, switches::kForceDirectionRTL); + + const std::string text("text"); + InitStyledLabel(text); styled()->SetBounds(0, 0, 1000, 1000); styled()->Layout(); + ASSERT_EQ(1, styled()->child_count()); - Label label_one(ASCIIToUTF16("one")); - Label label_two(ASCIIToUTF16("two")); - Label label_three(ASCIIToUTF16("three")); + // Test the default alignment puts the text on the leading side (right). + // Note that x-coordinates in RTL place the origin (0) on the right. + EXPECT_EQ(0, styled()->child_at(0)->bounds().x()); - ASSERT_EQ(3, styled()->child_count()); - EXPECT_EQ((1000 - label_one.GetPreferredSize().width()) / 2, + // Setting |ALIGN_LEFT| should be flipped to |ALIGN_RIGHT|. + styled()->SetHorizontalAlignment(gfx::ALIGN_LEFT); + styled()->Layout(); + EXPECT_EQ(1000, styled()->child_at(0)->bounds().right()); + + // Setting |ALIGN_RIGHT| should be flipped to |ALIGN_LEFT|. + styled()->SetHorizontalAlignment(gfx::ALIGN_RIGHT); + styled()->Layout(); + EXPECT_EQ(0, styled()->child_at(0)->bounds().x()); + + styled()->SetHorizontalAlignment(gfx::ALIGN_CENTER); + styled()->Layout(); + Label label(ASCIIToUTF16(text)); + EXPECT_EQ((1000 - label.GetPreferredSize().width()) / 2, styled()->child_at(0)->bounds().x()); - EXPECT_EQ((1000 - label_two.GetPreferredSize().width()) / 2, - styled()->child_at(1)->bounds().x()); - EXPECT_EQ((1000 - label_three.GetPreferredSize().width()) / 2, - styled()->child_at(2)->bounds().x()); } TEST_P(MDStyledLabelTest, ViewsCenteredWithLinkAndCustomView) { diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc index bc33e72aca4..17f9259f012 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc @@ -36,17 +36,17 @@ namespace { // TODO(markusheintz|msw): Use NativeTheme colors. constexpr SkColor kTabTitleColor_InactiveBorder = - SkColorSetARGBMacro(0xFF, 0x64, 0x64, 0x64); + SkColorSetARGB(0xFF, 0x64, 0x64, 0x64); constexpr SkColor kTabTitleColor_InactiveHighlight = - SkColorSetARGBMacro(0xFF, 0x80, 0x86, 0x8B); + SkColorSetARGB(0xFF, 0x80, 0x86, 0x8B); constexpr SkColor kTabTitleColor_ActiveBorder = SK_ColorBLACK; constexpr SkColor kTabTitleColor_ActiveHighlight = - SkColorSetARGBMacro(0xFF, 0x42, 0x85, 0xF4); + SkColorSetARGB(0xFF, 0x42, 0x85, 0xF4); const SkColor kTabTitleColor_Hovered = SK_ColorBLACK; const SkColor kTabBorderColor = SkColorSetRGB(0xC8, 0xC8, 0xC8); const SkScalar kTabBorderThickness = 1.0f; constexpr SkColor kTabHighlightBackgroundColor = - SkColorSetARGBMacro(0xFF, 0xE8, 0xF0, 0xFE); + SkColorSetARGB(0xFF, 0xE8, 0xF0, 0xFE); constexpr int kTabHighlightBorderRadius = 32; constexpr int kTabHighlightPreferredHeight = 32; constexpr int kTabHighlightPreferredWidth = 208; 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 f499e202ee4..5ad416d35b1 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc @@ -7,7 +7,6 @@ #include <memory> #include "base/macros.h" -#include "base/message_loop/message_loop.h" #include "base/strings/utf_string_conversions.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/ax_action_data.h" diff --git a/chromium/ui/views/controls/table/table_view.cc b/chromium/ui/views/controls/table/table_view.cc index d7f37cf103f..c4325de2745 100644 --- a/chromium/ui/views/controls/table/table_view.cc +++ b/chromium/ui/views/controls/table/table_view.cc @@ -31,15 +31,12 @@ #include "ui/views/layout/layout_provider.h" #include "ui/views/style/typography.h" -// Size of images. -static const int kImageSize = 16; - -static const int kGroupingIndicatorSize = 6; - namespace views { namespace { +constexpr int kGroupingIndicatorSize = 6; + // Returns result, unless ascending is false in which case -result is returned. int SwapCompareResult(int result, bool ascending) { return ascending ? result : -result; @@ -588,14 +585,15 @@ void TableView::OnPaint(gfx::Canvas* canvas) { if (j == 0 && table_type_ == ICON_AND_TEXT) { gfx::ImageSkia image = model_->GetIcon(model_index); if (!image.isNull()) { - int image_x = GetMirroredXWithWidthInView(text_x, kImageSize); + int image_x = + GetMirroredXWithWidthInView(text_x, ui::TableModel::kIconSize); canvas->DrawImageInt( - image, 0, 0, image.width(), image.height(), - image_x, - cell_bounds.y() + (cell_bounds.height() - kImageSize) / 2, - kImageSize, kImageSize, true); + image, 0, 0, image.width(), image.height(), image_x, + cell_bounds.y() + + (cell_bounds.height() - ui::TableModel::kIconSize) / 2, + ui::TableModel::kIconSize, ui::TableModel::kIconSize, true); } - text_x += kImageSize + cell_element_spacing; + text_x += ui::TableModel::kIconSize + cell_element_spacing; } if (text_x < cell_bounds.right() - cell_margin) { canvas->DrawStringRectWithFlags( @@ -743,7 +741,7 @@ void TableView::AdjustCellBoundsForText(int visible_column_index, if (grouper_) text_x += kGroupingIndicatorSize + cell_element_spacing; if (table_type_ == ICON_AND_TEXT) - text_x += kImageSize + cell_element_spacing; + text_x += ui::TableModel::kIconSize + cell_element_spacing; } bounds->set_x(text_x); bounds->set_width(std::max(0, bounds->right() - cell_margin - text_x)); @@ -770,7 +768,7 @@ void TableView::UpdateVisibleColumnSizes() { const int cell_element_spacing = GetCellElementSpacing(); int first_column_padding = 0; if (table_type_ == ICON_AND_TEXT && header_) - first_column_padding += kImageSize + cell_element_spacing; + first_column_padding += ui::TableModel::kIconSize + cell_element_spacing; if (grouper_) first_column_padding += kGroupingIndicatorSize + cell_element_spacing; diff --git a/chromium/ui/views/controls/textfield/textfield.cc b/chromium/ui/views/controls/textfield/textfield.cc index 82cdf5b8669..3c1d189c674 100644 --- a/chromium/ui/views/controls/textfield/textfield.cc +++ b/chromium/ui/views/controls/textfield/textfield.cc @@ -57,7 +57,6 @@ #if defined(OS_WIN) #include "base/win/win_util.h" -#include "ui/base/ime/win/osk_display_manager.h" #endif #if defined(OS_LINUX) && !defined(OS_CHROMEOS) @@ -83,12 +82,6 @@ namespace views { namespace { #if defined(OS_MACOSX) -const ui::EventFlags kPlatformModifier = ui::EF_COMMAND_DOWN; -#else -const ui::EventFlags kPlatformModifier = ui::EF_CONTROL_DOWN; -#endif // OS_MACOSX - -#if defined(OS_MACOSX) const gfx::SelectionBehavior kLineSelectionBehavior = gfx::SELECTION_EXTEND; const gfx::SelectionBehavior kWordSelectionBehavior = gfx::SELECTION_CARET; const gfx::SelectionBehavior kMoveParagraphSelectionBehavior = @@ -299,6 +292,9 @@ Textfield::Textfield() UpdateBorder(); SetFocusBehavior(FocusBehavior::ALWAYS); + if (use_focus_ring_) + focus_ring_ = FocusRing::Install(this); + #if !defined(OS_MACOSX) // Do not map accelerators on Mac. E.g. They might not reflect custom // keybindings that a user has set. But also on Mac, these commands dispatch @@ -595,12 +591,8 @@ void Textfield::SetInvalid(bool invalid) { return; invalid_ = invalid; UpdateBorder(); - - if (HasFocus() && use_focus_ring_) { - FocusRing::Install(this, invalid_ - ? ui::NativeTheme::kColorId_AlertSeverityHigh - : ui::NativeTheme::kColorId_NumColors); - } + if (focus_ring_) + focus_ring_->SetInvalid(invalid); } void Textfield::ClearEditHistory() { @@ -646,9 +638,9 @@ const char* Textfield::GetClassName() const { } void Textfield::SetBorder(std::unique_ptr<Border> b) { - if (use_focus_ring_ && HasFocus()) - FocusRing::Uninstall(this); use_focus_ring_ = false; + if (focus_ring_) + focus_ring_.reset(); View::SetBorder(std::move(b)); } @@ -664,16 +656,22 @@ gfx::NativeCursor Textfield::GetCursor(const ui::MouseEvent& event) { bool Textfield::OnMousePressed(const ui::MouseEvent& event) { const bool had_focus = HasFocus(); bool handled = controller_ && controller_->HandleMouseEvent(this, event); + + // If the controller triggered the focus, then record the focus reason as + // other. + if (!had_focus && HasFocus()) + focus_reason_ = ui::TextInputClient::FOCUS_REASON_OTHER; + if (!handled && (event.IsOnlyLeftMouseButton() || event.IsOnlyRightMouseButton())) { if (!had_focus) - RequestFocus(); + RequestFocusWithPointer(ui::EventPointerType::POINTER_TYPE_MOUSE); ShowImeIfNeeded(); } #if defined(OS_LINUX) && !defined(OS_CHROMEOS) if (!handled && !had_focus && event.IsOnlyMiddleMouseButton()) - RequestFocus(); + RequestFocusWithPointer(ui::EventPointerType::POINTER_TYPE_MOUSE); #endif return selection_controller_.OnMousePressed( @@ -747,7 +745,7 @@ bool Textfield::OnKeyReleased(const ui::KeyEvent& event) { void Textfield::OnGestureEvent(ui::GestureEvent* event) { switch (event->type()) { case ui::ET_GESTURE_TAP_DOWN: - RequestFocus(); + RequestFocusWithPointer(event->details().primary_pointer_type()); ShowImeIfNeeded(); event->SetHandled(); break; @@ -776,13 +774,6 @@ void Textfield::OnGestureEvent(ui::GestureEvent* event) { OnAfterUserAction(); } CreateTouchSelectionControllerAndNotifyIt(); -#if defined(OS_WIN) - if (!read_only()) { - DCHECK(ui::OnScreenKeyboardDisplayManager::GetInstance()); - ui::OnScreenKeyboardDisplayManager::GetInstance() - ->DisplayVirtualKeyboard(nullptr); - } -#endif event->SetHandled(); break; case ui::ET_GESTURE_LONG_PRESS: @@ -857,6 +848,28 @@ bool Textfield::CanHandleAccelerators() const { return GetRenderText()->focused() && View::CanHandleAccelerators(); } +void Textfield::RequestFocusWithPointer(ui::EventPointerType pointer_type) { + if (HasFocus()) + return; + + switch (pointer_type) { + case ui::EventPointerType::POINTER_TYPE_MOUSE: + focus_reason_ = ui::TextInputClient::FOCUS_REASON_MOUSE; + break; + case ui::EventPointerType::POINTER_TYPE_PEN: + focus_reason_ = ui::TextInputClient::FOCUS_REASON_PEN; + break; + case ui::EventPointerType::POINTER_TYPE_TOUCH: + focus_reason_ = ui::TextInputClient::FOCUS_REASON_TOUCH; + break; + default: + focus_reason_ = ui::TextInputClient::FOCUS_REASON_OTHER; + break; + } + + View::RequestFocus(); +} + void Textfield::AboutToRequestFocusFromTabTraversal(bool reverse) { SelectAll(PlatformStyle::kTextfieldScrollsToStartOnFocusChange); } @@ -1074,6 +1087,10 @@ void Textfield::OnPaint(gfx::Canvas* canvas) { } void Textfield::OnFocus() { + // Set focus reason if focused was gained without mouse or touch input. + if (focus_reason_ == ui::TextInputClient::FOCUS_REASON_NONE) + focus_reason_ = ui::TextInputClient::FOCUS_REASON_OTHER; + #if defined(OS_MACOSX) if (text_input_type_ == ui::TEXT_INPUT_TYPE_PASSWORD) password_input_enabler_.reset(new ui::ScopedPasswordInputEnabler()); @@ -1089,16 +1106,13 @@ void Textfield::OnFocus() { OnCaretBoundsChanged(); if (ShouldBlinkCursor()) StartBlinkingCursor(); - if (use_focus_ring_) { - FocusRing::Install(this, invalid_ - ? ui::NativeTheme::kColorId_AlertSeverityHigh - : ui::NativeTheme::kColorId_NumColors); - } SchedulePaint(); View::OnFocus(); } void Textfield::OnBlur() { + focus_reason_ = ui::TextInputClient::FOCUS_REASON_NONE; + gfx::RenderText* render_text = GetRenderText(); render_text->set_focused(false); @@ -1118,8 +1132,6 @@ void Textfield::OnBlur() { DestroyTouchSelection(); - if (use_focus_ring_) - FocusRing::Uninstall(this); SchedulePaint(); View::OnBlur(); @@ -1354,23 +1366,23 @@ bool Textfield::GetAcceleratorForCommandId(int command_id, ui::Accelerator* accelerator) const { switch (command_id) { case IDS_APP_UNDO: - *accelerator = ui::Accelerator(ui::VKEY_Z, kPlatformModifier); + *accelerator = ui::Accelerator(ui::VKEY_Z, ui::EF_PLATFORM_ACCELERATOR); return true; case IDS_APP_CUT: - *accelerator = ui::Accelerator(ui::VKEY_X, kPlatformModifier); + *accelerator = ui::Accelerator(ui::VKEY_X, ui::EF_PLATFORM_ACCELERATOR); return true; case IDS_APP_COPY: - *accelerator = ui::Accelerator(ui::VKEY_C, kPlatformModifier); + *accelerator = ui::Accelerator(ui::VKEY_C, ui::EF_PLATFORM_ACCELERATOR); return true; case IDS_APP_PASTE: - *accelerator = ui::Accelerator(ui::VKEY_V, kPlatformModifier); + *accelerator = ui::Accelerator(ui::VKEY_V, ui::EF_PLATFORM_ACCELERATOR); return true; case IDS_APP_SELECT_ALL: - *accelerator = ui::Accelerator(ui::VKEY_A, kPlatformModifier); + *accelerator = ui::Accelerator(ui::VKEY_A, ui::EF_PLATFORM_ACCELERATOR); return true; case IDS_CONTENT_CONTEXT_EMOJI: @@ -1535,6 +1547,10 @@ bool Textfield::HasCompositionText() const { return model_->HasCompositionText(); } +ui::TextInputClient::FocusReason Textfield::GetFocusReason() const { + return focus_reason_; +} + bool Textfield::GetTextRange(gfx::Range* range) const { if (!ImeEditingAllowed()) return false; @@ -1601,13 +1617,14 @@ bool Textfield::ChangeTextDirectionAndLayoutAlignment( // Restore text directionality mode when the indicated direction matches the // current forced mode; otherwise, force the mode indicated. This helps users // manage BiDi text layout without getting stuck in forced LTR or RTL modes. - const gfx::DirectionalityMode mode = direction == base::i18n::RIGHT_TO_LEFT - ? gfx::DIRECTIONALITY_FORCE_RTL - : gfx::DIRECTIONALITY_FORCE_LTR; - if (mode == GetRenderText()->directionality_mode()) - GetRenderText()->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_TEXT); - else - GetRenderText()->SetDirectionalityMode(mode); + const bool default_rtl = direction == base::i18n::RIGHT_TO_LEFT; + const auto new_mode = default_rtl ? gfx::DIRECTIONALITY_FORCE_RTL + : gfx::DIRECTIONALITY_FORCE_LTR; + auto* render_text = GetRenderText(); + const bool modes_match = new_mode == render_text->directionality_mode(); + render_text->SetDirectionalityMode(modes_match ? gfx::DIRECTIONALITY_FROM_TEXT + : new_mode); + SetHorizontalAlignment(default_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); SchedulePaint(); return true; } @@ -1732,6 +1749,12 @@ const std::string& Textfield::GetClientSourceInfo() const { return base::EmptyString(); } +bool Textfield::ShouldDoLearning() { + // TODO(https://crbug.com/311180): Implement this method. + NOTIMPLEMENTED_LOG_ONCE(); + return false; +} + //////////////////////////////////////////////////////////////////////////////// // Textfield, protected: @@ -2157,7 +2180,9 @@ void Textfield::OnCaretBoundsChanged() { #if defined(OS_MACOSX) // On Mac, the context menu contains a look up item which displays the // selected text. As such, the menu needs to be updated if the selection has - // changed. + // changed. Be careful to reset the MenuRunner first so it doesn't reference + // the old model. + context_menu_runner_.reset(); context_menu_contents_.reset(); #endif diff --git a/chromium/ui/views/controls/textfield/textfield.h b/chromium/ui/views/controls/textfield/textfield.h index 8f26353fa19..3df902cef6d 100644 --- a/chromium/ui/views/controls/textfield/textfield.h +++ b/chromium/ui/views/controls/textfield/textfield.h @@ -28,6 +28,7 @@ #include "ui/gfx/selection_model.h" #include "ui/gfx/text_constants.h" #include "ui/views/context_menu_controller.h" +#include "ui/views/controls/focus_ring.h" #include "ui/views/controls/textfield/textfield_model.h" #include "ui/views/drag_controller.h" #include "ui/views/selection_controller.h" @@ -343,6 +344,7 @@ class VIEWS_EXPORT Textfield : public View, bool GetCompositionCharacterBounds(uint32_t index, gfx::Rect* rect) const override; bool HasCompositionText() const override; + FocusReason GetFocusReason() const override; bool GetTextRange(gfx::Range* range) const override; bool GetCompositionTextRange(gfx::Range* range) const override; bool GetSelectionRange(gfx::Range* range) const override; @@ -358,6 +360,7 @@ class VIEWS_EXPORT Textfield : public View, bool IsTextEditCommandEnabled(ui::TextEditCommand command) const override; void SetTextEditCommandForNextKeyEvent(ui::TextEditCommand command) override; const std::string& GetClientSourceInfo() const override; + bool ShouldDoLearning() override; protected: // Inserts or appends a character in response to an IME operation. @@ -481,6 +484,13 @@ class VIEWS_EXPORT Textfield : public View, // Textfield::GetCaretBlinkMs(). void OnCursorBlinkTimerFired(); + // Like RequestFocus, but explicitly states that the focus is triggered by + // a pointer event. + void RequestFocusWithPointer(ui::EventPointerType pointer_type); + + // Returns the color to use for the FocusRing, if one is present. + SkColor GetFocusRingColor() const; + // The text model. std::unique_ptr<TextfieldModel> model_; @@ -602,6 +612,13 @@ class VIEWS_EXPORT Textfield : public View, std::unique_ptr<ui::ScopedPasswordInputEnabler> password_input_enabler_; #endif // defined(OS_MACOSX) + // How this textfield was focused. + ui::TextInputClient::FocusReason focus_reason_ = + ui::TextInputClient::FOCUS_REASON_NONE; + + // The focus ring for this TextField. + std::unique_ptr<FocusRing> focus_ring_; + // Used to bind callback functions to this object. base::WeakPtrFactory<Textfield> weak_ptr_factory_; diff --git a/chromium/ui/views/controls/textfield/textfield_model.cc b/chromium/ui/views/controls/textfield/textfield_model.cc index 7b06af51838..4d77de34ffc 100644 --- a/chromium/ui/views/controls/textfield/textfield_model.cc +++ b/chromium/ui/views/controls/textfield/textfield_model.cc @@ -36,16 +36,15 @@ class Edit { // Revert the change made by this edit in |model|. void Undo(TextfieldModel* model) { - model->ModifyText(new_text_start_, new_text_end(), - old_text_, old_text_start_, - old_cursor_pos_); + model->ModifyText(new_text_start_, new_text_end(), old_text_, + old_text_start_, old_selection_); } // Apply the change of this edit to the |model|. void Redo(TextfieldModel* model) { - model->ModifyText(old_text_start_, old_text_end(), - new_text_, new_text_start_, - new_cursor_pos_); + model->ModifyText(old_text_start_, old_text_end(), new_text_, + new_text_start_, + gfx::Range(new_cursor_pos_, new_cursor_pos_)); } // Try to merge the |edit| into this edit and returns true on success. The @@ -72,23 +71,22 @@ class Edit { Edit(Type type, MergeType merge_type, - size_t old_cursor_pos, const base::string16& old_text, size_t old_text_start, + gfx::Range old_selection, bool delete_backward, size_t new_cursor_pos, const base::string16& new_text, size_t new_text_start) : type_(type), merge_type_(merge_type), - old_cursor_pos_(old_cursor_pos), old_text_(old_text), old_text_start_(old_text_start), + old_selection_(old_selection), delete_backward_(delete_backward), new_cursor_pos_(new_cursor_pos), new_text_(new_text), - new_text_start_(new_text_start) { - } + new_text_start_(new_text_start) {} // Each type of edit provides its own specific merge implementation. virtual bool DoMerge(const Edit* edit) = 0; @@ -131,12 +129,12 @@ class Edit { // The type of merging allowed. MergeType merge_type_; - // Old cursor position. - size_t old_cursor_pos_; // Deleted text by this edit. base::string16 old_text_; // The index of |old_text_|. size_t old_text_start_; + // The range of the text selection prior to the edit. + gfx::Range old_selection_; // True if the deletion is made backward. bool delete_backward_; // New cursor position. @@ -154,14 +152,13 @@ class InsertEdit : public Edit { InsertEdit(bool mergeable, const base::string16& new_text, size_t at) : Edit(INSERT_EDIT, mergeable ? MERGEABLE : DO_NOT_MERGE, - at /* old cursor */, base::string16(), at, - false /* N/A */, - at + new_text.length() /* new cursor */, + gfx::Range(at, at), + false /* N/A */, + at + new_text.length() /* new cursor */, new_text, - at) { - } + at) {} // Edit implementation. bool DoMerge(const Edit* edit) override { @@ -180,21 +177,21 @@ class ReplaceEdit : public Edit { public: ReplaceEdit(MergeType merge_type, const base::string16& old_text, - size_t old_cursor_pos, size_t old_text_start, + gfx::Range old_selection, bool backward, size_t new_cursor_pos, const base::string16& new_text, size_t new_text_start) - : Edit(REPLACE_EDIT, merge_type, - old_cursor_pos, + : Edit(REPLACE_EDIT, + merge_type, old_text, old_text_start, + old_selection, backward, new_cursor_pos, new_text, - new_text_start) { - } + new_text_start) {} // Edit implementation. bool DoMerge(const Edit* edit) override { @@ -214,17 +211,17 @@ class DeleteEdit : public Edit { DeleteEdit(bool mergeable, const base::string16& text, size_t text_start, - bool backward) + bool backward, + gfx::Range old_selection) : Edit(DELETE_EDIT, mergeable ? MERGEABLE : DO_NOT_MERGE, - (backward ? text_start + text.length() : text_start), text, text_start, + old_selection, backward, text_start, base::string16(), - text_start) { - } + text_start) {} // Edit implementation. bool DoMerge(const Edit* edit) override { @@ -334,14 +331,13 @@ bool TextfieldModel::SetText(const base::string16& new_text) { if (text() != new_text) { if (changed) // No need to remember composition. Undo(); - size_t old_cursor = GetCursorPosition(); // SetText moves the cursor to the end. size_t new_cursor = new_text.length(); - SelectAll(false); // If there is a composition text, don't merge with previous edit. // Otherwise, force merge the edits. ExecuteAndRecordReplace(changed ? DO_NOT_MERGE : FORCE_MERGE, - old_cursor, new_cursor, new_text, 0U); + gfx::Range(0, text().length()), new_cursor, + new_text, 0U); render_text_->SetCursorPosition(new_cursor); } ClearSelection(); @@ -627,11 +623,8 @@ void TextfieldModel::DeleteSelectionAndInsertTextAt( size_t position) { if (HasCompositionText()) CancelCompositionText(); - ExecuteAndRecordReplace(DO_NOT_MERGE, - GetCursorPosition(), - position + new_text.length(), - new_text, - position); + ExecuteAndRecordReplace(DO_NOT_MERGE, render_text_->selection(), + position + new_text.length(), new_text, position); } base::string16 TextfieldModel::GetTextFromRange(const gfx::Range& range) const { @@ -776,8 +769,9 @@ void TextfieldModel::ExecuteAndRecordDelete(gfx::Range range, bool mergeable) { size_t old_text_start = range.GetMin(); const base::string16 old_text = text().substr(old_text_start, range.length()); bool backward = range.is_reversed(); + gfx::Range curr_selection = render_text_->selection(); auto edit = std::make_unique<DeleteEdit>(mergeable, old_text, old_text_start, - backward); + backward, curr_selection); edit->Redo(this); AddOrMergeEditHistory(std::move(edit)); } @@ -787,23 +781,21 @@ void TextfieldModel::ExecuteAndRecordReplaceSelection( const base::string16& new_text) { size_t new_text_start = render_text_->selection().GetMin(); size_t new_cursor_pos = new_text_start + new_text.length(); - ExecuteAndRecordReplace(merge_type, - GetCursorPosition(), - new_cursor_pos, - new_text, - new_text_start); + ExecuteAndRecordReplace(merge_type, render_text_->selection(), new_cursor_pos, + new_text, new_text_start); } void TextfieldModel::ExecuteAndRecordReplace(MergeType merge_type, - size_t old_cursor_pos, + gfx::Range replacement_range, size_t new_cursor_pos, const base::string16& new_text, size_t new_text_start) { - size_t old_text_start = render_text_->selection().GetMin(); - bool backward = render_text_->selection().is_reversed(); + size_t old_text_start = replacement_range.GetMin(); + bool backward = replacement_range.is_reversed(); auto edit = std::make_unique<ReplaceEdit>( - merge_type, GetSelectedText(), old_cursor_pos, old_text_start, backward, - new_cursor_pos, new_text, new_text_start); + merge_type, GetTextFromRange(replacement_range), old_text_start, + render_text_->selection(), backward, new_cursor_pos, new_text, + new_text_start); edit->Redo(this); AddOrMergeEditHistory(std::move(edit)); } @@ -840,7 +832,7 @@ void TextfieldModel::ModifyText(size_t delete_from, size_t delete_to, const base::string16& new_text, size_t new_text_insert_at, - size_t new_cursor_pos) { + gfx::Range selection) { DCHECK_LE(delete_from, delete_to); base::string16 old_text = text(); ClearComposition(); @@ -848,8 +840,11 @@ void TextfieldModel::ModifyText(size_t delete_from, render_text_->SetText(old_text.erase(delete_from, delete_to - delete_from)); if (!new_text.empty()) render_text_->SetText(old_text.insert(new_text_insert_at, new_text)); - render_text_->SetCursorPosition(new_cursor_pos); - // TODO(oshima): Select text that was just undone, like Mac (but not GTK). + if (selection.start() == selection.end()) { + render_text_->SetCursorPosition(selection.start()); + } else { + render_text_->SelectRange(selection); + } } // static diff --git a/chromium/ui/views/controls/textfield/textfield_model.h b/chromium/ui/views/controls/textfield/textfield_model.h index 20c6687a97c..771d7a9c330 100644 --- a/chromium/ui/views/controls/textfield/textfield_model.h +++ b/chromium/ui/views/controls/textfield/textfield_model.h @@ -260,7 +260,7 @@ class VIEWS_EXPORT TextfieldModel { void ExecuteAndRecordReplaceSelection(internal::MergeType merge_type, const base::string16& new_text); void ExecuteAndRecordReplace(internal::MergeType merge_type, - size_t old_cursor_pos, + gfx::Range replacement_range, size_t new_cursor_pos, const base::string16& new_text, size_t new_text_start); @@ -270,15 +270,15 @@ class VIEWS_EXPORT TextfieldModel { void AddOrMergeEditHistory(std::unique_ptr<internal::Edit> edit); // Modify the text buffer in following way: - // 1) Delete the string from |delete_from| to |delte_to|. + // 1) Delete the string from |delete_from| to |delete_to|. // 2) Insert the |new_text| at the index |new_text_insert_at|. // Note that the index is after deletion. - // 3) Move the cursor to |new_cursor_pos|. + // 3) Select |selection|. void ModifyText(size_t delete_from, size_t delete_to, const base::string16& new_text, size_t new_text_insert_at, - size_t new_cursor_pos); + gfx::Range selection); void ClearComposition(); diff --git a/chromium/ui/views/controls/textfield/textfield_model_unittest.cc b/chromium/ui/views/controls/textfield/textfield_model_unittest.cc index c35b12260bf..60884ff58b0 100644 --- a/chromium/ui/views/controls/textfield/textfield_model_unittest.cc +++ b/chromium/ui/views/controls/textfield/textfield_model_unittest.cc @@ -11,7 +11,6 @@ #include "base/auto_reset.h" #include "base/macros.h" -#include "base/message_loop/message_loop.h" #include "base/strings/string16.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" @@ -1387,7 +1386,9 @@ TEST_F(TextfieldModelTest, UndoRedo_CutCopyPasteTest) { EXPECT_EQ(1U, model.GetCursorPosition()); EXPECT_TRUE(model.Undo()); EXPECT_STR_EQ("ABCDE", model.text()); - EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.render_text()->selection().EqualsIgnoringDirection( + gfx::Range(1, 3))); EXPECT_TRUE(model.Undo()); EXPECT_STR_EQ("", model.text()); EXPECT_EQ(0U, model.GetCursorPosition()); @@ -1418,7 +1419,9 @@ TEST_F(TextfieldModelTest, UndoRedo_CutCopyPasteTest) { EXPECT_EQ(1U, model.GetCursorPosition()); EXPECT_TRUE(model.Undo()); EXPECT_STR_EQ("ABCDE", model.text()); - EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.render_text()->selection().EqualsIgnoringDirection( + gfx::Range(1, 3))); EXPECT_TRUE(model.Undo()); EXPECT_STR_EQ("", model.text()); EXPECT_EQ(0U, model.GetCursorPosition()); @@ -1460,8 +1463,9 @@ TEST_F(TextfieldModelTest, UndoRedo_CutCopyPasteTest) { // An empty cut shouldn't create an edit. EXPECT_TRUE(model.Undo()); EXPECT_STR_EQ("ABCBCBCDE", model.text()); - EXPECT_EQ(3U, model.GetCursorPosition()); - + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.render_text()->selection().EqualsIgnoringDirection( + gfx::Range(1, 3))); // Test Copy. ResetModel(&model); model.SetText(base::ASCIIToUTF16("12345")); @@ -1533,6 +1537,47 @@ TEST_F(TextfieldModelTest, UndoRedo_CursorTest) { EXPECT_FALSE(model.Redo()); } +TEST_F(TextfieldModelTest, Undo_SelectionTest) { + gfx::Range range = gfx::Range(2, 4); + TextfieldModel model(nullptr); + model.SetText(base::ASCIIToUTF16("abcdef")); + model.SelectRange(range); + EXPECT_EQ(model.render_text()->selection(), range); + + // Deleting the selected text should change the text and the range. + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("abef", model.text()); + EXPECT_EQ(model.render_text()->selection(), gfx::Range(2, 2)); + + // Undoing the deletion should restore the former range. + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abcdef", model.text()); + EXPECT_EQ(model.render_text()->selection(), range); + + // When range.start = range.end, nothing is selected and + // range.start = range.end = cursor position + model.MoveCursor(gfx::CHARACTER_BREAK, gfx::CURSOR_LEFT, gfx::SELECTION_NONE); + EXPECT_EQ(model.render_text()->selection(), gfx::Range(2, 2)); + + // Deleting a single character should change the text and cursor location. + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("acdef", model.text()); + EXPECT_EQ(model.render_text()->selection(), gfx::Range(1, 1)); + + // Undoing the deletion should restore the former range. + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abcdef", model.text()); + EXPECT_EQ(model.render_text()->selection(), gfx::Range(2, 2)); + + MoveCursorTo(model, model.text().length()); + EXPECT_TRUE(model.Backspace()); + model.SelectRange(gfx::Range(1, 3)); + model.SetText(base::ASCIIToUTF16("[set]")); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abcde", model.text()); + EXPECT_EQ(model.render_text()->selection(), gfx::Range(1, 3)); +} + void RunInsertReplaceTest(TextfieldModel& model) { const bool reverse = model.render_text()->selection().is_reversed(); model.InsertChar('1'); diff --git a/chromium/ui/views/controls/textfield/textfield_unittest.cc b/chromium/ui/views/controls/textfield/textfield_unittest.cc index 775c61fd31d..12e9a987863 100644 --- a/chromium/ui/views/controls/textfield/textfield_unittest.cc +++ b/chromium/ui/views/controls/textfield/textfield_unittest.cc @@ -26,6 +26,7 @@ #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/emoji/emoji_panel_helper.h" #include "ui/base/ime/input_method_base.h" #include "ui/base/ime/input_method_delegate.h" #include "ui/base/ime/input_method_factory.h" @@ -704,9 +705,11 @@ class TextfieldTest : public ViewsTestBase, public TextfieldController { EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* LOOK UP */)); EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); } - EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* EMOJI */)); - EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); #endif + if (ui::IsEmojiPanelSupported()) { + EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* EMOJI */)); + EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); + } EXPECT_EQ(can_undo, menu->IsEnabledAt(menu_index++ /* UNDO */)); EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); @@ -3432,6 +3435,69 @@ TEST_F(TextfieldTest, SendingDeletePreservesShiftFlag) { EXPECT_EQ(ui::EF_SHIFT_DOWN, textfield_->event_flags()); } +TEST_F(TextfieldTest, EmojiItem_EmptyField) { + InitTextfield(); + EXPECT_TRUE(textfield_->context_menu_controller()); + + // Enable the emoji feature. + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(features::kEnableEmojiContextMenu); + + // A normal empty field may show the Emoji option (if supported). + ui::MenuModel* context_menu = GetContextMenuModel(); + EXPECT_TRUE(context_menu); + EXPECT_GT(context_menu->GetItemCount(), 0); + // Not all OS/versions support the emoji menu. + EXPECT_EQ(ui::IsEmojiPanelSupported(), + context_menu->GetLabelAt(0) == + l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_EMOJI)); +} + +TEST_F(TextfieldTest, EmojiItem_ReadonlyField) { + InitTextfield(); + EXPECT_TRUE(textfield_->context_menu_controller()); + + // Enable the emoji feature. + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(features::kEnableEmojiContextMenu); + + textfield_->SetReadOnly(true); + // In no case is the emoji option showing on a read-only field. + ui::MenuModel* context_menu = GetContextMenuModel(); + EXPECT_TRUE(context_menu); + EXPECT_GT(context_menu->GetItemCount(), 0); + EXPECT_NE(context_menu->GetLabelAt(0), + l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_EMOJI)); +} + +TEST_F(TextfieldTest, EmojiItem_FieldWithText) { + InitTextfield(); + EXPECT_TRUE(textfield_->context_menu_controller()); + +#if defined(OS_MACOSX) + // On Mac, when there is text, the "Look up" item (+ separator) takes the top + // position, and emoji comes after. + constexpr int kExpectedEmojiIndex = 2; +#else + constexpr int kExpectedEmojiIndex = 0; +#endif + + // Enable the emoji feature. + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(features::kEnableEmojiContextMenu); + + // A field with text may still show the Emoji option (if supported). + textfield_->SetText(base::ASCIIToUTF16("some text")); + textfield_->SelectAll(false); + ui::MenuModel* context_menu = GetContextMenuModel(); + EXPECT_TRUE(context_menu); + EXPECT_GT(context_menu->GetItemCount(), 0); + // Not all OS/versions support the emoji menu. + EXPECT_EQ(ui::IsEmojiPanelSupported(), + context_menu->GetLabelAt(kExpectedEmojiIndex) == + l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_EMOJI)); +} + #if defined(OS_MACOSX) // Tests to see if the BiDi submenu items are updated correctly when the // textfield's text direction is changed. @@ -3470,6 +3536,10 @@ TEST_F(TextfieldTest, LookUpItemUpdate) { InitTextfield(); EXPECT_TRUE(textfield_->context_menu_controller()); + // Make sure the Emoji feature is disabled for this test. + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndDisableFeature(features::kEnableEmojiContextMenu); + const base::string16 kTextOne = ASCIIToUTF16("crake"); textfield_->SetText(kTextOne); textfield_->SelectAll(false); @@ -3552,4 +3622,122 @@ TEST_F(TextfieldTest, AccessibilitySelectionEvents) { textfield_->GetAccessibilitySelectionFiredCount()); } +TEST_F(TextfieldTest, FocusReasonMouse) { + InitTextfield(); + widget_->GetFocusManager()->ClearFocus(); + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_NONE, + textfield_->GetFocusReason()); + + const auto& bounds = textfield_->bounds(); + MouseClick(bounds, 10); + + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_MOUSE, + textfield_->GetFocusReason()); +} + +TEST_F(TextfieldTest, FocusReasonTouchTap) { + InitTextfield(); + widget_->GetFocusManager()->ClearFocus(); + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_NONE, + textfield_->GetFocusReason()); + + ui::GestureEventDetails tap_details(ui::ET_GESTURE_TAP_DOWN); + tap_details.set_primary_pointer_type( + ui::EventPointerType::POINTER_TYPE_TOUCH); + GestureEventForTest tap(GetCursorPositionX(0), 0, tap_details); + textfield_->OnGestureEvent(&tap); + + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_TOUCH, + textfield_->GetFocusReason()); +} + +TEST_F(TextfieldTest, FocusReasonPenTap) { + InitTextfield(); + widget_->GetFocusManager()->ClearFocus(); + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_NONE, + textfield_->GetFocusReason()); + + ui::GestureEventDetails tap_details(ui::ET_GESTURE_TAP_DOWN); + tap_details.set_primary_pointer_type(ui::EventPointerType::POINTER_TYPE_PEN); + GestureEventForTest tap(GetCursorPositionX(0), 0, tap_details); + textfield_->OnGestureEvent(&tap); + + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_PEN, + textfield_->GetFocusReason()); +} + +TEST_F(TextfieldTest, FocusReasonMultipleEvents) { + InitTextfield(); + widget_->GetFocusManager()->ClearFocus(); + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_NONE, + textfield_->GetFocusReason()); + + // Pen tap, followed by a touch tap + { + ui::GestureEventDetails tap_details(ui::ET_GESTURE_TAP_DOWN); + tap_details.set_primary_pointer_type( + ui::EventPointerType::POINTER_TYPE_PEN); + GestureEventForTest tap(GetCursorPositionX(0), 0, tap_details); + textfield_->OnGestureEvent(&tap); + } + + { + ui::GestureEventDetails tap_details(ui::ET_GESTURE_TAP_DOWN); + tap_details.set_primary_pointer_type( + ui::EventPointerType::POINTER_TYPE_TOUCH); + GestureEventForTest tap(GetCursorPositionX(0), 0, tap_details); + textfield_->OnGestureEvent(&tap); + } + + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_PEN, + textfield_->GetFocusReason()); +} + +TEST_F(TextfieldTest, FocusReasonFocusBlurFocus) { + InitTextfield(); + widget_->GetFocusManager()->ClearFocus(); + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_NONE, + textfield_->GetFocusReason()); + + // Pen tap, blur, then programmatic focus. + ui::GestureEventDetails tap_details(ui::ET_GESTURE_TAP_DOWN); + tap_details.set_primary_pointer_type(ui::EventPointerType::POINTER_TYPE_PEN); + GestureEventForTest tap(GetCursorPositionX(0), 0, tap_details); + textfield_->OnGestureEvent(&tap); + + widget_->GetFocusManager()->ClearFocus(); + + textfield_->RequestFocus(); + + EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_OTHER, + textfield_->GetFocusReason()); +} + +TEST_F(TextfieldTest, ChangeTextDirectionAndLayoutAlignmentTest) { + InitTextfield(); + + textfield_->ChangeTextDirectionAndLayoutAlignment( + base::i18n::TextDirection::RIGHT_TO_LEFT); + EXPECT_EQ(textfield_->GetTextDirection(), + base::i18n::TextDirection::RIGHT_TO_LEFT); + EXPECT_EQ(textfield_->GetHorizontalAlignment(), + gfx::HorizontalAlignment::ALIGN_RIGHT); + + textfield_->ChangeTextDirectionAndLayoutAlignment( + base::i18n::TextDirection::RIGHT_TO_LEFT); + const base::string16& text = test_api_->GetRenderText()->GetDisplayText(); + base::i18n::TextDirection text_direction = + base::i18n::GetFirstStrongCharacterDirection(text); + EXPECT_EQ(textfield_->GetTextDirection(), text_direction); + EXPECT_EQ(textfield_->GetHorizontalAlignment(), + gfx::HorizontalAlignment::ALIGN_RIGHT); + + textfield_->ChangeTextDirectionAndLayoutAlignment( + base::i18n::TextDirection::LEFT_TO_RIGHT); + EXPECT_EQ(textfield_->GetTextDirection(), + base::i18n::TextDirection::LEFT_TO_RIGHT); + EXPECT_EQ(textfield_->GetHorizontalAlignment(), + gfx::HorizontalAlignment::ALIGN_LEFT); +} + } // namespace views diff --git a/chromium/ui/views/controls/tree/tree_view.cc b/chromium/ui/views/controls/tree/tree_view.cc index 4b8d78b239a..b8b07ce5377 100644 --- a/chromium/ui/views/controls/tree/tree_view.cc +++ b/chromium/ui/views/controls/tree/tree_view.cc @@ -7,7 +7,6 @@ #include <algorithm> #include "base/i18n/rtl.h" -#include "base/message_loop/message_loop.h" #include "build/build_config.h" #include "components/vector_icons/vector_icons.h" #include "ui/accessibility/ax_node_data.h" @@ -262,8 +261,13 @@ void TreeView::SetSelectedNode(TreeModelNode* model_node) { SchedulePaintForNode(selected_node_); } - if (selected_node_) - ScrollRectToVisible(GetForegroundBoundsForNode(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. @@ -739,6 +743,9 @@ void TreeView::LayoutEditor() { DCHECK(selected_node_); // Position the editor so that its text aligns with the text we drew. gfx::Rect row_bounds = GetForegroundBoundsForNode(selected_node_); + + // GetForegroundBoundsForNode() returns a "flipped" x for painting. First, un- + // flip it for the following calculations and ScrollRectToVisible(). row_bounds.set_x( GetMirroredXWithWidthInView(row_bounds.x(), row_bounds.width())); row_bounds.set_x(row_bounds.x() + text_offset_); diff --git a/chromium/ui/views/controls/views_text_services_context_menu.cc b/chromium/ui/views/controls/views_text_services_context_menu.cc index ec5975312cc..03a3a7b6e61 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu.cc +++ b/chromium/ui/views/controls/views_text_services_context_menu.cc @@ -4,7 +4,10 @@ #include "ui/views/controls/views_text_services_context_menu.h" +#include <memory> + #include "base/logging.h" +#include "ui/views/controls/views_text_services_context_menu_base.h" namespace views { @@ -12,7 +15,7 @@ namespace views { std::unique_ptr<ViewsTextServicesContextMenu> ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, Textfield* client) { - return nullptr; + return std::make_unique<ViewsTextServicesContextMenuBase>(menu, client); } bool ViewsTextServicesContextMenu::IsTextDirectionCheckedForTesting( 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 new file mode 100644 index 00000000000..e5fdea933b5 --- /dev/null +++ b/chromium/ui/views/controls/views_text_services_context_menu_base.cc @@ -0,0 +1,53 @@ +// 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_base.h" + +#include "ui/base/emoji/emoji_panel_helper.h" +#include "ui/base/models/simple_menu_model.h" +#include "ui/resources/grit/ui_resources.h" +#include "ui/strings/grit/ui_strings.h" +#include "ui/views/controls/textfield/textfield.h" + +namespace views { + +ViewsTextServicesContextMenuBase::ViewsTextServicesContextMenuBase( + ui::SimpleMenuModel* menu, + Textfield* client) + : client_(client) { + DCHECK(client); + DCHECK(menu); + // Not inserted on read-only fields or if the OS/version doesn't support it. + if (!client_->read_only() && ui::IsEmojiPanelSupported()) { + menu->InsertSeparatorAt(0, ui::NORMAL_SEPARATOR); + menu->InsertItemWithStringIdAt(0, IDS_CONTENT_CONTEXT_EMOJI, + IDS_CONTENT_CONTEXT_EMOJI); + } +} + +ViewsTextServicesContextMenuBase::~ViewsTextServicesContextMenuBase() {} + +bool ViewsTextServicesContextMenuBase::SupportsCommand(int command_id) const { + return command_id == IDS_CONTENT_CONTEXT_EMOJI; +} + +bool ViewsTextServicesContextMenuBase::IsCommandIdChecked( + int command_id) const { + return false; +} + +bool ViewsTextServicesContextMenuBase::IsCommandIdEnabled( + int command_id) const { + if (command_id == IDS_CONTENT_CONTEXT_EMOJI) + return true; + + return false; +} + +void ViewsTextServicesContextMenuBase::ExecuteCommand(int command_id) { + if (command_id == IDS_CONTENT_CONTEXT_EMOJI) + ui::ShowEmojiPanel(); +} + +} // namespace views
\ No newline at end of file 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 new file mode 100644 index 00000000000..7c39439b83a --- /dev/null +++ b/chromium/ui/views/controls/views_text_services_context_menu_base.h @@ -0,0 +1,41 @@ +// 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. + +#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 "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 +// context menu. Specific platforms may subclass and add additional items. +class ViewsTextServicesContextMenuBase : public ViewsTextServicesContextMenu { + public: + ViewsTextServicesContextMenuBase(ui::SimpleMenuModel* menu, + Textfield* client); + ~ViewsTextServicesContextMenuBase() override; + + // Returns true if the given |command_id| is handled by the menu. + bool SupportsCommand(int command_id) 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; + + protected: + Textfield* client() const { return client_; } + + private: + // The view associated with the menu. Weak. Owns |this|. + Textfield* client_ = nullptr; + + DISALLOW_COPY_AND_ASSIGN(ViewsTextServicesContextMenuBase); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_BASE_H_ 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 0c4cd260bed..c7731fb7547 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 @@ -6,18 +6,16 @@ #import <Cocoa/Cocoa.h> -#include "base/feature_list.h" #include "ui/base/cocoa/text_services_context_menu.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/models/simple_menu_model.h" #include "ui/base/resource/resource_bundle.h" -#include "ui/base/ui_base_features.h" #include "ui/gfx/decorated_text.h" #import "ui/gfx/decorated_text_mac.h" #include "ui/resources/grit/ui_resources.h" #include "ui/strings/grit/ui_strings.h" #include "ui/views/controls/textfield/textfield.h" -#include "ui/views/view.h" +#include "ui/views/controls/views_text_services_context_menu_base.h" #include "ui/views/widget/widget.h" namespace views { @@ -28,26 +26,21 @@ namespace { // text service items in the context menu. The items include Speech, Look Up // and BiDi. class ViewsTextServicesContextMenuMac - : public ViewsTextServicesContextMenu, + : public ViewsTextServicesContextMenuBase, public ui::TextServicesContextMenu::Delegate { public: ViewsTextServicesContextMenuMac(ui::SimpleMenuModel* menu, Textfield* client) - : text_services_menu_(this), client_(client) { - // The index to use when inserting items into the menu. - int index = 0; - + : 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( - index++, IDS_CONTENT_CONTEXT_LOOK_UP, + 0, IDS_CONTENT_CONTEXT_LOOK_UP, l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, text)); - menu->InsertSeparatorAt(index++, ui::NORMAL_SEPARATOR); - } - if (base::FeatureList::IsEnabled(features::kEnableEmojiContextMenu)) { - menu->InsertItemWithStringIdAt(index++, IDS_CONTENT_CONTEXT_EMOJI, - IDS_CONTENT_CONTEXT_EMOJI); - menu->InsertSeparatorAt(index++, ui::NORMAL_SEPARATOR); } + text_services_menu_.AppendToContextMenu(menu); text_services_menu_.AppendEditableItems(menu); } @@ -57,13 +50,8 @@ class ViewsTextServicesContextMenuMac // ViewsTextServicesContextMenu: bool SupportsCommand(int command_id) const override { return text_services_menu_.SupportsCommand(command_id) || - command_id == IDS_CONTENT_CONTEXT_EMOJI || - command_id == IDS_CONTENT_CONTEXT_LOOK_UP; - } - - bool IsCommandIdChecked(int command_id) const override { - DCHECK_EQ(IDS_CONTENT_CONTEXT_LOOK_UP, command_id); - return false; + command_id == IDS_CONTENT_CONTEXT_LOOK_UP || + ViewsTextServicesContextMenuBase::SupportsCommand(command_id); } bool IsCommandIdEnabled(int command_id) const override { @@ -71,35 +59,32 @@ class ViewsTextServicesContextMenuMac return text_services_menu_.IsCommandIdEnabled(command_id); switch (command_id) { - case IDS_CONTENT_CONTEXT_EMOJI: - return true; - case IDS_CONTENT_CONTEXT_LOOK_UP: return true; default: - return false; + return ViewsTextServicesContextMenuBase::IsCommandIdEnabled(command_id); } } void ExecuteCommand(int command_id) override { switch (command_id) { - case IDS_CONTENT_CONTEXT_EMOJI: - [NSApp orderFrontCharacterPalette:nil]; - break; - case IDS_CONTENT_CONTEXT_LOOK_UP: LookUpInDictionary(); break; + + default: + ViewsTextServicesContextMenuBase::ExecuteCommand(command_id); + break; } } // TextServicesContextMenu::Delegate: base::string16 GetSelectedText() const override { - if (client_->GetTextInputType() == ui::TEXT_INPUT_TYPE_PASSWORD) + if (client()->GetTextInputType() == ui::TEXT_INPUT_TYPE_PASSWORD) return base::string16(); - return client_->GetSelectedText(); + return client()->GetSelectedText(); } bool IsTextDirectionEnabled( @@ -110,7 +95,7 @@ class ViewsTextServicesContextMenuMac bool IsTextDirectionChecked( base::i18n::TextDirection direction) const override { return direction != base::i18n::UNKNOWN_DIRECTION && - client_->GetTextDirection() == direction; + client()->GetTextDirection() == direction; } void UpdateTextDirection(base::i18n::TextDirection direction) override { @@ -119,7 +104,7 @@ class ViewsTextServicesContextMenuMac 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); + client()->ChangeTextDirectionAndLayoutAlignment(text_direction); } private: @@ -127,10 +112,10 @@ class ViewsTextServicesContextMenuMac void LookUpInDictionary() { gfx::Point baseline_point; gfx::DecoratedText text; - if (client_->GetWordLookupDataFromSelection(&text, &baseline_point)) { - Widget* widget = client_->GetWidget(); + if (client()->GetWordLookupDataFromSelection(&text, &baseline_point)) { + Widget* widget = client()->GetWidget(); gfx::NativeView view = widget->GetNativeView(); - views::View::ConvertPointToTarget(client_, widget->GetRootView(), + views::View::ConvertPointToTarget(client(), widget->GetRootView(), &baseline_point); NSPoint lookup_point = NSMakePoint( @@ -144,9 +129,6 @@ class ViewsTextServicesContextMenuMac // Appends and handles the text service menu. ui::TextServicesContextMenu text_services_menu_; - // The view associated with the menu. Weak. Owns |this|. - Textfield* client_; - DISALLOW_COPY_AND_ASSIGN(ViewsTextServicesContextMenuMac); }; diff --git a/chromium/ui/views/controls/webview/web_dialog_view.cc b/chromium/ui/views/controls/webview/web_dialog_view.cc index ef8b793f745..323d9f895f8 100644 --- a/chromium/ui/views/controls/webview/web_dialog_view.cc +++ b/chromium/ui/views/controls/webview/web_dialog_view.cc @@ -307,19 +307,16 @@ content::WebContents* WebDialogView::OpenURLFromTab( return WebDialogWebContentsDelegate::OpenURLFromTab(source, params); } -void WebDialogView::AddNewContents(content::WebContents* source, - content::WebContents* new_contents, - WindowOpenDisposition disposition, - const gfx::Rect& initial_rect, - bool user_gesture, - bool* was_blocked) { - if (delegate_ && delegate_->HandleAddNewContents( - source, new_contents, disposition, initial_rect, user_gesture)) { - return; - } - WebDialogWebContentsDelegate::AddNewContents( - source, new_contents, disposition, initial_rect, user_gesture, - was_blocked); +void WebDialogView::AddNewContents( + content::WebContents* source, + std::unique_ptr<content::WebContents> new_contents, + WindowOpenDisposition disposition, + const gfx::Rect& initial_rect, + bool user_gesture, + bool* was_blocked) { + WebDialogWebContentsDelegate::AddNewContents(source, std::move(new_contents), + disposition, initial_rect, + user_gesture, was_blocked); } void WebDialogView::LoadingStateChanged(content::WebContents* source, diff --git a/chromium/ui/views/controls/webview/web_dialog_view.h b/chromium/ui/views/controls/webview/web_dialog_view.h index 3a91e99a7f5..bfba06181e7 100644 --- a/chromium/ui/views/controls/webview/web_dialog_view.h +++ b/chromium/ui/views/controls/webview/web_dialog_view.h @@ -104,7 +104,7 @@ class WEBVIEW_EXPORT WebDialogView : public views::ClientView, content::WebContents* source, const content::OpenURLParams& params) override; void AddNewContents(content::WebContents* source, - content::WebContents* new_contents, + std::unique_ptr<content::WebContents> new_contents, WindowOpenDisposition disposition, const gfx::Rect& initial_rect, bool user_gesture, diff --git a/chromium/ui/views/controls/webview/webview.cc b/chromium/ui/views/controls/webview/webview.cc index 4ec50f3b056..161185af3b8 100644 --- a/chromium/ui/views/controls/webview/webview.cc +++ b/chromium/ui/views/controls/webview/webview.cc @@ -6,6 +6,7 @@ #include <utility> +#include "base/no_destructor.h" #include "build/build_config.h" #include "content/public/browser/browser_accessibility_state.h" #include "content/public/browser/browser_context.h" @@ -23,6 +24,27 @@ namespace views { +namespace { + +// A testing stub that creates web contents. +WebView::WebContentsCreator* GetCreatorForTesting() { + static base::NoDestructor<WebView::WebContentsCreator> creator; + return creator.get(); +} + +} // namespace + +WebView::ScopedWebContentsCreatorForTesting::ScopedWebContentsCreatorForTesting( + WebContentsCreator creator) { + DCHECK(!*GetCreatorForTesting()); + *GetCreatorForTesting() = creator; +} + +WebView::ScopedWebContentsCreatorForTesting:: + ~ScopedWebContentsCreatorForTesting() { + *GetCreatorForTesting() = WebView::WebContentsCreator(); +} + // static const char WebView::kViewClassName[] = "WebView"; @@ -44,7 +66,7 @@ WebView::~WebView() { content::WebContents* WebView::GetWebContents() { if (!web_contents()) { - wc_owner_.reset(CreateWebContents(browser_context_)); + wc_owner_ = CreateWebContents(browser_context_); wc_owner_->SetDelegate(this); SetWebContents(wc_owner_.get()); } @@ -364,7 +386,6 @@ void WebView::UpdateCrashedOverlayView() { base::TERMINATION_STATUS_STILL_RUNNING && crashed_overlay_view_) { SetFocusBehavior(FocusBehavior::NEVER); - holder_->SetVisible(false); crashed_overlay_view_->SetVisible(true); return; } @@ -374,7 +395,6 @@ void WebView::UpdateCrashedOverlayView() { if (crashed_overlay_view_) crashed_overlay_view_->SetVisible(false); - holder_->SetVisible(true); } void WebView::NotifyAccessibilityWebContentsChanged() { @@ -382,12 +402,11 @@ void WebView::NotifyAccessibilityWebContentsChanged() { NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged, false); } -content::WebContents* WebView::CreateWebContents( - content::BrowserContext* browser_context) { - content::WebContents* contents = NULL; - if (ViewsDelegate::GetInstance()) { - contents = - ViewsDelegate::GetInstance()->CreateWebContents(browser_context, NULL); +std::unique_ptr<content::WebContents> WebView::CreateWebContents( + content::BrowserContext* browser_context) { + std::unique_ptr<content::WebContents> contents; + if (*GetCreatorForTesting()) { + contents = GetCreatorForTesting()->Run(browser_context); } if (!contents) { diff --git a/chromium/ui/views/controls/webview/webview.h b/chromium/ui/views/controls/webview/webview.h index 99affc147bc..87d9a1e7259 100644 --- a/chromium/ui/views/controls/webview/webview.h +++ b/chromium/ui/views/controls/webview/webview.h @@ -9,6 +9,7 @@ #include <memory> +#include "base/callback.h" #include "base/macros.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_observer.h" @@ -94,6 +95,20 @@ class WEBVIEW_EXPORT WebView : public View, const char* GetClassName() const override; NativeViewHost* holder() { return holder_; } + using WebContentsCreator = + base::RepeatingCallback<std::unique_ptr<content::WebContents>( + content::BrowserContext*)>; + + // An instance of this class registers a WebContentsCreator on construction + // and deregisters the WebContentsCreator on destruction. + class WEBVIEW_EXPORT ScopedWebContentsCreatorForTesting { + public: + explicit ScopedWebContentsCreatorForTesting(WebContentsCreator creator); + ~ScopedWebContentsCreatorForTesting(); + + private: + DISALLOW_COPY_AND_ASSIGN(ScopedWebContentsCreatorForTesting); + }; protected: // Swaps the owned WebContents |wc_owner_| with |new_web_contents|. Returns @@ -154,7 +169,7 @@ class WEBVIEW_EXPORT WebView : public View, // Create a regular or test web contents (based on whether we're running // in a unit test or not). - content::WebContents* CreateWebContents( + std::unique_ptr<content::WebContents> CreateWebContents( content::BrowserContext* browser_context); NativeViewHost* const holder_; diff --git a/chromium/ui/views/controls/webview/webview_unittest.cc b/chromium/ui/views/controls/webview/webview_unittest.cc index b679af9d8d2..f1d0ce7758d 100644 --- a/chromium/ui/views/controls/webview/webview_unittest.cc +++ b/chromium/ui/views/controls/webview/webview_unittest.cc @@ -33,29 +33,11 @@ namespace views { namespace { -// Provides functionality to create a test WebContents. -class WebViewTestViewsDelegate : public views::TestViewsDelegate { - public: - WebViewTestViewsDelegate() {} - ~WebViewTestViewsDelegate() override {} - - // Overriden from TestViewsDelegate. - content::WebContents* CreateWebContents( - content::BrowserContext* browser_context, - content::SiteInstance* site_instance) override { - return content::WebContentsTester::CreateTestWebContents(browser_context, - site_instance); - } - - private: - DISALLOW_COPY_AND_ASSIGN(WebViewTestViewsDelegate); -}; - // Provides functionality to observe events on a WebContents like // OnVisibilityChanged/WebContentsDestroyed. class WebViewTestWebContentsObserver : public content::WebContentsObserver { public: - WebViewTestWebContentsObserver(content::WebContents* web_contents) + explicit WebViewTestWebContentsObserver(content::WebContents* web_contents) : web_contents_(web_contents), was_shown_(false), shown_count_(0), @@ -146,8 +128,19 @@ class WebViewUnitTest : public views::test::WidgetTest { ~WebViewUnitTest() override {} + std::unique_ptr<content::WebContents> CreateWebContentsForWebView( + content::BrowserContext* browser_context) { + return content::WebContentsTester::CreateTestWebContents(browser_context, + nullptr); + } + void SetUp() override { - set_views_delegate(base::WrapUnique(new WebViewTestViewsDelegate)); + views::WebView::WebContentsCreator creator = base::BindRepeating( + &WebViewUnitTest::CreateWebContentsForWebView, base::Unretained(this)); + scoped_web_contents_creator_ = + std::make_unique<views::WebView::ScopedWebContentsCreatorForTesting>( + creator); + set_views_delegate(base::WrapUnique(new views::TestViewsDelegate)); browser_context_.reset(new content::TestBrowserContext); WidgetTest::SetUp(); // Set the test content browser client to avoid pulling in needless @@ -171,6 +164,7 @@ class WebViewUnitTest : public views::test::WidgetTest { } void TearDown() override { + scoped_web_contents_creator_.reset(); top_level_widget_->Close(); // Deletes all children and itself. RunPendingMessages(); @@ -187,18 +181,16 @@ class WebViewUnitTest : public views::test::WidgetTest { NativeViewHost* holder() const { return web_view_->holder_; } std::unique_ptr<content::WebContents> CreateWebContents() const { - return base::WrapUnique(content::WebContents::Create( - content::WebContents::CreateParams(browser_context_.get()))); + return content::WebContents::Create( + content::WebContents::CreateParams(browser_context_.get())); } private: - // TODO(lukasza): https://crbug.com/832100: Move the factory into - // TestingProfile, so individual tests don't need to worry about it. - content::ScopedMockRenderProcessHostFactory process_factory_; - content::TestBrowserThreadBundle test_browser_thread_bundle_; std::unique_ptr<content::TestBrowserContext> browser_context_; content::TestContentBrowserClient test_browser_client_; + std::unique_ptr<views::WebView::ScopedWebContentsCreatorForTesting> + scoped_web_contents_creator_; Widget* top_level_widget_ = nullptr; WebView* web_view_ = nullptr; |