web_drag_dest_gtk.cc revision 201ade2fbba22bfb27ae029f4d23fca6ded109a0
1// Copyright (c) 2009 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/tab_contents/web_drag_dest_gtk.h"
6
7#include <string>
8
9#include "app/gtk_dnd_util.h"
10#include "base/file_path.h"
11#include "base/utf_string_conversions.h"
12#include "chrome/browser/bookmarks/bookmark_node_data.h"
13#include "chrome/browser/gtk/bookmark_utils_gtk.h"
14#include "chrome/browser/gtk/gtk_util.h"
15#include "chrome/browser/renderer_host/render_view_host.h"
16#include "chrome/browser/tab_contents/tab_contents.h"
17#include "chrome/common/url_constants.h"
18#include "net/base/net_util.h"
19
20using WebKit::WebDragOperation;
21using WebKit::WebDragOperationNone;
22
23namespace {
24
25// Returns the bookmark target atom, based on the underlying toolkit.
26//
27// For GTK, bookmark drag data is encoded as pickle and associated with
28// gtk_dnd_util::CHROME_BOOKMARK_ITEM. See
29// bookmark_utils::WriteBookmarksToSelection() for details.
30// For Views, bookmark drag data is encoded in the same format, and
31// associated with a custom format. See BookmarkNodeData::Write() for
32// details.
33GdkAtom GetBookmarkTargetAtom() {
34#if defined(TOOLKIT_VIEWS)
35  return BookmarkNodeData::GetBookmarkCustomFormat();
36#else
37  return gtk_dnd_util::GetAtomForTarget(gtk_dnd_util::CHROME_BOOKMARK_ITEM);
38#endif
39}
40
41}  // namespace
42
43WebDragDestGtk::WebDragDestGtk(TabContents* tab_contents, GtkWidget* widget)
44    : tab_contents_(tab_contents),
45      widget_(widget),
46      context_(NULL),
47      method_factory_(this) {
48  gtk_drag_dest_set(widget, static_cast<GtkDestDefaults>(0),
49                    NULL, 0,
50                    static_cast<GdkDragAction>(GDK_ACTION_COPY |
51                                               GDK_ACTION_LINK |
52                                               GDK_ACTION_MOVE));
53  g_signal_connect(widget, "drag-motion",
54                   G_CALLBACK(OnDragMotionThunk), this);
55  g_signal_connect(widget, "drag-leave",
56                   G_CALLBACK(OnDragLeaveThunk), this);
57  g_signal_connect(widget, "drag-drop",
58                   G_CALLBACK(OnDragDropThunk), this);
59  g_signal_connect(widget, "drag-data-received",
60                   G_CALLBACK(OnDragDataReceivedThunk), this);
61  // TODO(tony): Need a drag-data-delete handler for moving content out of
62  // the tab contents.  http://crbug.com/38989
63
64  destroy_handler_ = g_signal_connect(
65      widget, "destroy", G_CALLBACK(gtk_widget_destroyed), &widget_);
66}
67
68WebDragDestGtk::~WebDragDestGtk() {
69  if (widget_) {
70    gtk_drag_dest_unset(widget_);
71    g_signal_handler_disconnect(widget_, destroy_handler_);
72  }
73}
74
75void WebDragDestGtk::UpdateDragStatus(WebDragOperation operation) {
76  if (context_) {
77    is_drop_target_ = operation != WebDragOperationNone;
78    gdk_drag_status(context_, gtk_util::WebDragOpToGdkDragAction(operation),
79                    drag_over_time_);
80  }
81}
82
83void WebDragDestGtk::DragLeave() {
84  tab_contents_->render_view_host()->DragTargetDragLeave();
85
86  if (tab_contents_->GetBookmarkDragDelegate()) {
87    tab_contents_->GetBookmarkDragDelegate()->OnDragLeave(bookmark_drag_data_);
88  }
89}
90
91gboolean WebDragDestGtk::OnDragMotion(GtkWidget* sender,
92                                      GdkDragContext* context,
93                                      gint x, gint y,
94                                      guint time) {
95  if (context_ != context) {
96    context_ = context;
97    drop_data_.reset(new WebDropData);
98    bookmark_drag_data_.Clear();
99    is_drop_target_ = false;
100
101    // text/plain must come before text/uri-list. This is a hack that works in
102    // conjunction with OnDragDataReceived. Since some file managers populate
103    // text/plain with file URLs when dragging files, we want to handle
104    // text/uri-list after text/plain so that the plain text can be cleared if
105    // it's a file drag.
106    static int supported_targets[] = {
107      gtk_dnd_util::TEXT_PLAIN,
108      gtk_dnd_util::TEXT_URI_LIST,
109      gtk_dnd_util::TEXT_HTML,
110      gtk_dnd_util::NETSCAPE_URL,
111      gtk_dnd_util::CHROME_NAMED_URL,
112      // TODO(estade): support image drags?
113    };
114
115    // Add the bookmark target as well.
116    data_requests_ = arraysize(supported_targets) + 1;
117    for (size_t i = 0; i < arraysize(supported_targets); ++i) {
118      gtk_drag_get_data(widget_, context,
119                        gtk_dnd_util::GetAtomForTarget(supported_targets[i]),
120                        time);
121    }
122
123    gtk_drag_get_data(widget_, context, GetBookmarkTargetAtom(), time);
124  } else if (data_requests_ == 0) {
125    tab_contents_->render_view_host()->
126        DragTargetDragOver(
127            gtk_util::ClientPoint(widget_),
128            gtk_util::ScreenPoint(widget_),
129            gtk_util::GdkDragActionToWebDragOp(context->actions));
130    if (tab_contents_->GetBookmarkDragDelegate())
131      tab_contents_->GetBookmarkDragDelegate()->OnDragOver(bookmark_drag_data_);
132    drag_over_time_ = time;
133  }
134
135  // Pretend we are a drag destination because we don't want to wait for
136  // the renderer to tell us if we really are or not.
137  return TRUE;
138}
139
140void WebDragDestGtk::OnDragDataReceived(
141    GtkWidget* sender, GdkDragContext* context, gint x, gint y,
142    GtkSelectionData* data, guint info, guint time) {
143  // We might get the data from an old get_data() request that we no longer
144  // care about.
145  if (context != context_)
146    return;
147
148  data_requests_--;
149
150  // Decode the data.
151  if (data->data && data->length > 0) {
152    // If the source can't provide us with valid data for a requested target,
153    // data->data will be NULL.
154    if (data->target ==
155        gtk_dnd_util::GetAtomForTarget(gtk_dnd_util::TEXT_PLAIN)) {
156      guchar* text = gtk_selection_data_get_text(data);
157      if (text) {
158        drop_data_->plain_text =
159            UTF8ToUTF16(std::string(reinterpret_cast<char*>(text),
160                                    data->length));
161        g_free(text);
162      }
163    } else if (data->target ==
164               gtk_dnd_util::GetAtomForTarget(gtk_dnd_util::TEXT_URI_LIST)) {
165      gchar** uris = gtk_selection_data_get_uris(data);
166      if (uris) {
167        drop_data_->url = GURL();
168        for (gchar** uri_iter = uris; *uri_iter; uri_iter++) {
169          // Most file managers populate text/uri-list with file URLs when
170          // dragging files. To avoid exposing file system paths to web content,
171          // file URLs are never set as the URL content for the drop.
172          // TODO(estade): Can the filenames have a non-UTF8 encoding?
173          GURL url(*uri_iter);
174          FilePath file_path;
175          if (url.SchemeIs(chrome::kFileScheme) &&
176              net::FileURLToFilePath(url, &file_path)) {
177            drop_data_->filenames.push_back(UTF8ToUTF16(file_path.value()));
178            // This is a hack. Some file managers also populate text/plain with
179            // a file URL when dragging files, so we clear it to avoid exposing
180            // it to the web content.
181            drop_data_->plain_text.clear();
182          } else if (!drop_data_->url.is_valid()) {
183            // Also set the first non-file URL as the URL content for the drop.
184            drop_data_->url = url;
185          }
186        }
187        g_strfreev(uris);
188      }
189    } else if (data->target ==
190               gtk_dnd_util::GetAtomForTarget(gtk_dnd_util::TEXT_HTML)) {
191      // TODO(estade): Can the html have a non-UTF8 encoding?
192      drop_data_->text_html =
193          UTF8ToUTF16(std::string(reinterpret_cast<char*>(data->data),
194                                  data->length));
195      // We leave the base URL empty.
196    } else if (data->target ==
197               gtk_dnd_util::GetAtomForTarget(gtk_dnd_util::NETSCAPE_URL)) {
198      std::string netscape_url(reinterpret_cast<char*>(data->data),
199                               data->length);
200      size_t split = netscape_url.find_first_of('\n');
201      if (split != std::string::npos) {
202        drop_data_->url = GURL(netscape_url.substr(0, split));
203        if (split < netscape_url.size() - 1)
204          drop_data_->url_title = UTF8ToUTF16(netscape_url.substr(split + 1));
205      }
206    } else if (data->target ==
207               gtk_dnd_util::GetAtomForTarget(gtk_dnd_util::CHROME_NAMED_URL)) {
208      gtk_dnd_util::ExtractNamedURL(data,
209                                    &drop_data_->url, &drop_data_->url_title);
210    }
211  }
212
213  // For CHROME_BOOKMARK_ITEM, we have to handle the case where the drag source
214  // doesn't have any data available for us. In this case we try to synthesize a
215  // URL bookmark.
216  // Note that bookmark drag data is encoded in the same format for both
217  // GTK and Views, hence we can share the same logic here.
218  if (data->target == GetBookmarkTargetAtom()) {
219    if (data->data && data->length > 0) {
220      bookmark_drag_data_.ReadFromVector(
221          bookmark_utils::GetNodesFromSelection(
222              NULL, data,
223              gtk_dnd_util::CHROME_BOOKMARK_ITEM,
224              tab_contents_->profile(), NULL, NULL));
225      bookmark_drag_data_.SetOriginatingProfile(tab_contents_->profile());
226    } else {
227      bookmark_drag_data_.ReadFromTuple(drop_data_->url,
228                                        drop_data_->url_title);
229    }
230  }
231
232  if (data_requests_ == 0) {
233    // Tell the renderer about the drag.
234    // |x| and |y| are seemingly arbitrary at this point.
235    tab_contents_->render_view_host()->
236        DragTargetDragEnter(*drop_data_.get(),
237            gtk_util::ClientPoint(widget_),
238            gtk_util::ScreenPoint(widget_),
239            gtk_util::GdkDragActionToWebDragOp(context->actions));
240
241    // This is non-null if tab_contents_ is showing an ExtensionDOMUI with
242    // support for (at the moment experimental) drag and drop extensions.
243    if (tab_contents_->GetBookmarkDragDelegate()) {
244      tab_contents_->GetBookmarkDragDelegate()->OnDragEnter(
245          bookmark_drag_data_);
246    }
247
248    drag_over_time_ = time;
249  }
250}
251
252// The drag has left our widget; forward this information to the renderer.
253void WebDragDestGtk::OnDragLeave(GtkWidget* sender, GdkDragContext* context,
254                                 guint time) {
255  // Set |context_| to NULL to make sure we will recognize the next DragMotion
256  // as an enter.
257  context_ = NULL;
258  drop_data_.reset();
259  // When GTK sends us a drag-drop signal, it is shortly (and synchronously)
260  // preceded by a drag-leave. The renderer doesn't like getting the signals
261  // in this order so delay telling it about the drag-leave till we are sure
262  // we are not getting a drop as well.
263  MessageLoop::current()->PostTask(FROM_HERE,
264      method_factory_.NewRunnableMethod(&WebDragDestGtk::DragLeave));
265}
266
267// Called by GTK when the user releases the mouse, executing a drop.
268gboolean WebDragDestGtk::OnDragDrop(GtkWidget* sender, GdkDragContext* context,
269                                    gint x, gint y, guint time) {
270  // Cancel that drag leave!
271  method_factory_.RevokeAll();
272
273  tab_contents_->render_view_host()->
274      DragTargetDrop(gtk_util::ClientPoint(widget_),
275                     gtk_util::ScreenPoint(widget_));
276
277  // This is non-null if tab_contents_ is showing an ExtensionDOMUI with
278  // support for (at the moment experimental) drag and drop extensions.
279  if (tab_contents_->GetBookmarkDragDelegate())
280    tab_contents_->GetBookmarkDragDelegate()->OnDrop(bookmark_drag_data_);
281
282  // The second parameter is just an educated guess as to whether or not the
283  // drag succeeded, but at least we will get the drag-end animation right
284  // sometimes.
285  gtk_drag_finish(context, is_drop_target_, FALSE, time);
286  return TRUE;
287}
288