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
39namespace WebKit {
40using namespace WebCore;
41
42DragAndDropHandler::DragAndDropHandler(WebPageProxy& page)
43 : m_page(page)
44{
45}
46
47DragAndDropHandler::DroppingContext::DroppingContext(GdkDragContext* gdkContext, const IntPoint& position)
48 : gdkContext(gdkContext)
49 , lastMotionPosition(position)
50 , selectionData(SelectionData::create())
51{
52}
53
54static 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
72static 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
85static 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
107void 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
146void 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
162void 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
188SelectionData* 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
207void 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
221SelectionData* 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
242void 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
254void 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
286bool 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)
306