// Copyright 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/widget/root_view.h"

#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "build/build_config.h"
#include "ui/events/event_utils.h"
#include "ui/views/context_menu_controller.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view_targeter.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget_deletion_observer.h"
#include "ui/views/window/dialog_delegate.h"

namespace views {
namespace test {

typedef ViewsTestBase RootViewTest;

class DeleteOnKeyEventView : public View {
 public:
  explicit DeleteOnKeyEventView(bool* set_on_key) : set_on_key_(set_on_key) {}
  ~DeleteOnKeyEventView() override {}

  bool OnKeyPressed(const ui::KeyEvent& event) override {
    *set_on_key_ = true;
    delete this;
    return true;
  }

 private:
  // Set to true in OnKeyPressed().
  bool* set_on_key_;

  DISALLOW_COPY_AND_ASSIGN(DeleteOnKeyEventView);
};

// Verifies deleting a View in OnKeyPressed() doesn't crash and that the
// target is marked as destroyed in the returned EventDispatchDetails.
TEST_F(RootViewTest, DeleteViewDuringKeyEventDispatch) {
  Widget widget;
  Widget::InitParams init_params =
      CreateParams(Widget::InitParams::TYPE_POPUP);
  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
  widget.Init(init_params);
  widget.Show();

  bool got_key_event = false;

  View* content = new View;
  widget.SetContentsView(content);

  View* child = new DeleteOnKeyEventView(&got_key_event);
  content->AddChildView(child);

  // Give focus to |child| so that it will be the target of the key event.
  child->SetFocusBehavior(View::FocusBehavior::ALWAYS);
  child->RequestFocus();

  internal::RootView* root_view =
      static_cast<internal::RootView*>(widget.GetRootView());
  ViewTargeter* view_targeter = new ViewTargeter(root_view);
  root_view->SetEventTargeter(base::WrapUnique(view_targeter));

  ui::KeyEvent key_event(ui::ET_KEY_PRESSED, ui::VKEY_ESCAPE, ui::EF_NONE);
  ui::EventDispatchDetails details = root_view->OnEventFromSource(&key_event);
  EXPECT_TRUE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_TRUE(got_key_event);
}

// Tracks whether a context menu is shown.
class TestContextMenuController : public ContextMenuController {
 public:
  TestContextMenuController()
      : show_context_menu_calls_(0),
        menu_source_view_(NULL),
        menu_source_type_(ui::MENU_SOURCE_NONE) {
  }
  ~TestContextMenuController() override {}

  int show_context_menu_calls() const { return show_context_menu_calls_; }
  View* menu_source_view() const { return menu_source_view_; }
  ui::MenuSourceType menu_source_type() const { return menu_source_type_; }

  void Reset() {
    show_context_menu_calls_ = 0;
    menu_source_view_ = NULL;
    menu_source_type_ = ui::MENU_SOURCE_NONE;
  }

  // ContextMenuController:
  void ShowContextMenuForView(View* source,
                              const gfx::Point& point,
                              ui::MenuSourceType source_type) override {
    show_context_menu_calls_++;
    menu_source_view_ = source;
    menu_source_type_ = source_type;
  }

 private:
  int show_context_menu_calls_;
  View* menu_source_view_;
  ui::MenuSourceType menu_source_type_;

