// 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 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(NULL), static_cast(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 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(drag_drop_tracker_->capture_window()), static_cast(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