1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/ui/gtk/tab_contents_drag_source.h"
6
7 #include <string>
8
9 #include "base/file_util.h"
10 #include "base/mime_util.h"
11 #include "base/utf_string_conversions.h"
12 #include "chrome/browser/download/download_util.h"
13 #include "chrome/browser/download/drag_download_file.h"
14 #include "chrome/browser/download/drag_download_util.h"
15 #include "chrome/browser/ui/gtk/gtk_util.h"
16 #include "content/browser/renderer_host/render_view_host.h"
17 #include "content/browser/renderer_host/render_view_host_delegate.h"
18 #include "content/browser/tab_contents/tab_contents.h"
19 #include "content/browser/tab_contents/tab_contents_view.h"
20 #include "net/base/file_stream.h"
21 #include "net/base/net_util.h"
22 #include "ui/base/dragdrop/gtk_dnd_util.h"
23 #include "ui/gfx/gtk_util.h"
24 #include "webkit/glue/webdropdata.h"
25
26 using WebKit::WebDragOperation;
27 using WebKit::WebDragOperationsMask;
28 using WebKit::WebDragOperationNone;
29
TabContentsDragSource(TabContentsView * tab_contents_view)30 TabContentsDragSource::TabContentsDragSource(
31 TabContentsView* tab_contents_view)
32 : tab_contents_view_(tab_contents_view),
33 drag_pixbuf_(NULL),
34 drag_failed_(false),
35 drag_widget_(gtk_invisible_new()),
36 drag_context_(NULL),
37 drag_icon_(gtk_window_new(GTK_WINDOW_POPUP)) {
38 signals_.Connect(drag_widget_, "drag-failed",
39 G_CALLBACK(OnDragFailedThunk), this);
40 signals_.Connect(drag_widget_, "drag-begin",
41 G_CALLBACK(OnDragBeginThunk),
42 this);
43 signals_.Connect(drag_widget_, "drag-end",
44 G_CALLBACK(OnDragEndThunk), this);
45 signals_.Connect(drag_widget_, "drag-data-get",
46 G_CALLBACK(OnDragDataGetThunk), this);
47
48 signals_.Connect(drag_icon_, "expose-event",
49 G_CALLBACK(OnDragIconExposeThunk), this);
50 }
51
~TabContentsDragSource()52 TabContentsDragSource::~TabContentsDragSource() {
53 // Break the current drag, if any.
54 if (drop_data_.get()) {
55 gtk_grab_add(drag_widget_);
56 gtk_grab_remove(drag_widget_);
57 MessageLoopForUI::current()->RemoveObserver(this);
58 drop_data_.reset();
59 }
60
61 gtk_widget_destroy(drag_widget_);
62 gtk_widget_destroy(drag_icon_);
63 }
64
tab_contents() const65 TabContents* TabContentsDragSource::tab_contents() const {
66 return tab_contents_view_->tab_contents();
67 }
68
StartDragging(const WebDropData & drop_data,WebDragOperationsMask allowed_ops,GdkEventButton * last_mouse_down,const SkBitmap & image,const gfx::Point & image_offset)69 void TabContentsDragSource::StartDragging(const WebDropData& drop_data,
70 WebDragOperationsMask allowed_ops,
71 GdkEventButton* last_mouse_down,
72 const SkBitmap& image,
73 const gfx::Point& image_offset) {
74 // Guard against re-starting before previous drag completed.
75 if (drag_context_) {
76 NOTREACHED();
77 tab_contents()->SystemDragEnded();
78 return;
79 }
80
81 int targets_mask = 0;
82
83 if (!drop_data.plain_text.empty())
84 targets_mask |= ui::TEXT_PLAIN;
85 if (drop_data.url.is_valid()) {
86 targets_mask |= ui::TEXT_URI_LIST;
87 targets_mask |= ui::CHROME_NAMED_URL;
88 targets_mask |= ui::NETSCAPE_URL;
89 }
90 if (!drop_data.text_html.empty())
91 targets_mask |= ui::TEXT_HTML;
92 if (!drop_data.file_contents.empty())
93 targets_mask |= ui::CHROME_WEBDROP_FILE_CONTENTS;
94 if (!drop_data.download_metadata.empty() &&
95 drag_download_util::ParseDownloadMetadata(drop_data.download_metadata,
96 &wide_download_mime_type_,
97 &download_file_name_,
98 &download_url_)) {
99 targets_mask |= ui::DIRECT_SAVE_FILE;
100 }
101
102 // NOTE: Begin a drag even if no targets present. Otherwise, things like
103 // draggable list elements will not work.
104
105 drop_data_.reset(new WebDropData(drop_data));
106
107 // The image we get from WebKit makes heavy use of alpha-shading. This looks
108 // bad on non-compositing WMs. Fall back to the default drag icon.
109 if (!image.isNull() && gtk_util::IsScreenComposited())
110 drag_pixbuf_ = gfx::GdkPixbufFromSkBitmap(&image);
111 image_offset_ = image_offset;
112
113 GtkTargetList* list = ui::GetTargetListFromCodeMask(targets_mask);
114 if (targets_mask & ui::CHROME_WEBDROP_FILE_CONTENTS) {
115 drag_file_mime_type_ = gdk_atom_intern(
116 mime_util::GetDataMimeType(drop_data.file_contents).c_str(), FALSE);
117 gtk_target_list_add(list, drag_file_mime_type_,
118 0, ui::CHROME_WEBDROP_FILE_CONTENTS);
119 }
120
121 drag_failed_ = false;
122 // If we don't pass an event, GDK won't know what event time to start grabbing
123 // mouse events. Technically it's the mouse motion event and not the mouse
124 // down event that causes the drag, but there's no reliable way to know
125 // *which* motion event initiated the drag, so this will have to do.
126 // TODO(estade): This can sometimes be very far off, e.g. if the user clicks
127 // and holds and doesn't start dragging for a long time. I doubt it matters
128 // much, but we should probably look into the possibility of getting the
129 // initiating event from webkit.
130 drag_context_ = gtk_drag_begin(drag_widget_, list,
131 gtk_util::WebDragOpToGdkDragAction(allowed_ops),
132 1, // Drags are always initiated by the left button.
133 reinterpret_cast<GdkEvent*>(last_mouse_down));
134 // The drag adds a ref; let it own the list.
135 gtk_target_list_unref(list);
136
137 // Sometimes the drag fails to start; |context| will be NULL and we won't
138 // get a drag-end signal.
139 if (!drag_context_) {
140 drag_failed_ = true;
141 drop_data_.reset();
142 tab_contents()->SystemDragEnded();
143 return;
144 }
145
146 MessageLoopForUI::current()->AddObserver(this);
147 }
148
WillProcessEvent(GdkEvent * event)149 void TabContentsDragSource::WillProcessEvent(GdkEvent* event) {
150 // No-op.
151 }
152
DidProcessEvent(GdkEvent * event)153 void TabContentsDragSource::DidProcessEvent(GdkEvent* event) {
154 if (event->type != GDK_MOTION_NOTIFY)
155 return;
156
157 GdkEventMotion* event_motion = reinterpret_cast<GdkEventMotion*>(event);
158 gfx::Point client = gtk_util::ClientPoint(GetContentNativeView());
159
160 if (tab_contents()->render_view_host()) {
161 tab_contents()->render_view_host()->DragSourceMovedTo(
162 client.x(), client.y(),
163 static_cast<int>(event_motion->x_root),
164 static_cast<int>(event_motion->y_root));
165 }
166 }
167
OnDragDataGet(GtkWidget * sender,GdkDragContext * context,GtkSelectionData * selection_data,guint target_type,guint time)168 void TabContentsDragSource::OnDragDataGet(GtkWidget* sender,
169 GdkDragContext* context, GtkSelectionData* selection_data,
170 guint target_type, guint time) {
171 const int kBitsPerByte = 8;
172
173 switch (target_type) {
174 case ui::TEXT_PLAIN: {
175 std::string utf8_text = UTF16ToUTF8(drop_data_->plain_text);
176 gtk_selection_data_set_text(selection_data, utf8_text.c_str(),
177 utf8_text.length());
178 break;
179 }
180
181 case ui::TEXT_HTML: {
182 // TODO(estade): change relative links to be absolute using
183 // |html_base_url|.
184 std::string utf8_text = UTF16ToUTF8(drop_data_->text_html);
185 gtk_selection_data_set(selection_data,
186 ui::GetAtomForTarget(ui::TEXT_HTML),
187 kBitsPerByte,
188 reinterpret_cast<const guchar*>(utf8_text.c_str()),
189 utf8_text.length());
190 break;
191 }
192
193 case ui::TEXT_URI_LIST:
194 case ui::CHROME_NAMED_URL:
195 case ui::NETSCAPE_URL: {
196 ui::WriteURLWithName(selection_data, drop_data_->url,
197 drop_data_->url_title, target_type);
198 break;
199 }
200
201 case ui::CHROME_WEBDROP_FILE_CONTENTS: {
202 gtk_selection_data_set(
203 selection_data,
204 drag_file_mime_type_, kBitsPerByte,
205 reinterpret_cast<const guchar*>(drop_data_->file_contents.data()),
206 drop_data_->file_contents.length());
207 break;
208 }
209
210 case ui::DIRECT_SAVE_FILE: {
211 char status_code = 'E';
212
213 // Retrieves the full file path (in file URL format) provided by the
214 // drop target by reading from the source window's XdndDirectSave0
215 // property.
216 gint file_url_len = 0;
217 guchar* file_url_value = NULL;
218 if (gdk_property_get(context->source_window,
219 ui::GetAtomForTarget(ui::DIRECT_SAVE_FILE),
220 ui::GetAtomForTarget(ui::TEXT_PLAIN_NO_CHARSET),
221 0,
222 1024,
223 FALSE,
224 NULL,
225 NULL,
226 &file_url_len,
227 &file_url_value) &&
228 file_url_value) {
229 // Convert from the file url to the file path.
230 GURL file_url(std::string(reinterpret_cast<char*>(file_url_value),
231 file_url_len));
232 g_free(file_url_value);
233 FilePath file_path;
234 if (net::FileURLToFilePath(file_url, &file_path)) {
235 // Open the file as a stream.
236 net::FileStream* file_stream =
237 drag_download_util::CreateFileStreamForDrop(&file_path);
238 if (file_stream) {
239 // Start downloading the file to the stream.
240 TabContents* tab_contents = tab_contents_view_->tab_contents();
241 scoped_refptr<DragDownloadFile> drag_file_downloader =
242 new DragDownloadFile(file_path,
243 linked_ptr<net::FileStream>(file_stream),
244 download_url_,
245 tab_contents->GetURL(),
246 tab_contents->encoding(),
247 tab_contents);
248 drag_file_downloader->Start(
249 new drag_download_util::PromiseFileFinalizer(
250 drag_file_downloader));
251
252 // Set the status code to success.
253 status_code = 'S';
254 }
255 }
256
257 // Return the status code to the file manager.
258 gtk_selection_data_set(selection_data,
259 selection_data->target,
260 8,
261 reinterpret_cast<guchar*>(&status_code),
262 1);
263 }
264 break;
265 }
266
267 default:
268 NOTREACHED();
269 }
270 }
271
OnDragFailed(GtkWidget * sender,GdkDragContext * context,GtkDragResult result)272 gboolean TabContentsDragSource::OnDragFailed(GtkWidget* sender,
273 GdkDragContext* context,
274 GtkDragResult result) {
275 drag_failed_ = true;
276
277 gfx::Point root = gtk_util::ScreenPoint(GetContentNativeView());
278 gfx::Point client = gtk_util::ClientPoint(GetContentNativeView());
279
280 if (tab_contents()->render_view_host()) {
281 tab_contents()->render_view_host()->DragSourceEndedAt(
282 client.x(), client.y(), root.x(), root.y(),
283 WebDragOperationNone);
284 }
285
286 // Let the native failure animation run.
287 return FALSE;
288 }
289
OnDragBegin(GtkWidget * sender,GdkDragContext * drag_context)290 void TabContentsDragSource::OnDragBegin(GtkWidget* sender,
291 GdkDragContext* drag_context) {
292 if (!download_url_.is_empty()) {
293 // Generate the file name based on both mime type and proposed file name.
294 std::string download_mime_type = UTF16ToUTF8(wide_download_mime_type_);
295 std::string content_disposition("attachment; filename=");
296 content_disposition += download_file_name_.value();
297 FilePath generated_download_file_name;
298 download_util::GenerateFileName(download_url_,
299 content_disposition,
300 std::string(),
301 download_mime_type,
302 &generated_download_file_name);
303
304 // Pass the file name to the drop target by setting the source window's
305 // XdndDirectSave0 property.
306 gdk_property_change(drag_context->source_window,
307 ui::GetAtomForTarget(ui::DIRECT_SAVE_FILE),
308 ui::GetAtomForTarget(ui::TEXT_PLAIN_NO_CHARSET),
309 8,
310 GDK_PROP_MODE_REPLACE,
311 reinterpret_cast<const guchar*>(
312 generated_download_file_name.value().c_str()),
313 generated_download_file_name.value().length());
314 }
315
316 if (drag_pixbuf_) {
317 gtk_widget_set_size_request(drag_icon_,
318 gdk_pixbuf_get_width(drag_pixbuf_),
319 gdk_pixbuf_get_height(drag_pixbuf_));
320
321 // We only need to do this once.
322 if (!GTK_WIDGET_REALIZED(drag_icon_)) {
323 GdkScreen* screen = gtk_widget_get_screen(drag_icon_);
324 GdkColormap* rgba = gdk_screen_get_rgba_colormap(screen);
325 if (rgba)
326 gtk_widget_set_colormap(drag_icon_, rgba);
327 }
328
329 gtk_drag_set_icon_widget(drag_context, drag_icon_,
330 image_offset_.x(), image_offset_.y());
331 }
332 }
333
OnDragEnd(GtkWidget * sender,GdkDragContext * drag_context)334 void TabContentsDragSource::OnDragEnd(GtkWidget* sender,
335 GdkDragContext* drag_context) {
336 if (drag_pixbuf_) {
337 g_object_unref(drag_pixbuf_);
338 drag_pixbuf_ = NULL;
339 }
340
341 MessageLoopForUI::current()->RemoveObserver(this);
342
343 if (!download_url_.is_empty()) {
344 gdk_property_delete(drag_context->source_window,
345 ui::GetAtomForTarget(ui::DIRECT_SAVE_FILE));
346 }
347
348 if (!drag_failed_) {
349 gfx::Point root = gtk_util::ScreenPoint(GetContentNativeView());
350 gfx::Point client = gtk_util::ClientPoint(GetContentNativeView());
351
352 if (tab_contents()->render_view_host()) {
353 tab_contents()->render_view_host()->DragSourceEndedAt(
354 client.x(), client.y(), root.x(), root.y(),
355 gtk_util::GdkDragActionToWebDragOp(drag_context->action));
356 }
357 }
358
359 tab_contents()->SystemDragEnded();
360
361 drop_data_.reset();
362 drag_context_ = NULL;
363 }
364
GetContentNativeView() const365 gfx::NativeView TabContentsDragSource::GetContentNativeView() const {
366 return tab_contents_view_->GetContentNativeView();
367 }
368
OnDragIconExpose(GtkWidget * sender,GdkEventExpose * event)369 gboolean TabContentsDragSource::OnDragIconExpose(GtkWidget* sender,
370 GdkEventExpose* event) {
371 cairo_t* cr = gdk_cairo_create(event->window);
372 gdk_cairo_rectangle(cr, &event->area);
373 cairo_clip(cr);
374 cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
375 gdk_cairo_set_source_pixbuf(cr, drag_pixbuf_, 0, 0);
376 cairo_paint(cr);
377 cairo_destroy(cr);
378
379 return TRUE;
380 }
381