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