  DISALLOW_COPY_AND_ASSIGN(TestContextMenuController);
};

// Tests that context menus are shown for certain key events (Shift+F10
// and VKEY_APPS) by the pre-target handler installed on RootView.
TEST_F(RootViewTest, ContextMenuFromKeyEvent) {
#if defined(OS_MACOSX)
  // This behavior is intentionally unsupported on macOS.
  return;
#endif
  Widget widget;
  Widget::InitParams init_params =
      CreateParams(Widget::InitParams::TYPE_POPUP);
  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
  widget.Init(init_params);
  widget.Show();
  internal::RootView* root_view =
      static_cast<internal::RootView*>(widget.GetRootView());

  TestContextMenuController controller;
  View* focused_view = new View;
  focused_view->set_context_menu_controller(&controller);
  widget.SetContentsView(focused_view);
  focused_view->SetFocusBehavior(View::FocusBehavior::ALWAYS);
  focused_view->RequestFocus();

  // No context menu should be shown for a keypress of 'A'.
  ui::KeyEvent nomenu_key_event('a', ui::VKEY_A, ui::EF_NONE);
  ui::EventDispatchDetails details =
      root_view->OnEventFromSource(&nomenu_key_event);
  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(0, controller.show_context_menu_calls());
  EXPECT_EQ(NULL, controller.menu_source_view());
  EXPECT_EQ(ui::MENU_SOURCE_NONE, controller.menu_source_type());
  controller.Reset();

  // A context menu should be shown for a keypress of Shift+F10.
  ui::KeyEvent menu_key_event(
      ui::ET_KEY_PRESSED, ui::VKEY_F10, ui::EF_SHIFT_DOWN);
  details = root_view->OnEventFromSource(&menu_key_event);
  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(1, controller.show_context_menu_calls());
  EXPECT_EQ(focused_view, controller.menu_source_view());
  EXPECT_EQ(ui::MENU_SOURCE_KEYBOARD, controller.menu_source_type());
  controller.Reset();

  // A context menu should be shown for a keypress of VKEY_APPS.
  ui::KeyEvent menu_key_event2(ui::ET_KEY_PRESSED, ui::VKEY_APPS, ui::EF_NONE);
  details = root_view->OnEventFromSource(&menu_key_event2);
  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(1, controller.show_context_menu_calls());
  EXPECT_EQ(focused_view, controller.menu_source_view());
  EXPECT_EQ(ui::MENU_SOURCE_KEYBOARD, controller.menu_source_type());
  controller.Reset();
}

// View which handles all gesture events.
class GestureHandlingView : public View {
 public:
  GestureHandlingView() {
  }

  ~GestureHandlingView() override {}

  void OnGestureEvent(ui::GestureEvent* event) override { event->SetHandled(); }

 private:
  DISALLOW_COPY_AND_ASSIGN(GestureHandlingView);
};

// Tests that context menus are shown for long press by the post-target handler
// installed on the RootView only if the event is targetted at a view which can
// show a context menu.
TEST_F(RootViewTest, ContextMenuFromLongPress) {
  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(100, 100);
  widget.Init(init_params);
  internal::RootView* root_view =
      static_cast<internal::RootView*>(widget.GetRootView());

  // Create a view capable of showing the context menu with two children one of
  // which handles all gesture events (e.g. a button).
  TestContextMenuController controller;
  View* parent_view = new View;
  parent_view->set_context_menu_controller(&controller);
  widget.SetContentsView(parent_view);

  View* gesture_handling_child_view = new GestureHandlingView;
  gesture_handling_child_view->SetBoundsRect(gfx::Rect(10, 10));
  parent_view->AddChildView(gesture_handling_child_view);

  View* other_child_view = new View;
  other_child_view->SetBoundsRect(gfx::Rect(20, 0, 10, 10));
  parent_view->AddChildView(other_child_view);

  // |parent_view| should not show a context menu as a result of a long press on
  // |gesture_handling_child_view|.
  ui::GestureEvent long_press1(
      5, 5, 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::ET_GESTURE_LONG_PRESS));
  ui::EventDispatchDetails details = root_view->OnEventFromSource(&long_press1);

  ui::GestureEvent end1(5, 5, 0, base::TimeTicks(),
                        ui::GestureEventDetails(ui::ET_GESTURE_END));
  details = root_view->OnEventFromSource(&end1);

  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(0, controller.show_context_menu_calls());
  controller.Reset();

  // |parent_view| should show a context menu as a result of a long press on
  // |other_child_view|.
  ui::GestureEvent long_press2(
      25, 5, 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::ET_GESTURE_LONG_PRESS));
  details = root_view->OnEventFromSource(&long_press2);

  ui::GestureEvent end2(25, 5, 0, base::TimeTicks(),
                        ui::GestureEventDetails(ui::ET_GESTURE_END));
  details = root_view->OnEventFromSource(&end2);

  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(1, controller.show_context_menu_calls());
  controller.Reset();

  // |parent_view| should show a context menu as a result of a long press on
  // itself.
  ui::GestureEvent long_press3(
      50, 50, 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::ET_GESTURE_LONG_PRESS));
  details = root_view->OnEventFromSource(&long_press3);

  ui::GestureEvent end3(25, 5, 0, base::TimeTicks(),
                        ui::GestureEventDetails(ui::ET_GESTURE_END));
  details = root_view->OnEventFromSource(&end3);

  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(1, controller.show_context_menu_calls());
}

