blob: 4a7e05f70b8be9a2eb5bde1e1ee628976d22d064 [file] [log] [blame]
// 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