Home | History | Annotate | Download | only in gtk
      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 
     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 
     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 
     65 TabContents* TabContentsDragSource::tab_contents() const {
     66   return tab_contents_view_->tab_contents();
     67 }
     68 
     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 
    149 void TabContentsDragSource::WillProcessEvent(GdkEvent* event) {
    150   // No-op.
    151 }
    152 
    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 
    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 
    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 
    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 
    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 
    365 gfx::NativeView TabContentsDragSource::GetContentNativeView() const {
    366   return tab_contents_view_->GetContentNativeView();
    367 }
    368 
    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