// Copyright (c) 2010 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_bubble_gtk.h" #include #include "app/l10n_util.h" #include "base/basictypes.h" #include "base/i18n/rtl.h" #include "base/logging.h" #include "base/message_loop.h" #include "base/string16.h" #include "base/utf_string_conversions.h" #include "chrome/browser/bookmarks/bookmark_editor.h" #include "chrome/browser/bookmarks/bookmark_model.h" #include "chrome/browser/bookmarks/bookmark_utils.h" #include "chrome/browser/bookmarks/recently_used_folders_combo_model.h" #include "chrome/browser/gtk/gtk_chrome_link_button.h" #include "chrome/browser/gtk/gtk_theme_provider.h" #include "chrome/browser/gtk/gtk_util.h" #include "chrome/browser/gtk/info_bubble_gtk.h" #include "chrome/browser/metrics/user_metrics.h" #include "chrome/browser/profile.h" #include "chrome/common/notification_service.h" #include "grit/generated_resources.h" namespace { // We basically have a singleton, since a bubble is sort of app-modal. This // keeps track of the currently open bubble, or NULL if none is open. BookmarkBubbleGtk* g_bubble = NULL; // Padding between content and edge of info bubble. const int kContentBorder = 7; } // namespace // static void BookmarkBubbleGtk::Show(GtkWidget* anchor, Profile* profile, const GURL& url, bool newly_bookmarked) { DCHECK(!g_bubble); g_bubble = new BookmarkBubbleGtk(anchor, profile, url, newly_bookmarked); } void BookmarkBubbleGtk::InfoBubbleClosing(InfoBubbleGtk* info_bubble, bool closed_by_escape) { if (closed_by_escape) { remove_bookmark_ = newly_bookmarked_; apply_edits_ = false; } NotificationService::current()->Notify( NotificationType::BOOKMARK_BUBBLE_HIDDEN, Source(profile_->GetOriginalProfile()), NotificationService::NoDetails()); } void BookmarkBubbleGtk::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { DCHECK(type == NotificationType::BROWSER_THEME_CHANGED); gtk_chrome_link_button_set_use_gtk_theme( GTK_CHROME_LINK_BUTTON(remove_button_), theme_provider_->UseGtkTheme()); if (theme_provider_->UseGtkTheme()) { for (std::vector::iterator it = labels_.begin(); it != labels_.end(); ++it) { gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, NULL); } } else { for (std::vector::iterator it = labels_.begin(); it != labels_.end(); ++it) { gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, >k_util::kGdkBlack); } } } BookmarkBubbleGtk::BookmarkBubbleGtk(GtkWidget* anchor, Profile* profile, const GURL& url, bool newly_bookmarked) : url_(url), profile_(profile), theme_provider_(GtkThemeProvider::GetFrom(profile_)), anchor_(anchor), content_(NULL), name_entry_(NULL), folder_combo_(NULL), bubble_(NULL), factory_(this), newly_bookmarked_(newly_bookmarked), apply_edits_(true), remove_bookmark_(false) { GtkWidget* label = gtk_label_new(l10n_util::GetStringUTF8( newly_bookmarked_ ? IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED : IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK).c_str()); labels_.push_back(label); remove_button_ = gtk_chrome_link_button_new( l10n_util::GetStringUTF8(IDS_BOOMARK_BUBBLE_REMOVE_BOOKMARK).c_str()); GtkWidget* edit_button = gtk_button_new_with_label( l10n_util::GetStringUTF8(IDS_BOOMARK_BUBBLE_OPTIONS).c_str()); GtkWidget* close_button = gtk_button_new_with_label( l10n_util::GetStringUTF8(IDS_DONE).c_str()); // Our content is arranged in 3 rows. |top| contains a left justified // message, and a right justified remove link button. |table| is the middle // portion with the name entry and the folder combo. |bottom| is the final // row with a spacer, and the edit... and close buttons on the right. GtkWidget* content = gtk_vbox_new(FALSE, 5); gtk_container_set_border_width(GTK_CONTAINER(content), kContentBorder); GtkWidget* top = gtk_hbox_new(FALSE, 0); gtk_misc_set_alignment(GTK_MISC(label), 0, 1); gtk_box_pack_start(GTK_BOX(top), label, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(top), remove_button_, FALSE, FALSE, 0); folder_combo_ = gtk_combo_box_new_text(); InitFolderComboModel(); // Create the edit entry for updating the bookmark name / title. name_entry_ = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(name_entry_), GetTitle().c_str()); // We use a table to allow the labels to line up with each other, along // with the entry and folder combo lining up. GtkWidget* table = gtk_util::CreateLabeledControlsGroup( &labels_, l10n_util::GetStringUTF8(IDS_BOOMARK_BUBBLE_TITLE_TEXT).c_str(), name_entry_, l10n_util::GetStringUTF8(IDS_BOOMARK_BUBBLE_FOLDER_TEXT).c_str(), folder_combo_, NULL); GtkWidget* bottom = gtk_hbox_new(FALSE, 0); // We want the buttons on the right, so just use an expanding label to fill // all of the extra space on the right. gtk_box_pack_start(GTK_BOX(bottom), gtk_label_new(""), TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(bottom), edit_button, FALSE, FALSE, 4); gtk_box_pack_start(GTK_BOX(bottom), close_button, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(content), top, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(content), table, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(content), bottom, TRUE, TRUE, 0); // We want the focus to start on the entry, not on the remove button. gtk_container_set_focus_child(GTK_CONTAINER(content), table); InfoBubbleGtk::ArrowLocationGtk arrow_location = base::i18n::IsRTL() ? InfoBubbleGtk::ARROW_LOCATION_TOP_LEFT : InfoBubbleGtk::ARROW_LOCATION_TOP_RIGHT; bubble_ = InfoBubbleGtk::Show(anchor_, NULL, content, arrow_location, true, // match_system_theme true, // grab_input theme_provider_, this); // delegate if (!bubble_) { NOTREACHED(); return; } g_signal_connect(content, "destroy", G_CALLBACK(&OnDestroyThunk), this); g_signal_connect(name_entry_, "activate", G_CALLBACK(&OnNameActivateThunk), this); g_signal_connect(folder_combo_, "changed", G_CALLBACK(&OnFolderChangedThunk), this); g_signal_connect(folder_combo_, "notify::popup-shown", G_CALLBACK(&OnFolderPopupShownThunk), this); g_signal_connect(edit_button, "clicked", G_CALLBACK(&OnEditClickedThunk), this); g_signal_connect(close_button, "clicked", G_CALLBACK(&OnCloseClickedThunk), this); g_signal_connect(remove_button_, "clicked", G_CALLBACK(&OnRemoveClickedThunk), this); registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, NotificationService::AllSources()); theme_provider_->InitThemesFor(this); } BookmarkBubbleGtk::~BookmarkBubbleGtk() { DCHECK(!content_); // |content_| should have already been destroyed. DCHECK(g_bubble); g_bubble = NULL; if (apply_edits_) { ApplyEdits(); } else if (remove_bookmark_) { BookmarkModel* model = profile_->GetBookmarkModel(); const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_); if (node) model->Remove(node->GetParent(), node->GetParent()->IndexOfChild(node)); } } void BookmarkBubbleGtk::OnDestroy(GtkWidget* widget) { // We are self deleting, we have a destroy signal setup to catch when we // destroyed (via the InfoBubble being destroyed), and delete ourself. content_ = NULL; // We are being destroyed. delete this; } void BookmarkBubbleGtk::OnNameActivate(GtkWidget* widget) { bubble_->Close(); } void BookmarkBubbleGtk::OnFolderChanged(GtkWidget* widget) { int index = gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_)); if (index == folder_combo_model_->GetItemCount() - 1) { UserMetrics::RecordAction( UserMetricsAction("BookmarkBubble_EditFromCombobox"), profile_); // GTK doesn't handle having the combo box destroyed from the changed // signal. Since showing the editor also closes the bubble, delay this // so that GTK can unwind. Specifically gtk_menu_shell_button_release // will run, and we need to keep the combo box alive until then. MessageLoop::current()->PostTask(FROM_HERE, factory_.NewRunnableMethod(&BookmarkBubbleGtk::ShowEditor)); } } void BookmarkBubbleGtk::OnFolderPopupShown(GtkWidget* widget, GParamSpec* property) { // GtkComboBox grabs the keyboard and pointer when it displays its popup, // which steals the grabs that InfoBubbleGtk had installed. When the popup is // hidden, we notify InfoBubbleGtk so it can try to reacquire the grabs // (otherwise, GTK won't activate our widgets when the user clicks in them). gboolean popup_shown = FALSE; g_object_get(G_OBJECT(folder_combo_), "popup-shown", &popup_shown, NULL); if (!popup_shown) bubble_->HandlePointerAndKeyboardUngrabbedByContent(); } void BookmarkBubbleGtk::OnEditClicked(GtkWidget* widget) { UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"), profile_); ShowEditor(); } void BookmarkBubbleGtk::OnCloseClicked(GtkWidget* widget) { bubble_->Close(); } void BookmarkBubbleGtk::OnRemoveClicked(GtkWidget* widget) { UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"), profile_); apply_edits_ = false; remove_bookmark_ = true; bubble_->Close(); } void BookmarkBubbleGtk::ApplyEdits() { // Set this to make sure we don't attempt to apply edits again. apply_edits_ = false; BookmarkModel* model = profile_->GetBookmarkModel(); const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_); if (node) { const string16 new_title( UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(name_entry_)))); if (new_title != node->GetTitle()) { model->SetTitle(node, new_title); UserMetrics::RecordAction( UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"), profile_); } int index = gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_)); // Last index means 'Choose another folder...' if (index < folder_combo_model_->GetItemCount() - 1) { const BookmarkNode* new_parent = folder_combo_model_->GetNodeAt(index); if (new_parent != node->GetParent()) { UserMetrics::RecordAction( UserMetricsAction("BookmarkBubble_ChangeParent"), profile_); model->Move(node, new_parent, new_parent->GetChildCount()); } } } } std::string BookmarkBubbleGtk::GetTitle() { BookmarkModel* bookmark_model= profile_->GetBookmarkModel(); const BookmarkNode* node = bookmark_model->GetMostRecentlyAddedNodeForURL(url_); if (!node) { NOTREACHED(); return std::string(); } return UTF16ToUTF8(node->GetTitle()); } void BookmarkBubbleGtk::ShowEditor() { const BookmarkNode* node = profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_); // Commit any edits now. ApplyEdits(); // Closing might delete us, so we'll cache what we need on the stack. Profile* profile = profile_; GtkWindow* toplevel = GTK_WINDOW(gtk_widget_get_toplevel(anchor_)); // Close the bubble, deleting the C++ objects, etc. bubble_->Close(); if (node) { BookmarkEditor::Show(toplevel, profile, NULL, BookmarkEditor::EditDetails(node), BookmarkEditor::SHOW_TREE); } } void BookmarkBubbleGtk::InitFolderComboModel() { folder_combo_model_.reset(new RecentlyUsedFoldersComboModel( profile_->GetBookmarkModel(), profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_))); // We always have nodes + 1 entries in the combo. The last entry will be // the 'Select another folder...' entry that opens the bookmark editor. for (int i = 0; i < folder_combo_model_->GetItemCount(); ++i) { gtk_combo_box_append_text(GTK_COMBO_BOX(folder_combo_), UTF16ToUTF8(folder_combo_model_->GetItemAt(i)).c_str()); } gtk_combo_box_set_active(GTK_COMBO_BOX(folder_combo_), folder_combo_model_->node_parent_index()); }