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_dest_gtk.h"
      6 
      7 #include <string>
      8 
      9 #include "base/bind.h"
     10 #include "base/files/file_path.h"
     11 #include "base/message_loop/message_loop.h"
     12 #include "base/strings/utf_string_conversions.h"
     13 #include "content/browser/renderer_host/render_view_host_impl.h"
     14 #include "content/browser/web_contents/drag_utils_gtk.h"
     15 #include "content/browser/web_contents/web_contents_impl.h"
     16 #include "content/public/browser/web_contents_delegate.h"
     17 #include "content/public/browser/web_drag_dest_delegate.h"
     18 #include "content/public/common/url_constants.h"
     19 #include "net/base/net_util.h"
     20 #include "third_party/WebKit/public/web/WebInputEvent.h"
     21 #include "ui/base/clipboard/custom_data_helper.h"
     22 #include "ui/base/dragdrop/gtk_dnd_util.h"
     23 #include "ui/base/gtk/gtk_screen_util.h"
     24 
     25 using WebKit::WebDragOperation;
     26 using WebKit::WebDragOperationNone;
     27 
     28 namespace content {
     29 
     30 namespace {
     31 const int kNumGtkHandlers = 5;
     32 
     33 int GetModifierFlags(GtkWidget* widget) {
     34   int modifier_state = 0;
     35   GdkModifierType state;
     36   gdk_window_get_pointer(gtk_widget_get_window(widget), NULL, NULL, &state);
     37 
     38   if (state & GDK_SHIFT_MASK)
     39     modifier_state |= WebKit::WebInputEvent::ShiftKey;
     40   if (state & GDK_CONTROL_MASK)
     41     modifier_state |= WebKit::WebInputEvent::ControlKey;
     42   if (state & GDK_MOD1_MASK)
     43     modifier_state |= WebKit::WebInputEvent::AltKey;
     44   if (state & GDK_META_MASK)
     45     modifier_state |= WebKit::WebInputEvent::MetaKey;
     46   return modifier_state;
     47 }
     48 
     49 }  // namespace
     50 
     51 WebDragDestGtk::WebDragDestGtk(WebContents* web_contents, GtkWidget* widget)
     52     : web_contents_(web_contents),
     53       widget_(widget),
     54       context_(NULL),
     55       data_requests_(0),
     56       delegate_(NULL),
     57       canceled_(false),
     58       method_factory_(this) {
     59   gtk_drag_dest_set(widget, static_cast<GtkDestDefaults>(0),
     60                     NULL, 0,
     61                     static_cast<GdkDragAction>(GDK_ACTION_COPY |
     62                                                GDK_ACTION_LINK |
     63                                                GDK_ACTION_MOVE));
     64 
     65   // If adding a handler, make sure to update kNumGtkHandlers and add it to the
     66   // |handlers_| array so that it can be disconnected later on.
     67   handlers_.reset(new int[kNumGtkHandlers]);
     68   handlers_.get()[0] = g_signal_connect(
     69       widget, "drag-motion", G_CALLBACK(OnDragMotionThunk), this);
     70   handlers_.get()[1] = g_signal_connect(
     71       widget, "drag-leave", G_CALLBACK(OnDragLeaveThunk), this);
     72   handlers_.get()[2] = g_signal_connect(
     73       widget, "drag-drop", G_CALLBACK(OnDragDropThunk), this);
     74   handlers_.get()[3] = g_signal_connect(
     75       widget, "drag-data-received", G_CALLBACK(OnDragDataReceivedThunk), this);
     76   // TODO(tony): Need a drag-data-delete handler for moving content out of
     77   // the WebContents.  http://crbug.com/38989
     78 
     79   handlers_.get()[4] = g_signal_connect(
     80       widget, "destroy", G_CALLBACK(gtk_widget_destroyed), &widget_);
     81 }
     82 
     83 WebDragDestGtk::~WebDragDestGtk() {
     84   if (widget_) {
     85     gtk_drag_dest_unset(widget_);
     86     for (int i = 0; i < kNumGtkHandlers; ++i)
     87       g_signal_handler_disconnect(widget_, handlers_.get()[i]);
     88   }
     89 }
     90 
     91 void WebDragDestGtk::UpdateDragStatus(WebDragOperation operation) {
     92   if (context_) {
     93     is_drop_target_ = operation != WebDragOperationNone;
     94     gdk_drag_status(context_, WebDragOpToGdkDragAction(operation),
     95                     drag_over_time_);
     96   }
     97 }
     98 
     99 void WebDragDestGtk::DragLeave() {
    100   GetRenderViewHost()->DragTargetDragLeave();
    101   if (delegate())
    102     delegate()->OnDragLeave();
    103 
    104   drop_data_.reset();
    105 }
    106 
    107 gboolean WebDragDestGtk::OnDragMotion(GtkWidget* sender,
    108                                       GdkDragContext* context,
    109                                       gint x, gint y,
    110                                       guint time) {
    111   if (context_ != context) {
    112     context_ = context;
    113     drop_data_.reset(new DropData);
    114     is_drop_target_ = false;
    115 
    116     if (delegate())
    117       delegate()->DragInitialize(web_contents_);
    118 
    119     // text/plain must come before text/uri-list. This is a hack that works in
    120     // conjunction with OnDragDataReceived. Since some file managers populate
    121     // text/plain with file URLs when dragging files, we want to handle
    122     // text/uri-list after text/plain so that the plain text can be cleared if
    123     // it's a file drag.
    124     static int supported_targets[] = {
    125       ui::TEXT_PLAIN,
    126       ui::TEXT_URI_LIST,
    127       ui::TEXT_HTML,
    128       ui::NETSCAPE_URL,
    129       ui::CHROME_NAMED_URL,
    130       // TODO(estade): support image drags?
    131       ui::CUSTOM_DATA,
    132     };
    133 
    134     // Add the delegate's requested target if applicable. Need to do this here
    135     // since gtk_drag_get_data will dispatch to our drag-data-received.
    136     data_requests_ = arraysize(supported_targets) + (delegate() ? 1 : 0);
    137     for (size_t i = 0; i < arraysize(supported_targets); ++i) {
    138       gtk_drag_get_data(widget_, context,
    139                         ui::GetAtomForTarget(supported_targets[i]),
    140                         time);
    141     }
    142 
    143     if (delegate()) {
    144       gtk_drag_get_data(widget_, context, delegate()->GetBookmarkTargetAtom(),
    145                         time);
    146     }
    147   } else if (data_requests_ == 0) {
    148     if (canceled_)
    149       return FALSE;
    150 
    151     GetRenderViewHost()->DragTargetDragOver(
    152         ui::ClientPoint(widget_),
    153         ui::ScreenPoint(widget_),
    154         GdkDragActionToWebDragOp(context->actions),
    155         GetModifierFlags(widget_));
    156 
    157     if (delegate())
    158       delegate()->OnDragOver();
    159 
    160     drag_over_time_ = time;
    161   }
    162 
    163   // Pretend we are a drag destination because we don't want to wait for
    164   // the renderer to tell us if we really are or not.
    165   return TRUE;
    166 }
    167 
    168 void WebDragDestGtk::OnDragDataReceived(
    169     GtkWidget* sender, GdkDragContext* context, gint x, gint y,
    170     GtkSelectionData* data, guint info, guint time) {
    171   // We might get the data from an old get_data() request that we no longer
    172   // care about.
    173   if (context != context_)
    174     return;
    175 
    176   data_requests_--;
    177 
    178   // Decode the data.
    179   gint data_length = gtk_selection_data_get_length(data);
    180   const guchar* raw_data = gtk_selection_data_get_data(data);
    181   GdkAtom target = gtk_selection_data_get_target(data);
    182   if (raw_data && data_length > 0) {
    183     // If the source can't provide us with valid data for a requested target,
    184     // raw_data will be NULL.
    185     if (target == ui::GetAtomForTarget(ui::TEXT_PLAIN)) {
    186       guchar* text = gtk_selection_data_get_text(data);
    187       if (text) {
    188         drop_data_->text = base::NullableString16(
    189             UTF8ToUTF16(std::string(reinterpret_cast<const char*>(text))),
    190             false);
    191         g_free(text);
    192       }
    193     } else if (target == ui::GetAtomForTarget(ui::TEXT_URI_LIST)) {
    194       gchar** uris = gtk_selection_data_get_uris(data);
    195       if (uris) {
    196         drop_data_->url = GURL();
    197         for (gchar** uri_iter = uris; *uri_iter; uri_iter++) {
    198           // Most file managers populate text/uri-list with file URLs when
    199           // dragging files. To avoid exposing file system paths to web content,
    200           // file URLs are never set as the URL content for the drop.
    201           // TODO(estade): Can the filenames have a non-UTF8 encoding?
    202           GURL url(*uri_iter);
    203           base::FilePath file_path;
    204           if (url.SchemeIs(chrome::kFileScheme) &&
    205               net::FileURLToFilePath(url, &file_path)) {
    206             drop_data_->filenames.push_back(
    207                 DropData::FileInfo(UTF8ToUTF16(file_path.value()), string16()));
    208             // This is a hack. Some file managers also populate text/plain with
    209             // a file URL when dragging files, so we clear it to avoid exposing
    210             // it to the web content.
    211             drop_data_->text = base::NullableString16();
    212           } else if (!drop_data_->url.is_valid()) {
    213             // Also set the first non-file URL as the URL content for the drop.
    214             drop_data_->url = url;
    215           }
    216         }
    217         g_strfreev(uris);
    218       }
    219     } else if (target == ui::GetAtomForTarget(ui::TEXT_HTML)) {
    220       // TODO(estade): Can the html have a non-UTF8 encoding?
    221       drop_data_->html = base::NullableString16(
    222           UTF8ToUTF16(std::string(reinterpret_cast<const char*>(raw_data),
    223                                   data_length)),
    224           false);
    225       // We leave the base URL empty.
    226     } else if (target == ui::GetAtomForTarget(ui::NETSCAPE_URL)) {
    227       std::string netscape_url(reinterpret_cast<const char*>(raw_data),
    228                                data_length);
    229       size_t split = netscape_url.find_first_of('\n');
    230       if (split != std::string::npos) {
    231         drop_data_->url = GURL(netscape_url.substr(0, split));
    232         if (split < netscape_url.size() - 1)
    233           drop_data_->url_title = UTF8ToUTF16(netscape_url.substr(split + 1));
    234       }
    235     } else if (target == ui::GetAtomForTarget(ui::CHROME_NAMED_URL)) {
    236       ui::ExtractNamedURL(data, &drop_data_->url, &drop_data_->url_title);
    237     } else if (target == ui::GetAtomForTarget(ui::CUSTOM_DATA)) {
    238       ui::ReadCustomDataIntoMap(
    239           raw_data, data_length, &drop_data_->custom_data);
    240     }
    241   }
    242 
    243   if (data_requests_ == 0) {
    244     // Give the delegate an opportunity to cancel the drag.
    245     canceled_ = !web_contents_->GetDelegate()->CanDragEnter(
    246         web_contents_,
    247         *drop_data_,
    248         GdkDragActionToWebDragOp(context->actions));
    249     if (canceled_) {
    250       drag_over_time_ = time;
    251       UpdateDragStatus(WebDragOperationNone);
    252       drop_data_.reset();
    253       return;
    254     }
    255   }
    256 
    257   // For CHROME_BOOKMARK_ITEM, we have to handle the case where the drag source
    258   // doesn't have any data available for us. In this case we try to synthesize a
    259   // URL bookmark.
    260   // Note that bookmark drag data is encoded in the same format for both
    261   // GTK and Views, hence we can share the same logic here.
    262   if (delegate() && target == delegate()->GetBookmarkTargetAtom()) {
    263     if (raw_data && data_length > 0) {
    264       delegate()->OnReceiveDataFromGtk(data);
    265     } else {
    266       delegate()->OnReceiveProcessedData(drop_data_->url,
    267                                          drop_data_->url_title);
    268     }
    269   }
    270 
    271   if (data_requests_ == 0) {
    272     // Tell the renderer about the drag.
    273     // |x| and |y| are seemingly arbitrary at this point.
    274     GetRenderViewHost()->DragTargetDragEnter(
    275         *drop_data_.get(),
    276         ui::ClientPoint(widget_),
    277         ui::ScreenPoint(widget_),
    278         GdkDragActionToWebDragOp(context->actions),
    279         GetModifierFlags(widget_));
    280 
    281     if (delegate())
    282       delegate()->OnDragEnter();
    283 
    284     drag_over_time_ = time;
    285   }
    286 }
    287 
    288 // The drag has left our widget; forward this information to the renderer.
    289 void WebDragDestGtk::OnDragLeave(GtkWidget* sender, GdkDragContext* context,
    290                                  guint time) {
    291   // Set |context_| to NULL to make sure we will recognize the next DragMotion
    292   // as an enter.
    293   context_ = NULL;
    294 
    295   if (canceled_)
    296     return;
    297 
    298   // Sometimes we get a drag-leave event before getting a drag-data-received
    299   // event. In that case, we don't want to bother the renderer with a
    300   // DragLeave event.
    301   if (data_requests_ != 0)
    302     return;
    303 
    304   // When GTK sends us a drag-drop signal, it is shortly (and synchronously)
    305   // preceded by a drag-leave. The renderer doesn't like getting the signals
    306   // in this order so delay telling it about the drag-leave till we are sure
    307   // we are not getting a drop as well.
    308   base::MessageLoop::current()->PostTask(
    309       FROM_HERE,
    310       base::Bind(&WebDragDestGtk::DragLeave, method_factory_.GetWeakPtr()));
    311 }
    312 
    313 // Called by GTK when the user releases the mouse, executing a drop.
    314 gboolean WebDragDestGtk::OnDragDrop(GtkWidget* sender, GdkDragContext* context,
    315                                     gint x, gint y, guint time) {
    316   // Cancel that drag leave!
    317   method_factory_.InvalidateWeakPtrs();
    318 
    319   GetRenderViewHost()->
    320       DragTargetDrop(ui::ClientPoint(widget_), ui::ScreenPoint(widget_),
    321           GetModifierFlags(widget_));
    322 
    323   if (delegate())
    324     delegate()->OnDrop();
    325 
    326   // The second parameter is just an educated guess as to whether or not the
    327   // drag succeeded, but at least we will get the drag-end animation right
    328   // sometimes.
    329   gtk_drag_finish(context, is_drop_target_, FALSE, time);
    330 
    331   return TRUE;
    332 }
    333 
    334 RenderViewHostImpl* WebDragDestGtk::GetRenderViewHost() const {
    335   return static_cast<RenderViewHostImpl*>(web_contents_->GetRenderViewHost());
    336 }
    337 
    338 }  // namespace content
    339