| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2010 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/gtk/bookmark_bubble_gtk.h" | |
| 6 | |
| 7 #include <gtk/gtk.h> | |
| 8 | |
| 9 #include "app/l10n_util.h" | |
| 10 #include "base/basictypes.h" | |
| 11 #include "base/i18n/rtl.h" | |
| 12 #include "base/logging.h" | |
| 13 #include "base/message_loop.h" | |
| 14 #include "base/string16.h" | |
| 15 #include "base/utf_string_conversions.h" | |
| 16 #include "chrome/browser/bookmarks/bookmark_editor.h" | |
| 17 #include "chrome/browser/bookmarks/bookmark_model.h" | |
| 18 #include "chrome/browser/bookmarks/bookmark_utils.h" | |
| 19 #include "chrome/browser/bookmarks/recently_used_folders_combo_model.h" | |
| 20 #include "chrome/browser/gtk/gtk_chrome_link_button.h" | |
| 21 #include "chrome/browser/gtk/gtk_theme_provider.h" | |
| 22 #include "chrome/browser/gtk/gtk_util.h" | |
| 23 #include "chrome/browser/gtk/info_bubble_gtk.h" | |
| 24 #include "chrome/browser/metrics/user_metrics.h" | |
| 25 #include "chrome/browser/profiles/profile.h" | |
| 26 #include "chrome/common/notification_service.h" | |
| 27 #include "grit/generated_resources.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_provider_->UseGtkTheme()); | |
| 71 | |
| 72 if (theme_provider_->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_provider_(GtkThemeProvider::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_provider_, | |
| 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_provider_->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->GetParent(), node->GetParent()->IndexOfChild(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->GetParent()) { | |
| 291 UserMetrics::RecordAction( | |
| 292 UserMetricsAction("BookmarkBubble_ChangeParent"), profile_); | |
| 293 model->Move(node, new_parent, new_parent->GetChildCount()); | |
| 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 } | |
| OLD | NEW |