// Copyright 2020 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "components/exo/extended_drag_source.h" #include #include #include "ash/public/cpp/window_properties.h" #include "ash/shell.h" #include "ash/wm/toplevel_window_event_handler.h" #include "base/check.h" #include "base/check_op.h" #include "base/logging.h" #include "base/notreached.h" #include "components/exo/data_source.h" #include "components/exo/surface.h" #include "components/exo/surface_observer.h" #include "components/exo/wm_helper.h" #include "third_party/abseil-cpp/absl/types/optional.h" #include "ui/aura/client/aura_constants.h" #include "ui/aura/client/screen_position_client.h" #include "ui/aura/window.h" #include "ui/aura/window_delegate.h" #include "ui/aura/window_event_dispatcher.h" #include "ui/aura/window_observer.h" #include "ui/base/dragdrop/drag_drop_types.h" #include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h" #include "ui/base/hit_test.h" #include "ui/events/event.h" #include "ui/events/event_target.h" #include "ui/events/event_utils.h" #include "ui/events/types/event_type.h" #include "ui/gfx/geometry/point.h" #include "ui/gfx/geometry/point_conversions.h" #include "ui/gfx/geometry/point_f.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/wm/core/coordinate_conversion.h" #include "ui/wm/public/window_move_client.h" namespace exo { using ::ui::mojom::DragOperation; // static ExtendedDragSource* ExtendedDragSource::instance_ = nullptr; // Internal representation of a toplevel window, backed by an Exo shell surface, // which is being dragged. It supports both already mapped/visible windows as // well as newly created ones (i.e: not added to a root window yet), in which // case OnDraggedWindowVisibilityChanged callback is called to notify when it // has just got visible. class ExtendedDragSource::DraggedWindowHolder : public aura::WindowObserver, public SurfaceObserver { public: DraggedWindowHolder(Surface* surface, const gfx::Vector2d& drag_offset, ExtendedDragSource* source) : surface_(surface), drag_offset_(drag_offset), source_(source) { DCHECK(surface_); DCHECK(surface_->window()); surface_->AddSurfaceObserver(this); if (!FindToplevelWindow()) { DVLOG(1) << "Dragged window not added to root window yet."; surface_->window()->AddObserver(this); } } DraggedWindowHolder(const DraggedWindowHolder&) = delete; DraggedWindowHolder& operator=(const DraggedWindowHolder&) = delete; ~DraggedWindowHolder() override { if (toplevel_window_) { toplevel_window_->RemoveObserver(this); toplevel_window_ = nullptr; } else if (surface_) { surface_->window()->RemoveObserver(this); } if (surface_) surface_->RemoveSurfaceObserver(this); } aura::Window* toplevel_window() { return toplevel_window_; } const gfx::Vector2d& offset() const { return drag_offset_; } private: // aura::WindowObserver: void OnWindowAddedToRootWindow(aura::Window* window) override { if (toplevel_window_) return; DCHECK_EQ(window, surface_->window()); FindToplevelWindow(); DCHECK(toplevel_window_); surface_->window()->RemoveObserver(this); } void OnWindowVisibilityChanging(aura::Window* window, bool visible) override { DCHECK(window); if (window == toplevel_window_) source_->OnDraggedWindowVisibilityChanging(visible); } void OnWindowVisibilityChanged(aura::Window* window, bool visible) override { DCHECK(window); if (window == toplevel_window_) source_->OnDraggedWindowVisibilityChanged(visible); } void OnWindowDestroying(aura::Window* window) override { DCHECK(window); if (window == toplevel_window_) { toplevel_window_->RemoveObserver(this); toplevel_window_ = nullptr; } } // SurfaceObserver: void OnSurfaceDestroying(Surface* surface) override { if (surface_ == surface) { surface_->RemoveSurfaceObserver(this); surface_ = nullptr; } } bool FindToplevelWindow() { if (!surface_->window()->GetRootWindow()) return false; toplevel_window_ = surface_->window()->GetToplevelWindow(); toplevel_window_->AddObserver(this); // Disable visibility change animations on the dragged window. toplevel_window_->SetProperty(aura::client::kAnimationsDisabledKey, true); return true; } Surface* surface_; gfx::Vector2d drag_offset_; ExtendedDragSource* const source_; aura::Window* toplevel_window_ = nullptr; }; // static ExtendedDragSource* ExtendedDragSource::Get() { return instance_; } ExtendedDragSource::ExtendedDragSource(DataSource* source, Delegate* delegate) : source_(source), delegate_(delegate) { DCHECK(source_); DCHECK(delegate_); source_->AddObserver(this); DCHECK(!instance_); instance_ = this; } ExtendedDragSource::~ExtendedDragSource() { delegate_->OnDataSourceDestroying(); for (auto& observer : observers_) observer.OnExtendedDragSourceDestroying(this); if (source_) source_->RemoveObserver(this); if (drag_source_window_) drag_source_window_->RemoveObserver(this); DCHECK_EQ(instance_, this); instance_ = nullptr; } void ExtendedDragSource::AddObserver(Observer* observer) { DCHECK(observer); observers_.AddObserver(observer); } void ExtendedDragSource::RemoveObserver(Observer* observer) { DCHECK(observer); observers_.RemoveObserver(observer); } void ExtendedDragSource::Drag(Surface* dragged_surface, const gfx::Vector2d& drag_offset) { // Associated data source already destroyed. if (!source_) return; if (!dragged_surface) { DVLOG(1) << "Unsetting dragged surface."; dragged_window_holder_.reset(); return; } DVLOG(1) << "Dragged surface changed:" << " surface=" << dragged_surface << " offset=" << drag_offset.ToString(); // Ensure that the surface already has a "role" assigned. DCHECK(dragged_surface->HasSurfaceDelegate()); dragged_window_holder_ = std::make_unique(dragged_surface, drag_offset, this); // Drag process will be started once OnToplevelWindowDragStarted() gets // called, unless it is called (raced) prior to this one. In this case, we // trigger the dragging here. // // TODO(https://crrev.com/1360884: Unify where tab-dragging is initiated. if (pending_drag_start_ && dragged_window_holder_->toplevel_window() == drag_source_window_) { StartDrag(dragged_window_holder_->toplevel_window()); pending_drag_start_ = false; } } bool ExtendedDragSource::IsActive() const { return !!source_; } void ExtendedDragSource::OnToplevelWindowDragStarted( const gfx::PointF& start_location, ui::mojom::DragEventSource source, aura::Window* drag_source_window) { pointer_location_ = start_location; drag_event_source_ = source; drag_source_window_ = drag_source_window ? drag_source_window->GetToplevelWindow() : nullptr; if (drag_source_window_) drag_source_window_->AddObserver(this); MaybeLockCursor(); if (dragged_window_holder_ && dragged_window_holder_->toplevel_window() && dragged_window_holder_->toplevel_window()->IsVisible()) { StartDrag(dragged_window_holder_->toplevel_window()); } else { pending_drag_start_ = true; } } DragOperation ExtendedDragSource::OnToplevelWindowDragDropped() { DVLOG(1) << "OnDragDropped()"; Cleanup(); return delegate_->ShouldAllowDropAnywhere() ? DragOperation::kMove : DragOperation::kNone; } void ExtendedDragSource::OnToplevelWindowDragCancelled() { DVLOG(1) << "OnDragCancelled()"; auto* handler = ash::Shell::Get()->toplevel_window_event_handler(); handler->RevertDrag(); Cleanup(); } void ExtendedDragSource::OnToplevelWindowDragEvent(ui::LocatedEvent* event) { DCHECK(event); aura::Window* target = static_cast(event->target()); pointer_location_ = event->root_location_f(); wm::ConvertPointToScreen(target->GetRootWindow(), &pointer_location_); if (!dragged_window_holder_) return; auto* handler = ash::Shell::Get()->toplevel_window_event_handler(); if (event->IsMouseEvent()) { handler->OnMouseEvent(event->AsMouseEvent()); return; } if (event->IsGestureEvent()) { handler->OnGestureEvent(event->AsGestureEvent()); return; } NOTREACHED() << "Only mouse and touch events are supported."; } void ExtendedDragSource::OnDataSourceDestroying(DataSource* source) { DCHECK_EQ(source, source_); source_->RemoveObserver(this); source_ = nullptr; } void ExtendedDragSource::OnWindowDestroyed(aura::Window* window) { if (drag_source_window_ == window) drag_source_window_ = nullptr; } void ExtendedDragSource::MaybeLockCursor() { if (delegate_->ShouldLockCursor()) { ash::Shell::Get()->cursor_manager()->LockCursor(); cursor_locked_ = true; } } void ExtendedDragSource::UnlockCursor() { if (cursor_locked_) { ash::Shell::Get()->cursor_manager()->UnlockCursor(); cursor_locked_ = false; } } void ExtendedDragSource::StartDrag(aura::Window* toplevel) { DCHECK(!event_blocker_); // Ensure |toplevel| window does skip events while it's being dragged. event_blocker_ = std::make_unique(toplevel); DVLOG(1) << "Starting drag. pointer_loc=" << pointer_location_.ToString(); auto* toplevel_handler = ash::Shell::Get()->toplevel_window_event_handler(); auto move_source = drag_event_source_ == ui::mojom::DragEventSource::kTouch ? ::wm::WINDOW_MOVE_SOURCE_TOUCH : ::wm::WINDOW_MOVE_SOURCE_MOUSE; auto end_closure = base::BindOnce( [](base::WeakPtr self, ash::ToplevelWindowEventHandler::DragResult result) { if (!self) return; if (auto* window_holder = self->dragged_window_holder_.get()) { if (auto* toplevel = window_holder->toplevel_window()) { toplevel->ClearProperty(ash::kIsDraggingTabsKey); toplevel->ClearProperty(ash::kTabDraggingSourceWindowKey); } } self->dragged_window_holder_.reset(); self->event_blocker_.reset(); }, weak_factory_.GetWeakPtr()); // TODO(crbug.com/1167581): Experiment setting |update_gesture_target| back // to true when capture is removed from drag and drop. gfx::PointF pointer_location_in_parent(pointer_location_); wm::ConvertPointFromScreen(toplevel->parent(), &pointer_location_in_parent); toplevel_handler->AttemptToStartDrag( toplevel, pointer_location_in_parent, HTCAPTION, move_source, std::move(end_closure), /*update_gesture_target=*/false, /*grab_capture =*/ drag_event_source_ != ui::mojom::DragEventSource::kTouch); } void ExtendedDragSource::OnDraggedWindowVisibilityChanging(bool visible) { DCHECK(dragged_window_holder_); DVLOG(1) << "Dragged window visibility changing. visible=" << visible; if (!visible) { dragged_window_holder_.reset(); return; } aura::Window* toplevel = dragged_window_holder_->toplevel_window(); DCHECK(toplevel); toplevel->SetProperty(ash::kIsDraggingTabsKey, true); if (drag_source_window_ && drag_source_window_ != toplevel) { toplevel->SetProperty(ash::kTabDraggingSourceWindowKey, drag_source_window_); } } void ExtendedDragSource::OnDraggedWindowVisibilityChanged(bool visible) { DCHECK(dragged_window_holder_); DVLOG(1) << "Dragged window visibility changed. visible=" << visible; if (!visible) { dragged_window_holder_.reset(); return; } aura::Window* toplevel = dragged_window_holder_->toplevel_window(); DCHECK(toplevel); // The |toplevel| window for the dragged surface has just been created and // it's about to be mapped. Calculate and set its position based on // |drag_offset_| and |pointer_location_| before starting the actual drag. auto screen_location = gfx::ToFlooredPoint(pointer_location_ - dragged_window_holder_->offset()); auto toplevel_bounds = gfx::Rect({screen_location, toplevel->bounds().size()}); auto display = display::Screen::GetScreen()->GetDisplayNearestWindow( drag_source_window_ ? drag_source_window_ : toplevel); toplevel->SetBoundsInScreen(toplevel_bounds, display); if (WMHelper::GetInstance()->InTabletMode()) { // The bounds that is stored in ash::kRestoreBoundsOverrideKey will be used // by DragDetails to calculate the detached window bounds during dragging // when detaching in tablet mode to ensure the detached window is correctly // placed under the pointer/finger. toplevel->SetProperty(ash::kRestoreBoundsOverrideKey, toplevel_bounds); } DVLOG(1) << "Dragged window mapped. toplevel=" << toplevel << " origin=" << screen_location.ToString(); StartDrag(toplevel); } void ExtendedDragSource::Cleanup() { if (dragged_window_holder_ && dragged_window_holder_->toplevel_window()) { dragged_window_holder_->toplevel_window()->ClearProperty( aura::client::kAnimationsDisabledKey); } event_blocker_.reset(); if (drag_source_window_) drag_source_window_->RemoveObserver(this); drag_source_window_ = nullptr; pending_drag_start_ = false; UnlockCursor(); } aura::Window* ExtendedDragSource::GetDraggedWindowForTesting() { return dragged_window_holder_ ? dragged_window_holder_->toplevel_window() : nullptr; } absl::optional ExtendedDragSource::GetDragOffsetForTesting() const { return dragged_window_holder_ ? absl::optional(dragged_window_holder_->offset()) : absl::nullopt; } aura::Window* ExtendedDragSource::GetDragSourceWindowForTesting() { return drag_source_window_; } } // namespace exo