chromium/components/exo/data_device.cc

// Copyright 2017 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/data_device.h"

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "components/exo/data_device_delegate.h"
#include "components/exo/data_exchange_delegate.h"
#include "components/exo/data_offer.h"
#include "components/exo/data_source.h"
#include "components/exo/seat.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "ui/aura/client/drag_drop_delegate.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_monitor.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/drop_target_event.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"

namespace exo {
namespace {

using ::ui::mojom::DragOperation;

constexpr int kDataDeviceSeatObserverPriority = 0;
static_assert(Seat::IsValidObserverPriority(kDataDeviceSeatObserverPriority),
              "kDataDeviceSeatObserverPriority is not in the valid range.");

constexpr base::TimeDelta kDataOfferDestructionTimeout =
    base::Milliseconds(1000);

DragOperation DndActionToDragOperation(DndAction dnd_action) {
  switch (dnd_action) {
    case DndAction::kMove:
      return DragOperation::kMove;
    case DndAction::kCopy:
      return DragOperation::kCopy;
    case DndAction::kAsk:
      return DragOperation::kLink;
    case DndAction::kNone:
      return DragOperation::kNone;
  }
}

}  // namespace

DataDevice::DataDevice(DataDeviceDelegate* delegate, Seat* seat)
    : delegate_(delegate), seat_(seat), drop_succeeded_(false) {
  ui::ClipboardMonitor::GetInstance()->AddObserver(this);

  seat_->AddObserver(this, kDataDeviceSeatObserverPriority);

  OnSurfaceFocused(seat_->GetFocusedSurface(), nullptr,
                   !!seat_->GetFocusedSurface());
}

DataDevice::~DataDevice() {
  delegate_->OnDataDeviceDestroying(this);

  ui::ClipboardMonitor::GetInstance()->RemoveObserver(this);

  seat_->RemoveObserver(this);
}

void DataDevice::StartDrag(DataSource* source,
                           Surface* origin,
                           Surface* icon,
                           ui::mojom::DragEventSource event_source) {
  seat_->StartDrag(source, origin, icon, event_source);
}

void DataDevice::SetSelection(DataSource* source) {
  seat_->SetSelection(source);
}

void DataDevice::OnDragEntered(const ui::DropTargetEvent& event) {
  DCHECK(!data_offer_);

  Surface* surface = GetEffectiveTargetForEvent(event);
  if (!surface)
    return;

  base::flat_set<DndAction> dnd_actions;
  if (event.source_operations() & ui::DragDropTypes::DRAG_MOVE) {
    dnd_actions.insert(DndAction::kMove);
  }
  if (event.source_operations() & ui::DragDropTypes::DRAG_COPY) {
    dnd_actions.insert(DndAction::kCopy);
  }
  if (event.source_operations() & ui::DragDropTypes::DRAG_LINK) {
    dnd_actions.insert(DndAction::kAsk);
  }

  data_offer_ =
      std::make_unique<ScopedDataOffer>(delegate_->OnDataOffer(), this);
  data_offer_->get()->SetDropData(seat_->data_exchange_delegate(),
                                  surface->window(), event.data());
  data_offer_->get()->SetSourceActions(dnd_actions);
  data_offer_->get()->SetActions(base::flat_set<DndAction>(), DndAction::kAsk);
  delegate_->OnEnter(surface, event.location_f(), *data_offer_->get());
}

aura::client::DragUpdateInfo DataDevice::OnDragUpdated(
    const ui::DropTargetEvent& event) {
  if (!data_offer_)
    return aura::client::DragUpdateInfo();

  ui::EndpointType endpoint_type = ui::EndpointType::kDefault;
  Surface* surface = GetEffectiveTargetForEvent(event);
  if (surface) {
    endpoint_type =
        seat_->data_exchange_delegate()->GetDataTransferEndpointType(
            surface->window());
  }
  aura::client::DragUpdateInfo drag_info(
      ui::DragDropTypes::DRAG_NONE, ui::DataTransferEndpoint(endpoint_type));

  delegate_->OnMotion(event.time_stamp(), event.location_f());

  // TODO(hirono): dnd_action() here may not be updated. Chrome needs to provide
  // a way to update DND action asynchronously.
  drag_info.drag_operation = static_cast<int>(
      DndActionToDragOperation(data_offer_->get()->dnd_action()));
  return drag_info;
}

void DataDevice::OnDragExited() {
  if (!data_offer_)
    return;

  delegate_->OnLeave();
  data_offer_.reset();
}

aura::client::DragDropDelegate::DropCallback DataDevice::GetDropCallback(
    const ui::DropTargetEvent& event) {
  base::ScopedClosureRunner drag_exit(
      base::BindOnce(&DataDevice::OnDragExited, weak_factory_.GetWeakPtr()));
  return base::BindOnce(&DataDevice::PerformDropOrExitDrag,
                        drop_weak_factory_.GetWeakPtr(), std::move(drag_exit));
}

void DataDevice::OnClipboardDataChanged() {
  if (!focused_surface_)
    return;
  SetSelectionToCurrentClipboardData();
}

void DataDevice::OnSurfaceCreated(Surface* surface) {
  if (delegate_->CanAcceptDataEventsForSurface(surface)) {
    aura::client::SetDragDropDelegate(surface->window(), this);
  }
}

void DataDevice::OnSurfaceFocused(Surface* gained_surface,
                                  Surface* lost_focused,
                                  bool has_focused_surface) {
  Surface* next_focused_surface =
      gained_surface && delegate_->CanAcceptDataEventsForSurface(gained_surface)
          ? gained_surface
          : nullptr;
  // Check if focused surface is not changed.
  if ((focused_surface_ && focused_surface_->get() == next_focused_surface) ||
      (!focused_surface_ && !next_focused_surface)) {
    return;
  }

  std::unique_ptr<ScopedSurface> last_focused_surface =
      std::move(focused_surface_);

  focused_surface_ = next_focused_surface ? std::make_unique<ScopedSurface>(
                                                next_focused_surface, this)
                                          : nullptr;
  // Check if the client newly obtained focus.
  if (focused_surface_ && !last_focused_surface)
    SetSelectionToCurrentClipboardData();
}

void DataDevice::OnDataOfferDestroying(DataOffer* data_offer) {
  if (data_offer_ && data_offer_->get() == data_offer) {
    drop_succeeded_ = data_offer_->get()->finished();
    if (quit_closure_)
      std::move(quit_closure_).Run();
    data_offer_.reset();
  }
  drop_weak_factory_.InvalidateWeakPtrs();
}

void DataDevice::OnSurfaceDestroying(Surface* surface) {
  if (focused_surface_ && focused_surface_->get() == surface) {
    DCHECK(surface->window());
    if (surface->window()) {
      aura::client::SetDragDropDelegate(surface->window(), nullptr);
    }
    focused_surface_.reset();
  }
}

Surface* DataDevice::GetEffectiveTargetForEvent(
    const ui::DropTargetEvent& event) const {
  aura::Window* window = static_cast<aura::Window*>(event.target());
  if (!window)
    return nullptr;
  Surface* target = Surface::AsSurface(window);
  if (!target)
    return nullptr;

  return delegate_->CanAcceptDataEventsForSurface(target) ? target : nullptr;
}

void DataDevice::SetSelectionToCurrentClipboardData() {
  DCHECK(focused_surface_);
  DataOffer* data_offer = delegate_->OnDataOffer();
  data_offer->SetClipboardData(
      seat_->data_exchange_delegate(), *ui::Clipboard::GetForCurrentThread(),
      seat_->data_exchange_delegate()->GetDataTransferEndpointType(
          focused_surface_->get()->window()));
  delegate_->OnSelection(*data_offer);
}

void DataDevice::PerformDropOrExitDrag(
    base::ScopedClosureRunner exit_drag,
    std::unique_ptr<ui::OSExchangeData> data,
    ui::mojom::DragOperation& output_drag_op,
    std::unique_ptr<ui::LayerTreeOwner> drag_image_layer_owner) {
  exit_drag.ReplaceClosure(base::DoNothing());

  if (!data_offer_) {
    output_drag_op = DragOperation::kNone;
    return;
  }

  DndAction dnd_action = data_offer_->get()->dnd_action();

  delegate_->OnDrop();

  // TODO(crbug.com/40162278): Avoid using nested loop by adding asynchronous
  // callback to aura::client::DragDropDelegate.
  base::WeakPtr<DataDevice> alive(weak_factory_.GetWeakPtr());
  base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, run_loop.QuitClosure(), kDataOfferDestructionTimeout);
  quit_closure_ = run_loop.QuitClosure();
  run_loop.Run();

  if (!alive) {
    output_drag_op = DragOperation::kNone;
    return;
  }

  if (quit_closure_) {
    // DataOffer not destroyed by the client until the timeout.
    quit_closure_.Reset();
    data_offer_.reset();
    drop_succeeded_ = false;
  }

  if (!drop_succeeded_)
    output_drag_op = DragOperation::kNone;
  else
    output_drag_op = DndActionToDragOperation(dnd_action);
}

}  // namespace exo