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/tab_contents/web_drag_dest_gtk.h"
6
7 #include <string>
8
9 #include "base/file_path.h"
10 #include "base/utf_string_conversions.h"
11 #include "chrome/browser/bookmarks/bookmark_node_data.h"
12 #include "chrome/browser/ui/gtk/bookmarks/bookmark_utils_gtk.h"
13 #include "chrome/browser/ui/gtk/gtk_util.h"
14 #include "chrome/common/url_constants.h"
15 #include "content/browser/renderer_host/render_view_host.h"
16 #include "content/browser/tab_contents/tab_contents.h"
17 #include "net/base/net_util.h"
18 #include "ui/base/dragdrop/gtk_dnd_util.h"
19
20 using WebKit::WebDragOperation;
21 using WebKit::WebDragOperationNone;
22
23 namespace {
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 // ui::CHROME_BOOKMARK_ITEM. See // bookmark_utils::WriteBookmarksToSelection()
29 // 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.
GetBookmarkTargetAtom()33 GdkAtom GetBookmarkTargetAtom() {
34 #if defined(TOOLKIT_VIEWS)
35 return BookmarkNodeData::GetBookmarkCustomFormat();
36 #else
37 return ui::GetAtomForTarget(ui::CHROME_BOOKMARK_ITEM);
38 #endif
39 }
40
41 } // namespace
42
WebDragDestGtk(TabContents * tab_contents,GtkWidget * widget)43 WebDragDestGtk::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
~WebDragDestGtk()68 WebDragDestGtk::~WebDragDestGtk() {
69 if (widget_) {
70 gtk_drag_dest_unset(widget_);
71 g_signal_handler_disconnect(widget_, destroy_handler_);
72 }
73 }
74
UpdateDragStatus(WebDragOperation operation)75 void 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
DragLeave()83 void 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
OnDragMotion(GtkWidget * sender,GdkDragContext * context,gint x,gint y,guint time)91 gboolean 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 ui::TEXT_PLAIN,
108 ui::TEXT_URI_LIST,
109 ui::TEXT_HTML,
110 ui::NETSCAPE_URL,
111 ui::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 ui::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
OnDragDataReceived(GtkWidget * sender,GdkDragContext * context,gint x,gint y,GtkSelectionData * data,guint info,guint time)140 void 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 == ui::GetAtomForTarget(ui::TEXT_PLAIN)) {
155 guchar* text = gtk_selection_data_get_text(data);
156 if (text) {
157 drop_data_->plain_text =
158 UTF8ToUTF16(std::string(reinterpret_cast<char*>(text),
159 data->length));
160 g_free(text);
161 }
162 } else if (data->target == ui::GetAtomForTarget(ui::TEXT_URI_LIST)) {
163 gchar** uris = gtk_selection_data_get_uris(data);
164 if (uris) {
165 drop_data_->url = GURL();
166 for (gchar** uri_iter = uris; *uri_iter; uri_iter++) {
167 // Most file managers populate text/uri-list with file URLs when
168 // dragging files. To avoid exposing file system paths to web content,
169 // file URLs are never set as the URL content for the drop.
170 // TODO(estade): Can the filenames have a non-UTF8 encoding?
171 GURL url(*uri_iter);
172 FilePath file_path;
173 if (url.SchemeIs(chrome::kFileScheme) &&
174 net::FileURLToFilePath(url, &file_path)) {
175 drop_data_->filenames.push_back(UTF8ToUTF16(file_path.value()));
176 // This is a hack. Some file managers also populate text/plain with
177 // a file URL when dragging files, so we clear it to avoid exposing
178 // it to the web content.
179 drop_data_->plain_text.clear();
180 } else if (!drop_data_->url.is_valid()) {
181 // Also set the first non-file URL as the URL content for the drop.
182 drop_data_->url = url;
183 }
184 }
185 g_strfreev(uris);
186 }
187 } else if (data->target == ui::GetAtomForTarget(ui::TEXT_HTML)) {
188 // TODO(estade): Can the html have a non-UTF8 encoding?
189 drop_data_->text_html =
190 UTF8ToUTF16(std::string(reinterpret_cast<char*>(data->data),
191 data->length));
192 // We leave the base URL empty.
193 } else if (data->target == ui::GetAtomForTarget(ui::NETSCAPE_URL)) {
194 std::string netscape_url(reinterpret_cast<char*>(data->data),
195 data->length);
196 size_t split = netscape_url.find_first_of('\n');
197 if (split != std::string::npos) {
198 drop_data_->url = GURL(netscape_url.substr(0, split));
199 if (split < netscape_url.size() - 1)
200 drop_data_->url_title = UTF8ToUTF16(netscape_url.substr(split + 1));
201 }
202 } else if (data->target == ui::GetAtomForTarget(ui::CHROME_NAMED_URL)) {
203 ui::ExtractNamedURL(data, &drop_data_->url, &drop_data_->url_title);
204 }
205 }
206
207 // For CHROME_BOOKMARK_ITEM, we have to handle the case where the drag source
208 // doesn't have any data available for us. In this case we try to synthesize a
209 // URL bookmark.
210 // Note that bookmark drag data is encoded in the same format for both
211 // GTK and Views, hence we can share the same logic here.
212 if (data->target == GetBookmarkTargetAtom()) {
213 if (data->data && data->length > 0) {
214 bookmark_drag_data_.ReadFromVector(
215 bookmark_utils::GetNodesFromSelection(
216 NULL, data,
217 ui::CHROME_BOOKMARK_ITEM,
218 tab_contents_->profile(), NULL, NULL));
219 bookmark_drag_data_.SetOriginatingProfile(tab_contents_->profile());
220 } else {
221 bookmark_drag_data_.ReadFromTuple(drop_data_->url,
222 drop_data_->url_title);
223 }
224 }
225
226 if (data_requests_ == 0) {
227 // Tell the renderer about the drag.
228 // |x| and |y| are seemingly arbitrary at this point.
229 tab_contents_->render_view_host()->
230 DragTargetDragEnter(*drop_data_.get(),
231 gtk_util::ClientPoint(widget_),
232 gtk_util::ScreenPoint(widget_),
233 gtk_util::GdkDragActionToWebDragOp(context->actions));
234
235 // This is non-null if tab_contents_ is showing an ExtensionWebUI with
236 // support for (at the moment experimental) drag and drop extensions.
237 if (tab_contents_->GetBookmarkDragDelegate()) {
238 tab_contents_->GetBookmarkDragDelegate()->OnDragEnter(
239 bookmark_drag_data_);
240 }
241
242 drag_over_time_ = time;
243 }
244 }
245
246 // The drag has left our widget; forward this information to the renderer.
OnDragLeave(GtkWidget * sender,GdkDragContext * context,guint time)247 void WebDragDestGtk::OnDragLeave(GtkWidget* sender, GdkDragContext* context,
248 guint time) {
249 // Set |context_| to NULL to make sure we will recognize the next DragMotion
250 // as an enter.
251 context_ = NULL;
252 drop_data_.reset();
253 // When GTK sends us a drag-drop signal, it is shortly (and synchronously)
254 // preceded by a drag-leave. The renderer doesn't like getting the signals
255 // in this order so delay telling it about the drag-leave till we are sure
256 // we are not getting a drop as well.
257 MessageLoop::current()->PostTask(FROM_HERE,
258 method_factory_.NewRunnableMethod(&WebDragDestGtk::DragLeave));
259 }
260
261 // Called by GTK when the user releases the mouse, executing a drop.
OnDragDrop(GtkWidget * sender,GdkDragContext * context,gint x,gint y,guint time)262 gboolean WebDragDestGtk::OnDragDrop(GtkWidget* sender, GdkDragContext* context,
263 gint x, gint y, guint time) {
264 // Cancel that drag leave!
265 method_factory_.RevokeAll();
266
267 tab_contents_->render_view_host()->
268 DragTargetDrop(gtk_util::ClientPoint(widget_),
269 gtk_util::ScreenPoint(widget_));
270
271 // This is non-null if tab_contents_ is showing an ExtensionWebUI with
272 // support for (at the moment experimental) drag and drop extensions.
273 if (tab_contents_->GetBookmarkDragDelegate())
274 tab_contents_->GetBookmarkDragDelegate()->OnDrop(bookmark_drag_data_);
275
276 // The second parameter is just an educated guess as to whether or not the
277 // drag succeeded, but at least we will get the drag-end animation right
278 // sometimes.
279 gtk_drag_finish(context, is_drop_target_, FALSE, time);
280 return TRUE;
281 }
282