• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
Show(GtkWidget * anchor,Profile * profile,const GURL & url,bool newly_bookmarked)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 
InfoBubbleClosing(InfoBubbleGtk * info_bubble,bool closed_by_escape)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 
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)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, &gtk_util::kGdkBlack);
81     }
82   }
83 }
84 
BookmarkBubbleGtk(GtkWidget * anchor,Profile * profile,const GURL & url,bool newly_bookmarked)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 
~BookmarkBubbleGtk()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 
OnDestroy(GtkWidget * widget)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 
OnNameActivate(GtkWidget * widget)219 void BookmarkBubbleGtk::OnNameActivate(GtkWidget* widget) {
220   bubble_->Close();
221 }
222 
OnFolderChanged(GtkWidget * widget)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 
OnFolderPopupShown(GtkWidget * widget,GParamSpec * property)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 
OnEditClicked(GtkWidget * widget)249 void BookmarkBubbleGtk::OnEditClicked(GtkWidget* widget) {
250   UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"),
251                             profile_);
252   ShowEditor();
253 }
254 
OnCloseClicked(GtkWidget * widget)255 void BookmarkBubbleGtk::OnCloseClicked(GtkWidget* widget) {
256   bubble_->Close();
257 }
258 
OnRemoveClicked(GtkWidget * widget)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 
ApplyEdits()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 
GetTitle()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 
ShowEditor()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 
InitFolderComboModel()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