// Tests that context menus are not shown for disabled views on a long press.
TEST_F(RootViewTest, ContextMenuFromLongPressOnDisabledView) {
  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(100, 100);
  widget.Init(init_params);
  internal::RootView* root_view =
      static_cast<internal::RootView*>(widget.GetRootView());

  // Create a view capable of showing the context menu with two children one of
  // which handles all gesture events (e.g. a button). Also mark this view
  // as disabled.
  TestContextMenuController controller;
  View* parent_view = new View;
  parent_view->set_context_menu_controller(&controller);
  parent_view->SetEnabled(false);
  widget.SetContentsView(parent_view);

  View* gesture_handling_child_view = new GestureHandlingView;
  gesture_handling_child_view->SetBoundsRect(gfx::Rect(10, 10));
  parent_view->AddChildView(gesture_handling_child_view);

  View* other_child_view = new View;
  other_child_view->SetBoundsRect(gfx::Rect(20, 0, 10, 10));
  parent_view->AddChildView(other_child_view);

  // |parent_view| should not show a context menu as a result of a long press on
  // |gesture_handling_child_view|.
  ui::GestureEvent long_press1(
      5, 5, 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::ET_GESTURE_LONG_PRESS));
  ui::EventDispatchDetails details = root_view->OnEventFromSource(&long_press1);

  ui::GestureEvent end1(5, 5, 0, base::TimeTicks(),
                        ui::GestureEventDetails(ui::ET_GESTURE_END));
  details = root_view->OnEventFromSource(&end1);

  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(0, controller.show_context_menu_calls());
  controller.Reset();

  // |parent_view| should not show a context menu as a result of a long press on
  // |other_child_view|.
  ui::GestureEvent long_press2(
      25, 5, 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::ET_GESTURE_LONG_PRESS));
  details = root_view->OnEventFromSource(&long_press2);

  ui::GestureEvent end2(25, 5, 0, base::TimeTicks(),
                        ui::GestureEventDetails(ui::ET_GESTURE_END));
  details = root_view->OnEventFromSource(&end2);

  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(0, controller.show_context_menu_calls());
  controller.Reset();

  // |parent_view| should not show a context menu as a result of a long press on
  // itself.
  ui::GestureEvent long_press3(
      50, 50, 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::ET_GESTURE_LONG_PRESS));
  details = root_view->OnEventFromSource(&long_press3);

  ui::GestureEvent end3(25, 5, 0, base::TimeTicks(),
                        ui::GestureEventDetails(ui::ET_GESTURE_END));
  details = root_view->OnEventFromSource(&end3);

  EXPECT_FALSE(details.target_destroyed);
  EXPECT_FALSE(details.dispatcher_destroyed);
  EXPECT_EQ(0, controller.show_context_menu_calls());
}

namespace {

// View class which destroys itself when it gets an event of type
// |delete_event_type|.
class DeleteViewOnEvent : public View {
 public:
  DeleteViewOnEvent(ui::EventType delete_event_type, bool* was_destroyed)
      : delete_event_type_(delete_event_type), was_destroyed_(was_destroyed) {}

  ~DeleteViewOnEvent() override {
      *was_destroyed_ = true;
  }

  void OnEvent(ui::Event* event) override {
    if (event->type() == delete_event_type_)
      delete this;
  }

 private:
  // The event type which causes the view to destroy itself.
  ui::EventType delete_event_type_;

  // Tracks whether the view was destroyed.
  bool* was_destroyed_;

  DISALLOW_COPY_AND_ASSIGN(DeleteViewOnEvent);
};

}  // namespace

