c29c3fa72a90eb34d5e5c856f38a33c26b8471b5
[WebKit-https.git] / Source / WebKit / UIProcess / gtk / DragAndDropHandler.cpp
1 /*
2  * Copyright (C) 2014 Igalia S.L.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "DragAndDropHandler.h"
28
29 #if ENABLE(DRAG_SUPPORT)
30
31 #include "WebPageProxy.h"
32 #include <WebCore/DragData.h>
33 #include <WebCore/GRefPtrGtk.h>
34 #include <WebCore/GUniquePtrGtk.h>
35 #include <WebCore/GtkUtilities.h>
36 #include <WebCore/PasteboardHelper.h>
37 #include <wtf/RunLoop.h>
38
39 namespace WebKit {
40 using namespace WebCore;
41
42 DragAndDropHandler::DragAndDropHandler(WebPageProxy& page)
43     : m_page(page)
44 {
45 }
46
47 DragAndDropHandler::DroppingContext::DroppingContext(GdkDragContext* gdkContext, const IntPoint& position)
48     : gdkContext(gdkContext)
49     , lastMotionPosition(position)
50     , selectionData(SelectionData::create())
51 {
52 }
53
54 static inline GdkDragAction dragOperationToGdkDragActions(DragOperation coreAction)
55 {
56     GdkDragAction gdkAction = static_cast<GdkDragAction>(0);
57     if (coreAction == DragOperationNone)
58         return gdkAction;
59
60     if (coreAction & DragOperationCopy)
61         gdkAction = static_cast<GdkDragAction>(GDK_ACTION_COPY | gdkAction);
62     if (coreAction & DragOperationMove)
63         gdkAction = static_cast<GdkDragAction>(GDK_ACTION_MOVE | gdkAction);
64     if (coreAction & DragOperationLink)
65         gdkAction = static_cast<GdkDragAction>(GDK_ACTION_LINK | gdkAction);
66     if (coreAction & DragOperationPrivate)
67         gdkAction = static_cast<GdkDragAction>(GDK_ACTION_PRIVATE | gdkAction);
68
69     return gdkAction;
70 }
71
72 static inline GdkDragAction dragOperationToSingleGdkDragAction(DragOperation coreAction)
73 {
74     if (coreAction == DragOperationEvery || coreAction & DragOperationCopy)
75         return GDK_ACTION_COPY;
76     if (coreAction & DragOperationMove)
77         return GDK_ACTION_MOVE;
78     if (coreAction & DragOperationLink)
79         return GDK_ACTION_LINK;
80     if (coreAction & DragOperationPrivate)
81         return GDK_ACTION_PRIVATE;
82     return static_cast<GdkDragAction>(0);
83 }
84
85 static inline DragOperation gdkDragActionToDragOperation(GdkDragAction gdkAction)
86 {
87     // We have no good way to detect DragOperationEvery other than
88     // to use it when all applicable flags are on.
89     if (gdkAction & GDK_ACTION_COPY
90         && gdkAction & GDK_ACTION_MOVE
91         && gdkAction & GDK_ACTION_LINK
92         && gdkAction & GDK_ACTION_PRIVATE)
93         return DragOperationEvery;
94
95     unsigned action = DragOperationNone;
96     if (gdkAction & GDK_ACTION_COPY)
97         action |= DragOperationCopy;
98     if (gdkAction & GDK_ACTION_MOVE)
99         action |= DragOperationMove;
100     if (gdkAction & GDK_ACTION_LINK)
101         action |= DragOperationLink;
102     if (gdkAction & GDK_ACTION_PRIVATE)
103         action |= DragOperationPrivate;
104     return static_cast<DragOperation>(action);
105 }
106
107 void DragAndDropHandler::startDrag(Ref<SelectionData>&& selection, DragOperation dragOperation, RefPtr<ShareableBitmap>&& dragImage)
108 {
109 #if GTK_CHECK_VERSION(3, 16, 0)
110     // WebCore::EventHandler does not support more than one DnD operation at the same time for
111     // a given page, so we should cancel any previous operation whose context we might have
112     // stored, should we receive a new startDrag event before finishing a previous DnD operation.
113     if (m_dragContext) {
114         gtk_drag_cancel(m_dragContext.get());
115         m_dragContext = nullptr;
116     }
117
118     m_draggingSelectionData = WTFMove(selection);
119     GRefPtr<GtkTargetList> targetList = PasteboardHelper::singleton().targetListForSelectionData(*m_draggingSelectionData);
120 #else
121     RefPtr<SelectionData> selectionData = WTFMove(selection);
122     GRefPtr<GtkTargetList> targetList = PasteboardHelper::singleton().targetListForSelectionData(*selectionData);
123 #endif
124
125     GUniquePtr<GdkEvent> currentEvent(gtk_get_current_event());
126     GdkDragContext* context = gtk_drag_begin(m_page.viewWidget(), targetList.get(), dragOperationToGdkDragActions(dragOperation),
127         GDK_BUTTON_PRIMARY, currentEvent.get());
128
129 #if GTK_CHECK_VERSION(3, 16, 0)
130     m_dragContext = context;
131 #else
132     // We don't have gtk_drag_cancel() in GTK+ < 3.16, so we use the old code.
133     // See https://bugs.webkit.org/show_bug.cgi?id=138468
134     m_draggingSelectionDataMap.set(context, WTFMove(selectionData));
135 #endif
136
137     if (dragImage) {
138         RefPtr<cairo_surface_t> image(dragImage->createCairoSurface());
139         // Use the center of the drag image as hotspot.
140         cairo_surface_set_device_offset(image.get(), -cairo_image_surface_get_width(image.get()) / 2, -cairo_image_surface_get_height(image.get()) / 2);
141         gtk_drag_set_icon_surface(context, image.get());
142     } else
143         gtk_drag_set_icon_default(context);
144 }
145
146 void DragAndDropHandler::fillDragData(GdkDragContext* context, GtkSelectionData* selectionData, unsigned info)
147 {
148 #if GTK_CHECK_VERSION(3, 16, 0)
149     // This can happen when attempting to call finish drag from webkitWebViewBaseDragDataGet()
150     // for a obsolete DnD operation that got previously cancelled in startDrag().
151     if (m_dragContext.get() != context)
152         return;
153
154     ASSERT(m_draggingSelectionData);
155     PasteboardHelper::singleton().fillSelectionData(*m_draggingSelectionData, info, selectionData);
156 #else
157     if (auto* selection = m_draggingSelectionDataMap.get(context))
158         PasteboardHelper::singleton().fillSelectionData(*selection, info, selectionData);
159 #endif
160 }
161
162 void DragAndDropHandler::finishDrag(GdkDragContext* context)
163 {
164 #if GTK_CHECK_VERSION(3, 16, 0)
165     // This can happen when attempting to call finish drag from webkitWebViewBaseDragEnd()
166     // for a obsolete DnD operation that got previously cancelled in startDrag().
167     if (m_dragContext.get() != context)
168         return;
169
170     if (!m_draggingSelectionData)
171         return;
172
173     m_dragContext = nullptr;
174     m_draggingSelectionData = nullptr;
175 #else
176     if (!m_draggingSelectionDataMap.remove(context))
177         return;
178 #endif
179
180     GdkDevice* device = gdk_drag_context_get_device(context);
181     int x = 0, y = 0;
182     gdk_device_get_window_at_position(device, &x, &y);
183     int xRoot = 0, yRoot = 0;
184     gdk_device_get_position(device, nullptr, &xRoot, &yRoot);
185     m_page.dragEnded(IntPoint(x, y), IntPoint(xRoot, yRoot), gdkDragActionToDragOperation(gdk_drag_context_get_selected_action(context)));
186 }
187
188 SelectionData* DragAndDropHandler::dropDataSelection(GdkDragContext* context, GtkSelectionData* selectionData, unsigned info, IntPoint& position)
189 {
190     DroppingContext* droppingContext = m_droppingContexts.get(context);
191     if (!droppingContext)
192         return nullptr;
193
194     droppingContext->pendingDataRequests--;
195     PasteboardHelper::singleton().fillSelectionData(selectionData, info, droppingContext->selectionData);
196     if (droppingContext->pendingDataRequests)
197         return nullptr;
198
199     // The coordinates passed to drag-data-received signal are sometimes
200     // inaccurate in WTR, so use the coordinates of the last motion event.
201     position = droppingContext->lastMotionPosition;
202
203     // If there are no more pending requests, start sending dragging data to WebCore.
204     return droppingContext->selectionData.ptr();
205 }
206
207 void DragAndDropHandler::dragEntered(GdkDragContext* context, GtkSelectionData* selectionData, unsigned info, unsigned time)
208 {
209     IntPoint position;
210     auto* selection = dropDataSelection(context, selectionData, info, position);
211     if (!selection)
212         return;
213
214     DragData dragData(selection, position, convertWidgetPointToScreenPoint(m_page.viewWidget(), position), gdkDragActionToDragOperation(gdk_drag_context_get_actions(context)));
215     m_page.resetCurrentDragInformation();
216     m_page.dragEntered(dragData);
217     DragOperation operation = m_page.currentDragOperation();
218     gdk_drag_status(context, dragOperationToSingleGdkDragAction(operation), time);
219 }
220
221 SelectionData* DragAndDropHandler::dragDataSelection(GdkDragContext* context, const IntPoint& position, unsigned time)
222 {
223     std::unique_ptr<DroppingContext>& droppingContext = m_droppingContexts.add(context, nullptr).iterator->value;
224     if (!droppingContext) {
225         GtkWidget* widget = m_page.viewWidget();
226         droppingContext = std::make_unique<DroppingContext>(context, position);
227         Vector<GdkAtom> acceptableTargets(PasteboardHelper::singleton().dropAtomsForContext(widget, droppingContext->gdkContext));
228         droppingContext->pendingDataRequests = acceptableTargets.size();
229         for (auto& target : acceptableTargets)
230             gtk_drag_get_data(widget, droppingContext->gdkContext, target, time);
231     } else
232         droppingContext->lastMotionPosition = position;
233
234     // Don't send any drag information to WebCore until we've retrieved all the data for this drag operation.
235     // Otherwise we'd have to block to wait for the drag's data.
236     if (droppingContext->pendingDataRequests > 0)
237         return nullptr;
238
239     return droppingContext->selectionData.ptr();
240 }
241
242 void DragAndDropHandler::dragMotion(GdkDragContext* context, const IntPoint& position, unsigned time)
243 {
244     auto* selection = dragDataSelection(context, position, time);
245     if (!selection)
246         return;
247
248     DragData dragData(selection, position, convertWidgetPointToScreenPoint(m_page.viewWidget(), position), gdkDragActionToDragOperation(gdk_drag_context_get_actions(context)));
249     m_page.dragUpdated(dragData);
250     DragOperation operation = m_page.currentDragOperation();
251     gdk_drag_status(context, dragOperationToSingleGdkDragAction(operation), time);
252 }
253
254 void DragAndDropHandler::dragLeave(GdkDragContext* context)
255 {
256     DroppingContext* droppingContext = m_droppingContexts.get(context);
257     if (!droppingContext)
258         return;
259
260     // During a drop GTK+ will fire a drag-leave signal right before firing
261     // the drag-drop signal. We want the actions for drag-leave to happen after
262     // those for drag-drop, so schedule them to happen asynchronously here.
263     RunLoop::main().dispatch([this, context, droppingContext]() {
264         auto it = m_droppingContexts.find(context);
265         if (it == m_droppingContexts.end())
266             return;
267
268         // If the view doesn't know about the drag yet (there are still pending data requests),
269         // don't update it with information about the drag.
270         if (droppingContext->pendingDataRequests)
271             return;
272
273         if (!droppingContext->dropHappened) {
274             // Don't call dragExited if we have just received a drag-drop signal. This
275             // happens in the case of a successful drop onto the view.
276             const IntPoint& position = droppingContext->lastMotionPosition;
277             DragData dragData(droppingContext->selectionData.ptr(), position, convertWidgetPointToScreenPoint(m_page.viewWidget(), position), DragOperationNone);
278             m_page.dragExited(dragData);
279             m_page.resetCurrentDragInformation();
280         }
281
282         m_droppingContexts.remove(it);
283     });
284 }
285
286 bool DragAndDropHandler::drop(GdkDragContext* context, const IntPoint& position, unsigned time)
287 {
288     DroppingContext* droppingContext = m_droppingContexts.get(context);
289     if (!droppingContext)
290         return false;
291
292     droppingContext->dropHappened = true;
293
294     uint32_t flags = 0;
295     if (gdk_drag_context_get_selected_action(context) == GDK_ACTION_COPY)
296         flags |= WebCore::DragApplicationIsCopyKeyDown;
297     DragData dragData(droppingContext->selectionData.ptr(), position, convertWidgetPointToScreenPoint(m_page.viewWidget(), position), gdkDragActionToDragOperation(gdk_drag_context_get_actions(context)), static_cast<WebCore::DragApplicationFlags>(flags));
298     m_page.performDragOperation(dragData, String(), { }, { });
299     gtk_drag_finish(context, TRUE, FALSE, time);
300     return true;
301 }
302
303 } // namespace WebKit
304
305 #endif // ENABLE(DRAG_SUPPORT)