chromium/components/exo/extended_drag_source.cc

// 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 <memory>
#include <optional>
#include <string>

#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/memory/raw_ptr.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 "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);
      if (surface_->window()->HasObserver(this)) {
        surface_->window()->RemoveObserver(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;
  }

  raw_ptr<Surface> surface_;
  gfx::Vector2d drag_offset_;
  const raw_ptr<ExtendedDragSource> source_;
  raw_ptr<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<DraggedWindowHolder>(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) {
  if (!dragged_window_holder_)
    return;

  // The pointer location must be translated into screen coordinates.
  CHECK(event);
  aura::Window* target = static_cast<aura::Window*>(event->target());
  pointer_location_ = event->root_location_f();
  wm::ConvertPointToScreen(target->GetRootWindow(), &pointer_location_);

  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_IN_MIGRATION() << "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<aura::ScopedWindowEventTargetingBlocker>(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<ExtendedDragSource> 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);
          }
        }
        self->dragged_window_holder_.reset();
        self->event_blocker_.reset();
      },
      weak_factory_.GetWeakPtr());

  // TODO(crbug.com/40164668): 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);
}

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(toplevel);
  toplevel->SetBoundsInScreen(toplevel_bounds, display);

  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;
}

std::optional<gfx::Vector2d> ExtendedDragSource::GetDragOffsetForTesting()
    const {
  return dragged_window_holder_
             ? std::optional<gfx::Vector2d>(dragged_window_holder_->offset())
             : std::nullopt;
}

aura::Window* ExtendedDragSource::GetDragSourceWindowForTesting() {
  return drag_source_window_;
}

}  // namespace exo