// Verifies deleting a View in OnMouseExited() doesn't crash.
TEST_F(RootViewTest, DeleteViewOnMouseExitDispatch) {
  Widget widget;
  Widget::InitParams init_params =
      CreateParams(Widget::InitParams::TYPE_POPUP);
  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
  widget.Init(init_params);
  widget.SetBounds(gfx::Rect(10, 10, 500, 500));

  View* content = new View;
  widget.SetContentsView(content);

  bool view_destroyed = false;
  View* child = new DeleteViewOnEvent(ui::ET_MOUSE_EXITED, &view_destroyed);
  content->AddChildView(child);
  child->SetBounds(10, 10, 500, 500);

  internal::RootView* root_view =
      static_cast<internal::RootView*>(widget.GetRootView());

  // Generate a mouse move event which ensures that the mouse_moved_handler_
  // member is set in the RootView class.
  ui::MouseEvent moved_event(ui::ET_MOUSE_MOVED, gfx::Point(15, 15),
                             gfx::Point(15, 15), ui::EventTimeForNow(), 0,
                             0);
  root_view->OnMouseMoved(moved_event);
  ASSERT_FALSE(view_destroyed);

  // Generate a mouse exit event which in turn will delete the child view which
  // was the target of the mouse move event above. This should not crash when
  // the mouse exit handler returns from the child.
  ui::MouseEvent exit_event(ui::ET_MOUSE_EXITED, gfx::Point(), gfx::Point(),
                            ui::EventTimeForNow(), 0, 0);
  root_view->OnMouseExited(exit_event);

  EXPECT_TRUE(view_destroyed);
  EXPECT_FALSE(content->has_children());
}

// Verifies deleting a View in OnMouseEntered() doesn't crash.
TEST_F(RootViewTest, DeleteViewOnMouseEnterDispatch) {
  Widget widget;
  Widget::InitParams init_params =
      CreateParams(Widget::InitParams::TYPE_POPUP);
  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
  widget.Init(init_params);
  widget.SetBounds(gfx::Rect(10, 10, 500, 500));

  View* content = new View;
  widget.SetContentsView(content);

  bool view_destroyed = false;
  View* child = new DeleteViewOnEvent(ui::ET_MOUSE_ENTERED, &view_destroyed);
  content->AddChildView(child);

  // Make |child| smaller than the containing Widget and RootView.
  child->SetBounds(100, 100, 100, 100);

  internal::RootView* root_view =
      static_cast<internal::RootView*>(widget.GetRootView());

  // Move the mouse within |widget| but outside of |child|.
  ui::MouseEvent moved_event(ui::ET_MOUSE_MOVED, gfx::Point(15, 15),
                             gfx::Point(15, 15), ui::EventTimeForNow(), 0,
                             0);
  root_view->OnMouseMoved(moved_event);
  ASSERT_FALSE(view_destroyed);

  // Move the mouse within |child|, which should dispatch a mouse enter event to
  // |child| and destroy the view. This should not crash when the mouse enter
  // handler returns from the child.
  ui::MouseEvent moved_event2(ui::ET_MOUSE_MOVED, gfx::Point(115, 115),
                              gfx::Point(115, 115), ui::EventTimeForNow(), 0,
                              0);
  root_view->OnMouseMoved(moved_event2);

  EXPECT_TRUE(view_destroyed);
  EXPECT_FALSE(content->has_children());
}

namespace {

// View class which deletes its owning Widget when it gets a mouse exit event.
class DeleteWidgetOnMouseExit : public View {
 public:
  explicit DeleteWidgetOnMouseExit(Widget* widget)
      : widget_(widget) {
  }

  ~DeleteWidgetOnMouseExit() override {}

  void OnMouseExited(const ui::MouseEvent& event) override {
    delete widget_;
  }

 private:
  Widget* widget_;

  DISALLOW_COPY_AND_ASSIGN(DeleteWidgetOnMouseExit);
};

}  // namespace

