text: // 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 "ash/drag_drop/drag_drop_controller.h"
  
  #include "ash/drag_drop/drag_drop_tracker.h"
  #include "ash/drag_drop/drag_image_view.h"
  #include "ash/shell.h"
  #include "ash/wm/coordinate_conversion.h"
  #include "base/bind.h"
  #include "base/message_loop.h"
  #include "base/run_loop.h"
  #include "ui/aura/client/capture_client.h"
  #include "ui/aura/client/drag_drop_delegate.h"
  #include "ui/aura/env.h"
  #include "ui/aura/root_window.h"
  #include "ui/aura/window.h"
  #include "ui/aura/window_delegate.h"
  #include "ui/base/animation/linear_animation.h"
  #include "ui/base/dragdrop/drag_drop_types.h"
  #include "ui/base/dragdrop/os_exchange_data.h"
  #include "ui/base/events/event.h"
  #include "ui/base/events/event_utils.h"
  #include "ui/base/hit_test.h"
  #include "ui/gfx/path.h"
  #include "ui/gfx/point.h"
  #include "ui/gfx/rect.h"
  #include "ui/gfx/rect_conversions.h"
  #include "ui/views/views_delegate.h"
  #include "ui/views/widget/native_widget_aura.h"
  
  namespace ash {
  namespace internal {
  
  using aura::RootWindow;
  
  namespace {
  // The duration of the drag cancel animation in millisecond.
  const int kCancelAnimationDuration = 250;
  const int kTouchCancelAnimationDuration = 20;
  // The frame rate of the drag cancel animation in hertz.
  const int kCancelAnimationFrameRate = 60;
  
  // For touch initiated dragging, we scale and shift drag image by the following:
  static const float kTouchDragImageScale = 1.2f;
  static const int kTouchDragImageVerticalOffset = -25;
  
  // Adjusts the drag image bounds such that the new bounds are scaled by |scale|
  // and translated by the |drag_image_offset| and and additional
  // |vertical_offset|.
  gfx::Rect AdjustDragImageBoundsForScaleAndOffset(
      const gfx::Rect& drag_image_bounds,
      int vertical_offset,
      float scale,
      gfx::Vector2d* drag_image_offset) {
    gfx::PointF final_origin = drag_image_bounds.origin();
    gfx::SizeF final_size = drag_image_bounds.size();
    final_size.Scale(scale);
    drag_image_offset->set_x(drag_image_offset->x() * scale);
    drag_image_offset->set_y(drag_image_offset->y() * scale);
    float total_x_offset = drag_image_offset->x();
    float total_y_offset = drag_image_offset->y() - vertical_offset;
    final_origin.Offset(-total_x_offset, -total_y_offset);
    return gfx::ToEnclosingRect(gfx::RectF(final_origin, final_size));
  }
  
  void DispatchGestureEndToWindow(aura::Window* window) {
    if (window && window->delegate()) {
      ui::GestureEvent gesture_end(
          ui::ET_GESTURE_END,
          0,
          0,
          0,
          ui::EventTimeForNow(),
          ui::GestureEventDetails(ui::ET_GESTURE_END, 0, 0),
          0);
      window->delegate()->OnGestureEvent(&gesture_end);
    }
  }
  }  // namespace
  
  class DragDropTrackerDelegate : public aura::WindowDelegate {
   public:
    explicit DragDropTrackerDelegate(DragDropController* controller)
        : drag_drop_controller_(controller) {}
    virtual ~DragDropTrackerDelegate() {}
  
    // Overridden from WindowDelegate:
    virtual gfx::Size GetMinimumSize() const OVERRIDE {
      return gfx::Size();
    }
  
    virtual gfx::Size GetMaximumSize() const OVERRIDE {
      return gfx::Size();
    }
  
    virtual void OnBoundsChanged(const gfx::Rect& old_bounds,
                                 const gfx::Rect& new_bounds) OVERRIDE {}
    virtual gfx::NativeCursor GetCursor(const gfx::Point& point) OVERRIDE {
      return gfx::kNullCursor;
    }
    virtual int GetNonClientComponent(const gfx::Point& point) const OVERRIDE {
      return HTCAPTION;
    }
    virtual bool ShouldDescendIntoChildForEventHandling(
        aura::Window* child,
        const gfx::Point& location) OVERRIDE {
      return true;
    }
    virtual bool CanFocus() OVERRIDE { return true; }
    virtual void OnCaptureLost() OVERRIDE {
      if (drag_drop_controller_->IsDragDropInProgress())
        drag_drop_controller_->DragCancel();
    }
    virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
    }
    virtual void OnDeviceScaleFactorChanged(float device_scale_factor) OVERRIDE {}
    virtual void OnWindowDestroying() OVERRIDE {}
    virtual void OnWindowDestroyed() OVERRIDE {}
    virtual void OnWindowTargetVisibilityChanged(bool visible) OVERRIDE {}
    virtual bool HasHitTestMask() const OVERRIDE {
      return true;
    }
    virtual void GetHitTestMask(gfx::Path* mask) const OVERRIDE {
      DCHECK(mask->isEmpty());
    }
    virtual scoped_refptr<ui::Texture> CopyTexture() OVERRIDE {
      return scoped_refptr<ui::Texture>();
    }
  
   private:
    DragDropController* drag_drop_controller_;
  
    DISALLOW_COPY_AND_ASSIGN(DragDropTrackerDelegate);
  };
  
  ////////////////////////////////////////////////////////////////////////////////
  // DragDropController, public:
  
  DragDropController::DragDropController()
      : drag_data_(NULL),
        drag_operation_(0),
        drag_window_(NULL),
        drag_source_window_(NULL),
        should_block_during_drag_drop_(true),
        drag_drop_window_delegate_(new DragDropTrackerDelegate(this)),
        current_drag_event_source_(ui::DragDropTypes::DRAG_EVENT_SOURCE_MOUSE),
        weak_factory_(this) {
    Shell::GetInstance()->PrependPreTargetHandler(this);
  }
  
  DragDropController::~DragDropController() {
    Shell::GetInstance()->RemovePreTargetHandler(this);
    Cleanup();
    if (cancel_animation_)
      cancel_animation_->End();
    if (drag_image_)
      drag_image_.reset();
  }
  
  int DragDropController::StartDragAndDrop(
      const ui::OSExchangeData& data,
      aura::RootWindow* root_window,
      aura::Window* source_window,
      const gfx::Point& root_location,
      int operation,
      ui::DragDropTypes::DragEventSource source) {
    if (IsDragDropInProgress())
      return 0;
  
    const ui::OSExchangeData::Provider* provider = &data.provider();
    // We do not support touch drag/drop without a drag image.
    if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH &&
        provider->GetDragImage().size().IsEmpty())
      return 0;
  
    current_drag_event_source_ = source;
    DragDropTracker* tracker =
        new DragDropTracker(root_window, drag_drop_window_delegate_.get());
    if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH) {
      // We need to transfer the current gesture sequence and the GR's touch event
      // queue to the |drag_drop_tracker_|'s capture window so that when it takes
      // capture, it still gets a valid gesture state.
      root_window->gesture_recognizer()->TransferEventsTo(source_window,
          tracker->capture_window());
      // We also send a gesture end to the source window so it can clear state.
      // TODO(varunjain): Remove this whole block when gesture sequence
      // transferring is properly done in the GR (http://crbug.com/160558)
      DispatchGestureEndToWindow(source_window);
    }
    tracker->TakeCapture();
    drag_drop_tracker_.reset(tracker);
    drag_source_window_ = source_window;
    if (drag_source_window_)
      drag_source_window_->AddObserver(this);
    pending_long_tap_.reset();
  
    drag_data_ = &data;
    drag_operation_ = operation;
  
    float drag_image_scale = 1;
    int drag_image_vertical_offset = 0;
    if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH) {
      drag_image_scale = kTouchDragImageScale;
      drag_image_vertical_offset = kTouchDragImageVerticalOffset;
    }
    gfx::Point start_location = root_location;
    ash::wm::ConvertPointToScreen(root_window, &start_location);
    drag_image_final_bounds_for_cancel_animation_ = gfx::Rect(
        start_location - provider->GetDragImageOffset(),
        provider->GetDragImage().size());
    drag_image_.reset(new DragImageView(source_window->GetRootWindow()));
    drag_image_->SetImage(provider->GetDragImage());
    drag_image_offset_ = provider->GetDragImageOffset();
    gfx::Rect drag_image_bounds(start_location, drag_image_->GetPreferredSize());
    drag_image_bounds = AdjustDragImageBoundsForScaleAndOffset(drag_image_bounds,
        drag_image_vertical_offset, drag_image_scale, &drag_image_offset_);
    drag_image_->SetBoundsInScreen(drag_image_bounds);
    drag_image_->SetWidgetVisible(true);
  
    drag_window_ = NULL;
  
    // Ends cancel animation if it's in progress.
    if (cancel_animation_)
      cancel_animation_->End();
  
  #if !defined(OS_MACOSX)
    if (should_block_during_drag_drop_) {
      base::RunLoop run_loop(aura::Env::GetInstance()->GetDispatcher());
      quit_closure_ = run_loop.QuitClosure();
      base::MessageLoopForUI* loop = base::MessageLoopForUI::current();
      base::MessageLoop::ScopedNestableTaskAllower allow_nested(loop);
      run_loop.Run();
    }
  #endif  // !defined(OS_MACOSX)
  
    if (!cancel_animation_.get() || !cancel_animation_->is_animating() ||
        !pending_long_tap_.get()) {
      // If drag cancel animation is running, this cleanup is done when the
      // animation completes.
      if (drag_source_window_)
        drag_source_window_->RemoveObserver(this);
      drag_source_window_ = NULL;
    }
  
    return drag_operation_;
  }
  
  void DragDropController::DragUpdate(aura::Window* target,
                                      const ui::LocatedEvent& event) {
    aura::client::DragDropDelegate* delegate = NULL;
    if (target != drag_window_) {
      if (drag_window_) {
        if ((delegate = aura::client::GetDragDropDelegate(drag_window_)))
          delegate->OnDragExited();
        if (drag_window_ != drag_source_window_)
          drag_window_->RemoveObserver(this);
      }
      drag_window_ = target;
      // We are already an observer of |drag_source_window_| so no need to add.
      if (drag_window_ != drag_source_window_)
        drag_window_->AddObserver(this);
      if ((delegate = aura::client::GetDragDropDelegate(drag_window_))) {
        ui::DropTargetEvent e(*drag_data_,
                              event.location(),
                              event.root_location(),
                              drag_operation_);
        e.set_flags(event.flags());
        delegate->OnDragEntered(e);
      }
    } else {
      if ((delegate = aura::client::GetDragDropDelegate(drag_window_))) {
        ui::DropTargetEvent e(*drag_data_,
                              event.location(),
                              event.root_location(),
                              drag_operation_);
        e.set_flags(event.flags());
        int op = delegate->OnDragUpdated(e);
        gfx::NativeCursor cursor = ui::kCursorNoDrop;
        if (op & ui::DragDropTypes::DRAG_COPY)
          cursor = ui::kCursorCopy;
        else if (op & ui::DragDropTypes::DRAG_LINK)
          cursor = ui::kCursorAlias;
        else if (op & ui::DragDropTypes::DRAG_MOVE)
          cursor = ui::kCursorGrabbing;
        ash::Shell::GetInstance()->cursor_manager()->SetCursor(cursor);
      }
    }
  
    DCHECK(drag_image_.get());
    if (drag_image_->visible()) {
      gfx::Point root_location_in_screen = event.root_location();
      ash::wm::ConvertPointToScreen(target->GetRootWindow(),
                                    &root_location_in_screen);
      drag_image_->SetScreenPosition(
          root_location_in_screen - drag_image_offset_);
    }
  }
  
  void DragDropController::Drop(aura::Window* target,
                                const ui::LocatedEvent& event) {
    ash::Shell::GetInstance()->cursor_manager()->SetCursor(ui::kCursorPointer);
    aura::client::DragDropDelegate* delegate = NULL;
  
    // We must guarantee that a target gets a OnDragEntered before Drop. WebKit
    // depends on not getting a Drop without DragEnter. This behavior is
    // consistent with drag/drop on other platforms.
    if (target != drag_window_)
      DragUpdate(target, event);
    DCHECK(target == drag_window_);
  
    if ((delegate = aura::client::GetDragDropDelegate(target))) {
      ui::DropTargetEvent e(
          *drag_data_, event.location(), event.root_location(), drag_operation_);
      e.set_flags(event.flags());
      drag_operation_ = delegate->OnPerformDrop(e);
      if (drag_operation_ == 0)
        StartCanceledAnimation(kCancelAnimationDuration);
      else
        drag_image_.reset();
    } else {
      drag_image_.reset();
    }
  
    Cleanup();
    if (should_block_during_drag_drop_)
      quit_closure_.Run();
  }
  
  void DragDropController::DragCancel() {
    DoDragCancel(kCancelAnimationDuration);
  }
  
  bool DragDropController::IsDragDropInProgress() {
    return !!drag_drop_tracker_.get();
  }
  
  void DragDropController::OnKeyEvent(ui::KeyEvent* event) {
    if (IsDragDropInProgress() && event->key_code() == ui::VKEY_ESCAPE) {
      DragCancel();
      event->StopPropagation();
    }
  }
  
  void DragDropController::OnMouseEvent(ui::MouseEvent* event) {
    if (!IsDragDropInProgress())
      return;
  
    // If current drag session was not started by mouse, dont process this mouse
    // event, but consume it so it does not interfere with current drag session.
    if (current_drag_event_source_ !=
        ui::DragDropTypes::DRAG_EVENT_SOURCE_MOUSE) {
      event->StopPropagation();
      return;
    }
  
    aura::Window* translated_target = drag_drop_tracker_->GetTarget(*event);
    if (!translated_target) {
      DragCancel();
      event->StopPropagation();
      return;
    }
    scoped_ptr<ui::LocatedEvent> translated_event(
        drag_drop_tracker_->ConvertEvent(translated_target, *event));
    switch (translated_event->type()) {
      case ui::ET_MOUSE_DRAGGED:
        DragUpdate(translated_target, *translated_event.get());
        break;
      case ui::ET_MOUSE_RELEASED:
        Drop(translated_target, *translated_event.get());
        break;
      default:
        // We could also reach here because RootWindow may sometimes generate a
        // bunch of fake mouse events
        // (aura::RootWindow::PostMouseMoveEventAfterWindowChange).
        break;
    }
    event->StopPropagation();
  }
  
  void DragDropController::OnTouchEvent(ui::TouchEvent* event) {
    if (!IsDragDropInProgress())
      return;
  
    // If current drag session was not started by touch, dont process this touch
    // event, but consume it so it does not interfere with current drag session.
    if (current_drag_event_source_ != ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH)
      event->StopPropagation();
  
    if (event->handled())
      return;
  
    if (event->type() == ui::ET_TOUCH_CANCELLED)
      DragCancel();
  }
  
  void DragDropController::OnGestureEvent(ui::GestureEvent* event) {
    if (!IsDragDropInProgress())
      return;
  
    // No one else should handle gesture events when in drag drop. Note that it is
    // not enough to just set ER_HANDLED because the dispatcher only stops
    // dispatching when the event has ER_CONSUMED. If we just set ER_HANDLED, the
    // event will still be dispatched to other handlers and we depend on
    // individual handlers' kindness to not touch events marked ER_HANDLED (not
    // all handlers are so kind and may cause bugs like crbug.com/236493).
    event->StopPropagation();
  
    // If current drag session was not started by touch, dont process this event.
    if (current_drag_event_source_ != ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH)
      return;
  
    // Apply kTouchDragImageVerticalOffset to the location.
    ui::GestureEvent touch_offset_event(*event,
                                        static_cast<aura::Window*>(NULL),
                                        static_cast<aura::Window*>(NULL));
    gfx::Point touch_offset_location = touch_offset_event.location();
    gfx::Point touch_offset_root_location = touch_offset_event.root_location();
    touch_offset_location.Offset(0, kTouchDragImageVerticalOffset);
    touch_offset_root_location.Offset(0, kTouchDragImageVerticalOffset);
    touch_offset_event.set_location(touch_offset_location);
    touch_offset_event.set_root_location(touch_offset_root_location);
  
    aura::Window* translated_target =
        drag_drop_tracker_->GetTarget(touch_offset_event);
    if (!translated_target) {
      DragCancel();
      event->SetHandled();
      return;
    }
    scoped_ptr<ui::LocatedEvent> translated_event(
        drag_drop_tracker_->ConvertEvent(translated_target, touch_offset_event));
  
    switch (event->type()) {
      case ui::ET_GESTURE_SCROLL_UPDATE:
        DragUpdate(translated_target, *translated_event.get());
        break;
      case ui::ET_GESTURE_SCROLL_END:
        Drop(translated_target, *translated_event.get());
        break;
      case ui::ET_SCROLL_FLING_START:
      case ui::ET_GESTURE_LONG_TAP:
        // Ideally we would want to just forward this long tap event to the
        // |drag_source_window_|. However, webkit does not accept events while a
        // drag drop is still in progress. The drag drop ends only when the nested
        // message loop ends. Due to this stupidity, we have to defer forwarding
        // the long tap.
        pending_long_tap_.reset(
            new ui::GestureEvent(*event,
                static_cast<aura::Window*>(drag_drop_tracker_->capture_window()),
                static_cast<aura::Window*>(drag_source_window_)));
        DoDragCancel(kTouchCancelAnimationDuration);
        break;
      default:
        break;
    }
    event->SetHandled();
  }
  
  void DragDropController::OnWindowDestroyed(aura::Window* window) {
    if (drag_window_ == window) {
      drag_window_->RemoveObserver(this);
      drag_window_ = NULL;
    }
    if (drag_source_window_ == window) {
      drag_source_window_->RemoveObserver(this);
      drag_source_window_ = NULL;
    }
  }
  
  ////////////////////////////////////////////////////////////////////////////////
  // DragDropController, protected:
  
  ui::LinearAnimation* DragDropController::CreateCancelAnimation(
      int duration,
      int frame_rate,
      ui::AnimationDelegate* delegate) {
    return new ui::LinearAnimation(duration, frame_rate, delegate);
  }
  
  ////////////////////////////////////////////////////////////////////////////////
  // DragDropController, private:
  
  void DragDropController::AnimationEnded(const ui::Animation* animation) {
    cancel_animation_.reset();
  
    // By the time we finish animation, another drag/drop session may have
    // started. We do not want to destroy the drag image in that case.
    if (!IsDragDropInProgress())
      drag_image_.reset();
    if (pending_long_tap_) {
      // If not in a nested message loop, we can forward the long tap right now.
      if (!should_block_during_drag_drop_)
        ForwardPendingLongTap();
      else {
        // See comment about this in OnGestureEvent().
        base::MessageLoopForUI::current()->PostTask(
            FROM_HERE,
            base::Bind(&DragDropController::ForwardPendingLongTap,
                       weak_factory_.GetWeakPtr()));
      }
    }
  }
  
  void DragDropController::DoDragCancel(int drag_cancel_animation_duration_ms) {
    ash::Shell::GetInstance()->cursor_manager()->SetCursor(ui::kCursorPointer);
  
    // |drag_window_| can be NULL if we have just started the drag and have not
    // received any DragUpdates, or, if the |drag_window_| gets destroyed during
    // a drag/drop.
    aura::client::DragDropDelegate* delegate = drag_window_?
        aura::client::GetDragDropDelegate(drag_window_) : NULL;
    if (delegate)
      delegate->OnDragExited();
  
    Cleanup();
    drag_operation_ = 0;
    StartCanceledAnimation(drag_cancel_animation_duration_ms);
    if (should_block_during_drag_drop_)
      quit_closure_.Run();
  }
  
  void DragDropController::AnimationProgressed(const ui::Animation* animation) {
    gfx::Rect current_bounds = animation->CurrentValueBetween(
        drag_image_initial_bounds_for_cancel_animation_,
        drag_image_final_bounds_for_cancel_animation_);
    drag_image_->SetBoundsInScreen(current_bounds);
  }
  
  void DragDropController::AnimationCanceled(const ui::Animation* animation) {
    AnimationEnded(animation);
  }
  
  void DragDropController::StartCanceledAnimation(int animation_duration_ms) {
    DCHECK(drag_image_.get());
    drag_image_initial_bounds_for_cancel_animation_ =
        drag_image_->GetBoundsInScreen();
    cancel_animation_.reset(CreateCancelAnimation(animation_duration_ms,
                                                  kCancelAnimationFrameRate,
                                                  this));
    cancel_animation_->Start();
  }
  
  void DragDropController::ForwardPendingLongTap() {
    if (drag_source_window_ && drag_source_window_->delegate()) {
      drag_source_window_->delegate()->OnGestureEvent(pending_long_tap_.get());
      DispatchGestureEndToWindow(drag_source_window_);
    }
    pending_long_tap_.reset();
    if (drag_source_window_)
      drag_source_window_->RemoveObserver(this);
    drag_source_window_ = NULL;
  }
  
  void DragDropController::Cleanup() {
    if (drag_window_)
      drag_window_->RemoveObserver(this);
    drag_window_ = NULL;
    drag_data_ = NULL;
    // Cleanup can be called again while deleting DragDropTracker, so use Pass
    // instead of reset to avoid double free.
    drag_drop_tracker_.Pass();
  }
  
  }  // namespace internal
  }  // namespace ash
  