diff options
Diffstat (limited to 'chromium/ash/drag_drop/drag_drop_controller.cc')
-rw-r--r-- | chromium/ash/drag_drop/drag_drop_controller.cc | 566 |
1 files changed, 566 insertions, 0 deletions
diff --git a/chromium/ash/drag_drop/drag_drop_controller.cc b/chromium/ash/drag_drop/drag_drop_controller.cc new file mode 100644 index 00000000000..1d5d636347e --- /dev/null +++ b/chromium/ash/drag_drop/drag_drop_controller.cc @@ -0,0 +1,566 @@ +// 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/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/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" + +namespace ash { +namespace internal { + +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 void DidRecreateLayer(ui::Layer* old_layer, + ui::Layer* new_layer) OVERRIDE {} + + 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(aura::Env::GetInstance()->GetDispatcher()); + 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) { + aura::client::DragDropDelegate* delegate = NULL; + int op = ui::DragDropTypes::DRAG_NONE; + 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()); + 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); + 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_ = 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 internal +} // namespace ash |