// Copyright 2017 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 "components/exo/data_device.h" #include "base/bind.h" #include "base/callback_helpers.h" #include "base/run_loop.h" #include "build/chromeos_buildflags.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" #if BUILDFLAG(IS_CHROMEOS_ASH) #include "chromeos/ui/base/window_properties.h" #include "components/exo/extended_drag_source.h" #endif 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) { WMHelper::GetInstance()->AddDragDropObserver(this); ui::ClipboardMonitor::GetInstance()->AddObserver(this); seat_->AddObserver(this, kDataDeviceSeatObserverPriority); OnSurfaceFocused(seat_->GetFocusedSurface(), nullptr, !!seat_->GetFocusedSurface()); } DataDevice::~DataDevice() { delegate_->OnDataDeviceDestroying(this); WMHelper::GetInstance()->RemoveDragDropObserver(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 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(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::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)); bool prevent_motion_drag_events = false; #if BUILDFLAG(IS_CHROMEOS_ASH) // chromeos::kCanAttachToAnotherWindowKey controls if a drag operation should // trigger swallow/unswallow tab. if (focused_surface_) { // The ExtendedDragSource instance can be null for tests. auto* extended_drag_source = ExtendedDragSource::Get(); bool is_extended_drag_source_active = extended_drag_source && extended_drag_source->IsActive(); prevent_motion_drag_events = is_extended_drag_source_active && !focused_surface_->get()->window()->GetToplevelWindow()->GetProperty( chromeos::kCanAttachToAnotherWindowKey); } #endif if (!prevent_motion_drag_events) 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( DndActionToDragOperation(data_offer_->get()->dnd_action())); return drag_info; } void DataDevice::OnDragExited() { if (!data_offer_) return; delegate_->OnLeave(); data_offer_.reset(); } WMHelper::DragDropObserver::DropCallback DataDevice::GetDropCallback() { 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::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) return; std::unique_ptr last_focused_surface = std::move(focused_surface_); focused_surface_ = next_focused_surface ? std::make_unique( 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) focused_surface_.reset(); } Surface* DataDevice::GetEffectiveTargetForEvent( const ui::DropTargetEvent& event) const { aura::Window* window = static_cast(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, ui::mojom::DragOperation& output_drag_op) { 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/1160925): Avoid using nested loop by adding asynchronous // callback to aura::client::DragDropDelegate. base::WeakPtr alive(weak_factory_.GetWeakPtr()); base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed); base::ThreadTaskRunnerHandle::Get()->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