// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/tab_contents/web_drag_dest_gtk.h" #include <string> #include "base/file_path.h" #include "base/utf_string_conversions.h" #include "chrome/browser/bookmarks/bookmark_node_data.h" #include "chrome/browser/ui/gtk/bookmarks/bookmark_utils_gtk.h" #include "chrome/browser/ui/gtk/gtk_util.h" #include "chrome/common/url_constants.h" #include "content/browser/renderer_host/render_view_host.h" #include "content/browser/tab_contents/tab_contents.h" #include "net/base/net_util.h" #include "ui/base/dragdrop/gtk_dnd_util.h" using WebKit::WebDragOperation; using WebKit::WebDragOperationNone; namespace { // Returns the bookmark target atom, based on the underlying toolkit. // // For GTK, bookmark drag data is encoded as pickle and associated with // ui::CHROME_BOOKMARK_ITEM. See // bookmark_utils::WriteBookmarksToSelection() // for details. // For Views, bookmark drag data is encoded in the same format, and // associated with a custom format. See BookmarkNodeData::Write() for // details. GdkAtom GetBookmarkTargetAtom() { #if defined(TOOLKIT_VIEWS) return BookmarkNodeData::GetBookmarkCustomFormat(); #else return ui::GetAtomForTarget(ui::CHROME_BOOKMARK_ITEM); #endif } } // namespace WebDragDestGtk::WebDragDestGtk(TabContents* tab_contents, GtkWidget* widget) : tab_contents_(tab_contents), widget_(widget), context_(NULL), method_factory_(this) { gtk_drag_dest_set(widget, static_cast<GtkDestDefaults>(0), NULL, 0, static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_LINK | GDK_ACTION_MOVE)); g_signal_connect(widget, "drag-motion", G_CALLBACK(OnDragMotionThunk), this); g_signal_connect(widget, "drag-leave", G_CALLBACK(OnDragLeaveThunk), this); g_signal_connect(widget, "drag-drop", G_CALLBACK(OnDragDropThunk), this); g_signal_connect(widget, "drag-data-received", G_CALLBACK(OnDragDataReceivedThunk), this); // TODO(tony): Need a drag-data-delete handler for moving content out of // the tab contents. http://crbug.com/38989 destroy_handler_ = g_signal_connect( widget, "destroy", G_CALLBACK(gtk_widget_destroyed), &widget_); } WebDragDestGtk::~WebDragDestGtk() { if (widget_) { gtk_drag_dest_unset(widget_); g_signal_handler_disconnect(widget_, destroy_handler_); } } void WebDragDestGtk::UpdateDragStatus(WebDragOperation operation) { if (context_) { is_drop_target_ = operation != WebDragOperationNone; gdk_drag_status(context_, gtk_util::WebDragOpToGdkDragAction(operation), drag_over_time_); } } void WebDragDestGtk::DragLeave() { tab_contents_->render_view_host()->DragTargetDragLeave(); if (tab_contents_->GetBookmarkDragDelegate()) { tab_contents_->GetBookmarkDragDelegate()->OnDragLeave(bookmark_drag_data_); } } gboolean WebDragDestGtk::OnDragMotion(GtkWidget* sender, GdkDragContext* context, gint x, gint y, guint time) { if (context_ != context) { context_ = context; drop_data_.reset(new WebDropData); bookmark_drag_data_.Clear(); is_drop_target_ = false; // text/plain must come before text/uri-list. This is a hack that works in // conjunction with OnDragDataReceived. Since some file managers populate // text/plain with file URLs when dragging files, we want to handle // text/uri-list after text/plain so that the plain text can be cleared if // it's a file drag. static int supported_targets[] = { ui::TEXT_PLAIN, ui::TEXT_URI_LIST, ui::TEXT_HTML, ui::NETSCAPE_URL, ui::CHROME_NAMED_URL, // TODO(estade): support image drags? }; // Add the bookmark target as well. data_requests_ = arraysize(supported_targets) + 1; for (size_t i = 0; i < arraysize(supported_targets); ++i) { gtk_drag_get_data(widget_, context, ui::GetAtomForTarget(supported_targets[i]), time); } gtk_drag_get_data(widget_, context, GetBookmarkTargetAtom(), time); } else if (data_requests_ == 0) { tab_contents_->render_view_host()-> DragTargetDragOver( gtk_util::ClientPoint(widget_), gtk_util::ScreenPoint(widget_), gtk_util::GdkDragActionToWebDragOp(context->actions)); if (tab_contents_->GetBookmarkDragDelegate()) tab_contents_->GetBookmarkDragDelegate()->OnDragOver(bookmark_drag_data_); drag_over_time_ = time; } // Pretend we are a drag destination because we don't want to wait for // the renderer to tell us if we really are or not. return TRUE; } void WebDragDestGtk::OnDragDataReceived( GtkWidget* sender, GdkDragContext* context, gint x, gint y, GtkSelectionData* data, guint info, guint time) { // We might get the data from an old get_data() request that we no longer // care about. if (context != context_) return; data_requests_--; // Decode the data. if (data->data && data->length > 0) { // If the source can't provide us with valid data for a requested target, // data->data will be NULL. if (data->target == ui::GetAtomForTarget(ui::TEXT_PLAIN)) { guchar* text = gtk_selection_data_get_text(data); if (text) { drop_data_->plain_text = UTF8ToUTF16(std::string(reinterpret_cast<char*>(text), data->length)); g_free(text); } } else if (data->target == ui::GetAtomForTarget(ui::TEXT_URI_LIST)) { gchar** uris = gtk_selection_data_get_uris(data); if (uris) { drop_data_->url = GURL(); for (gchar** uri_iter = uris; *uri_iter; uri_iter++) { // Most file managers populate text/uri-list with file URLs when // dragging files. To avoid exposing file system paths to web content, // file URLs are never set as the URL content for the drop. // TODO(estade): Can the filenames have a non-UTF8 encoding? GURL url(*uri_iter); FilePath file_path; if (url.SchemeIs(chrome::kFileScheme) && net::FileURLToFilePath(url, &file_path)) { drop_data_->filenames.push_back(UTF8ToUTF16(file_path.value())); // This is a hack. Some file managers also populate text/plain with // a file URL when dragging files, so we clear it to avoid exposing // it to the web content. drop_data_->plain_text.clear(); } else if (!drop_data_->url.is_valid()) { // Also set the first non-file URL as the URL content for the drop. drop_data_->url = url; } } g_strfreev(uris); } } else if (data->target == ui::GetAtomForTarget(ui::TEXT_HTML)) { // TODO(estade): Can the html have a non-UTF8 encoding? drop_data_->text_html = UTF8ToUTF16(std::string(reinterpret_cast<char*>(data->data), data->length)); // We leave the base URL empty. } else if (data->target == ui::GetAtomForTarget(ui::NETSCAPE_URL)) { std::string netscape_url(reinterpret_cast<char*>(data->data), data->length); size_t split = netscape_url.find_first_of('\n'); if (split != std::string::npos) { drop_data_->url = GURL(netscape_url.substr(0, split)); if (split < netscape_url.size() - 1) drop_data_->url_title = UTF8ToUTF16(netscape_url.substr(split + 1)); } } else if (data->target == ui::GetAtomForTarget(ui::CHROME_NAMED_URL)) { ui::ExtractNamedURL(data, &drop_data_->url, &drop_data_->url_title); } } // For CHROME_BOOKMARK_ITEM, we have to handle the case where the drag source // doesn't have any data available for us. In this case we try to synthesize a // URL bookmark. // Note that bookmark drag data is encoded in the same format for both // GTK and Views, hence we can share the same logic here. if (data->target == GetBookmarkTargetAtom()) { if (data->data && data->length > 0) { bookmark_drag_data_.ReadFromVector( bookmark_utils::GetNodesFromSelection( NULL, data, ui::CHROME_BOOKMARK_ITEM, tab_contents_->profile(), NULL, NULL)); bookmark_drag_data_.SetOriginatingProfile(tab_contents_->profile()); } else { bookmark_drag_data_.ReadFromTuple(drop_data_->url, drop_data_->url_title); } } if (data_requests_ == 0) { // Tell the renderer about the drag. // |x| and |y| are seemingly arbitrary at this point. tab_contents_->render_view_host()-> DragTargetDragEnter(*drop_data_.get(), gtk_util::ClientPoint(widget_), gtk_util::ScreenPoint(widget_), gtk_util::GdkDragActionToWebDragOp(context->actions)); // This is non-null if tab_contents_ is showing an ExtensionWebUI with // support for (at the moment experimental) drag and drop extensions. if (tab_contents_->GetBookmarkDragDelegate()) { tab_contents_->GetBookmarkDragDelegate()->OnDragEnter( bookmark_drag_data_); } drag_over_time_ = time; } } // The drag has left our widget; forward this information to the renderer. void WebDragDestGtk::OnDragLeave(GtkWidget* sender, GdkDragContext* context, guint time) { // Set |context_| to NULL to make sure we will recognize the next DragMotion // as an enter. context_ = NULL; drop_data_.reset(); // When GTK sends us a drag-drop signal, it is shortly (and synchronously) // preceded by a drag-leave. The renderer doesn't like getting the signals // in this order so delay telling it about the drag-leave till we are sure // we are not getting a drop as well. MessageLoop::current()->PostTask(FROM_HERE, method_factory_.NewRunnableMethod(&WebDragDestGtk::DragLeave)); } // Called by GTK when the user releases the mouse, executing a drop. gboolean WebDragDestGtk::OnDragDrop(GtkWidget* sender, GdkDragContext* context, gint x, gint y, guint time) { // Cancel that drag leave! method_factory_.RevokeAll(); tab_contents_->render_view_host()-> DragTargetDrop(gtk_util::ClientPoint(widget_), gtk_util::ScreenPoint(widget_)); // This is non-null if tab_contents_ is showing an ExtensionWebUI with // support for (at the moment experimental) drag and drop extensions. if (tab_contents_->GetBookmarkDragDelegate()) tab_contents_->GetBookmarkDragDelegate()->OnDrop(bookmark_drag_data_); // The second parameter is just an educated guess as to whether or not the // drag succeeded, but at least we will get the drag-end animation right // sometimes. gtk_drag_finish(context, is_drop_target_, FALSE, time); return TRUE; }