| // Copyright 2014 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/view_targeter.h" |
| |
| #include "ui/events/event_targeter.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/gfx/path.h" |
| #include "ui/views/masked_view_targeter.h" |
| #include "ui/views/test/views_test_base.h" |
| #include "ui/views/widget/root_view.h" |
| |
| namespace views { |
| |
| // A class used to define a triangular-shaped hit test mask on a View. |
| class TestMaskedViewTargeter : public MaskedViewTargeter { |
| public: |
| explicit TestMaskedViewTargeter(View* masked_view) |
| : MaskedViewTargeter(masked_view) {} |
| virtual ~TestMaskedViewTargeter() {} |
| |
| private: |
| virtual bool GetHitTestMask(const View* view, |
| gfx::Path* mask) const OVERRIDE { |
| SkScalar w = SkIntToScalar(view->width()); |
| SkScalar h = SkIntToScalar(view->height()); |
| |
| // Create a triangular mask within the bounds of |view|. |
| mask->moveTo(w / 2, 0); |
| mask->lineTo(w, h); |
| mask->lineTo(0, h); |
| mask->close(); |
| |
| return true; |
| } |
| |
| DISALLOW_COPY_AND_ASSIGN(TestMaskedViewTargeter); |
| }; |
| |
| // A derived class of View used for testing purposes. |
| class TestingView : public View { |
| public: |
| TestingView() : can_process_events_within_subtree_(true) {} |
| virtual ~TestingView() {} |
| |
| // Reset all test state. |
| void Reset() { can_process_events_within_subtree_ = true; } |
| |
| void set_can_process_events_within_subtree(bool can_process) { |
| can_process_events_within_subtree_ = can_process; |
| } |
| |
| // View: |
| virtual bool CanProcessEventsWithinSubtree() const OVERRIDE { |
| return can_process_events_within_subtree_; |
| } |
| |
| private: |
| // Value to return from CanProcessEventsWithinSubtree(). |
| bool can_process_events_within_subtree_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestingView); |
| }; |
| |
| namespace test { |
| |
| typedef ViewsTestBase ViewTargeterTest; |
| |
| // Verifies that the the functions ViewTargeter::FindTargetForEvent() |
| // and ViewTargeter::FindNextBestTarget() are implemented correctly |
| // for key events. |
| TEST_F(ViewTargeterTest, ViewTargeterForKeyEvents) { |
| Widget widget; |
| Widget::InitParams init_params = |
| CreateParams(Widget::InitParams::TYPE_POPUP); |
| init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| widget.Init(init_params); |
| |
| View* content = new View; |
| View* child = new View; |
| View* grandchild = new View; |
| |
| widget.SetContentsView(content); |
| content->AddChildView(child); |
| child->AddChildView(grandchild); |
| |
| grandchild->SetFocusable(true); |
| grandchild->RequestFocus(); |
| |
| ui::EventTargeter* targeter = new ViewTargeter(); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget.GetRootView()); |
| root_view->SetEventTargeter(make_scoped_ptr(targeter)); |
| |
| ui::KeyEvent key_event(ui::ET_KEY_PRESSED, ui::VKEY_A, 0, true); |
| |
| // The focused view should be the initial target of the event. |
| ui::EventTarget* current_target = targeter->FindTargetForEvent(root_view, |
| &key_event); |
| EXPECT_EQ(grandchild, static_cast<View*>(current_target)); |
| |
| // Verify that FindNextBestTarget() will return the parent view of the |
| // argument (and NULL if the argument has no parent view). |
| current_target = targeter->FindNextBestTarget(grandchild, &key_event); |
| EXPECT_EQ(child, static_cast<View*>(current_target)); |
| current_target = targeter->FindNextBestTarget(child, &key_event); |
| EXPECT_EQ(content, static_cast<View*>(current_target)); |
| current_target = targeter->FindNextBestTarget(content, &key_event); |
| EXPECT_EQ(widget.GetRootView(), static_cast<View*>(current_target)); |
| current_target = targeter->FindNextBestTarget(widget.GetRootView(), |
| &key_event); |
| EXPECT_EQ(NULL, static_cast<View*>(current_target)); |
| } |
| |
| // Verifies that the the functions ViewTargeter::FindTargetForEvent() |
| // and ViewTargeter::FindNextBestTarget() are implemented correctly |
| // for scroll events. |
| TEST_F(ViewTargeterTest, ViewTargeterForScrollEvents) { |
| Widget widget; |
| Widget::InitParams init_params = |
| CreateParams(Widget::InitParams::TYPE_POPUP); |
| init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| init_params.bounds = gfx::Rect(0, 0, 200, 200); |
| widget.Init(init_params); |
| |
| // The coordinates used for SetBounds() are in the parent coordinate space. |
| View* content = new View; |
| content->SetBounds(0, 0, 100, 100); |
| View* child = new View; |
| child->SetBounds(50, 50, 20, 20); |
| View* grandchild = new View; |
| grandchild->SetBounds(0, 0, 5, 5); |
| |
| widget.SetContentsView(content); |
| content->AddChildView(child); |
| child->AddChildView(grandchild); |
| |
| ui::EventTargeter* targeter = new ViewTargeter(); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget.GetRootView()); |
| root_view->SetEventTargeter(make_scoped_ptr(targeter)); |
| |
| // The event falls within the bounds of |child| and |content| but not |
| // |grandchild|, so |child| should be the initial target for the event. |
| ui::ScrollEvent scroll(ui::ET_SCROLL, |
| gfx::Point(60, 60), |
| ui::EventTimeForNow(), |
| 0, |
| 0, 3, |
| 0, 3, |
| 2); |
| ui::EventTarget* current_target = targeter->FindTargetForEvent(root_view, |
| &scroll); |
| EXPECT_EQ(child, static_cast<View*>(current_target)); |
| |
| // Verify that FindNextBestTarget() will return the parent view of the |
| // argument (and NULL if the argument has no parent view). |
| current_target = targeter->FindNextBestTarget(child, &scroll); |
| EXPECT_EQ(content, static_cast<View*>(current_target)); |
| current_target = targeter->FindNextBestTarget(content, &scroll); |
| EXPECT_EQ(widget.GetRootView(), static_cast<View*>(current_target)); |
| current_target = targeter->FindNextBestTarget(widget.GetRootView(), |
| &scroll); |
| EXPECT_EQ(NULL, static_cast<View*>(current_target)); |
| |
| // The event falls outside of the original specified bounds of |content|, |
| // |child|, and |grandchild|. But since |content| is the contents view, |
| // and contents views are resized to fill the entire area of the root |
| // view, the event's initial target should still be |content|. |
| scroll = ui::ScrollEvent(ui::ET_SCROLL, |
| gfx::Point(150, 150), |
| ui::EventTimeForNow(), |
| 0, |
| 0, 3, |
| 0, 3, |
| 2); |
| current_target = targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(content, static_cast<View*>(current_target)); |
| } |
| |
| // Tests the basic functionality of the method |
| // ViewTargeter::SubtreeShouldBeExploredForEvent(). |
| TEST_F(ViewTargeterTest, SubtreeShouldBeExploredForEvent) { |
| Widget widget; |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP); |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.bounds = gfx::Rect(0, 0, 650, 650); |
| widget.Init(params); |
| |
| ui::EventTargeter* targeter = new ViewTargeter(); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget.GetRootView()); |
| root_view->SetEventTargeter(make_scoped_ptr(targeter)); |
| |
| // The coordinates used for SetBounds() are in the parent coordinate space. |
| View v1, v2, v3; |
| v1.SetBounds(0, 0, 300, 300); |
| v2.SetBounds(100, 100, 100, 100); |
| v3.SetBounds(0, 0, 10, 10); |
| v3.SetVisible(false); |
| root_view->AddChildView(&v1); |
| v1.AddChildView(&v2); |
| v2.AddChildView(&v3); |
| |
| // Note that the coordinates used below are in |v1|'s coordinate space, |
| // and that SubtreeShouldBeExploredForEvent() expects the event location |
| // to be in the coordinate space of the target's parent. |v1| and |
| // its parent share a common coordinate space. |
| |
| // Event located within |v1| only. |
| gfx::Point point(10, 10); |
| ui::MouseEvent event(ui::ET_MOUSE_PRESSED, point, point, |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); |
| EXPECT_TRUE(targeter->SubtreeShouldBeExploredForEvent(&v1, event)); |
| EXPECT_FALSE(targeter->SubtreeShouldBeExploredForEvent(&v2, event)); |
| v1.ConvertEventToTarget(&v2, &event); |
| EXPECT_FALSE(targeter->SubtreeShouldBeExploredForEvent(&v3, event)); |
| |
| // Event located within |v1| and |v2| only. |
| event.set_location(gfx::Point(150, 150)); |
| EXPECT_TRUE(targeter->SubtreeShouldBeExploredForEvent(&v1, event)); |
| EXPECT_TRUE(targeter->SubtreeShouldBeExploredForEvent(&v2, event)); |
| v1.ConvertEventToTarget(&v2, &event); |
| EXPECT_FALSE(targeter->SubtreeShouldBeExploredForEvent(&v3, event)); |
| |
| // Event located within |v1|, |v2|, and |v3|. Note that |v3| is not |
| // visible, so it cannot handle the event. |
| event.set_location(gfx::Point(105, 105)); |
| EXPECT_TRUE(targeter->SubtreeShouldBeExploredForEvent(&v1, event)); |
| EXPECT_TRUE(targeter->SubtreeShouldBeExploredForEvent(&v2, event)); |
| v1.ConvertEventToTarget(&v2, &event); |
| EXPECT_FALSE(targeter->SubtreeShouldBeExploredForEvent(&v3, event)); |
| |
| // Event located outside the bounds of all views. |
| event.set_location(gfx::Point(400, 400)); |
| EXPECT_FALSE(targeter->SubtreeShouldBeExploredForEvent(&v1, event)); |
| EXPECT_FALSE(targeter->SubtreeShouldBeExploredForEvent(&v2, event)); |
| v1.ConvertEventToTarget(&v2, &event); |
| EXPECT_FALSE(targeter->SubtreeShouldBeExploredForEvent(&v3, event)); |
| |
| // TODO(tdanderson): Move the hit-testing unit tests out of view_unittest |
| // and into here. See crbug.com/355425. |
| } |
| |
| // Tests that FindTargetForEvent() returns the correct target when some |
| // views in the view tree return false when CanProcessEventsWithinSubtree() |
| // is called on them. |
| TEST_F(ViewTargeterTest, CanProcessEventsWithinSubtree) { |
| Widget widget; |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP); |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.bounds = gfx::Rect(0, 0, 650, 650); |
| widget.Init(params); |
| |
| ui::EventTargeter* targeter = new ViewTargeter(); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget.GetRootView()); |
| root_view->SetEventTargeter(make_scoped_ptr(targeter)); |
| |
| // The coordinates used for SetBounds() are in the parent coordinate space. |
| TestingView v1, v2, v3; |
| v1.SetBounds(0, 0, 300, 300); |
| v2.SetBounds(100, 100, 100, 100); |
| v3.SetBounds(0, 0, 10, 10); |
| root_view->AddChildView(&v1); |
| v1.AddChildView(&v2); |
| v2.AddChildView(&v3); |
| |
| // Note that the coordinates used below are in the coordinate space of |
| // the root view. |
| |
| // Define |scroll| to be (105, 105) (in the coordinate space of the root |
| // view). This is located within all of |v1|, |v2|, and |v3|. |
| gfx::Point scroll_point(105, 105); |
| ui::ScrollEvent scroll( |
| ui::ET_SCROLL, scroll_point, ui::EventTimeForNow(), 0, 0, 3, 0, 3, 2); |
| |
| // If CanProcessEventsWithinSubtree() returns true for each view, |
| // |scroll| should be targeted at the deepest view in the hierarchy, |
| // which is |v3|. |
| ui::EventTarget* current_target = |
| targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(&v3, current_target); |
| |
| // If CanProcessEventsWithinSubtree() returns |false| when called |
| // on |v3|, then |v3| cannot be the target of |scroll| (this should |
| // instead be |v2|). Note we need to reset the location of |scroll| |
| // because it may have been mutated by the previous call to |
| // FindTargetForEvent(). |
| scroll.set_location(scroll_point); |
| v3.set_can_process_events_within_subtree(false); |
| current_target = targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(&v2, current_target); |
| |
| // If CanProcessEventsWithinSubtree() returns |false| when called |
| // on |v2|, then neither |v2| nor |v3| can be the target of |scroll| |
| // (this should instead be |v1|). |
| scroll.set_location(scroll_point); |
| v3.Reset(); |
| v2.set_can_process_events_within_subtree(false); |
| current_target = targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(&v1, current_target); |
| |
| // If CanProcessEventsWithinSubtree() returns |false| when called |
| // on |v1|, then none of |v1|, |v2| or |v3| can be the target of |scroll| |
| // (this should instead be the root view itself). |
| scroll.set_location(scroll_point); |
| v2.Reset(); |
| v1.set_can_process_events_within_subtree(false); |
| current_target = targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(root_view, current_target); |
| |
| // TODO(tdanderson): We should also test that targeting works correctly |
| // with gestures. See crbug.com/375822. |
| } |
| |
| // Tests that FindTargetForEvent() returns the correct target when some |
| // views in the view tree have a MaskedViewTargeter installed, i.e., |
| // they have a custom-shaped hit test mask. |
| TEST_F(ViewTargeterTest, MaskedViewTargeter) { |
| Widget widget; |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP); |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.bounds = gfx::Rect(0, 0, 650, 650); |
| widget.Init(params); |
| |
| ui::EventTargeter* targeter = new ViewTargeter(); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget.GetRootView()); |
| root_view->SetEventTargeter(make_scoped_ptr(targeter)); |
| |
| // The coordinates used for SetBounds() are in the parent coordinate space. |
| View masked_view, unmasked_view, masked_child; |
| masked_view.SetBounds(0, 0, 200, 200); |
| unmasked_view.SetBounds(300, 0, 300, 300); |
| masked_child.SetBounds(0, 0, 100, 100); |
| root_view->AddChildView(&masked_view); |
| root_view->AddChildView(&unmasked_view); |
| unmasked_view.AddChildView(&masked_child); |
| |
| // Install event targeters of type TestMaskedViewTargeter on the two masked |
| // views to define their hit test masks. |
| ui::EventTargeter* masked_targeter = new TestMaskedViewTargeter(&masked_view); |
| masked_view.SetEventTargeter(make_scoped_ptr(masked_targeter)); |
| masked_targeter = new TestMaskedViewTargeter(&masked_child); |
| masked_child.SetEventTargeter(make_scoped_ptr(masked_targeter)); |
| |
| // Note that the coordinates used below are in the coordinate space of |
| // the root view. |
| |
| // Event located within the hit test mask of |masked_view|. |
| ui::ScrollEvent scroll(ui::ET_SCROLL, |
| gfx::Point(100, 190), |
| ui::EventTimeForNow(), |
| 0, |
| 0, |
| 3, |
| 0, |
| 3, |
| 2); |
| ui::EventTarget* current_target = |
| targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(&masked_view, static_cast<View*>(current_target)); |
| |
| // Event located outside the hit test mask of |masked_view|. |
| scroll.set_location(gfx::Point(10, 10)); |
| current_target = targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(root_view, static_cast<View*>(current_target)); |
| |
| // Event located within the hit test mask of |masked_child|. |
| scroll.set_location(gfx::Point(350, 3)); |
| current_target = targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(&masked_child, static_cast<View*>(current_target)); |
| |
| // Event located within the hit test mask of |masked_child|. |
| scroll.set_location(gfx::Point(300, 12)); |
| current_target = targeter->FindTargetForEvent(root_view, &scroll); |
| EXPECT_EQ(&unmasked_view, static_cast<View*>(current_target)); |
| |
| // TODO(tdanderson): We should also test that targeting of masked views |
| // works correctly with gestures. See crbug.com/375822. |
| } |
| |
| } // namespace test |
| } // namespace views |