Use "= default" to denote default constructor or destructor
[WebKit-https.git] / Source / WebCore / platform / gtk / PasteboardHelper.cpp
1 /*
2  * Copyright (C) 2010 Martin Robinson <mrobinson@webkit.org>
3  * Copyright (C) Igalia S.L.
4  * Copyright (C) 2013 Collabora Ltd.
5  * All rights reserved.
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public License
18  * along with this library; see the file COPYING.LIB.  If not, write to
19  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20  * Boston, MA 02110-1301, USA.
21  *
22  */
23 #include "config.h"
24 #include "PasteboardHelper.h"
25
26 #include "BitmapImage.h"
27 #include "GtkVersioning.h"
28 #include "SelectionData.h"
29 #include <gtk/gtk.h>
30 #include <wtf/SetForScope.h>
31 #include <wtf/glib/GUniquePtr.h>
32 #include <wtf/text/CString.h>
33
34 namespace WebCore {
35
36 static GdkAtom textPlainAtom;
37 static GdkAtom markupAtom;
38 static GdkAtom netscapeURLAtom;
39 static GdkAtom uriListAtom;
40 static GdkAtom smartPasteAtom;
41 static GdkAtom unknownAtom;
42
43 static const String gMarkupPrefix = ASCIILiteral("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">");
44
45 static void removeMarkupPrefix(String& markup)
46 {
47     // The markup prefix is not harmful, but we remove it from the string anyway, so that
48     // we can have consistent results with other ports during the layout tests.
49     if (markup.startsWith(gMarkupPrefix))
50         markup.remove(0, gMarkupPrefix.length());
51 }
52
53 PasteboardHelper& PasteboardHelper::singleton()
54 {
55     static PasteboardHelper helper;
56     return helper;
57 }
58
59 PasteboardHelper::PasteboardHelper()
60     : m_targetList(adoptGRef(gtk_target_list_new(nullptr, 0)))
61 {
62     textPlainAtom = gdk_atom_intern_static_string("text/plain;charset=utf-8");
63     markupAtom = gdk_atom_intern_static_string("text/html");
64     netscapeURLAtom = gdk_atom_intern_static_string("_NETSCAPE_URL");
65     uriListAtom = gdk_atom_intern_static_string("text/uri-list");
66     smartPasteAtom = gdk_atom_intern_static_string("application/vnd.webkitgtk.smartpaste");
67     unknownAtom = gdk_atom_intern_static_string("application/vnd.webkitgtk.unknown");
68
69     gtk_target_list_add_text_targets(m_targetList.get(), PasteboardHelper::TargetTypeText);
70     gtk_target_list_add(m_targetList.get(), markupAtom, 0, PasteboardHelper::TargetTypeMarkup);
71     gtk_target_list_add_uri_targets(m_targetList.get(), PasteboardHelper::TargetTypeURIList);
72     gtk_target_list_add(m_targetList.get(), netscapeURLAtom, 0, PasteboardHelper::TargetTypeNetscapeURL);
73     gtk_target_list_add_image_targets(m_targetList.get(), PasteboardHelper::TargetTypeImage, TRUE);
74     gtk_target_list_add(m_targetList.get(), unknownAtom, 0, PasteboardHelper::TargetTypeUnknown);
75 }
76
77 PasteboardHelper::~PasteboardHelper() = default;
78
79 GtkTargetList* PasteboardHelper::targetList() const
80 {
81     return m_targetList.get();
82 }
83
84 static String selectionDataToUTF8String(GtkSelectionData* data)
85 {
86     if (!gtk_selection_data_get_length(data))
87         return String();
88
89     // g_strndup guards against selection data that is not null-terminated.
90     GUniquePtr<gchar> markupString(g_strndup(reinterpret_cast<const char*>(gtk_selection_data_get_data(data)), gtk_selection_data_get_length(data)));
91     return String::fromUTF8(markupString.get());
92 }
93
94 void PasteboardHelper::getClipboardContents(GtkClipboard* clipboard, SelectionData& selection)
95 {
96     if (gtk_clipboard_wait_is_text_available(clipboard)) {
97         GUniquePtr<gchar> textData(gtk_clipboard_wait_for_text(clipboard));
98         if (textData)
99             selection.setText(String::fromUTF8(textData.get()));
100     }
101
102     if (gtk_clipboard_wait_is_target_available(clipboard, markupAtom)) {
103         if (GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, markupAtom)) {
104             String markup(selectionDataToUTF8String(data));
105             removeMarkupPrefix(markup);
106             selection.setMarkup(markup);
107             gtk_selection_data_free(data);
108         }
109     }
110
111     if (gtk_clipboard_wait_is_target_available(clipboard, uriListAtom)) {
112         if (GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, uriListAtom)) {
113             selection.setURIList(selectionDataToUTF8String(data));
114             gtk_selection_data_free(data);
115         }
116     }
117
118 #ifndef GTK_API_VERSION_2
119     if (gtk_clipboard_wait_is_image_available(clipboard)) {
120         if (GRefPtr<GdkPixbuf> pixbuf = adoptGRef(gtk_clipboard_wait_for_image(clipboard))) {
121             RefPtr<cairo_surface_t> surface = adoptRef(gdk_cairo_surface_create_from_pixbuf(pixbuf.get(), 1, nullptr));
122             Ref<Image> image = BitmapImage::create(WTFMove(surface));
123             selection.setImage(image.ptr());
124         }
125     }
126 #endif
127
128     selection.setCanSmartReplace(gtk_clipboard_wait_is_target_available(clipboard, smartPasteAtom));
129 }
130
131 GRefPtr<GtkTargetList> PasteboardHelper::targetListForSelectionData(const SelectionData& selection)
132 {
133     GRefPtr<GtkTargetList> list = adoptGRef(gtk_target_list_new(nullptr, 0));
134
135     if (selection.hasText())
136         gtk_target_list_add_text_targets(list.get(), TargetTypeText);
137
138     if (selection.hasMarkup())
139         gtk_target_list_add(list.get(), markupAtom, 0, TargetTypeMarkup);
140
141     if (selection.hasURIList()) {
142         gtk_target_list_add_uri_targets(list.get(), TargetTypeURIList);
143         gtk_target_list_add(list.get(), netscapeURLAtom, 0, TargetTypeNetscapeURL);
144     }
145
146     if (selection.hasImage())
147         gtk_target_list_add_image_targets(list.get(), TargetTypeImage, TRUE);
148
149     if (selection.hasUnknownTypeData())
150         gtk_target_list_add(list.get(), unknownAtom, 0, TargetTypeUnknown);
151
152     if (selection.canSmartReplace())
153         gtk_target_list_add(list.get(), smartPasteAtom, 0, TargetTypeSmartPaste);
154
155     return list;
156 }
157
158 void PasteboardHelper::fillSelectionData(const SelectionData& selection, unsigned info, GtkSelectionData* selectionData)
159 {
160     if (info == TargetTypeText)
161         gtk_selection_data_set_text(selectionData, selection.text().utf8().data(), -1);
162
163     else if (info == TargetTypeMarkup) {
164         // Some Linux applications refuse to accept pasted markup unless it is
165         // prefixed by a content-type meta tag.
166         CString markup = String(gMarkupPrefix + selection.markup()).utf8();
167         gtk_selection_data_set(selectionData, markupAtom, 8, reinterpret_cast<const guchar*>(markup.data()), markup.length());
168
169     } else if (info == TargetTypeURIList) {
170         CString uriList = selection.uriList().utf8();
171         gtk_selection_data_set(selectionData, uriListAtom, 8, reinterpret_cast<const guchar*>(uriList.data()), uriList.length());
172
173     } else if (info == TargetTypeNetscapeURL && selection.hasURL()) {
174         String url(selection.url());
175         String result(url);
176         result.append("\n");
177
178         if (selection.hasText())
179             result.append(selection.text());
180         else
181             result.append(url);
182
183         GUniquePtr<gchar> resultData(g_strdup(result.utf8().data()));
184         gtk_selection_data_set(selectionData, netscapeURLAtom, 8, reinterpret_cast<const guchar*>(resultData.get()), strlen(resultData.get()));
185
186     } else if (info == TargetTypeImage && selection.hasImage()) {
187         GRefPtr<GdkPixbuf> pixbuf = adoptGRef(selection.image()->getGdkPixbuf());
188         gtk_selection_data_set_pixbuf(selectionData, pixbuf.get());
189
190     } else if (info == TargetTypeSmartPaste)
191         gtk_selection_data_set_text(selectionData, "", -1);
192
193     else if (info == TargetTypeUnknown) {
194         GVariantBuilder builder;
195         g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY);
196
197         for (auto& it : selection.unknownTypes()) {
198             GUniquePtr<gchar> dictItem(g_strdup_printf("{'%s', '%s'}", it.key.utf8().data(), it.value.utf8().data()));
199             g_variant_builder_add_parsed(&builder, dictItem.get());
200         }
201
202         GRefPtr<GVariant> variant = g_variant_builder_end(&builder);
203         GUniquePtr<gchar> serializedVariant(g_variant_print(variant.get(), TRUE));
204         gtk_selection_data_set(selectionData, unknownAtom, 1, reinterpret_cast<const guchar*>(serializedVariant.get()), strlen(serializedVariant.get()));
205     }
206 }
207
208 void PasteboardHelper::fillSelectionData(GtkSelectionData* data, unsigned /* info */, SelectionData& selection)
209 {
210     if (gtk_selection_data_get_length(data) < 0)
211         return;
212
213     GdkAtom target = gtk_selection_data_get_target(data);
214     if (target == textPlainAtom)
215         selection.setText(selectionDataToUTF8String(data));
216     else if (target == markupAtom) {
217         String markup(selectionDataToUTF8String(data));
218         removeMarkupPrefix(markup);
219         selection.setMarkup(markup);
220     } else if (target == uriListAtom) {
221         selection.setURIList(selectionDataToUTF8String(data));
222     } else if (target == netscapeURLAtom) {
223         String urlWithLabel(selectionDataToUTF8String(data));
224         Vector<String> pieces;
225         urlWithLabel.split('\n', pieces);
226
227         // Give preference to text/uri-list here, as it can hold more
228         // than one URI but still take  the label if there is one.
229         if (!selection.hasURIList() && !pieces.isEmpty())
230             selection.setURIList(pieces[0]);
231         if (pieces.size() > 1)
232             selection.setText(pieces[1]);
233     } else if (target == unknownAtom && gtk_selection_data_get_length(data)) {
234         GRefPtr<GVariant> variant = g_variant_new_parsed(reinterpret_cast<const char*>(gtk_selection_data_get_data(data)));
235
236         GUniqueOutPtr<gchar> key;
237         GUniqueOutPtr<gchar> value;
238         GVariantIter iter;
239
240         g_variant_iter_init(&iter, variant.get());
241         while (g_variant_iter_next(&iter, "{ss}", &key.outPtr(), &value.outPtr()))
242             selection.setUnknownTypeData(key.get(), value.get());
243     }
244 }
245
246 Vector<GdkAtom> PasteboardHelper::dropAtomsForContext(GtkWidget* widget, GdkDragContext* context)
247 {
248     // Always search for these common atoms.
249     Vector<GdkAtom> dropAtoms;
250     dropAtoms.append(textPlainAtom);
251     dropAtoms.append(markupAtom);
252     dropAtoms.append(uriListAtom);
253     dropAtoms.append(netscapeURLAtom);
254     dropAtoms.append(unknownAtom);
255
256     // For images, try to find the most applicable image type.
257     GRefPtr<GtkTargetList> list = adoptGRef(gtk_target_list_new(0, 0));
258     gtk_target_list_add_image_targets(list.get(), TargetTypeImage, TRUE);
259     GdkAtom atom = gtk_drag_dest_find_target(widget, context, list.get());
260     if (atom != GDK_NONE)
261         dropAtoms.append(atom);
262
263     return dropAtoms;
264 }
265
266 static SelectionData* settingClipboardSelection;
267
268 struct ClipboardSetData {
269     ClipboardSetData(SelectionData& selection, WTF::Function<void()>&& selectionClearedCallback)
270         : selectionData(selection)
271         , selectionClearedCallback(WTFMove(selectionClearedCallback))
272     {
273     }
274
275     ~ClipboardSetData() = default;
276
277     Ref<SelectionData> selectionData;
278     WTF::Function<void()> selectionClearedCallback;
279 };
280
281 static void getClipboardContentsCallback(GtkClipboard*, GtkSelectionData *selectionData, guint info, gpointer userData)
282 {
283     auto* data = static_cast<ClipboardSetData*>(userData);
284     PasteboardHelper::singleton().fillSelectionData(data->selectionData, info, selectionData);
285 }
286
287 static void clearClipboardContentsCallback(GtkClipboard*, gpointer userData)
288 {
289     std::unique_ptr<ClipboardSetData> data(static_cast<ClipboardSetData*>(userData));
290     if (data->selectionClearedCallback)
291         data->selectionClearedCallback();
292 }
293
294 void PasteboardHelper::writeClipboardContents(GtkClipboard* clipboard, const SelectionData& selection, WTF::Function<void()>&& primarySelectionCleared)
295 {
296     GRefPtr<GtkTargetList> list = targetListForSelectionData(selection);
297
298     int numberOfTargets;
299     GtkTargetEntry* table = gtk_target_table_new_from_list(list.get(), &numberOfTargets);
300
301     if (numberOfTargets > 0 && table) {
302         SetForScope<SelectionData*> change(settingClipboardSelection, const_cast<SelectionData*>(&selection));
303         auto data = std::make_unique<ClipboardSetData>(*settingClipboardSelection, WTFMove(primarySelectionCleared));
304         if (gtk_clipboard_set_with_data(clipboard, table, numberOfTargets, getClipboardContentsCallback, clearClipboardContentsCallback, data.get())) {
305             gtk_clipboard_set_can_store(clipboard, nullptr, 0);
306             // When gtk_clipboard_set_with_data() succeeds clearClipboardContentsCallback takes the ownership of data, so we leak it here.
307             data.release();
308         }
309     } else
310         gtk_clipboard_clear(clipboard);
311
312     if (table)
313         gtk_target_table_free(table, numberOfTargets);
314 }
315
316 }
317