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