| /* |
| * Copyright (C) 2014 Igalia S.L. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "DragAndDropHandler.h" |
| |
| #if ENABLE(DRAG_SUPPORT) |
| |
| #include "WebPageProxy.h" |
| #include <WebCore/DragData.h> |
| #include <WebCore/GRefPtrGtk.h> |
| #include <WebCore/GtkUtilities.h> |
| #include <WebCore/PasteboardHelper.h> |
| #include <wtf/RunLoop.h> |
| #include <wtf/glib/GUniquePtr.h> |
| |
| using namespace WebCore; |
| |
| namespace WebKit { |
| |
| DragAndDropHandler::DragAndDropHandler(WebPageProxy& page) |
| : m_page(page) |
| { |
| } |
| |
| DragAndDropHandler::DroppingContext::DroppingContext(GdkDragContext* gdkContext, const IntPoint& position) |
| : gdkContext(gdkContext) |
| , lastMotionPosition(position) |
| , selectionData(SelectionData::create()) |
| { |
| } |
| |
| static inline GdkDragAction dragOperationToGdkDragActions(DragOperation coreAction) |
| { |
| GdkDragAction gdkAction = static_cast<GdkDragAction>(0); |
| if (coreAction == DragOperationNone) |
| return gdkAction; |
| |
| if (coreAction & DragOperationCopy) |
| gdkAction = static_cast<GdkDragAction>(GDK_ACTION_COPY | gdkAction); |
| if (coreAction & DragOperationMove) |
| gdkAction = static_cast<GdkDragAction>(GDK_ACTION_MOVE | gdkAction); |
| if (coreAction & DragOperationLink) |
| gdkAction = static_cast<GdkDragAction>(GDK_ACTION_LINK | gdkAction); |
| if (coreAction & DragOperationPrivate) |
| gdkAction = static_cast<GdkDragAction>(GDK_ACTION_PRIVATE | gdkAction); |
| |
| return gdkAction; |
| } |
| |
| static inline GdkDragAction dragOperationToSingleGdkDragAction(DragOperation coreAction) |
| { |
| if (coreAction == DragOperationEvery || coreAction & DragOperationCopy) |
| return GDK_ACTION_COPY; |
| if (coreAction & DragOperationMove) |
| return GDK_ACTION_MOVE; |
| if (coreAction & DragOperationLink) |
| return GDK_ACTION_LINK; |
| if (coreAction & DragOperationPrivate) |
| return GDK_ACTION_PRIVATE; |
| return static_cast<GdkDragAction>(0); |
| } |
| |
| static inline DragOperation gdkDragActionToDragOperation(GdkDragAction gdkAction) |
| { |
| // We have no good way to detect DragOperationEvery other than |
| // to use it when all applicable flags are on. |
| if (gdkAction & GDK_ACTION_COPY |
| && gdkAction & GDK_ACTION_MOVE |
| && gdkAction & GDK_ACTION_LINK |
| && gdkAction & GDK_ACTION_PRIVATE) |
| return DragOperationEvery; |
| |
| unsigned action = DragOperationNone; |
| if (gdkAction & GDK_ACTION_COPY) |
| action |= DragOperationCopy; |
| if (gdkAction & GDK_ACTION_MOVE) |
| action |= DragOperationMove; |
| if (gdkAction & GDK_ACTION_LINK) |
| action |= DragOperationLink; |
| if (gdkAction & GDK_ACTION_PRIVATE) |
| action |= DragOperationPrivate; |
| return static_cast<DragOperation>(action); |
| } |
| |
| void DragAndDropHandler::startDrag(Ref<SelectionData>&& selection, DragOperation dragOperation, RefPtr<ShareableBitmap>&& dragImage) |
| { |
| #if GTK_CHECK_VERSION(3, 16, 0) |
| m_draggingSelectionData = WTFMove(selection); |
| GRefPtr<GtkTargetList> targetList = PasteboardHelper::singleton().targetListForSelectionData(*m_draggingSelectionData); |
| #else |
| RefPtr<SelectionData> selectionData = WTFMove(selection); |
| GRefPtr<GtkTargetList> targetList = PasteboardHelper::singleton().targetListForSelectionData(*selectionData); |
| #endif |
| |
| GUniquePtr<GdkEvent> currentEvent(gtk_get_current_event()); |
| GdkDragContext* context = gtk_drag_begin(m_page.viewWidget(), targetList.get(), dragOperationToGdkDragActions(dragOperation), |
| GDK_BUTTON_PRIMARY, currentEvent.get()); |
| |
| #if GTK_CHECK_VERSION(3, 16, 0) |
| // WebCore::EventHandler does not support more than one DnD operation at the same time for |
| // a given page, so we should cancel any previous operation whose context we might have |
| // stored, should we receive a new startDrag event before finishing a previous DnD operation. |
| if (m_dragContext) |
| gtk_drag_cancel(m_dragContext.get()); |
| m_dragContext = context; |
| #else |
| // We don't have gtk_drag_cancel() in GTK+ < 3.16, so we use the old code. |
| // See https://bugs.webkit.org/show_bug.cgi?id=138468 |
| m_draggingSelectionDataMap.set(context, WTFMove(selectionData)); |
| #endif |
| |
| if (dragImage) { |
| RefPtr<cairo_surface_t> image(dragImage->createCairoSurface()); |
| // Use the center of the drag image as hotspot. |
| cairo_surface_set_device_offset(image.get(), -cairo_image_surface_get_width(image.get()) / 2, -cairo_image_surface_get_height(image.get()) / 2); |
| gtk_drag_set_icon_surface(context, image.get()); |
| } else |
| gtk_drag_set_icon_default(context); |
| } |
| |
| void DragAndDropHandler::fillDragData(GdkDragContext* context, GtkSelectionData* selectionData, unsigned info) |
| { |
| #if GTK_CHECK_VERSION(3, 16, 0) |
| // This can happen when attempting to call finish drag from webkitWebViewBaseDragDataGet() |
| // for a obsolete DnD operation that got previously cancelled in startDrag(). |
| if (m_dragContext.get() != context) |
| return; |
| |
| ASSERT(m_draggingSelectionData); |
| PasteboardHelper::singleton().fillSelectionData(*m_draggingSelectionData, info, selectionData); |
| #else |
| if (auto* selection = m_draggingSelectionDataMap.get(context)) |
| PasteboardHelper::singleton().fillSelectionData(*selection, info, selectionData); |
| #endif |
| } |
| |
| void DragAndDropHandler::finishDrag(GdkDragContext* context) |
| { |
| #if GTK_CHECK_VERSION(3, 16, 0) |
| // This can happen when attempting to call finish drag from webkitWebViewBaseDragEnd() |
| // for a obsolete DnD operation that got previously cancelled in startDrag(). |
| if (m_dragContext.get() != context) |
| return; |
| |
| if (!m_draggingSelectionData) |
| return; |
| |
| m_dragContext = nullptr; |
| m_draggingSelectionData = nullptr; |
| #else |
| if (!m_draggingSelectionDataMap.remove(context)) |
| return; |
| #endif |
| |
| GdkDevice* device = gdk_drag_context_get_device(context); |
| int x = 0, y = 0; |
| gdk_device_get_window_at_position(device, &x, &y); |
| int xRoot = 0, yRoot = 0; |
| gdk_device_get_position(device, nullptr, &xRoot, &yRoot); |
| m_page.dragEnded(IntPoint(x, y), IntPoint(xRoot, yRoot), gdkDragActionToDragOperation(gdk_drag_context_get_selected_action(context))); |
| } |
| |
| SelectionData* DragAndDropHandler::dropDataSelection(GdkDragContext* context, GtkSelectionData* selectionData, unsigned info, IntPoint& position) |
| { |
| DroppingContext* droppingContext = m_droppingContexts.get(context); |
| if (!droppingContext) |
| return nullptr; |
| |
| droppingContext->pendingDataRequests--; |
| PasteboardHelper::singleton().fillSelectionData(selectionData, info, droppingContext->selectionData); |
| if (droppingContext->pendingDataRequests) |
| return nullptr; |
| |
| // The coordinates passed to drag-data-received signal are sometimes |
| // inaccurate in WTR, so use the coordinates of the last motion event. |
| position = droppingContext->lastMotionPosition; |
| |
| // If there are no more pending requests, start sending dragging data to WebCore. |
| return droppingContext->selectionData.ptr(); |
| } |
| |
| void DragAndDropHandler::dragEntered(GdkDragContext* context, GtkSelectionData* selectionData, unsigned info, unsigned time) |
| { |
| IntPoint position; |
| auto* selection = dropDataSelection(context, selectionData, info, position); |
| if (!selection) |
| return; |
| |
| DragData dragData(selection, position, convertWidgetPointToScreenPoint(m_page.viewWidget(), position), gdkDragActionToDragOperation(gdk_drag_context_get_actions(context))); |
| m_page.resetCurrentDragInformation(); |
| m_page.dragEntered(dragData); |
| DragOperation operation = m_page.currentDragOperation(); |
| gdk_drag_status(context, dragOperationToSingleGdkDragAction(operation), time); |
| } |
| |
| SelectionData* DragAndDropHandler::dragDataSelection(GdkDragContext* context, const IntPoint& position, unsigned time) |
| { |
| std::unique_ptr<DroppingContext>& droppingContext = m_droppingContexts.add(context, nullptr).iterator->value; |
| if (!droppingContext) { |
| GtkWidget* widget = m_page.viewWidget(); |
| droppingContext = std::make_unique<DroppingContext>(context, position); |
| Vector<GdkAtom> acceptableTargets(PasteboardHelper::singleton().dropAtomsForContext(widget, droppingContext->gdkContext)); |
| droppingContext->pendingDataRequests = acceptableTargets.size(); |
| for (auto& target : acceptableTargets) |
| gtk_drag_get_data(widget, droppingContext->gdkContext, target, time); |
| } else |
| droppingContext->lastMotionPosition = position; |
| |
| // Don't send any drag information to WebCore until we've retrieved all the data for this drag operation. |
| // Otherwise we'd have to block to wait for the drag's data. |
| if (droppingContext->pendingDataRequests > 0) |
| return nullptr; |
| |
| return droppingContext->selectionData.ptr(); |
| } |
| |
| void DragAndDropHandler::dragMotion(GdkDragContext* context, const IntPoint& position, unsigned time) |
| { |
| auto* selection = dragDataSelection(context, position, time); |
| if (!selection) |
| return; |
| |
| DragData dragData(selection, position, convertWidgetPointToScreenPoint(m_page.viewWidget(), position), gdkDragActionToDragOperation(gdk_drag_context_get_actions(context))); |
| m_page.dragUpdated(dragData); |
| DragOperation operation = m_page.currentDragOperation(); |
| gdk_drag_status(context, dragOperationToSingleGdkDragAction(operation), time); |
| } |
| |
| void DragAndDropHandler::dragLeave(GdkDragContext* context) |
| { |
| DroppingContext* droppingContext = m_droppingContexts.get(context); |
| if (!droppingContext) |
| return; |
| |
| // During a drop GTK+ will fire a drag-leave signal right before firing |
| // the drag-drop signal. We want the actions for drag-leave to happen after |
| // those for drag-drop, so schedule them to happen asynchronously here. |
| RunLoop::main().dispatch([this, droppingContext]() { |
| auto it = m_droppingContexts.find(droppingContext->gdkContext); |
| if (it == m_droppingContexts.end()) |
| return; |
| |
| // If the view doesn't know about the drag yet (there are still pending data requests), |
| // don't update it with information about the drag. |
| if (droppingContext->pendingDataRequests) |
| return; |
| |
| if (!droppingContext->dropHappened) { |
| // Don't call dragExited if we have just received a drag-drop signal. This |
| // happens in the case of a successful drop onto the view. |
| const IntPoint& position = droppingContext->lastMotionPosition; |
| DragData dragData(droppingContext->selectionData.ptr(), position, convertWidgetPointToScreenPoint(m_page.viewWidget(), position), DragOperationNone); |
| m_page.dragExited(dragData); |
| m_page.resetCurrentDragInformation(); |
| } |
| |
| m_droppingContexts.remove(it); |
| }); |
| } |
| |
| bool DragAndDropHandler::drop(GdkDragContext* context, const IntPoint& position, unsigned time) |
| { |
| DroppingContext* droppingContext = m_droppingContexts.get(context); |
| if (!droppingContext) |
| return false; |
| |
| droppingContext->dropHappened = true; |
| |
| uint32_t flags = 0; |
| if (gdk_drag_context_get_selected_action(context) == GDK_ACTION_COPY) |
| flags |= WebCore::DragApplicationIsCopyKeyDown; |
| DragData dragData(droppingContext->selectionData.ptr(), position, convertWidgetPointToScreenPoint(m_page.viewWidget(), position), gdkDragActionToDragOperation(gdk_drag_context_get_actions(context)), static_cast<WebCore::DragApplicationFlags>(flags)); |
| SandboxExtension::Handle handle; |
| SandboxExtension::HandleArray sandboxExtensionForUpload; |
| m_page.performDragOperation(dragData, String(), handle, sandboxExtensionForUpload); |
| gtk_drag_finish(context, TRUE, FALSE, time); |
| return true; |
| } |
| |
| } // namespace WebKit |
| |
| #endif // ENABLE(DRAG_SUPPORT) |