// Test that there is no crash if a View deletes its parent Widget in
// View::OnMouseExited().
TEST_F(RootViewTest, DeleteWidgetOnMouseExitDispatch) {
  Widget* widget = new Widget;
  Widget::InitParams init_params =
      CreateParams(Widget::InitParams::TYPE_POPUP);
  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
  widget->Init(init_params);
  widget->SetBounds(gfx::Rect(10, 10, 500, 500));
  WidgetDeletionObserver widget_deletion_observer(widget);

  View* content = new View();
  View* child = new DeleteWidgetOnMouseExit(widget);
  content->AddChildView(child);
  widget->SetContentsView(content);

  // Make |child| smaller than the containing Widget and RootView.
  child->SetBounds(100, 100, 100, 100);

  internal::RootView* root_view =
      static_cast<internal::RootView*>(widget->GetRootView());

  // Move the mouse within |child|.
  ui::MouseEvent moved_event(ui::ET_MOUSE_MOVED, gfx::Point(115, 115),
                             gfx::Point(115, 115), ui::EventTimeForNow(), 0,
                             0);
  root_view->OnMouseMoved(moved_event);
  ASSERT_TRUE(widget_deletion_observer.IsWidgetAlive());

  // Move the mouse outside of |child| which should dispatch a mouse exit event
  // to |child| and destroy the widget. This should not crash when the mouse
  // exit handler returns from the child.
  ui::MouseEvent move_event2(ui::ET_MOUSE_MOVED, gfx::Point(15, 15),
                             gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
  root_view->OnMouseMoved(move_event2);
  EXPECT_FALSE(widget_deletion_observer.IsWidgetAlive());
}

// Test that there is no crash if a View deletes its parent widget as a result
// of a mouse exited event which was propagated from one of its children.
TEST_F(RootViewTest, DeleteWidgetOnMouseExitDispatchFromChild) {
  Widget* widget = new Widget;
  Widget::InitParams init_params =
      CreateParams(Widget::InitParams::TYPE_POPUP);
  init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
  widget->Init(init_params);
  widget->SetBounds(gfx::Rect(10, 10, 500, 500));
  WidgetDeletionObserver widget_deletion_observer(widget);

  View* content = new View();
  View* child = new DeleteWidgetOnMouseExit(widget);
  View* subchild = new View();
  widget->SetContentsView(content);
  content->AddChildView(child);
  child->AddChildView(subchild);

  // Make |child| and |subchild| smaller than the containing Widget and
  // RootView.
  child->SetBounds(100, 100, 100, 100);
  subchild->SetBounds(0, 0, 100, 100);

  // Make mouse enter and exit events get propagated from |subchild| to |child|.
  child->set_notify_enter_exit_on_child(true);

  internal::RootView* root_view =
      static_cast<internal::RootView*>(widget->GetRootView());

  // Move the mouse within |subchild| and |child|.
  ui::MouseEvent moved_event(ui::ET_MOUSE_MOVED, gfx::Point(115, 115),
                             gfx::Point(115, 115), ui::EventTimeForNow(), 0, 0);
  root_view->OnMouseMoved(moved_event);
  ASSERT_TRUE(widget_deletion_observer.IsWidgetAlive());

  // Move the mouse outside of |subchild| and |child| which should dispatch a
  // mouse exit event to |subchild| and destroy the widget. This should not
  // crash when the mouse exit handler returns from |subchild|.
  ui::MouseEvent move_event2(ui::ET_MOUSE_MOVED, gfx::Point(15, 15),
                             gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
  root_view->OnMouseMoved(move_event2);
  EXPECT_FALSE(widget_deletion_observer.IsWidgetAlive());
}

namespace {
class RootViewTestDialogDelegate : public DialogDelegateView {
 public:
  RootViewTestDialogDelegate() {}

  int layout_count() const { return layout_count_; }

  // DialogDelegateView:
  gfx::Size CalculatePreferredSize() const override { return preferred_size_; }
  void Layout() override {
    EXPECT_EQ(size(), preferred_size_);
    ++layout_count_;
  }
  int GetDialogButtons() const override {
    return ui::DIALOG_BUTTON_NONE;  // Ensure buttons do not influence size.
  }

 private:
  const gfx::Size preferred_size_ = gfx::Size(111, 111);

  int layout_count_ = 0;

  DISALLOW_COPY_AND_ASSIGN(RootViewTestDialogDelegate);
};
}  // namespace

// Ensure only one call to Layout() happens during Widget initialization, and
// ensure it happens at the ContentView's preferred size.
TEST_F(RootViewTest, SingleLayoutDuringInit) {
  RootViewTestDialogDelegate* delegate = new RootViewTestDialogDelegate();
  Widget* widget =
      DialogDelegate::CreateDialogWidget(delegate, GetContext(), nullptr);
  EXPECT_EQ(1, delegate->layout_count());
  widget->CloseNow();

  // Also test Aura desktop Widget codepaths.
  test_views_delegate()->set_use_desktop_native_widgets(true);
  delegate = new RootViewTestDialogDelegate();
  widget = DialogDelegate::CreateDialogWidget(delegate, GetContext(), nullptr);
  EXPECT_EQ(1, delegate->layout_count());
  widget->CloseNow();
}

}  // namespace test
}  // namespace views
