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/ui/gtk/bookmarks/bookmark_bubble_gtk.h" 6 7 #include <gtk/gtk.h> 8 9 #include "base/basictypes.h" 10 #include "base/i18n/rtl.h" 11 #include "base/logging.h" 12 #include "base/message_loop.h" 13 #include "base/string16.h" 14 #include "base/utf_string_conversions.h" 15 #include "chrome/browser/bookmarks/bookmark_editor.h" 16 #include "chrome/browser/bookmarks/bookmark_model.h" 17 #include "chrome/browser/bookmarks/bookmark_utils.h" 18 #include "chrome/browser/bookmarks/recently_used_folders_combo_model.h" 19 #include "chrome/browser/metrics/user_metrics.h" 20 #include "chrome/browser/profiles/profile.h" 21 #include "chrome/browser/ui/gtk/gtk_chrome_link_button.h" 22 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 23 #include "chrome/browser/ui/gtk/gtk_util.h" 24 #include "chrome/browser/ui/gtk/info_bubble_gtk.h" 25 #include "content/common/notification_service.h" 26 #include "grit/generated_resources.h" 27 #include "ui/base/l10n/l10n_util.h" 28 29 namespace { 30 31 // We basically have a singleton, since a bubble is sort of app-modal. This 32 // keeps track of the currently open bubble, or NULL if none is open. 33 BookmarkBubbleGtk* g_bubble = NULL; 34 35 // Padding between content and edge of info bubble. 36 const int kContentBorder = 7; 37 38 39 } // namespace 40 41 // static 42 void BookmarkBubbleGtk::Show(GtkWidget* anchor, 43 Profile* profile, 44 const GURL& url, 45 bool newly_bookmarked) { 46 DCHECK(!g_bubble); 47 g_bubble = new BookmarkBubbleGtk(anchor, profile, url, newly_bookmarked); 48 } 49 50 void BookmarkBubbleGtk::InfoBubbleClosing(InfoBubbleGtk* info_bubble, 51 bool closed_by_escape) { 52 if (closed_by_escape) { 53 remove_bookmark_ = newly_bookmarked_; 54 apply_edits_ = false; 55 } 56 57 NotificationService::current()->Notify( 58 NotificationType::BOOKMARK_BUBBLE_HIDDEN, 59 Source<Profile>(profile_->GetOriginalProfile()), 60 NotificationService::NoDetails()); 61 } 62 63 void BookmarkBubbleGtk::Observe(NotificationType type, 64 const NotificationSource& source, 65 const NotificationDetails& details) { 66 DCHECK(type == NotificationType::BROWSER_THEME_CHANGED); 67 68 gtk_chrome_link_button_set_use_gtk_theme( 69 GTK_CHROME_LINK_BUTTON(remove_button_), 70 theme_service_->UseGtkTheme()); 71 72 if (theme_service_->UseGtkTheme()) { 73 for (std::vector<GtkWidget*>::iterator it = labels_.begin(); 74 it != labels_.end(); ++it) { 75 gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, NULL); 76 } 77 } else { 78 for (std::vector<GtkWidget*>::iterator it = labels_.begin(); 79 it != labels_.end(); ++it) { 80 gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, >k_util::kGdkBlack); 81 } 82 } 83 } 84 85 BookmarkBubbleGtk::BookmarkBubbleGtk(GtkWidget* anchor, 86 Profile* profile, 87 const GURL& url, 88 bool newly_bookmarked) 89 : url_(url), 90 profile_(profile), 91 theme_service_(GtkThemeService::GetFrom(profile_)), 92 anchor_(anchor), 93 content_(NULL), 94 name_entry_(NULL), 95 folder_combo_(NULL), 96 bubble_(NULL), 97 factory_(this), 98 newly_bookmarked_(newly_bookmarked), 99 apply_edits_(true), 100 remove_bookmark_(false) { 101 GtkWidget* label = gtk_label_new(l10n_util::GetStringUTF8( 102 newly_bookmarked_ ? IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED : 103 IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK).c_str()); 104 labels_.push_back(label); 105 remove_button_ = gtk_chrome_link_button_new( 106 l10n_util::GetStringUTF8(IDS_BOOMARK_BUBBLE_REMOVE_BOOKMARK).c_str()); 107 GtkWidget* edit_button = gtk_button_new_with_label( 108 l10n_util::GetStringUTF8(IDS_BOOMARK_BUBBLE_OPTIONS).c_str()); 109 GtkWidget* close_button = gtk_button_new_with_label( 110 l10n_util::GetStringUTF8(IDS_DONE).c_str()); 111 112 // Our content is arranged in 3 rows. |top| contains a left justified 113 // message, and a right justified remove link button. |table| is the middle 114 // portion with the name entry and the folder combo. |bottom| is the final 115 // row with a spacer, and the edit... and close buttons on the right. 116 GtkWidget* content = gtk_vbox_new(FALSE, 5); 117 gtk_container_set_border_width(GTK_CONTAINER(content), kContentBorder); 118 GtkWidget* top = gtk_hbox_new(FALSE, 0); 119 120 gtk_misc_set_alignment(GTK_MISC(label), 0, 1); 121 gtk_box_pack_start(GTK_BOX(top), label, 122 TRUE, TRUE, 0); 123 gtk_box_pack_start(GTK_BOX(top), remove_button_, 124 FALSE, FALSE, 0); 125 126 folder_combo_ = gtk_combo_box_new_text(); 127 InitFolderComboModel(); 128 129 // Create the edit entry for updating the bookmark name / title. 130 name_entry_ = gtk_entry_new(); 131 gtk_entry_set_text(GTK_ENTRY(name_entry_), GetTitle().c_str()); 132 133 // We use a table to allow the labels to line up with each other, along 134 // with the entry and folder combo lining up. 135 GtkWidget* table = gtk_util::CreateLabeledControlsGroup( 136 &labels_, 137 l10n_util::GetStringUTF8(IDS_BOOMARK_BUBBLE_TITLE_TEXT).c_str(), 138 name_entry_, 139 l10n_util::GetStringUTF8(IDS_BOOMARK_BUBBLE_FOLDER_TEXT).c_str(), 140 folder_combo_, 141 NULL); 142 143 GtkWidget* bottom = gtk_hbox_new(FALSE, 0); 144 // We want the buttons on the right, so just use an expanding label to fill 145 // all of the extra space on the right. 146 gtk_box_pack_start(GTK_BOX(bottom), gtk_label_new(""), 147 TRUE, TRUE, 0); 148 gtk_box_pack_start(GTK_BOX(bottom), edit_button, 149 FALSE, FALSE, 4); 150 gtk_box_pack_start(GTK_BOX(bottom), close_button, 151 FALSE, FALSE, 0); 152 153 gtk_box_pack_start(GTK_BOX(content), top, TRUE, TRUE, 0); 154 gtk_box_pack_start(GTK_BOX(content), table, TRUE, TRUE, 0); 155 gtk_box_pack_start(GTK_BOX(content), bottom, TRUE, TRUE, 0); 156 // We want the focus to start on the entry, not on the remove button. 157 gtk_container_set_focus_child(GTK_CONTAINER(content), table); 158 159 InfoBubbleGtk::ArrowLocationGtk arrow_location = 160 base::i18n::IsRTL() ? 161 InfoBubbleGtk::ARROW_LOCATION_TOP_LEFT : 162 InfoBubbleGtk::ARROW_LOCATION_TOP_RIGHT; 163 bubble_ = InfoBubbleGtk::Show(anchor_, 164 NULL, 165 content, 166 arrow_location, 167 true, // match_system_theme 168 true, // grab_input 169 theme_service_, 170 this); // delegate 171 if (!bubble_) { 172 NOTREACHED(); 173 return; 174 } 175 176 g_signal_connect(content, "destroy", 177 G_CALLBACK(&OnDestroyThunk), this); 178 g_signal_connect(name_entry_, "activate", 179 G_CALLBACK(&OnNameActivateThunk), this); 180 g_signal_connect(folder_combo_, "changed", 181 G_CALLBACK(&OnFolderChangedThunk), this); 182 g_signal_connect(folder_combo_, "notify::popup-shown", 183 G_CALLBACK(&OnFolderPopupShownThunk), this); 184 g_signal_connect(edit_button, "clicked", 185 G_CALLBACK(&OnEditClickedThunk), this); 186 g_signal_connect(close_button, "clicked", 187 G_CALLBACK(&OnCloseClickedThunk), this); 188 g_signal_connect(remove_button_, "clicked", 189 G_CALLBACK(&OnRemoveClickedThunk), this); 190 191 registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, 192 NotificationService::AllSources()); 193 theme_service_->InitThemesFor(this); 194 } 195 196 BookmarkBubbleGtk::~BookmarkBubbleGtk() { 197 DCHECK(!content_); // |content_| should have already been destroyed. 198 199 DCHECK(g_bubble); 200 g_bubble = NULL; 201 202 if (apply_edits_) { 203 ApplyEdits(); 204 } else if (remove_bookmark_) { 205 BookmarkModel* model = profile_->GetBookmarkModel(); 206 const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_); 207 if (node) 208 model->Remove(node->parent(), node->parent()->GetIndexOf(node)); 209 } 210 } 211 212 void BookmarkBubbleGtk::OnDestroy(GtkWidget* widget) { 213 // We are self deleting, we have a destroy signal setup to catch when we 214 // destroyed (via the InfoBubble being destroyed), and delete ourself. 215 content_ = NULL; // We are being destroyed. 216 delete this; 217 } 218 219 void BookmarkBubbleGtk::OnNameActivate(GtkWidget* widget) { 220 bubble_->Close(); 221 } 222 223 void BookmarkBubbleGtk::OnFolderChanged(GtkWidget* widget) { 224 int index = gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_)); 225 if (index == folder_combo_model_->GetItemCount() - 1) { 226 UserMetrics::RecordAction( 227 UserMetricsAction("BookmarkBubble_EditFromCombobox"), profile_); 228 // GTK doesn't handle having the combo box destroyed from the changed 229 // signal. Since showing the editor also closes the bubble, delay this 230 // so that GTK can unwind. Specifically gtk_menu_shell_button_release 231 // will run, and we need to keep the combo box alive until then. 232 MessageLoop::current()->PostTask(FROM_HERE, 233 factory_.NewRunnableMethod(&BookmarkBubbleGtk::ShowEditor)); 234 } 235 } 236 237 void BookmarkBubbleGtk::OnFolderPopupShown(GtkWidget* widget, 238 GParamSpec* property) { 239 // GtkComboBox grabs the keyboard and pointer when it displays its popup, 240 // which steals the grabs that InfoBubbleGtk had installed. When the popup is 241 // hidden, we notify InfoBubbleGtk so it can try to reacquire the grabs 242 // (otherwise, GTK won't activate our widgets when the user clicks in them). 243 gboolean popup_shown = FALSE; 244 g_object_get(G_OBJECT(folder_combo_), "popup-shown", &popup_shown, NULL); 245 if (!popup_shown) 246 bubble_->HandlePointerAndKeyboardUngrabbedByContent(); 247 } 248 249 void BookmarkBubbleGtk::OnEditClicked(GtkWidget* widget) { 250 UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"), 251 profile_); 252 ShowEditor(); 253 } 254 255 void BookmarkBubbleGtk::OnCloseClicked(GtkWidget* widget) { 256 bubble_->Close(); 257 } 258 259 void BookmarkBubbleGtk::OnRemoveClicked(GtkWidget* widget) { 260 UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"), 261 profile_); 262 263 apply_edits_ = false; 264 remove_bookmark_ = true; 265 bubble_->Close(); 266 } 267 268 void BookmarkBubbleGtk::ApplyEdits() { 269 // Set this to make sure we don't attempt to apply edits again. 270 apply_edits_ = false; 271 272 BookmarkModel* model = profile_->GetBookmarkModel(); 273 const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_); 274 if (node) { 275 const string16 new_title( 276 UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(name_entry_)))); 277 278 if (new_title != node->GetTitle()) { 279 model->SetTitle(node, new_title); 280 UserMetrics::RecordAction( 281 UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"), 282 profile_); 283 } 284 285 int index = gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_)); 286 287 // Last index means 'Choose another folder...' 288 if (index < folder_combo_model_->GetItemCount() - 1) { 289 const BookmarkNode* new_parent = folder_combo_model_->GetNodeAt(index); 290 if (new_parent != node->parent()) { 291 UserMetrics::RecordAction( 292 UserMetricsAction("BookmarkBubble_ChangeParent"), profile_); 293 model->Move(node, new_parent, new_parent->child_count()); 294 } 295 } 296 } 297 } 298 299 std::string BookmarkBubbleGtk::GetTitle() { 300 BookmarkModel* bookmark_model= profile_->GetBookmarkModel(); 301 const BookmarkNode* node = 302 bookmark_model->GetMostRecentlyAddedNodeForURL(url_); 303 if (!node) { 304 NOTREACHED(); 305 return std::string(); 306 } 307 308 return UTF16ToUTF8(node->GetTitle()); 309 } 310 311 void BookmarkBubbleGtk::ShowEditor() { 312 const BookmarkNode* node = 313 profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_); 314 315 // Commit any edits now. 316 ApplyEdits(); 317 318 // Closing might delete us, so we'll cache what we need on the stack. 319 Profile* profile = profile_; 320 GtkWindow* toplevel = GTK_WINDOW(gtk_widget_get_toplevel(anchor_)); 321 322 // Close the bubble, deleting the C++ objects, etc. 323 bubble_->Close(); 324 325 if (node) { 326 BookmarkEditor::Show(toplevel, profile, NULL, 327 BookmarkEditor::EditDetails(node), 328 BookmarkEditor::SHOW_TREE); 329 } 330 } 331 332 void BookmarkBubbleGtk::InitFolderComboModel() { 333 folder_combo_model_.reset(new RecentlyUsedFoldersComboModel( 334 profile_->GetBookmarkModel(), 335 profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_))); 336 337 // We always have nodes + 1 entries in the combo. The last entry will be 338 // the 'Select another folder...' entry that opens the bookmark editor. 339 for (int i = 0; i < folder_combo_model_->GetItemCount(); ++i) { 340 gtk_combo_box_append_text(GTK_COMBO_BOX(folder_combo_), 341 UTF16ToUTF8(folder_combo_model_->GetItemAt(i)).c_str()); 342 } 343 344 gtk_combo_box_set_active(GTK_COMBO_BOX(folder_combo_), 345 folder_combo_model_->node_parent_index()); 346 } 347