// Copyright (c) 2009 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/gtk/bookmark_bar_gtk.h" #include "app/gfx/gtk_util.h" #include "app/gfx/text_elider.h" #include "app/l10n_util.h" #include "app/resource_bundle.h" #include "base/gfx/gtk_util.h" #include "base/pickle.h" #include "chrome/browser/bookmarks/bookmark_context_menu.h" #include "chrome/browser/bookmarks/bookmark_drag_data.h" #include "chrome/browser/bookmarks/bookmark_menu_controller_gtk.h" #include "chrome/browser/bookmarks/bookmark_utils.h" #include "chrome/browser/browser.h" #include "chrome/browser/gtk/custom_button.h" #include "chrome/browser/gtk/gtk_chrome_button.h" #include "chrome/browser/metrics/user_metrics.h" #include "chrome/browser/profile.h" #include "chrome/common/gtk_util.h" #include "chrome/common/pref_names.h" #include "chrome/common/pref_service.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" namespace { const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xe6, 0xed, 0xf4); // Padding around the container. const int kBarPadding = 2; // Maximum number of characters on a bookmark button. const size_t kMaxCharsOnAButton = 15; // Used in gtk_selection_data_set(). (I assume from this parameter that gtk has // to some really exotic hardware...) const int kBitsInAByte = 8; // Dictionary key used to store a BookmarkNode* on a GtkWidget. const char kBookmarkNode[] = "bookmark-node"; // Integers representing the various types of items that the bookmark bar // accepts as DnD types. enum BookmarkType { DROP_TARGET_INTERNAL }; // Mime types for DnD. Used to synchronize across applications. const char kInternalURIType[] = "application/x-chrome-bookmark-item"; // Table of the mime types that we accept with their options. const GtkTargetEntry kTargetTable[] = { { const_cast(kInternalURIType), GTK_TARGET_SAME_APP, DROP_TARGET_INTERNAL } // TODO(erg): Add "text/uri-list" support. }; const int kTargetTableSize = G_N_ELEMENTS(kTargetTable); } // namespace BookmarkBarGtk::BookmarkBarGtk(Profile* profile, Browser* browser) : profile_(NULL), page_navigator_(NULL), browser_(browser), model_(NULL), instructions_(NULL), show_instructions_(true) { Init(profile); SetProfile(profile); } BookmarkBarGtk::~BookmarkBarGtk() { if (model_) model_->RemoveObserver(this); RemoveAllBookmarkButtons(); bookmark_toolbar_.Destroy(); container_.Destroy(); } void BookmarkBarGtk::SetProfile(Profile* profile) { DCHECK(profile); if (profile_ == profile) return; RemoveAllBookmarkButtons(); profile_ = profile; if (model_) model_->RemoveObserver(this); gtk_widget_set_sensitive(other_bookmarks_button_, false); // TODO(erg): Handle extensions model_ = profile_->GetBookmarkModel(); model_->AddObserver(this); if (model_->IsLoaded()) Loaded(model_); // else case: we'll receive notification back from the BookmarkModel when done // loading, then we'll populate the bar. } void BookmarkBarGtk::SetPageNavigator(PageNavigator* navigator) { page_navigator_ = navigator; } void BookmarkBarGtk::Init(Profile* profile) { // Load the default images from the resource bundle. ResourceBundle& rb = ResourceBundle::GetSharedInstance(); static GdkPixbuf* folder_icon = rb.GetPixbufNamed(IDR_BOOKMARK_BAR_FOLDER); bookmark_hbox_ = gtk_hbox_new(FALSE, 0); container_.Own(gtk_util::CreateGtkBorderBin(bookmark_hbox_, &kBackgroundColor, kBarPadding, kBarPadding, kBarPadding, kBarPadding)); instructions_ = gtk_label_new( l10n_util::GetStringUTF8(IDS_BOOKMARKS_NO_ITEMS).c_str()); gtk_box_pack_start(GTK_BOX(bookmark_hbox_), instructions_, FALSE, FALSE, 0); bookmark_toolbar_.Own(gtk_toolbar_new()); g_signal_connect(G_OBJECT(bookmark_toolbar_.get()), "expose-event", G_CALLBACK(&OnToolbarExpose), this); gtk_box_pack_start(GTK_BOX(bookmark_hbox_), bookmark_toolbar_.get(), TRUE, TRUE, 0); gtk_drag_dest_set(bookmark_toolbar_.get(), GTK_DEST_DEFAULT_DROP, kTargetTable, kTargetTableSize, GDK_ACTION_MOVE); g_signal_connect(bookmark_toolbar_.get(), "drag-motion", G_CALLBACK(&OnToolbarDragMotion), this); g_signal_connect(bookmark_toolbar_.get(), "drag-leave", G_CALLBACK(&OnToolbarDragLeave), this); g_signal_connect(bookmark_toolbar_.get(), "drag-drop", G_CALLBACK(&OnToolbarDragDrop), this); g_signal_connect(bookmark_toolbar_.get(), "drag-data-received", G_CALLBACK(&OnToolbarDragReceived), this); g_signal_connect(bookmark_toolbar_.get(), "button-press-event", G_CALLBACK(&OnButtonPressed), this); gtk_box_pack_start(GTK_BOX(bookmark_hbox_), gtk_vseparator_new(), FALSE, FALSE, 0); other_bookmarks_button_ = gtk_chrome_button_new(); ConnectFolderButtonEvents(other_bookmarks_button_); gtk_button_set_label( GTK_BUTTON(other_bookmarks_button_), l10n_util::GetStringUTF8(IDS_BOOMARK_BAR_OTHER_BOOKMARKED).c_str()); gtk_button_set_image(GTK_BUTTON(other_bookmarks_button_), gtk_image_new_from_pixbuf(folder_icon)); gtk_box_pack_start(GTK_BOX(bookmark_hbox_), other_bookmarks_button_, FALSE, FALSE, 0); } void BookmarkBarGtk::AddBookmarkbarToBox(GtkWidget* box) { gtk_box_pack_start(GTK_BOX(box), container_.get(), FALSE, FALSE, 0); } void BookmarkBarGtk::Show() { gtk_widget_show_all(container_.get()); // Maybe show the instructions if (show_instructions_) { gtk_widget_show(instructions_); } else { gtk_widget_hide(instructions_); } } void BookmarkBarGtk::Hide() { gtk_widget_hide_all(container_.get()); } bool BookmarkBarGtk::OnNewTabPage() { return (browser_ && browser_->GetSelectedTabContents() && browser_->GetSelectedTabContents()->IsBookmarkBarAlwaysVisible()); } void BookmarkBarGtk::Loaded(BookmarkModel* model) { RemoveAllBookmarkButtons(); BookmarkNode* node = model_->GetBookmarkBarNode(); DCHECK(node && model_->other_node()); CreateAllBookmarkButtons(node); gtk_widget_set_sensitive(other_bookmarks_button_, true); } void BookmarkBarGtk::BookmarkModelBeingDeleted(BookmarkModel* model) { // The bookmark model should never be deleted before us. This code exists // to check for regressions in shutdown code and not crash. NOTREACHED(); // Do minimal cleanup, presumably we'll be deleted shortly. model_->RemoveObserver(this); model_ = NULL; } void BookmarkBarGtk::BookmarkNodeMoved(BookmarkModel* model, BookmarkNode* old_parent, int old_index, BookmarkNode* new_parent, int new_index) { BookmarkNodeRemoved(model, old_parent, old_index); BookmarkNodeAdded(model, new_parent, new_index); } void BookmarkBarGtk::BookmarkNodeAdded(BookmarkModel* model, BookmarkNode* parent, int index) { if (parent != model_->GetBookmarkBarNode()) { // We only care about nodes on the bookmark bar. return; } DCHECK(index >= 0 && index <= GetBookmarkButtonCount()); gtk_toolbar_insert(GTK_TOOLBAR(bookmark_toolbar_.get()), CreateBookmarkToolItem(parent->GetChild(index)), index); SetInstructionState(parent); } void BookmarkBarGtk::BookmarkNodeRemoved(BookmarkModel* model, BookmarkNode* parent, int index) { if (parent != model_->GetBookmarkBarNode()) { // We only care about nodes on the bookmark bar. return; } DCHECK(index >= 0 && index < GetBookmarkButtonCount()); GtkWidget* to_remove = GTK_WIDGET(gtk_toolbar_get_nth_item( GTK_TOOLBAR(bookmark_toolbar_.get()), index)); gtk_container_remove(GTK_CONTAINER(bookmark_toolbar_.get()), to_remove); SetInstructionState(parent); } void BookmarkBarGtk::BookmarkNodeChanged(BookmarkModel* model, BookmarkNode* node) { if (node->GetParent() != model_->GetBookmarkBarNode()) { // We only care about nodes on the bookmark bar. return; } int index = model_->GetBookmarkBarNode()->IndexOfChild(node); DCHECK(index != -1); GtkToolItem* item = gtk_toolbar_get_nth_item( GTK_TOOLBAR(bookmark_toolbar_.get()), index); GtkWidget* button = gtk_bin_get_child(GTK_BIN(item)); ConfigureButtonForNode(node, button); } void BookmarkBarGtk::BookmarkNodeFavIconLoaded(BookmarkModel* model, BookmarkNode* node) { BookmarkNodeChanged(model, node); } void BookmarkBarGtk::BookmarkNodeChildrenReordered(BookmarkModel* model, BookmarkNode* node) { if (node != model_->GetBookmarkBarNode()) return; // We only care about reordering of the bookmark bar node. // Purge and rebuild the bar. RemoveAllBookmarkButtons(); CreateAllBookmarkButtons(node); } void BookmarkBarGtk::CreateAllBookmarkButtons(BookmarkNode* node) { // Create a button for each of the children on the bookmark bar. for (int i = 0; i < node->GetChildCount(); ++i) { GtkToolItem* item = CreateBookmarkToolItem(node->GetChild(i)); gtk_toolbar_insert(GTK_TOOLBAR(bookmark_toolbar_.get()), item, -1); } SetInstructionState(node); } void BookmarkBarGtk::SetInstructionState(BookmarkNode* boomarks_bar_node) { show_instructions_ = (boomarks_bar_node->GetChildCount() == 0); if (show_instructions_) { gtk_widget_show(instructions_); } else { gtk_widget_hide(instructions_); } } void BookmarkBarGtk::RemoveAllBookmarkButtons() { gtk_util::RemoveAllChildren(bookmark_toolbar_.get()); } int BookmarkBarGtk::GetBookmarkButtonCount() { GList* children = gtk_container_get_children( GTK_CONTAINER(bookmark_toolbar_.get())); int count = g_list_length(children); g_list_free(children); return count; } bool BookmarkBarGtk::IsAlwaysShown() { return profile_->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); } void BookmarkBarGtk::ConfigureButtonForNode(BookmarkNode* node, GtkWidget* button) { gtk_widget_set_tooltip_text(button, BuildTooltip(node).c_str()); // TODO(erg): Consider a soft maximum instead of this hard 15. std::wstring title = node->GetTitle(); title = title.substr(0, std::min(title.size(), kMaxCharsOnAButton)); gtk_button_set_label(GTK_BUTTON(button), WideToUTF8(title).c_str()); if (node->is_url()) { if (node->GetFavIcon().width() != 0) { GdkPixbuf* pixbuf = gfx::GdkPixbufFromSkBitmap(&node->GetFavIcon()); gtk_button_set_image(GTK_BUTTON(button), gtk_image_new_from_pixbuf(pixbuf)); g_object_unref(pixbuf); } else { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); static GdkPixbuf* default_bookmark_icon = rb.GetPixbufNamed( IDR_DEFAULT_FAVICON); gtk_button_set_image(GTK_BUTTON(button), gtk_image_new_from_pixbuf(default_bookmark_icon)); } } else { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); static GdkPixbuf* default_bookmark_icon = rb.GetPixbufNamed( IDR_BOOKMARK_BAR_FOLDER); gtk_button_set_image(GTK_BUTTON(button), gtk_image_new_from_pixbuf(default_bookmark_icon)); } g_object_set_data(G_OBJECT(button), kBookmarkNode, reinterpret_cast(node)); } GtkWidget* BookmarkBarGtk::CreateBookmarkButton( BookmarkNode* node) { GtkWidget* button = gtk_chrome_button_new(); ConfigureButtonForNode(node, button); // The tool item is also a source for dragging gtk_drag_source_set(button, GDK_BUTTON1_MASK, kTargetTable, kTargetTableSize, GDK_ACTION_MOVE); g_signal_connect(G_OBJECT(button), "drag-begin", G_CALLBACK(&OnButtonDragBegin), this); g_signal_connect(G_OBJECT(button), "drag-end", G_CALLBACK(&OnButtonDragEnd), this); g_signal_connect(G_OBJECT(button), "drag-data-get", G_CALLBACK(&OnButtonDragGet), this); // We deliberately don't connect to "drag-data-delete" because the action of // moving a button will regenerate all the contents of the bookmarks bar // anyway. if (node->is_url()) { // Connect to 'button-release-event' instead of 'clicked' because we need // access to the modifier keys and we do different things on each // button. g_signal_connect(G_OBJECT(button), "button-press-event", G_CALLBACK(OnButtonPressed), this); g_signal_connect(G_OBJECT(button), "button-release-event", G_CALLBACK(OnButtonReleased), this); } else { // TODO(erg): This button can also be a drop target. ConnectFolderButtonEvents(button); } GTK_WIDGET_UNSET_FLAGS(button, GTK_CAN_FOCUS); return button; } GtkToolItem* BookmarkBarGtk::CreateBookmarkToolItem(BookmarkNode* node) { GtkWidget* button = CreateBookmarkButton(node); g_object_set_data(G_OBJECT(button), "left-align-popup", reinterpret_cast(true)); GtkToolItem* item = gtk_tool_item_new(); gtk_container_add(GTK_CONTAINER(item), button); gtk_widget_show_all(GTK_WIDGET(item)); return item; } void BookmarkBarGtk::ConnectFolderButtonEvents(GtkWidget* widget) { // Connect to 'button-release-event' instead of 'clicked' because we need // access to the modifier keys and we do different things on each // button. g_signal_connect(G_OBJECT(widget), "button-press-event", G_CALLBACK(OnButtonPressed), this); g_signal_connect(G_OBJECT(widget), "button-release-event", G_CALLBACK(OnFolderButtonReleased), this); } std::string BookmarkBarGtk::BuildTooltip(BookmarkNode* node) { // TODO(erg): Actually build the tooltip. For now, we punt and just return // the URL. return node->GetURL().possibly_invalid_spec(); } BookmarkNode* BookmarkBarGtk::GetNodeForToolButton(GtkWidget* widget) { // First check to see if |button| is special cased. if (widget == other_bookmarks_button_) return model_->other_node(); else if (widget == bookmark_toolbar_.get()) return model_->GetBookmarkBarNode(); // Search the contents of |bookmark_toolbar_| for the corresponding widget // and find its index. GtkWidget* item_to_find = gtk_widget_get_parent(widget); int index_to_use = -1; int index = 0; GList* children = gtk_container_get_children( GTK_CONTAINER(bookmark_toolbar_.get())); for (GList* item = children; item; item = item->next, index++) { if (item->data == item_to_find) { index_to_use = index; break; } } g_list_free(children); if (index_to_use != -1) return model_->GetBookmarkBarNode()->GetChild(index_to_use); return NULL; } void BookmarkBarGtk::PopupMenuForNode(GtkWidget* sender, BookmarkNode* node, GdkEventButton* event) { if (!model_->IsLoaded()) { // Don't do anything if the model isn't loaded. return; } BookmarkNode* parent = NULL; std::vector nodes; if (sender == other_bookmarks_button_) { parent = model_->GetBookmarkBarNode(); nodes.push_back(parent); } else if (sender != bookmark_toolbar_.get()) { nodes.push_back(node); parent = node->GetParent(); } else { parent = model_->GetBookmarkBarNode(); nodes.push_back(parent); } current_context_menu_.reset(new BookmarkContextMenu( GTK_WINDOW(gtk_widget_get_toplevel(sender)), profile_, browser_, page_navigator_, parent, nodes, BookmarkContextMenu::BOOKMARK_BAR)); current_context_menu_->PopupAsContext(event->time); } // static gboolean BookmarkBarGtk::OnButtonPressed(GtkWidget* sender, GdkEventButton* event, BookmarkBarGtk* bar) { if (sender != bar->other_bookmarks_button_ && sender != bar->bookmark_toolbar_.get()) bar->ignore_button_release_ = false; if (event->button == 3) { BookmarkNode* node = bar->GetNodeForToolButton(sender); DCHECK(node); DCHECK(bar->page_navigator_); bar->PopupMenuForNode(sender, node, event); } return FALSE; } // static gboolean BookmarkBarGtk::OnButtonReleased(GtkWidget* sender, GdkEventButton* event, BookmarkBarGtk* bar) { if (bar->ignore_button_release_) { // Don't handle this message; it was a drag. bar->ignore_button_release_ = false; return FALSE; } BookmarkNode* node = bar->GetNodeForToolButton(sender); DCHECK(node); DCHECK(bar->page_navigator_); if (node->is_url()) { bar->page_navigator_->OpenURL( node->GetURL(), GURL(), event_utils::DispositionFromEventFlags(event->state), PageTransition::AUTO_BOOKMARK); } else { bookmark_utils::OpenAll( GTK_WINDOW(gtk_widget_get_toplevel(sender)), bar->profile_, bar->page_navigator_, node, event_utils::DispositionFromEventFlags(event->state)); } UserMetrics::RecordAction(L"ClickedBookmarkBarURLButton", bar->profile_); // Allow other handlers to run so the button state is updated correctly. return FALSE; } // static void BookmarkBarGtk::OnButtonDragBegin(GtkWidget* button, GdkDragContext* drag_context, BookmarkBarGtk* bar) { // Signal to any future OnButtonReleased calls that we're dragging instead of // pressing. bar->ignore_button_release_ = true; BookmarkNode* node = bar->GetNodeForToolButton(button); DCHECK(node); bar->dragged_node_ = node; bar->toolbar_drop_item_ = NULL; // Build a windowed representation for our button. GtkWidget* window = gtk_window_new(GTK_WINDOW_POPUP); gtk_widget_realize(window); GtkWidget* frame = gtk_frame_new(NULL); gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_OUT); gtk_container_add(GTK_CONTAINER(window), frame); gtk_widget_show(frame); GtkWidget* floating_button = gtk_chrome_button_new(); bar->ConfigureButtonForNode(node, floating_button); gtk_container_add(GTK_CONTAINER(frame), floating_button); gtk_widget_show(floating_button); gint x, y; gtk_widget_get_pointer(button, &x, &y); gtk_drag_set_icon_widget(drag_context, window, x, y); // Hide our node gtk_widget_hide(button); } // static void BookmarkBarGtk::OnButtonDragEnd(GtkWidget* button, GdkDragContext* drag_context, BookmarkBarGtk* bar) { // Cleanup everything from this drag if (bar->toolbar_drop_item_) { g_object_unref(bar->toolbar_drop_item_); bar->toolbar_drop_item_ = NULL; } bar->dragged_node_ = NULL; gtk_widget_show(button); } // static void BookmarkBarGtk::OnButtonDragGet(GtkWidget* widget, GdkDragContext* context, GtkSelectionData* selection_data, guint target_type, guint time, BookmarkBarGtk* bar) { BookmarkNode* node = reinterpret_cast( g_object_get_data(G_OBJECT(widget), kBookmarkNode)); DCHECK(node); switch (target_type) { case DROP_TARGET_INTERNAL: { BookmarkDragData data(node); Pickle pickle; data.WriteToPickle(bar->profile_, &pickle); gtk_selection_data_set(selection_data, selection_data->target, kBitsInAByte, static_cast(pickle.data()), pickle.size()); break; } default: { DLOG(ERROR) << "Unsupported drag get type!"; } } } // static gboolean BookmarkBarGtk::OnFolderButtonReleased(GtkWidget* sender, GdkEventButton* event, BookmarkBarGtk* bar) { if (bar->ignore_button_release_) { // Don't handle this message; it was a drag. bar->ignore_button_release_ = false; return FALSE; } BookmarkNode* node = bar->GetNodeForToolButton(sender); DCHECK(node); DCHECK(bar->page_navigator_); bar->current_menu_.reset( new BookmarkMenuController(bar->browser_, bar->profile_, bar->page_navigator_, GTK_WINDOW(gtk_widget_get_toplevel(sender)), node, 0, false)); bar->current_menu_->Popup(sender, event->button, event->time); // Allow other handlers to run so the button state is updated correctly. return FALSE; } // static gboolean BookmarkBarGtk::OnToolbarExpose(GtkWidget* widget, GdkEventExpose* event, BookmarkBarGtk* bar) { // A GtkToolbar's expose handler first draws a box. We don't want that so we // need to propagate the expose event to all the container's children. GList* children = gtk_container_get_children(GTK_CONTAINER(widget)); for (GList* item = children; item; item = item->next) { gtk_container_propagate_expose(GTK_CONTAINER(widget), GTK_WIDGET(item->data), event); } g_list_free(children); return TRUE; } // static gboolean BookmarkBarGtk::OnToolbarDragMotion(GtkToolbar* toolbar, GdkDragContext* context, gint x, gint y, guint time, BookmarkBarGtk* bar) { if (!bar->toolbar_drop_item_) { bar->toolbar_drop_item_ = bar->CreateBookmarkToolItem(bar->dragged_node_); g_object_ref_sink(GTK_OBJECT(bar->toolbar_drop_item_)); } gdk_drag_status(context, GDK_ACTION_MOVE, time); gint index = gtk_toolbar_get_drop_index(toolbar, x, y); gtk_toolbar_set_drop_highlight_item(toolbar, GTK_TOOL_ITEM(bar->toolbar_drop_item_), index); return TRUE; } // static void BookmarkBarGtk::OnToolbarDragLeave(GtkToolbar* toolbar, GdkDragContext* context, guint time, BookmarkBarGtk* bar) { if (bar->toolbar_drop_item_) { g_object_unref(bar->toolbar_drop_item_); bar->toolbar_drop_item_ = NULL; } gtk_toolbar_set_drop_highlight_item(toolbar, NULL, 0); } // static gboolean BookmarkBarGtk::OnToolbarDragDrop( GtkWidget* widget, GdkDragContext* context, gint x, gint y, guint time, BookmarkBarGtk* bar) { gboolean is_valid_drop_site = FALSE; if (context->targets) { GdkAtom target_type = GDK_POINTER_TO_ATOM(g_list_nth_data(context->targets, DROP_TARGET_INTERNAL)); gtk_drag_get_data(widget, context, target_type, time); is_valid_drop_site = TRUE; } return is_valid_drop_site; } // static void BookmarkBarGtk::OnToolbarDragReceived(GtkWidget* widget, GdkDragContext* context, gint x, gint y, GtkSelectionData* selection_data, guint target_type, guint time, BookmarkBarGtk* bar) { gboolean dnd_success = FALSE; gboolean delete_selection_data = FALSE; // Deal with what we are given from source if ((selection_data != NULL) && (selection_data->length >= 0)) { if (context-> action == GDK_ACTION_MOVE) { delete_selection_data = TRUE; } switch (target_type) { case DROP_TARGET_INTERNAL: { Pickle pickle(reinterpret_cast(selection_data->data), selection_data->length); BookmarkDragData drag_data; drag_data.ReadFromPickle(&pickle); std::vector nodes = drag_data.GetNodes(bar->profile_); for (std::vector::iterator it = nodes.begin(); it != nodes.end(); ++it) { gint index = gtk_toolbar_get_drop_index( GTK_TOOLBAR(bar->bookmark_toolbar_.get()), x, y); bar->model_->Move(*it, bar->model_->GetBookmarkBarNode(), index); } dnd_success = TRUE; break; } default: { DLOG(ERROR) << "Unsupported drag received type: " << target_type; } } } gtk_drag_finish(context, dnd_success, delete_selection_data, time); }