| // Copyright (c) 2013 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/native/native_view_host_aura.h" |
| |
| #include <memory> |
| |
| #include "base/macros.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/cursor/cursor.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/views/controls/native/native_view_host.h" |
| #include "ui/views/controls/native/native_view_host_test_base.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_constants_aura.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| |
| namespace views { |
| |
| // Observer watching for window visibility and bounds change events. This is |
| // used to verify that the child and clipping window operations are done in the |
| // right order. |
| class NativeViewHostWindowObserver : public aura::WindowObserver { |
| public: |
| enum EventType { |
| EVENT_NONE, |
| EVENT_SHOWN, |
| EVENT_HIDDEN, |
| EVENT_BOUNDS_CHANGED, |
| EVENT_DESTROYED, |
| }; |
| |
| struct EventDetails { |
| EventType type; |
| aura::Window* window; |
| gfx::Rect bounds; |
| bool operator!=(const EventDetails& rhs) { |
| return type != rhs.type || window != rhs.window || bounds != rhs.bounds; |
| } |
| }; |
| |
| NativeViewHostWindowObserver() {} |
| ~NativeViewHostWindowObserver() override {} |
| |
| const std::vector<EventDetails>& events() const { return events_; } |
| |
| // aura::WindowObserver overrides |
| void OnWindowVisibilityChanged(aura::Window* window, bool visible) override { |
| EventDetails event; |
| event.type = visible ? EVENT_SHOWN : EVENT_HIDDEN; |
| event.window = window; |
| event.bounds = window->GetBoundsInRootWindow(); |
| |
| // Dedupe events as a single Hide() call can result in several |
| // notifications. |
| if (events_.size() == 0u || events_.back() != event) |
| events_.push_back(event); |
| } |
| |
| void OnWindowBoundsChanged(aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds, |
| ui::PropertyChangeReason reason) override { |
| EventDetails event; |
| event.type = EVENT_BOUNDS_CHANGED; |
| event.window = window; |
| event.bounds = window->GetBoundsInRootWindow(); |
| events_.push_back(event); |
| } |
| |
| void OnWindowDestroyed(aura::Window* window) override { |
| EventDetails event = { EVENT_DESTROYED, window, gfx::Rect() }; |
| events_.push_back(event); |
| } |
| |
| private: |
| std::vector<EventDetails> events_; |
| gfx::Rect bounds_at_visibility_changed_; |
| |
| DISALLOW_COPY_AND_ASSIGN(NativeViewHostWindowObserver); |
| }; |
| |
| class NativeViewHostAuraTest : public test::NativeViewHostTestBase { |
| public: |
| NativeViewHostAuraTest() { |
| } |
| |
| NativeViewHostAura* native_host() { |
| return static_cast<NativeViewHostAura*>(GetNativeWrapper()); |
| } |
| |
| Widget* child() { |
| return child_.get(); |
| } |
| |
| aura::Window* clipping_window() { |
| return native_host()->clipping_window_.get(); |
| } |
| |
| void CreateHost() { |
| CreateTopLevel(); |
| CreateTestingHost(); |
| child_.reset(CreateChildForHost(toplevel()->GetNativeView(), |
| toplevel()->GetRootView(), |
| new View, |
| host())); |
| } |
| |
| // test::NativeViewHostTestBase: |
| void TearDown() override { |
| child_.reset(); |
| test::NativeViewHostTestBase::TearDown(); |
| } |
| |
| private: |
| std::unique_ptr<Widget> child_; |
| |
| DISALLOW_COPY_AND_ASSIGN(NativeViewHostAuraTest); |
| }; |
| |
| // Verifies NativeViewHostAura stops observing native view on destruction. |
| TEST_F(NativeViewHostAuraTest, StopObservingNativeViewOnDestruct) { |
| CreateHost(); |
| aura::Window* child_win = child()->GetNativeView(); |
| NativeViewHostAura* aura_host = native_host(); |
| |
| EXPECT_TRUE(child_win->HasObserver(aura_host)); |
| DestroyHost(); |
| EXPECT_FALSE(child_win->HasObserver(aura_host)); |
| } |
| |
| // Tests that the kHostViewKey is correctly set and cleared. |
| TEST_F(NativeViewHostAuraTest, HostViewPropertyKey) { |
| // Create the NativeViewHost and attach a NativeView. |
| CreateHost(); |
| aura::Window* child_win = child()->GetNativeView(); |
| EXPECT_EQ(host(), child_win->GetProperty(views::kHostViewKey)); |
| EXPECT_EQ(host()->GetWidget()->GetNativeView(), |
| child_win->GetProperty(aura::client::kHostWindowKey)); |
| EXPECT_EQ(host(), clipping_window()->GetProperty(views::kHostViewKey)); |
| |
| host()->Detach(); |
| EXPECT_FALSE(child_win->GetProperty(views::kHostViewKey)); |
| EXPECT_FALSE(child_win->GetProperty(aura::client::kHostWindowKey)); |
| EXPECT_TRUE(clipping_window()->GetProperty(views::kHostViewKey)); |
| |
| host()->Attach(child_win); |
| EXPECT_EQ(host(), child_win->GetProperty(views::kHostViewKey)); |
| EXPECT_EQ(host()->GetWidget()->GetNativeView(), |
| child_win->GetProperty(aura::client::kHostWindowKey)); |
| EXPECT_EQ(host(), clipping_window()->GetProperty(views::kHostViewKey)); |
| |
| DestroyHost(); |
| EXPECT_FALSE(child_win->GetProperty(views::kHostViewKey)); |
| EXPECT_FALSE(child_win->GetProperty(aura::client::kHostWindowKey)); |
| } |
| |
| // Tests that the NativeViewHost reports the cursor set on its native view. |
| TEST_F(NativeViewHostAuraTest, CursorForNativeView) { |
| CreateHost(); |
| |
| toplevel()->SetCursor(ui::CursorType::kHand); |
| child()->SetCursor(ui::CursorType::kWait); |
| ui::MouseEvent move_event(ui::ET_MOUSE_MOVED, gfx::Point(0, 0), |
| gfx::Point(0, 0), ui::EventTimeForNow(), 0, 0); |
| |
| EXPECT_EQ(ui::CursorType::kWait, host()->GetCursor(move_event).native_type()); |
| |
| DestroyHost(); |
| } |
| |
| // Test that destroying the top level widget before destroying the attached |
| // NativeViewHost works correctly. Specifically the associated NVH should be |
| // destroyed and there shouldn't be any errors. |
| TEST_F(NativeViewHostAuraTest, DestroyWidget) { |
| ResetHostDestroyedCount(); |
| CreateHost(); |
| ReleaseHost(); |
| EXPECT_EQ(0, host_destroyed_count()); |
| DestroyTopLevel(); |
| EXPECT_EQ(1, host_destroyed_count()); |
| } |
| |
| // Test that the fast resize path places the clipping and content windows were |
| // they are supposed to be. |
| TEST_F(NativeViewHostAuraTest, FastResizePath) { |
| CreateHost(); |
| toplevel()->SetBounds(gfx::Rect(20, 20, 100, 100)); |
| |
| // Without fast resize, the clipping window should size to the native view |
| // with the native view positioned at the origin of the clipping window and |
| // the clipping window positioned where the native view was requested. |
| host()->set_fast_resize(false); |
| native_host()->ShowWidget(5, 10, 100, 100, 100, 100); |
| EXPECT_EQ(gfx::Rect(0, 0, 100, 100).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(5, 10, 100, 100).ToString(), |
| clipping_window()->bounds().ToString()); |
| |
| // With fast resize, the native view should remain the same size but be |
| // clipped the requested size. |
| host()->set_fast_resize(true); |
| native_host()->ShowWidget(10, 25, 50, 50, 50, 50); |
| EXPECT_EQ(gfx::Rect(0, 0, 100, 100).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(10, 25, 50, 50).ToString(), |
| clipping_window()->bounds().ToString()); |
| |
| // Turning off fast resize should make the native view start resizing again. |
| host()->set_fast_resize(false); |
| native_host()->ShowWidget(10, 25, 50, 50, 50, 50); |
| EXPECT_EQ(gfx::Rect(0, 0, 50, 50).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(10, 25, 50, 50).ToString(), |
| clipping_window()->bounds().ToString()); |
| |
| DestroyHost(); |
| } |
| |
| // Test that the clipping and content windows' bounds are set to the correct |
| // values while the native size is not equal to the View size. During fast |
| // resize, the size and transform of the NativeView should not be modified. |
| TEST_F(NativeViewHostAuraTest, BoundsWhileScaling) { |
| CreateHost(); |
| toplevel()->SetBounds(gfx::Rect(20, 20, 100, 100)); |
| EXPECT_EQ(gfx::Transform(), host()->native_view()->transform()); |
| |
| // Without fast resize, the clipping window should size to the native view |
| // with the native view positioned at the origin of the clipping window and |
| // the clipping window positioned where the native view was requested. The |
| // size of the native view should be 200x200 (so it's content will be |
| // shown at half-size). |
| host()->set_fast_resize(false); |
| native_host()->ShowWidget(5, 10, 100, 100, 200, 200); |
| EXPECT_EQ(gfx::Rect(0, 0, 200, 200).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(5, 10, 100, 100).ToString(), |
| clipping_window()->bounds().ToString()); |
| gfx::Transform expected_transform; |
| expected_transform.Scale(0.5, 0.5); |
| EXPECT_EQ(expected_transform, host()->native_view()->transform()); |
| |
| // With fast resize, the native view should remain the same size but be |
| // clipped the requested size. Also, its transform should not be changed. |
| host()->set_fast_resize(true); |
| native_host()->ShowWidget(10, 25, 50, 50, 200, 200); |
| EXPECT_EQ(gfx::Rect(0, 0, 200, 200).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(10, 25, 50, 50).ToString(), |
| clipping_window()->bounds().ToString()); |
| EXPECT_EQ(expected_transform, host()->native_view()->transform()); |
| |
| // Turning off fast resize should make the native view start resizing again, |
| // and its transform modified to show at the new quarter-size. |
| host()->set_fast_resize(false); |
| native_host()->ShowWidget(10, 25, 50, 50, 200, 200); |
| EXPECT_EQ(gfx::Rect(0, 0, 200, 200).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(10, 25, 50, 50).ToString(), |
| clipping_window()->bounds().ToString()); |
| expected_transform = gfx::Transform(); |
| expected_transform.Scale(0.25, 0.25); |
| EXPECT_EQ(expected_transform, host()->native_view()->transform()); |
| |
| // When the NativeView is detached, its original transform should be restored. |
| auto* const detached_view = host()->native_view(); |
| host()->Detach(); |
| EXPECT_EQ(gfx::Transform(), detached_view->transform()); |
| // Attach it again so it's torn down with everything else at the end. |
| host()->Attach(detached_view); |
| |
| DestroyHost(); |
| } |
| |
| // Test installing and uninstalling a clip. |
| TEST_F(NativeViewHostAuraTest, InstallClip) { |
| CreateHost(); |
| toplevel()->SetBounds(gfx::Rect(20, 20, 100, 100)); |
| |
| // Without a clip, the clipping window should always be positioned at the |
| // requested coordinates with the native view positioned at the origin of the |
| // clipping window. |
| native_host()->ShowWidget(10, 20, 100, 100, 100, 100); |
| EXPECT_EQ(gfx::Rect(0, 0, 100, 100).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(10, 20, 100, 100).ToString(), |
| clipping_window()->bounds().ToString()); |
| |
| // Clip to the bottom right quarter of the native view. |
| native_host()->InstallClip(60, 70, 50, 50); |
| native_host()->ShowWidget(10, 20, 100, 100, 100, 100); |
| EXPECT_EQ(gfx::Rect(-50, -50, 100, 100).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(60, 70, 50, 50).ToString(), |
| clipping_window()->bounds().ToString()); |
| |
| // Clip to the center of the native view. |
| native_host()->InstallClip(35, 45, 50, 50); |
| native_host()->ShowWidget(10, 20, 100, 100, 100, 100); |
| EXPECT_EQ(gfx::Rect(-25, -25, 100, 100).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(35, 45, 50, 50).ToString(), |
| clipping_window()->bounds().ToString()); |
| |
| // Uninstalling the clip should make the clipping window match the native view |
| // again. |
| native_host()->UninstallClip(); |
| native_host()->ShowWidget(10, 20, 100, 100, 100, 100); |
| EXPECT_EQ(gfx::Rect(0, 0, 100, 100).ToString(), |
| host()->native_view()->bounds().ToString()); |
| EXPECT_EQ(gfx::Rect(10, 20, 100, 100).ToString(), |
| clipping_window()->bounds().ToString()); |
| |
| DestroyHost(); |
| } |
| |
| // Ensure native view is parented to the root window after detaching. This is |
| // a regression test for http://crbug.com/389261. |
| TEST_F(NativeViewHostAuraTest, ParentAfterDetach) { |
| CreateHost(); |
| aura::Window* child_win = child()->GetNativeView(); |
| aura::Window* root_window = child_win->GetRootWindow(); |
| aura::WindowTreeHost* child_win_tree_host = child_win->GetHost(); |
| |
| NativeViewHostWindowObserver test_observer; |
| child_win->AddObserver(&test_observer); |
| |
| host()->Detach(); |
| EXPECT_EQ(root_window, child_win->GetRootWindow()); |
| EXPECT_EQ(child_win_tree_host, child_win->GetHost()); |
| |
| DestroyHost(); |
| DestroyTopLevel(); |
| if (!IsMus()) { |
| // The window is detached, so no longer associated with any Widget |
| // hierarchy. The root window still owns it, but the test harness checks |
| // for orphaned windows during TearDown(). |
| EXPECT_EQ(0u, test_observer.events().size()) |
| << (*test_observer.events().begin()).type; |
| delete child_win; |
| } else { |
| // In mus and aura-mus, the child window is still attached to the |
| // aura::WindowTreeHost for the Widget. So destroying the toplevel Widget |
| // takes down the child window with it. |
| } |
| |
| ASSERT_EQ(1u, test_observer.events().size()); |
| EXPECT_EQ(NativeViewHostWindowObserver::EVENT_DESTROYED, |
| test_observer.events().back().type); |
| } |
| |
| // Ensure the clipping window is hidden before setting the native view's bounds. |
| // This is a regression test for http://crbug.com/388699. |
| TEST_F(NativeViewHostAuraTest, RemoveClippingWindowOrder) { |
| CreateHost(); |
| toplevel()->SetBounds(gfx::Rect(20, 20, 100, 100)); |
| native_host()->ShowWidget(10, 20, 100, 100, 100, 100); |
| |
| NativeViewHostWindowObserver test_observer; |
| clipping_window()->AddObserver(&test_observer); |
| |
| aura::Window* child_win = child()->GetNativeView(); |
| child_win->AddObserver(&test_observer); |
| |
| host()->Detach(); |
| |
| ASSERT_EQ(3u, test_observer.events().size()); |
| EXPECT_EQ(NativeViewHostWindowObserver::EVENT_HIDDEN, |
| test_observer.events()[0].type); |
| EXPECT_EQ(clipping_window(), test_observer.events()[0].window); |
| EXPECT_EQ(NativeViewHostWindowObserver::EVENT_BOUNDS_CHANGED, |
| test_observer.events()[1].type); |
| EXPECT_EQ(child()->GetNativeView(), test_observer.events()[1].window); |
| EXPECT_EQ(NativeViewHostWindowObserver::EVENT_HIDDEN, |
| test_observer.events()[2].type); |
| EXPECT_EQ(child()->GetNativeView(), test_observer.events()[2].window); |
| |
| clipping_window()->RemoveObserver(&test_observer); |
| child()->GetNativeView()->RemoveObserver(&test_observer); |
| |
| DestroyHost(); |
| delete child_win; // See comments in ParentAfterDetach. |
| } |
| |
| // Ensure the native view receives the correct bounds notification when it is |
| // attached. This is a regression test for https://crbug.com/399420. |
| TEST_F(NativeViewHostAuraTest, Attach) { |
| CreateHost(); |
| host()->Detach(); |
| |
| child()->GetNativeView()->SetBounds(gfx::Rect(0, 0, 0, 0)); |
| toplevel()->SetBounds(gfx::Rect(0, 0, 100, 100)); |
| host()->SetBounds(10, 10, 80, 80); |
| |
| NativeViewHostWindowObserver test_observer; |
| child()->GetNativeView()->AddObserver(&test_observer); |
| |
| host()->Attach(child()->GetNativeView()); |
| |
| ASSERT_EQ(3u, test_observer.events().size()); |
| EXPECT_EQ(NativeViewHostWindowObserver::EVENT_BOUNDS_CHANGED, |
| test_observer.events()[0].type); |
| EXPECT_EQ(child()->GetNativeView(), test_observer.events()[0].window); |
| EXPECT_EQ(gfx::Rect(10, 10, 80, 80).ToString(), |
| test_observer.events()[0].bounds.ToString()); |
| EXPECT_EQ(NativeViewHostWindowObserver::EVENT_SHOWN, |
| test_observer.events()[1].type); |
| EXPECT_EQ(child()->GetNativeView(), test_observer.events()[1].window); |
| EXPECT_EQ(gfx::Rect(10, 10, 80, 80).ToString(), |
| test_observer.events()[1].bounds.ToString()); |
| EXPECT_EQ(NativeViewHostWindowObserver::EVENT_SHOWN, |
| test_observer.events()[2].type); |
| EXPECT_EQ(clipping_window(), test_observer.events()[2].window); |
| EXPECT_EQ(gfx::Rect(10, 10, 80, 80).ToString(), |
| test_observer.events()[2].bounds.ToString()); |
| |
| child()->GetNativeView()->RemoveObserver(&test_observer); |
| DestroyHost(); |
| } |
| |
| // Ensure the clipping window is hidden with the native view. This is a |
| // regression test for https://crbug.com/408877. |
| TEST_F(NativeViewHostAuraTest, SimpleShowAndHide) { |
| CreateHost(); |
| |
| toplevel()->SetBounds(gfx::Rect(20, 20, 100, 100)); |
| toplevel()->Show(); |
| |
| host()->SetBounds(10, 10, 80, 80); |
| EXPECT_TRUE(clipping_window()->IsVisible()); |
| EXPECT_TRUE(child()->IsVisible()); |
| |
| host()->SetVisible(false); |
| EXPECT_FALSE(clipping_window()->IsVisible()); |
| EXPECT_FALSE(child()->IsVisible()); |
| |
| DestroyHost(); |
| DestroyTopLevel(); |
| } |
| |
| namespace { |
| |
| class TestFocusChangeListener : public FocusChangeListener { |
| public: |
| TestFocusChangeListener(FocusManager* focus_manager) |
| : focus_manager_(focus_manager) { |
| focus_manager_->AddFocusChangeListener(this); |
| } |
| |
| ~TestFocusChangeListener() override { |
| focus_manager_->RemoveFocusChangeListener(this); |
| } |
| |
| int did_change_focus_count() const { return did_change_focus_count_; } |
| |
| private: |
| // FocusChangeListener: |
| void OnWillChangeFocus(View* focused_before, View* focused_now) override {} |
| void OnDidChangeFocus(View* focused_before, View* focused_now) override { |
| did_change_focus_count_++; |
| } |
| |
| FocusManager* focus_manager_; |
| int did_change_focus_count_ = 0; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestFocusChangeListener); |
| }; |
| |
| } // namespace |
| |
| // Verifies the FocusManager is properly updated if focus is in a child widget |
| // that is parented to a NativeViewHost and the NativeViewHost is destroyed. |
| TEST_F(NativeViewHostAuraTest, FocusManagerUpdatedDuringDestruction) { |
| CreateTopLevel(); |
| toplevel()->Show(); |
| |
| std::unique_ptr<aura::Window> window = |
| std::make_unique<aura::Window>(nullptr); |
| window->Init(ui::LAYER_NOT_DRAWN); |
| window->set_owned_by_parent(false); |
| |
| std::unique_ptr<NativeViewHost> native_view_host = |
| std::make_unique<NativeViewHost>(); |
| toplevel()->GetContentsView()->AddChildView(native_view_host.get()); |
| |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_CONTROL); |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.delegate = new views::WidgetDelegateView(); // Owned by the widget. |
| params.child = true; |
| params.bounds = gfx::Rect(10, 10, 100, 100); |
| params.parent = window.get(); |
| std::unique_ptr<Widget> child_widget = std::make_unique<Widget>(); |
| child_widget->Init(params); |
| |
| native_view_host->Attach(window.get()); |
| |
| View* view1 = new View; // Owned by |child_widget|. |
| view1->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| view1->SetBounds(0, 0, 20, 20); |
| child_widget->GetContentsView()->AddChildView(view1); |
| child_widget->Show(); |
| view1->RequestFocus(); |
| EXPECT_EQ(view1, toplevel()->GetFocusManager()->GetFocusedView()); |
| |
| TestFocusChangeListener focus_change_listener(toplevel()->GetFocusManager()); |
| |
| // ~NativeViewHost() unparents |window|. |
| native_view_host.reset(); |
| |
| EXPECT_EQ(nullptr, toplevel()->GetFocusManager()->GetFocusedView()); |
| EXPECT_EQ(1, focus_change_listener.did_change_focus_count()); |
| |
| child_widget.reset(); |
| EXPECT_EQ(nullptr, toplevel()->GetFocusManager()->GetFocusedView()); |
| } |
| |
| } // namespace views |