blob: 2ff526fe7681f6452321be3ff29e974547d38d28 [file] [log] [blame]
// 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/message_loop.h"
#include "base/run_loop.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/hit_test.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/animation/linear_animation.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"
#include "ui/wm/public/drag_drop_delegate.h"
namespace ash {
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(
0,
0,
0,
ui::EventTimeForNow(),
ui::GestureEventDetails(ui::ET_GESTURE_END, 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(aura::Window* window) OVERRIDE {}
virtual void OnWindowDestroyed(aura::Window* window) 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());
}
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::Window* 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.
ui::GestureRecognizer::Get()->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(), source));
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);
if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH) {
drag_image_->SetTouchDragOperationHintPosition(gfx::Point(
drag_image_offset_.x(),
drag_image_offset_.y() + drag_image_vertical_offset));
}
drag_window_ = NULL;
// Ends cancel animation if it's in progress.
if (cancel_animation_)
cancel_animation_->End();
if (should_block_during_drag_drop_) {
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
base::MessageLoopForUI* loop = base::MessageLoopForUI::current();
base::MessageLoop::ScopedNestableTaskAllower allow_nested(loop);
run_loop.Run();
}
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) {
int op = ui::DragDropTypes::DRAG_NONE;
if (target != drag_window_) {
if (drag_window_) {
aura::client::DragDropDelegate* delegate =
aura::client::GetDragDropDelegate(drag_window_);
if (delegate)
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);
aura::client::DragDropDelegate* delegate =
aura::client::GetDragDropDelegate(drag_window_);
if (delegate) {
ui::DropTargetEvent e(*drag_data_,
event.location(),
event.root_location(),
drag_operation_);
e.set_flags(event.flags());
delegate->OnDragEntered(e);
}
} else {
aura::client::DragDropDelegate* delegate =
aura::client::GetDragDropDelegate(drag_window_);
if (delegate) {
ui::DropTargetEvent e(*drag_data_,
event.location(),
event.root_location(),
drag_operation_);
e.set_flags(event.flags());
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_);
drag_image_->SetTouchDragOperation(op);
}
}
void DragDropController::Drop(aura::Window* target,
const ui::LocatedEvent& event) {
ash::Shell::GetInstance()->cursor_manager()->SetCursor(ui::kCursorPointer);
// 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_);
aura::client::DragDropDelegate* delegate =
aura::client::GetDragDropDelegate(target);
if (delegate) {
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_ = NULL;
if (drag_source_window_ == window)
drag_source_window_ = NULL;
}
////////////////////////////////////////////////////////////////////////////////
// DragDropController, protected:
gfx::LinearAnimation* DragDropController::CreateCancelAnimation(
int duration,
int frame_rate,
gfx::AnimationDelegate* delegate) {
return new gfx::LinearAnimation(duration, frame_rate, delegate);
}
////////////////////////////////////////////////////////////////////////////////
// DragDropController, private:
void DragDropController::AnimationEnded(const gfx::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 gfx::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 gfx::Animation* animation) {
AnimationEnded(animation);
}
void DragDropController::StartCanceledAnimation(int animation_duration_ms) {
DCHECK(drag_image_.get());
drag_image_->SetTouchDragOperationHintOff();
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 ash