// Copyright 2019 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/drag_drop_operation.h" #include "base/barrier_closure.h" #include "base/check.h" #include "base/pickle.h" #include "base/strings/string_split.h" #include "base/threading/sequenced_task_runner_handle.h" #include "build/chromeos_buildflags.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_base.h" #include "components/exo/shell_surface_util.h" #include "components/exo/surface.h" #include "components/exo/surface_tree_host.h" #include "components/viz/common/frame_sinks/copy_output_request.h" #include "components/viz/common/frame_sinks/copy_output_result.h" #include "ui/aura/client/drag_drop_client.h" #include "ui/aura/window_tracker.h" #include "ui/base/clipboard/file_info.h" #include "ui/base/data_transfer_policy/data_transfer_endpoint.h" #include "ui/base/dragdrop/drag_drop_types.h" #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #include "ui/base/dragdrop/os_exchange_data.h" #include "ui/compositor/layer.h" #include "ui/display/display.h" #include "ui/display/screen.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/transform_util.h" #include "ui/gfx/geometry/vector2d.h" #include "url/gurl.h" #if BUILDFLAG(IS_CHROMEOS_ASH) #include "ash/drag_drop/drag_drop_controller.h" #include "chromeos/ui/base/window_properties.h" #include "components/exo/extended_drag_source.h" #include "ui/base/data_transfer_policy/data_transfer_endpoint_serializer.h" #endif // BUILDFLAG(IS_CHROMEOS_ASH) namespace exo { namespace { using ::ui::mojom::DragOperation; uint32_t DndActionsToDragOperations(const base::flat_set& actions) { uint32_t dnd_operations = 0; for (const DndAction action : actions) { switch (action) { case DndAction::kNone: [[fallthrough]]; // We don't support the ask action case DndAction::kAsk: break; case DndAction::kCopy: dnd_operations |= ui::DragDropTypes::DragOperation::DRAG_COPY; break; case DndAction::kMove: dnd_operations |= ui::DragDropTypes::DragOperation::DRAG_MOVE; break; } } return dnd_operations; } #if BUILDFLAG(IS_CHROMEOS_ASH) DndAction DragOperationsToPreferredDndAction(int op) { if (op & ui::DragDropTypes::DragOperation::DRAG_COPY) return DndAction::kCopy; if (op & ui::DragDropTypes::DragOperation::DRAG_MOVE) return DndAction::kMove; return DndAction::kNone; } #endif DndAction DragOperationToDndAction(DragOperation op) { switch (op) { case DragOperation::kNone: return DndAction::kNone; case DragOperation::kMove: return DndAction::kMove; case DragOperation::kCopy: return DndAction::kCopy; case DragOperation::kLink: return DndAction::kAsk; default: NOTREACHED() << op; return DndAction::kNone; } NOTREACHED(); } } // namespace // Internal representation of a drag icon surface. Used when a non-null surface // is passed in wl_data_device::start_drag requests. // TODO(crbug.com/1119385): Rework icon implementation to avoid frame copies. class DragDropOperation::IconSurface final : public SurfaceTreeHost, public ScopedSurface { public: IconSurface(Surface* icon, DragDropOperation* operation) : SurfaceTreeHost("ExoDragIcon"), ScopedSurface(icon, operation), operation_(operation) { DCHECK(operation_); DCHECK(!icon->HasSurfaceDelegate()); Surface* origin_surface = operation_->origin_->get(); origin_surface->window()->AddChild(host_window()); SetRootSurface(icon); } IconSurface(const IconSurface&) = delete; IconSurface& operator=(const IconSurface&) = delete; ~IconSurface() override = default; private: // SurfaceTreeHost: void OnSurfaceCommit() override { SurfaceTreeHost::OnSurfaceCommit(); RequestCaptureIcon(); } void RequestCaptureIcon() { SubmitCompositorFrame(); std::unique_ptr request = std::make_unique( viz::CopyOutputRequest::ResultFormat::RGBA, viz::CopyOutputRequest::ResultDestination::kSystemMemory, base::BindOnce(&IconSurface::OnCaptured, weak_ptr_factory_.GetWeakPtr())); request->set_result_task_runner(base::SequencedTaskRunnerHandle::Get()); host_window()->layer()->RequestCopyOfOutput(std::move(request)); } void OnCaptured(std::unique_ptr icon_result) { // An empty response means the request was deleted before it was completed. // If this happens, and no operation has yet finished, restart the capture. if (icon_result->IsEmpty()) { RequestCaptureIcon(); return; } auto scoped_bitmap = icon_result->ScopedAccessSkBitmap(); operation_->OnDragIconCaptured(scoped_bitmap.GetOutScopedBitmap()); } DragDropOperation* const operation_; base::WeakPtrFactory weak_ptr_factory_{this}; }; base::WeakPtr DragDropOperation::Create( DataExchangeDelegate* data_exchange_delegate, DataSource* source, Surface* origin, Surface* icon, const gfx::PointF& drag_start_point, ui::mojom::DragEventSource event_source) { auto* dnd_op = new DragDropOperation(data_exchange_delegate, source, origin, icon, drag_start_point, event_source); return dnd_op->weak_ptr_factory_.GetWeakPtr(); } DragDropOperation::DragDropOperation( DataExchangeDelegate* data_exchange_delegate, DataSource* source, Surface* origin, Surface* icon, const gfx::PointF& drag_start_point, ui::mojom::DragEventSource event_source) : source_(std::make_unique(source, this)), origin_(std::make_unique(origin, this)), drag_start_point_(drag_start_point), os_exchange_data_(std::make_unique()), event_source_(event_source) { aura::Window* root_window = origin_->get()->window()->GetRootWindow(); DCHECK(root_window); #if BUILDFLAG(IS_CHROMEOS_ASH) drag_drop_controller_ = static_cast( aura::client::GetDragDropClient(root_window)); #else drag_drop_controller_ = aura::client::GetDragDropClient(root_window); #endif DCHECK(drag_drop_controller_); if (drag_drop_controller_->IsDragDropInProgress()) drag_drop_controller_->DragCancel(); drag_drop_controller_->AddObserver(this); ui::EndpointType endpoint_type = data_exchange_delegate->GetDataTransferEndpointType( origin_->get()->window()); os_exchange_data_->SetSource( std::make_unique(endpoint_type)); #if BUILDFLAG(IS_CHROMEOS_ASH) extended_drag_source_ = ExtendedDragSource::Get(); if (extended_drag_source_) { drag_drop_controller_->set_toplevel_window_drag_delegate( extended_drag_source_); extended_drag_source_->AddObserver(this); } #endif int num_additional_callbacks = 0; #if BUILDFLAG(IS_CHROMEOS_ASH) // TODO(crbug.com/1298033): Move DTE retrieval into // DataSource::GetDataForPreferredMimeTypes() // Lacros sends additional metadata, in a custom MIME type, to sync drag // source metadata. Hence, the number of callbacks is incremented by one. if (endpoint_type == ui::EndpointType::kLacros) ++num_additional_callbacks; #endif // When the icon is present, we increment the number of callbacks so we can // wait for the icon to be captured as well. if (icon) { icon_ = std::make_unique(icon, this); ++num_additional_callbacks; } auto start_op_callback = base::BindOnce(&DragDropOperation::ScheduleStartDragDropOperation, weak_ptr_factory_.GetWeakPtr()); counter_ = base::BarrierClosure(DataSource::kMaxDataTypes + num_additional_callbacks, std::move(start_op_callback)); #if BUILDFLAG(IS_CHROMEOS_ASH) // TODO(crbug.com/1298033): Move DTE retrieval into // DataSource::GetDataForPreferredMimeTypes() if (endpoint_type == ui::EndpointType::kLacros) { source->ReadDataTransferEndpoint( base::BindOnce(&DragDropOperation::OnDataTransferEndpointRead, weak_ptr_factory_.GetWeakPtr()), counter_); } #endif source->GetDataForPreferredMimeTypes( base::BindOnce(&DragDropOperation::OnTextRead, weak_ptr_factory_.GetWeakPtr()), DataSource::ReadDataCallback(), base::BindOnce(&DragDropOperation::OnHTMLRead, weak_ptr_factory_.GetWeakPtr()), DataSource::ReadDataCallback(), base::BindOnce(&DragDropOperation::OnFilenamesRead, weak_ptr_factory_.GetWeakPtr(), data_exchange_delegate, origin->window()), base::BindOnce(&DragDropOperation::OnFileContentsRead, weak_ptr_factory_.GetWeakPtr()), base::BindOnce(&DragDropOperation::OnWebCustomDataRead, weak_ptr_factory_.GetWeakPtr()), counter_); } DragDropOperation::~DragDropOperation() { drag_drop_controller_->RemoveObserver(this); if (source_) source_->get()->Cancelled(); if (drag_drop_controller_->IsDragDropInProgress() && started_by_this_object_) drag_drop_controller_->DragCancel(); #if BUILDFLAG(IS_CHROMEOS_ASH) if (extended_drag_source_) ResetExtendedDragSource(); #endif } void DragDropOperation::AbortIfPending() { if (!started_by_this_object_) delete this; } #if BUILDFLAG(IS_CHROMEOS_ASH) void DragDropOperation::OnDataTransferEndpointRead(const std::string& mime_type, std::u16string data) { DCHECK(os_exchange_data_); std::string utf8_json = base::UTF16ToUTF8(data); auto drag_source_dte = ui::ConvertJsonToDataTransferEndpoint(utf8_json); os_exchange_data_->SetSource(std::move(drag_source_dte)); counter_.Run(); } #endif void DragDropOperation::OnTextRead(const std::string& mime_type, std::u16string data) { DCHECK(os_exchange_data_); os_exchange_data_->SetString(std::move(data)); // Prefer to use the HTML MIME type if possible if (mime_type_.empty()) mime_type_ = mime_type; counter_.Run(); } void DragDropOperation::OnHTMLRead(const std::string& mime_type, std::u16string data) { DCHECK(os_exchange_data_); os_exchange_data_->SetHtml(std::move(data), GURL()); mime_type_ = mime_type; counter_.Run(); } void DragDropOperation::OnFilenamesRead( DataExchangeDelegate* data_exchange_delegate, aura::Window* source, const std::string& mime_type, const std::vector& data) { DCHECK(os_exchange_data_); os_exchange_data_->SetFilenames(data_exchange_delegate->GetFilenames( data_exchange_delegate->GetDataTransferEndpointType(source), data)); mime_type_ = mime_type; counter_.Run(); } void DragDropOperation::OnFileContentsRead(const std::string& mime_type, const base::FilePath& filename, const std::vector& data) { DCHECK(os_exchange_data_); os_exchange_data_->SetFileContents(filename, std::string(data.begin(), data.end())); mime_type_ = mime_type; counter_.Run(); } void DragDropOperation::OnWebCustomDataRead(const std::string& mime_type, const std::vector& data) { DCHECK(os_exchange_data_); base::Pickle pickle(reinterpret_cast(data.data()), data.size()); os_exchange_data_->SetPickledData( ui::ClipboardFormatType::WebCustomDataType(), pickle); mime_type_ = mime_type; counter_.Run(); } void DragDropOperation::OnDragIconCaptured(const SkBitmap& icon_bitmap) { DCHECK(icon_); float scale_factor = origin_->get()->window()->layer()->device_scale_factor(); gfx::ImageSkia icon_skia = gfx::ImageSkia::CreateFromBitmap(icon_bitmap, scale_factor); gfx::Vector2d icon_offset = -icon_->get()->GetBufferOffset(); if (os_exchange_data_) { os_exchange_data_->provider().SetDragImage(icon_skia, icon_offset); } else { #if BUILDFLAG(IS_CHROMEOS_ASH) drag_drop_controller_->SetDragImage(icon_skia, icon_offset); #endif } if (!captured_icon_) { captured_icon_ = true; counter_.Run(); } } void DragDropOperation::ScheduleStartDragDropOperation() { // StartDragAndDrop uses a nested run loop. When restarting, we a) don't want // to interrupt the callers task for an arbitrary period of time and b) want // to let any nested run loops that are currently running to have a chance to // exit to avoid arbitrarily deep nesting. We can accomplish both of those // things by posting a new task to actually start the drag and drop operation. #if BUILDFLAG(IS_CHROMEOS_ASH) if (extended_drag_source_) { ShellSurfaceBase* shell_surface = GetShellSurfaceBaseForWindow( origin_->get()->window()->GetToplevelWindow()); if (shell_surface) shell_surface->set_in_extended_drag(true); } #endif base::SequencedTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::BindOnce(&DragDropOperation::StartDragDropOperation, weak_ptr_factory_.GetWeakPtr())); } void DragDropOperation::StartDragDropOperation() { uint32_t dnd_operations = DndActionsToDragOperations(source_->get()->GetActions()); base::WeakPtr weak_ptr = weak_ptr_factory_.GetWeakPtr(); started_by_this_object_ = true; gfx::Point drag_start_point = gfx::ToFlooredPoint(drag_start_point_); // This triggers a nested run loop that terminates when the drag and drop // operation is completed. DragOperation op = drag_drop_controller_->StartDragAndDrop( std::move(os_exchange_data_), origin_->get()->window()->GetRootWindow(), origin_->get()->window(), drag_start_point, dnd_operations, event_source_); // The instance deleted during StartDragAndDrop's nested RunLoop. if (!weak_ptr) return; #if BUILDFLAG(IS_CHROMEOS_ASH) // Always reset the in_extended_drag becacuse ExtendedDragSource may be // destroyed during nested loop. if (origin_->get()) { ShellSurfaceBase* shell_surface = GetShellSurfaceBaseForWindow( origin_->get()->window()->GetToplevelWindow()); if (shell_surface) shell_surface->set_in_extended_drag(false); } #endif // In tests, drag_drop_controller_ does not create a nested message loop and // so StartDragAndDrop exits before the drag&drop session finishes. In that // case the cleanup process shouldn't be made. if (drag_drop_controller_->IsDragDropInProgress()) return; if (op != DragOperation::kNone) { // It is possible that Ash flags the dragged tab to snap its origin, and // it uses the `chromeos::kIsDeferredTabDraggingTargetWindowKey` property to // control that. The snap back behavior works as if the drag was cancelled. bool force_tab_swallow = false; #if BUILDFLAG(IS_CHROMEOS_ASH) aura::Window* source_window = origin_->get()->window()->GetToplevelWindow(); force_tab_swallow = (source_window && source_window->GetProperty( chromeos::kIsDeferredTabDraggingTargetWindowKey)); source_window->ClearProperty( chromeos::kIsDeferredTabDraggingTargetWindowKey); #endif if (force_tab_swallow) { source_->get()->Cancelled(); } else { // Success // TODO(crbug.com/994065) This is currently not the actual mime type // used by the recipient, just an arbitrary one we pick out of the // offered types so we can report back whether or not the drop can // succeed. This may need to change in the future. source_->get()->Target(mime_type_); source_->get()->Action(DragOperationToDndAction(op)); source_->get()->DndDropPerformed(); source_->get()->DndFinished(); } // Reset |source_| so it the destructor doesn't try to cancel it. source_.reset(); } // On failure the destructor will handle canceling the data source. delete this; } void DragDropOperation::OnDragStarted() { if (!started_by_this_object_) delete this; } #if BUILDFLAG(IS_CHROMEOS_ASH) void DragDropOperation::OnDragActionsChanged(int actions) { if (!started_by_this_object_) return; DndAction dnd_action = DragOperationsToPreferredDndAction(actions); // We send a mime type along with the action to indicate to the application // that dropping is/is not currently possible. We do not currently know of // any applications that care about the specific mime type until the drop is // actually performed. if (dnd_action != DndAction::kNone) source_->get()->Target(mime_type_); else source_->get()->Target(absl::nullopt); source_->get()->Action(dnd_action); } void DragDropOperation::OnExtendedDragSourceDestroying( ExtendedDragSource* source) { ResetExtendedDragSource(); } void DragDropOperation::ResetExtendedDragSource() { DCHECK(extended_drag_source_); extended_drag_source_->RemoveObserver(this); drag_drop_controller_->set_toplevel_window_drag_delegate(nullptr); extended_drag_source_ = nullptr; } #endif void DragDropOperation::OnSurfaceDestroying(Surface* surface) { DCHECK(surface == origin_->get() || (icon_ && surface == icon_->get())); delete this; } void DragDropOperation::OnDataSourceDestroying(DataSource* source) { DCHECK_EQ(source, source_->get()); source_.reset(); delete this; } } // namespace exo