Home | History | Annotate | Download | only in web_contents
      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