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) |
306 | |