| // 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/bubble/bubble_frame_view.h" |
| |
| #include <memory> |
| |
| #include "base/macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "ui/base/material_design/material_design_controller.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/text_utils.h" |
| #include "ui/views/bubble/bubble_border.h" |
| #include "ui/views/bubble/bubble_dialog_delegate.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/test/test_layout_provider.h" |
| #include "ui/views/test/test_views.h" |
| #include "ui/views/test/views_test_base.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| |
| namespace views { |
| |
| typedef ViewsTestBase BubbleFrameViewTest; |
| |
| namespace { |
| |
| bool UseMd() { |
| return ui::MaterialDesignController::IsSecondaryUiMaterial(); |
| } |
| |
| const BubbleBorder::Arrow kArrow = BubbleBorder::TOP_LEFT; |
| const SkColor kColor = SK_ColorRED; |
| const int kMargin = 6; |
| const int kMinimumClientWidth = 100; |
| const int kMinimumClientHeight = 200; |
| const int kMaximumClientWidth = 300; |
| const int kMaximumClientHeight = 300; |
| const int kPreferredClientWidth = 150; |
| const int kPreferredClientHeight = 250; |
| |
| // These account for non-client areas like the title bar, footnote etc. However |
| // these do not take the bubble border into consideration. |
| const int kExpectedAdditionalWidth = 12; |
| const int kExpectedAdditionalHeight = 12; |
| |
| class TestBubbleFrameViewWidgetDelegate : public WidgetDelegate { |
| public: |
| TestBubbleFrameViewWidgetDelegate(Widget* widget) : widget_(widget) {} |
| |
| ~TestBubbleFrameViewWidgetDelegate() override {} |
| |
| // WidgetDelegate overrides: |
| Widget* GetWidget() override { return widget_; } |
| const Widget* GetWidget() const override { return widget_; } |
| |
| View* GetContentsView() override { |
| if (!contents_view_) { |
| StaticSizedView* contents_view = new StaticSizedView( |
| gfx::Size(kPreferredClientWidth, kPreferredClientHeight)); |
| contents_view->set_minimum_size( |
| gfx::Size(kMinimumClientWidth, kMinimumClientHeight)); |
| contents_view->set_maximum_size( |
| gfx::Size(kMaximumClientWidth, kMaximumClientHeight)); |
| contents_view_ = contents_view; |
| } |
| return contents_view_; |
| } |
| |
| bool ShouldShowCloseButton() const override { return should_show_close_; } |
| void SetShouldShowCloseButton(bool should_show_close) { |
| should_show_close_ = should_show_close; |
| } |
| |
| private: |
| Widget* widget_; |
| View* contents_view_ = nullptr; // Owned by |widget_|. |
| bool should_show_close_ = false; |
| }; |
| |
| class TestBubbleFrameView : public BubbleFrameView { |
| public: |
| TestBubbleFrameView(ViewsTestBase* test_base) |
| : BubbleFrameView(gfx::Insets(), gfx::Insets(kMargin)), |
| test_base_(test_base), |
| available_bounds_(gfx::Rect(0, 0, 1000, 1000)) { |
| SetBubbleBorder(std::unique_ptr<BubbleBorder>( |
| new BubbleBorder(kArrow, BubbleBorder::NO_SHADOW, kColor))); |
| } |
| ~TestBubbleFrameView() override {} |
| |
| // View overrides: |
| const Widget* GetWidget() const override { |
| if (!widget_) { |
| widget_.reset(new Widget); |
| widget_delegate_.reset( |
| new TestBubbleFrameViewWidgetDelegate(widget_.get())); |
| Widget::InitParams params = |
| test_base_->CreateParams(Widget::InitParams::TYPE_BUBBLE); |
| params.delegate = widget_delegate_.get(); |
| params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| widget_->Init(params); |
| } |
| |
| return widget_.get(); |
| } |
| |
| // BubbleFrameView overrides: |
| gfx::Rect GetAvailableScreenBounds(const gfx::Rect& rect) const override { |
| return available_bounds_; |
| } |
| |
| TestBubbleFrameViewWidgetDelegate* widget_delegate() { |
| return widget_delegate_.get(); |
| } |
| |
| private: |
| ViewsTestBase* test_base_; |
| |
| gfx::Rect available_bounds_; |
| |
| // Widget returned by GetWidget(). Only created if GetWidget() is called. |
| mutable std::unique_ptr<TestBubbleFrameViewWidgetDelegate> widget_delegate_; |
| mutable std::unique_ptr<Widget> widget_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestBubbleFrameView); |
| }; |
| |
| } // namespace |
| |
| TEST_F(BubbleFrameViewTest, GetBoundsForClientView) { |
| TestBubbleFrameView frame(this); |
| EXPECT_EQ(kArrow, frame.bubble_border()->arrow()); |
| EXPECT_EQ(kColor, frame.bubble_border()->background_color()); |
| |
| int margin_x = frame.content_margins().left(); |
| int margin_y = frame.content_margins().top(); |
| gfx::Insets insets = frame.bubble_border()->GetInsets(); |
| EXPECT_EQ(insets.left() + margin_x, frame.GetBoundsForClientView().x()); |
| EXPECT_EQ(insets.top() + margin_y, frame.GetBoundsForClientView().y()); |
| } |
| |
| TEST_F(BubbleFrameViewTest, RemoveFootnoteView) { |
| TestBubbleFrameView frame(this); |
| EXPECT_EQ(nullptr, frame.footnote_container_); |
| View* footnote_dummy_view = new StaticSizedView(gfx::Size(200, 200)); |
| frame.SetFootnoteView(footnote_dummy_view); |
| EXPECT_EQ(footnote_dummy_view->parent(), frame.footnote_container_); |
| View* container_view = footnote_dummy_view->parent(); |
| delete footnote_dummy_view; |
| footnote_dummy_view = nullptr; |
| EXPECT_FALSE(container_view->visible()); |
| EXPECT_EQ(nullptr, frame.footnote_container_); |
| } |
| |
| TEST_F(BubbleFrameViewTest, GetBoundsForClientViewWithClose) { |
| TestBubbleFrameView frame(this); |
| // TestBubbleFrameView::GetWidget() is responsible for creating the widget and |
| // widget delegate at first call, so it is called here for that side-effect. |
| ignore_result(frame.GetWidget()); |
| frame.widget_delegate()->SetShouldShowCloseButton(true); |
| frame.ResetWindowControls(); |
| EXPECT_EQ(kArrow, frame.bubble_border()->arrow()); |
| EXPECT_EQ(kColor, frame.bubble_border()->background_color()); |
| |
| gfx::Insets frame_insets = frame.GetInsets(); |
| gfx::Insets border_insets = frame.bubble_border()->GetInsets(); |
| EXPECT_EQ(border_insets.left() + frame_insets.left(), |
| frame.GetBoundsForClientView().x()); |
| EXPECT_EQ(border_insets.top() + frame_insets.top(), |
| frame.GetBoundsForClientView().y()); |
| } |
| |
| TEST_F(BubbleFrameViewTest, |
| FootnoteContainerViewShouldMatchVisibilityOfFirstChild) { |
| TestBubbleFrameView frame(this); |
| View* footnote_dummy_view = new StaticSizedView(gfx::Size(200, 200)); |
| footnote_dummy_view->SetVisible(false); |
| frame.SetFootnoteView(footnote_dummy_view); |
| View* footnote_container_view = footnote_dummy_view->parent(); |
| EXPECT_FALSE(footnote_container_view->visible()); |
| footnote_dummy_view->SetVisible(true); |
| EXPECT_TRUE(footnote_container_view->visible()); |
| footnote_dummy_view->SetVisible(false); |
| EXPECT_FALSE(footnote_container_view->visible()); |
| } |
| |
| // Tests that the arrow is mirrored as needed to better fit the screen. |
| TEST_F(BubbleFrameViewTest, GetUpdatedWindowBounds) { |
| TestBubbleFrameView frame(this); |
| gfx::Rect window_bounds; |
| |
| gfx::Insets insets = frame.bubble_border()->GetInsets(); |
| int xposition = 95 - insets.width(); |
| |
| // Test that the info bubble displays normally when it fits. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_LEFT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_LEFT, frame.bubble_border()->arrow()); |
| EXPECT_GT(window_bounds.x(), xposition); |
| EXPECT_GT(window_bounds.y(), 100 + 50 - 10); // -10 to roughly compensate for |
| // arrow overlap. |
| |
| // Test bubble not fitting on left. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_RIGHT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_LEFT, frame.bubble_border()->arrow()); |
| EXPECT_GT(window_bounds.x(), xposition); |
| EXPECT_GT(window_bounds.y(), 100 + 50 - 10); // -10 to roughly compensate for |
| // arrow overlap. |
| |
| // Test bubble not fitting on left or top. |
| frame.bubble_border()->set_arrow(BubbleBorder::BOTTOM_RIGHT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_LEFT, frame.bubble_border()->arrow()); |
| EXPECT_GT(window_bounds.x(), xposition); |
| EXPECT_GT(window_bounds.y(), 100 + 50 - 10); // -10 to roughly compensate for |
| // arrow overlap. |
| |
| // Test bubble not fitting on top. |
| frame.bubble_border()->set_arrow(BubbleBorder::BOTTOM_LEFT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_LEFT, frame.bubble_border()->arrow()); |
| EXPECT_GT(window_bounds.x(), xposition); |
| EXPECT_GT(window_bounds.y(), 100 + 50 - 10); // -10 to roughly compensate for |
| // arrow overlap. |
| |
| // Test bubble not fitting on top and right. |
| frame.bubble_border()->set_arrow(BubbleBorder::BOTTOM_LEFT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(900, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_RIGHT, frame.bubble_border()->arrow()); |
| EXPECT_LT(window_bounds.x(), 900 + 50 - 500); |
| EXPECT_GT(window_bounds.y(), 100 + 50 - 10); // -10 to roughly compensate for |
| // arrow overlap. |
| |
| // Test bubble not fitting on right. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_LEFT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(900, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_RIGHT, frame.bubble_border()->arrow()); |
| EXPECT_LT(window_bounds.x(), 900 + 50 - 500); |
| EXPECT_GT(window_bounds.y(), 100 + 50 - 10); // -10 to roughly compensate for |
| // arrow overlap. |
| |
| // Test bubble not fitting on bottom and right. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_LEFT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(900, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::BOTTOM_RIGHT, frame.bubble_border()->arrow()); |
| EXPECT_LT(window_bounds.x(), 900 + 50 - 500); |
| EXPECT_LT(window_bounds.y(), 900 - 500 - 15); // -15 to roughly compensate |
| // for arrow height. |
| |
| // Test bubble not fitting at the bottom. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_LEFT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::BOTTOM_LEFT, frame.bubble_border()->arrow()); |
| // The window should be right aligned with the anchor_rect. |
| EXPECT_LT(window_bounds.x(), 900 + 50 - 500); |
| EXPECT_LT(window_bounds.y(), 900 - 500 - 15); // -15 to roughly compensate |
| // for arrow height. |
| |
| // Test bubble not fitting at the bottom and left. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_RIGHT); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::BOTTOM_LEFT, frame.bubble_border()->arrow()); |
| // The window should be right aligned with the anchor_rect. |
| EXPECT_LT(window_bounds.x(), 900 + 50 - 500); |
| EXPECT_LT(window_bounds.y(), 900 - 500 - 15); // -15 to roughly compensate |
| // for arrow height. |
| } |
| |
| // Tests that the arrow is not moved when the info-bubble does not fit the |
| // screen but moving it would make matter worse. |
| TEST_F(BubbleFrameViewTest, GetUpdatedWindowBoundsMirroringFails) { |
| TestBubbleFrameView frame(this); |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_LEFT); |
| frame.GetUpdatedWindowBounds( |
| gfx::Rect(400, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 700), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_LEFT, frame.bubble_border()->arrow()); |
| } |
| |
| TEST_F(BubbleFrameViewTest, TestMirroringForCenteredArrow) { |
| TestBubbleFrameView frame(this); |
| |
| // Test bubble not fitting above the anchor. |
| frame.bubble_border()->set_arrow(BubbleBorder::BOTTOM_CENTER); |
| frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 700), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_CENTER, frame.bubble_border()->arrow()); |
| |
| // Test bubble not fitting below the anchor. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_CENTER); |
| frame.GetUpdatedWindowBounds( |
| gfx::Rect(300, 800, 50, 50), // |anchor_rect| |
| gfx::Size(500, 200), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::BOTTOM_CENTER, frame.bubble_border()->arrow()); |
| |
| // Test bubble not fitting to the right of the anchor. |
| frame.bubble_border()->set_arrow(BubbleBorder::LEFT_CENTER); |
| frame.GetUpdatedWindowBounds( |
| gfx::Rect(800, 300, 50, 50), // |anchor_rect| |
| gfx::Size(200, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::RIGHT_CENTER, frame.bubble_border()->arrow()); |
| |
| // Test bubble not fitting to the left of the anchor. |
| frame.bubble_border()->set_arrow(BubbleBorder::RIGHT_CENTER); |
| frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 300, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::LEFT_CENTER, frame.bubble_border()->arrow()); |
| } |
| |
| // Test that the arrow will not be mirrored when |adjust_if_offscreen| is false. |
| TEST_F(BubbleFrameViewTest, GetUpdatedWindowBoundsDontTryMirror) { |
| TestBubbleFrameView frame(this); |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_RIGHT); |
| gfx::Rect window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| false); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_RIGHT, frame.bubble_border()->arrow()); |
| // The coordinates should be pointing to anchor_rect from TOP_RIGHT. |
| EXPECT_LT(window_bounds.x(), 100 + 50 - 500); |
| EXPECT_GT(window_bounds.y(), 900 + 50 - 10); // -10 to roughly compensate for |
| // arrow overlap. |
| } |
| |
| // Test that the center arrow is moved as needed to fit the screen. |
| TEST_F(BubbleFrameViewTest, GetUpdatedWindowBoundsCenterArrows) { |
| TestBubbleFrameView frame(this); |
| gfx::Rect window_bounds; |
| // Bubbles have a thicker shadow on the bottom in MD. |
| // Match definition of kLargeShadowVerticalOffset in bubble_border.cc. |
| const int kLargeShadowVerticalOffset = UseMd() ? 2 : 0; |
| |
| // Some of these tests may go away once --secondary-ui-md becomes the |
| // default. Under Material Design mode, the BubbleBorder doesn't support all |
| // "arrow" positions. If this changes, then the tests should be updated or |
| // added for MD mode. |
| |
| // Test that the bubble displays normally when it fits. |
| if (!UseMd()) { // TOP_CENTER isn't supported by the bubble_border() in MD. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(500, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.x() + window_bounds.width() / 2, 525); |
| } |
| |
| frame.bubble_border()->set_arrow(BubbleBorder::BOTTOM_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(500, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::BOTTOM_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.x() + window_bounds.width() / 2, 525); |
| |
| frame.bubble_border()->set_arrow(BubbleBorder::LEFT_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 400, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::LEFT_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.y() + window_bounds.height() / 2, |
| 425 + kLargeShadowVerticalOffset); |
| |
| frame.bubble_border()->set_arrow(BubbleBorder::RIGHT_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(900, 400, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::RIGHT_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.y() + window_bounds.height() / 2, |
| 425 + kLargeShadowVerticalOffset); |
| |
| // Test bubble not fitting left screen edge. |
| if (!UseMd()) { // TOP_CENTER isn't supported by the bubble_border() in MD. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.x(), 0); |
| EXPECT_EQ(window_bounds.x() + |
| frame.bubble_border()->GetArrowOffset(window_bounds.size()), |
| 125); |
| } |
| |
| frame.bubble_border()->set_arrow(BubbleBorder::BOTTOM_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::BOTTOM_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.x(), 0); |
| if (!UseMd()) { // There is no arrow offset in MD mode. |
| EXPECT_EQ(window_bounds.x() + |
| frame.bubble_border()->GetArrowOffset(window_bounds.size()), |
| 125); |
| } |
| |
| // Test bubble not fitting right screen edge. |
| if (!UseMd()) { // TOP_CENTER isn't supported by the bubble_border() in MD. |
| frame.bubble_border()->set_arrow(BubbleBorder::TOP_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(900, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::TOP_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.right(), 1000); |
| EXPECT_EQ(window_bounds.x() + |
| frame.bubble_border()->GetArrowOffset(window_bounds.size()), |
| 925); |
| } |
| |
| frame.bubble_border()->set_arrow(BubbleBorder::BOTTOM_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(900, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::BOTTOM_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.right(), 1000); |
| if (!UseMd()) { // There is no arrow offset in MD mode. |
| EXPECT_EQ(window_bounds.x() + |
| frame.bubble_border()->GetArrowOffset(window_bounds.size()), |
| 925); |
| } |
| |
| // Test bubble not fitting top screen edge. |
| if (!UseMd()) { // Moving the bubble by setting the arrow offset doesn't work |
| // in MD mode since there is no arrow displayed. |
| frame.bubble_border()->set_arrow(BubbleBorder::LEFT_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::LEFT_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.y(), 0); |
| EXPECT_EQ(window_bounds.y() + |
| frame.bubble_border()->GetArrowOffset(window_bounds.size()), |
| 125); |
| |
| frame.bubble_border()->set_arrow(BubbleBorder::RIGHT_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(900, 100, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::RIGHT_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.y(), 0); |
| EXPECT_EQ(window_bounds.y() + |
| frame.bubble_border()->GetArrowOffset(window_bounds.size()), |
| 125); |
| |
| // Test bubble not fitting bottom screen edge. |
| frame.bubble_border()->set_arrow(BubbleBorder::LEFT_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(100, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::LEFT_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.bottom(), 1000); |
| EXPECT_EQ(window_bounds.y() + |
| frame.bubble_border()->GetArrowOffset(window_bounds.size()), |
| 925); |
| |
| frame.bubble_border()->set_arrow(BubbleBorder::RIGHT_CENTER); |
| window_bounds = frame.GetUpdatedWindowBounds( |
| gfx::Rect(900, 900, 50, 50), // |anchor_rect| |
| gfx::Size(500, 500), // |client_size| |
| true); // |adjust_if_offscreen| |
| EXPECT_EQ(BubbleBorder::RIGHT_CENTER, frame.bubble_border()->arrow()); |
| EXPECT_EQ(window_bounds.bottom(), 1000); |
| EXPECT_EQ(window_bounds.y() + |
| frame.bubble_border()->GetArrowOffset(window_bounds.size()), |
| 925); |
| } |
| } |
| |
| TEST_F(BubbleFrameViewTest, GetPreferredSize) { |
| // Test border/insets. |
| TestBubbleFrameView frame(this); |
| gfx::Rect preferred_rect(frame.GetPreferredSize()); |
| // Expect that a border has been added to the preferred size. |
| preferred_rect.Inset(frame.bubble_border()->GetInsets()); |
| |
| gfx::Size expected_size(kPreferredClientWidth + kExpectedAdditionalWidth, |
| kPreferredClientHeight + kExpectedAdditionalHeight); |
| EXPECT_EQ(expected_size, preferred_rect.size()); |
| } |
| |
| TEST_F(BubbleFrameViewTest, GetPreferredSizeWithFootnote) { |
| // Test footnote view: adding a footnote should increase the preferred size, |
| // but only when the footnote is visible. |
| TestBubbleFrameView frame(this); |
| |
| constexpr int kFootnoteHeight = 20; |
| const gfx::Size no_footnote_size = frame.GetPreferredSize(); |
| View* footnote = new StaticSizedView(gfx::Size(10, kFootnoteHeight)); |
| footnote->SetVisible(false); |
| frame.SetFootnoteView(footnote); |
| EXPECT_EQ(no_footnote_size, frame.GetPreferredSize()); // No change. |
| |
| footnote->SetVisible(true); |
| gfx::Size with_footnote_size = no_footnote_size; |
| constexpr int kFootnoteTopBorderThickness = 1; |
| with_footnote_size.Enlarge(0, kFootnoteHeight + kFootnoteTopBorderThickness + |
| frame.content_margins().height()); |
| EXPECT_EQ(with_footnote_size, frame.GetPreferredSize()); |
| |
| footnote->SetVisible(false); |
| EXPECT_EQ(no_footnote_size, frame.GetPreferredSize()); |
| } |
| |
| TEST_F(BubbleFrameViewTest, GetMinimumSize) { |
| TestBubbleFrameView frame(this); |
| gfx::Rect minimum_rect(frame.GetMinimumSize()); |
| // Expect that a border has been added to the minimum size. |
| minimum_rect.Inset(frame.bubble_border()->GetInsets()); |
| |
| gfx::Size expected_size(kMinimumClientWidth + kExpectedAdditionalWidth, |
| kMinimumClientHeight + kExpectedAdditionalHeight); |
| EXPECT_EQ(expected_size, minimum_rect.size()); |
| } |
| |
| TEST_F(BubbleFrameViewTest, GetMaximumSize) { |
| TestBubbleFrameView frame(this); |
| gfx::Rect maximum_rect(frame.GetMaximumSize()); |
| #if defined(OS_WIN) |
| // On Windows, GetMaximumSize causes problems with DWM, so it should just be 0 |
| // (unlimited). See http://crbug.com/506206. |
| EXPECT_EQ(gfx::Size(), maximum_rect.size()); |
| #else |
| maximum_rect.Inset(frame.bubble_border()->GetInsets()); |
| |
| // Should ignore the contents view's maximum size and use the preferred size. |
| gfx::Size expected_size(kPreferredClientWidth + kExpectedAdditionalWidth, |
| kPreferredClientHeight + kExpectedAdditionalHeight); |
| EXPECT_EQ(expected_size, maximum_rect.size()); |
| #endif |
| } |
| |
| namespace { |
| |
| class TestBubbleDialogDelegateView : public BubbleDialogDelegateView { |
| public: |
| TestBubbleDialogDelegateView() |
| : BubbleDialogDelegateView(nullptr, BubbleBorder::NONE) { |
| set_shadow(BubbleBorder::NO_ASSETS); |
| SetAnchorRect(gfx::Rect()); |
| } |
| ~TestBubbleDialogDelegateView() override {} |
| |
| void ChangeTitle(const base::string16& title) { |
| title_ = title; |
| // Note UpdateWindowTitle() always does a layout, which will be invalid if |
| // the Widget needs to change size. But also SizeToContents() _only_ does a |
| // layout if the size is actually changing. |
| GetWidget()->UpdateWindowTitle(); |
| SizeToContents(); |
| } |
| |
| void set_icon(const gfx::ImageSkia& icon) { icon_ = icon; } |
| |
| // BubbleDialogDelegateView: |
| using BubbleDialogDelegateView::SetAnchorView; |
| using BubbleDialogDelegateView::SizeToContents; |
| gfx::ImageSkia GetWindowIcon() override { return icon_; } |
| bool ShouldShowWindowIcon() const override { return !icon_.isNull(); } |
| base::string16 GetWindowTitle() const override { return title_; } |
| bool ShouldShowWindowTitle() const override { return !title_.empty(); } |
| |
| void DeleteDelegate() override { |
| // This delegate is owned by the test case itself, so it should not delete |
| // itself here. But DialogDelegates shouldn't be reused, so check for that. |
| destroyed_ = true; |
| } |
| int GetDialogButtons() const override { return ui::DIALOG_BUTTON_OK; } |
| gfx::Size GetMinimumSize() const override { return gfx::Size(); } |
| gfx::Size CalculatePreferredSize() const override { |
| return gfx::Size(200, 200); |
| } |
| void Init() override { DCHECK(!destroyed_); } |
| |
| BubbleFrameView* GetBubbleFrameView() const { |
| return static_cast<BubbleFrameView*>( |
| GetWidget()->non_client_view()->frame_view()); |
| } |
| |
| private: |
| gfx::ImageSkia icon_; |
| base::string16 title_; |
| bool destroyed_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestBubbleDialogDelegateView); |
| }; |
| |
| class TestAnchor { |
| public: |
| explicit TestAnchor(Widget::InitParams params) { |
| params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| widget_.Init(params); |
| widget_.Show(); |
| } |
| |
| Widget& widget() { return widget_; } |
| |
| private: |
| Widget widget_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestAnchor); |
| }; |
| |
| // BubbleDialogDelegate with no margins to test width snapping. |
| class TestWidthSnapDelegate : public TestBubbleDialogDelegateView { |
| public: |
| TestWidthSnapDelegate(TestAnchor* anchor, bool should_snap) |
| : should_snap_(should_snap) { |
| SetAnchorView(anchor->widget().GetContentsView()); |
| set_margins(gfx::Insets()); |
| BubbleDialogDelegateView::CreateBubble(this); |
| GetWidget()->Show(); |
| } |
| |
| ~TestWidthSnapDelegate() override { GetWidget()->CloseNow(); } |
| |
| // TestBubbleDialogDelegateView: |
| bool ShouldSnapFrameWidth() const override { return should_snap_; } |
| |
| private: |
| bool should_snap_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestWidthSnapDelegate); |
| }; |
| |
| } // namespace |
| |
| // This test ensures that if the installed LayoutProvider snaps dialog widths, |
| // BubbleFrameView correctly sizes itself to that width. |
| TEST_F(BubbleFrameViewTest, WidthSnaps) { |
| test::TestLayoutProvider provider; |
| TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW)); |
| |
| { |
| TestWidthSnapDelegate delegate(&anchor, true); |
| EXPECT_EQ(delegate.GetPreferredSize().width(), |
| delegate.GetWidget()->GetWindowBoundsInScreen().width()); |
| } |
| |
| constexpr int kTestWidth = 300; |
| provider.SetSnappedDialogWidth(kTestWidth); |
| |
| { |
| TestWidthSnapDelegate delegate(&anchor, true); |
| // The Widget's snapped width should exactly match the width returned by the |
| // LayoutProvider. |
| EXPECT_EQ(kTestWidth, |
| delegate.GetWidget()->GetWindowBoundsInScreen().width()); |
| } |
| |
| { |
| // If the DialogDelegate asks not to snap, it should not snap. |
| TestWidthSnapDelegate delegate(&anchor, false); |
| EXPECT_EQ(delegate.GetPreferredSize().width(), |
| delegate.GetWidget()->GetWindowBoundsInScreen().width()); |
| } |
| } |
| |
| // Tests edge cases when the frame's title view starts to wrap text. This is to |
| // ensure that the calculations BubbleFrameView does to determine the Widget |
| // size for a given client view are consistent with the eventual size that the |
| // client view takes after Layout(). |
| TEST_F(BubbleFrameViewTest, LayoutEdgeCases) { |
| test::TestLayoutProvider provider; |
| TestBubbleDialogDelegateView delegate; |
| TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW)); |
| delegate.SetAnchorView(anchor.widget().GetContentsView()); |
| |
| Widget* bubble = BubbleDialogDelegateView::CreateBubble(&delegate); |
| bubble->Show(); |
| |
| // Even though the bubble has default margins, the dialog view should have |
| // been given its preferred size. |
| EXPECT_FALSE(delegate.margins().IsEmpty()); |
| EXPECT_EQ(delegate.size(), delegate.GetPreferredSize()); |
| |
| // Starting with a short title. |
| base::string16 title(1, 'i'); |
| delegate.ChangeTitle(title); |
| const int min_bubble_height = bubble->GetWindowBoundsInScreen().height(); |
| EXPECT_LT(delegate.GetPreferredSize().height(), min_bubble_height); |
| |
| // Grow the title incrementally until word wrap is required. There should |
| // never be a point where the BubbleFrameView over- or under-estimates the |
| // size required for the title. If it did, it would cause SizeToContents() to |
| // Widget size requiring the subsequent Layout() to fill the remaining client |
| // area with something other than |delegate|'s preferred size. |
| while (bubble->GetWindowBoundsInScreen().height() == min_bubble_height) { |
| title += ' '; |
| title += 'i'; |
| delegate.ChangeTitle(title); |
| EXPECT_EQ(delegate.GetPreferredSize(), delegate.size()) << title; |
| } |
| |
| // Sanity check that something interesting happened. The bubble should have |
| // grown by "a line" for the wrapped title, and the title should have reached |
| // a length that would have likely caused word wrap. A typical result would be |
| // a +17-20 change in height and title length of 53 characters. |
| const int two_line_height = bubble->GetWindowBoundsInScreen().height(); |
| EXPECT_LT(12, two_line_height - min_bubble_height); |
| EXPECT_GT(25, two_line_height - min_bubble_height); |
| EXPECT_LT(30u, title.size()); |
| EXPECT_GT(80u, title.size()); |
| |
| // Now add dialog snapping. |
| provider.SetSnappedDialogWidth(300); |
| delegate.SizeToContents(); |
| |
| // Height should go back to |min_bubble_height| since the window is wider: |
| // word wrapping should no longer happen. |
| EXPECT_EQ(min_bubble_height, bubble->GetWindowBoundsInScreen().height()); |
| EXPECT_EQ(300, bubble->GetWindowBoundsInScreen().width()); |
| |
| // Now we are allowed to diverge from the client view width, but not height. |
| EXPECT_EQ(delegate.GetPreferredSize().height(), delegate.height()); |
| EXPECT_LT(delegate.GetPreferredSize().width(), delegate.width()); |
| EXPECT_GT(300, delegate.width()); // Greater, since there are margins. |
| |
| const gfx::Size snapped_size = delegate.size(); |
| const size_t old_title_size = title.size(); |
| |
| // Grow the title again with width snapping until word wrapping occurs. |
| while (bubble->GetWindowBoundsInScreen().height() == min_bubble_height) { |
| title += ' '; |
| title += 'i'; |
| delegate.ChangeTitle(title); |
| EXPECT_EQ(snapped_size, delegate.size()) << title; |
| } |
| // Change to the height should have been the same as before. Title should |
| // have grown about 50%. |
| EXPECT_EQ(two_line_height, bubble->GetWindowBoundsInScreen().height()); |
| EXPECT_LT(15u, title.size() - old_title_size); |
| EXPECT_GT(40u, title.size() - old_title_size); |
| |
| // When |anchor| goes out of scope it should take |bubble| with it. |
| } |
| |
| TEST_F(BubbleFrameViewTest, LayoutWithIcon) { |
| TestBubbleDialogDelegateView delegate; |
| TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW)); |
| delegate.SetAnchorView(anchor.widget().GetContentsView()); |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(20, 80); |
| delegate.set_icon(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); |
| |
| Widget* widget = BubbleDialogDelegateView::CreateBubble(&delegate); |
| widget->Show(); |
| |
| delegate.ChangeTitle(base::ASCIIToUTF16("test title")); |
| BubbleFrameView* frame = delegate.GetBubbleFrameView(); |
| View* icon = frame->title_icon_; |
| View* title = frame->title(); |
| |
| // There should be equal amounts of space on the left and right of the icon. |
| EXPECT_EQ(icon->x() * 2 + icon->width(), title->x()); |
| |
| // The title should be vertically centered relative to the icon. |
| EXPECT_LT(title->height(), icon->height()); |
| const int title_offset_y = (icon->height() - title->height()) / 2; |
| EXPECT_EQ(icon->y() + title_offset_y, title->y()); |
| } |
| |
| // Test the size of the bubble allows a |gfx::NO_ELIDE| title to fit, even if |
| // there is no content. |
| TEST_F(BubbleFrameViewTest, NoElideTitle) { |
| test::TestLayoutProvider provider; |
| TestBubbleDialogDelegateView delegate; |
| TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW)); |
| delegate.SetAnchorView(anchor.widget().GetContentsView()); |
| |
| // Make sure the client area size doesn't interfere with the final size. |
| delegate.SetPreferredSize(gfx::Size()); |
| |
| Widget* bubble = BubbleDialogDelegateView::CreateBubble(&delegate); |
| bubble->Show(); |
| |
| // Before changing the title, get the base width of the bubble when there's no |
| // title or content in it. |
| const int empty_bubble_width = bubble->GetClientAreaBoundsInScreen().width(); |
| base::string16 title = base::ASCIIToUTF16("This is a title string"); |
| delegate.ChangeTitle(title); |
| Label* title_label = |
| static_cast<Label*>(delegate.GetBubbleFrameView()->title()); |
| |
| // Sanity check: Title labels default to multiline and elide tail. Either of |
| // which result in the Layout system making the title and resulting dialog |
| // very narrow. |
| EXPECT_EQ(gfx::ELIDE_TAIL, title_label->elide_behavior()); |
| EXPECT_TRUE(title_label->multi_line()); |
| EXPECT_GT(empty_bubble_width, title_label->size().width()); |
| EXPECT_EQ(empty_bubble_width, bubble->GetClientAreaBoundsInScreen().width()); |
| |
| // Set the title to a non-eliding label. |
| title_label->SetElideBehavior(gfx::NO_ELIDE); |
| title_label->SetMultiLine(false); |
| |
| // Update the bubble size now that some properties of the title have changed. |
| delegate.SizeToContents(); |
| |
| // The title/bubble should now be bigger than in multiline tail-eliding mode. |
| EXPECT_LT(empty_bubble_width, title_label->size().width()); |
| EXPECT_LT(empty_bubble_width, bubble->GetClientAreaBoundsInScreen().width()); |
| |
| // Make sure the bubble is wide enough to fit the title's full size. Frame |
| // sizing is done off the title label's minimum size. But since that label is |
| // set to NO_ELIDE, the minimum size should match the preferred size. |
| EXPECT_GE(bubble->GetClientAreaBoundsInScreen().width(), |
| title_label->GetPreferredSize().width()); |
| EXPECT_LE(title_label->GetPreferredSize().width(), |
| title_label->size().width()); |
| EXPECT_EQ(title, title_label->GetDisplayTextForTesting()); |
| } |
| |
| } // namespace views |