Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / gtk / bookmarks / bookmark_bubble_gtk.cc
1 // Copyright (c) 2012 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/bind.h"
11 #include "base/i18n/rtl.h"
12 #include "base/logging.h"
13 #include "base/message_loop/message_loop.h"
14 #include "base/strings/string16.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "chrome/browser/bookmarks/bookmark_model.h"
17 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
18 #include "chrome/browser/bookmarks/bookmark_utils.h"
19 #include "chrome/browser/chrome_notification_types.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/signin/signin_promo.h"
22 #include "chrome/browser/themes/theme_properties.h"
23 #include "chrome/browser/ui/bookmarks/bookmark_editor.h"
24 #include "chrome/browser/ui/bookmarks/recently_used_folders_combo_model.h"
25 #include "chrome/browser/ui/browser.h"
26 #include "chrome/browser/ui/browser_finder.h"
27 #include "chrome/browser/ui/browser_list.h"
28 #include "chrome/browser/ui/chrome_pages.h"
29 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
30 #include "chrome/browser/ui/gtk/gtk_util.h"
31 #include "chrome/browser/ui/sync/sync_promo_ui.h"
32 #include "content/public/browser/notification_source.h"
33 #include "content/public/browser/user_metrics.h"
34 #include "grit/generated_resources.h"
35 #include "ui/base/gtk/gtk_hig_constants.h"
36 #include "ui/base/l10n/l10n_util.h"
37 #include "ui/gfx/canvas_paint_gtk.h"
38
39 using base::UserMetricsAction;
40
41 namespace {
42
43 enum {
44   COLUMN_NAME,
45   COLUMN_IS_SEPARATOR,
46   COLUMN_COUNT
47 };
48
49 // Thickness of the bubble's border.
50 const int kBubbleBorderThickness = 1;
51
52 // Color of the bubble's border.
53 const SkColor kBubbleBorderColor = SkColorSetRGB(0x63, 0x63, 0x63);
54
55 // Background color of the sync promo.
56 const GdkColor kPromoBackgroundColor = GDK_COLOR_RGB(0xf5, 0xf5, 0xf5);
57
58 // Color of the border of the sync promo.
59 const SkColor kPromoBorderColor = SkColorSetRGB(0xe5, 0xe5, 0xe5);
60
61 // Color of the text in the sync promo.
62 const GdkColor kPromoTextColor = GDK_COLOR_RGB(0x66, 0x66, 0x66);
63
64 // Vertical padding inside the sync promo.
65 const int kPromoVerticalPadding = 15;
66
67 // Pango markup for the "Sign in" link in the sync promo.
68 const char kPromoLinkMarkup[] =
69     "<a href='signin'><span underline='none'>%s</span></a>";
70
71 // Style to make the sync promo link blue.
72 const char kPromoLinkStyle[] =
73     "style \"sign-in-link\" {\n"
74     "  GtkWidget::link-color=\"blue\"\n"
75     "}\n"
76     "widget \"*sign-in-link\" style \"sign-in-link\"\n";
77
78 gboolean IsSeparator(GtkTreeModel* model, GtkTreeIter* iter, gpointer data) {
79   gboolean is_separator;
80   gtk_tree_model_get(model, iter, COLUMN_IS_SEPARATOR, &is_separator, -1);
81   return is_separator;
82 }
83
84 }  // namespace
85
86 BookmarkBubbleGtk* BookmarkBubbleGtk::bookmark_bubble_ = NULL;
87
88 // static
89 void BookmarkBubbleGtk::Show(GtkWidget* anchor,
90                              Profile* profile,
91                              const GURL& url,
92                              bool newly_bookmarked) {
93   // Sometimes Ctrl+D may get pressed more than once on top level window
94   // before the bookmark bubble window is shown and takes the keyboad focus.
95   if (bookmark_bubble_)
96     return;
97   bookmark_bubble_ = new BookmarkBubbleGtk(anchor,
98                                            profile,
99                                            url,
100                                            newly_bookmarked);
101 }
102
103 void BookmarkBubbleGtk::BubbleClosing(BubbleGtk* bubble,
104                                       bool closed_by_escape) {
105   if (closed_by_escape) {
106     remove_bookmark_ = newly_bookmarked_;
107     apply_edits_ = false;
108   }
109 }
110
111 void BookmarkBubbleGtk::Observe(int type,
112                                 const content::NotificationSource& source,
113                                 const content::NotificationDetails& details) {
114   DCHECK(type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED);
115
116   if (theme_service_->UsingNativeTheme()) {
117     for (std::vector<GtkWidget*>::iterator it = labels_.begin();
118          it != labels_.end(); ++it) {
119       gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, NULL);
120     }
121   } else {
122     for (std::vector<GtkWidget*>::iterator it = labels_.begin();
123          it != labels_.end(); ++it) {
124       gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, &ui::kGdkBlack);
125     }
126   }
127
128   UpdatePromoColors();
129 }
130
131 BookmarkBubbleGtk::BookmarkBubbleGtk(GtkWidget* anchor,
132                                      Profile* profile,
133                                      const GURL& url,
134                                      bool newly_bookmarked)
135     : url_(url),
136       profile_(profile),
137       model_(BookmarkModelFactory::GetForProfile(profile)),
138       theme_service_(GtkThemeService::GetFrom(profile_)),
139       anchor_(anchor),
140       promo_(NULL),
141       promo_label_(NULL),
142       name_entry_(NULL),
143       folder_combo_(NULL),
144       bubble_(NULL),
145       newly_bookmarked_(newly_bookmarked),
146       apply_edits_(true),
147       remove_bookmark_(false),
148       factory_(this) {
149   GtkWidget* label = gtk_label_new(l10n_util::GetStringUTF8(
150       newly_bookmarked_ ? IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARKED :
151                           IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARK).c_str());
152   labels_.push_back(label);
153   remove_button_ = theme_service_->BuildChromeLinkButton(
154       l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_REMOVE_BOOKMARK));
155   GtkWidget* edit_button = gtk_button_new_with_label(
156       l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_OPTIONS).c_str());
157   GtkWidget* close_button = gtk_button_new_with_label(
158       l10n_util::GetStringUTF8(IDS_DONE).c_str());
159
160   GtkWidget* bubble_container = gtk_vbox_new(FALSE, 0);
161
162   // Prevent the content of the bubble to be drawn on the border.
163   gtk_container_set_border_width(GTK_CONTAINER(bubble_container),
164                                  kBubbleBorderThickness);
165
166   // Our content is arranged in 3 rows.  |top| contains a left justified
167   // message, and a right justified remove link button.  |table| is the middle
168   // portion with the name entry and the folder combo.  |bottom| is the final
169   // row with a spacer, and the edit... and close buttons on the right.
170   GtkWidget* content = gtk_vbox_new(FALSE, 5);
171   gtk_container_set_border_width(
172       GTK_CONTAINER(content),
173       ui::kContentAreaBorder - kBubbleBorderThickness);
174   GtkWidget* top = gtk_hbox_new(FALSE, 0);
175
176   gtk_misc_set_alignment(GTK_MISC(label), 0, 1);
177   gtk_box_pack_start(GTK_BOX(top), label,
178                      TRUE, TRUE, 0);
179   gtk_box_pack_start(GTK_BOX(top), remove_button_,
180                      FALSE, FALSE, 0);
181
182   InitFolderComboModel();
183
184   // Create the edit entry for updating the bookmark name / title.
185   name_entry_ = gtk_entry_new();
186   gtk_entry_set_text(GTK_ENTRY(name_entry_), GetTitle().c_str());
187
188   // We use a table to allow the labels to line up with each other, along
189   // with the entry and folder combo lining up.
190   GtkWidget* table = gtk_util::CreateLabeledControlsGroup(
191       &labels_,
192       l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_TITLE_TEXT).c_str(),
193       name_entry_,
194       l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_FOLDER_TEXT).c_str(),
195       folder_combo_,
196       NULL);
197
198   GtkWidget* bottom = gtk_hbox_new(FALSE, 0);
199   // We want the buttons on the right, so just use an expanding label to fill
200   // all of the extra space on the right.
201   gtk_box_pack_start(GTK_BOX(bottom), gtk_label_new(""),
202                      TRUE, TRUE, 0);
203   gtk_box_pack_start(GTK_BOX(bottom), edit_button,
204                      FALSE, FALSE, 4);
205   gtk_box_pack_start(GTK_BOX(bottom), close_button,
206                      FALSE, FALSE, 0);
207
208   gtk_box_pack_start(GTK_BOX(content), top, TRUE, TRUE, 0);
209   gtk_box_pack_start(GTK_BOX(content), table, TRUE, TRUE, 0);
210   gtk_box_pack_start(GTK_BOX(content), bottom, TRUE, TRUE, 0);
211   // We want the focus to start on the entry, not on the remove button.
212   gtk_container_set_focus_child(GTK_CONTAINER(content), table);
213
214   gtk_box_pack_start(GTK_BOX(bubble_container), content, TRUE, TRUE, 0);
215
216   if (SyncPromoUI::ShouldShowSyncPromo(profile_)) {
217     std::string link_text =
218         l10n_util::GetStringUTF8(IDS_BOOKMARK_SYNC_PROMO_LINK);
219     char* link_markup = g_markup_printf_escaped(kPromoLinkMarkup,
220                                                 link_text.c_str());
221     base::string16 link_markup_utf16;
222     base::UTF8ToUTF16(link_markup, strlen(link_markup), &link_markup_utf16);
223     g_free(link_markup);
224
225     std::string promo_markup = l10n_util::GetStringFUTF8(
226         IDS_BOOKMARK_SYNC_PROMO_MESSAGE,
227         link_markup_utf16);
228
229     promo_ = gtk_event_box_new();
230     gtk_widget_set_app_paintable(promo_, TRUE);
231
232     promo_label_ = gtk_label_new(NULL);
233     gtk_label_set_markup(GTK_LABEL(promo_label_), promo_markup.c_str());
234     gtk_misc_set_alignment(GTK_MISC(promo_label_), 0.0, 0.0);
235     gtk_misc_set_padding(GTK_MISC(promo_label_),
236                          ui::kContentAreaBorder,
237                          kPromoVerticalPadding);
238
239     // Custom link color.
240     gtk_rc_parse_string(kPromoLinkStyle);
241
242     UpdatePromoColors();
243
244     gtk_container_add(GTK_CONTAINER(promo_), promo_label_);
245     gtk_box_pack_start(GTK_BOX(bubble_container), promo_, TRUE, TRUE, 0);
246     g_signal_connect(promo_,
247                      "realize",
248                      G_CALLBACK(&OnSyncPromoRealizeThunk),
249                      this);
250     g_signal_connect(promo_,
251                      "expose-event",
252                      G_CALLBACK(&OnSyncPromoExposeThunk),
253                      this);
254     g_signal_connect(promo_label_,
255                      "activate-link",
256                      G_CALLBACK(&OnSignInClickedThunk),
257                      this);
258   }
259
260   bubble_ = BubbleGtk::Show(anchor_,
261                             NULL,
262                             bubble_container,
263                             BubbleGtk::ANCHOR_TOP_RIGHT,
264                             BubbleGtk::MATCH_SYSTEM_THEME |
265                                 BubbleGtk::POPUP_WINDOW |
266                                 BubbleGtk::GRAB_INPUT,
267                             theme_service_,
268                             this);  // delegate
269   if (!bubble_) {
270     NOTREACHED();
271     return;
272   }
273
274   g_signal_connect(content, "destroy",
275                    G_CALLBACK(&OnDestroyThunk), this);
276   g_signal_connect(name_entry_, "activate",
277                    G_CALLBACK(&OnNameActivateThunk), this);
278   g_signal_connect(folder_combo_, "changed",
279                    G_CALLBACK(&OnFolderChangedThunk), this);
280   g_signal_connect(edit_button, "clicked",
281                    G_CALLBACK(&OnEditClickedThunk), this);
282   g_signal_connect(close_button, "clicked",
283                    G_CALLBACK(&OnCloseClickedThunk), this);
284   g_signal_connect(remove_button_, "clicked",
285                    G_CALLBACK(&OnRemoveClickedThunk), this);
286
287   registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
288                  content::Source<ThemeService>(theme_service_));
289   theme_service_->InitThemesFor(this);
290 }
291
292 BookmarkBubbleGtk::~BookmarkBubbleGtk() {
293   DCHECK(bookmark_bubble_);
294   bookmark_bubble_ = NULL;
295
296   if (apply_edits_) {
297     ApplyEdits();
298   } else if (remove_bookmark_) {
299     const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_);
300     if (node)
301       model_->Remove(node->parent(), node->parent()->GetIndexOf(node));
302   }
303 }
304
305 void BookmarkBubbleGtk::OnDestroy(GtkWidget* widget) {
306   // We are self deleting, we have a destroy signal setup to catch when we
307   // destroyed (via the BubbleGtk being destroyed), and delete ourself.
308   delete this;
309 }
310
311 void BookmarkBubbleGtk::OnNameActivate(GtkWidget* widget) {
312   bubble_->Close();
313 }
314
315 void BookmarkBubbleGtk::OnFolderChanged(GtkWidget* widget) {
316   int index = gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_));
317   if (index == folder_combo_model_->GetItemCount() - 1) {
318     content::RecordAction(
319         UserMetricsAction("BookmarkBubble_EditFromCombobox"));
320     // GTK doesn't handle having the combo box destroyed from the changed
321     // signal.  Since showing the editor also closes the bubble, delay this
322     // so that GTK can unwind.  Specifically gtk_menu_shell_button_release
323     // will run, and we need to keep the combo box alive until then.
324     base::MessageLoop::current()->PostTask(
325         FROM_HERE,
326         base::Bind(&BookmarkBubbleGtk::ShowEditor, factory_.GetWeakPtr()));
327   }
328 }
329
330 void BookmarkBubbleGtk::OnEditClicked(GtkWidget* widget) {
331   content::RecordAction(UserMetricsAction("BookmarkBubble_Edit"));
332   ShowEditor();
333 }
334
335 void BookmarkBubbleGtk::OnCloseClicked(GtkWidget* widget) {
336   bubble_->Close();
337 }
338
339 void BookmarkBubbleGtk::OnRemoveClicked(GtkWidget* widget) {
340   content::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"));
341
342   apply_edits_ = false;
343   remove_bookmark_ = true;
344   bubble_->Close();
345 }
346
347 gboolean BookmarkBubbleGtk::OnSignInClicked(GtkWidget* widget, gchar* uri) {
348   GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(anchor_));
349   Browser* browser = chrome::FindBrowserWithWindow(window);
350   chrome::ShowBrowserSignin(browser, signin::SOURCE_BOOKMARK_BUBBLE);
351   bubble_->Close();
352   return TRUE;
353 }
354
355 void BookmarkBubbleGtk::OnSyncPromoRealize(GtkWidget* widget) {
356   int width = gtk_util::GetWidgetSize(widget).width();
357   gtk_util::SetLabelWidth(promo_label_, width);
358 }
359
360 gboolean BookmarkBubbleGtk::OnSyncPromoExpose(GtkWidget* widget,
361                                               GdkEventExpose* event) {
362   GtkAllocation allocation;
363   gtk_widget_get_allocation(widget, &allocation);
364
365   gfx::CanvasSkiaPaint canvas(event);
366
367   // Draw a border on top of the promo.
368   canvas.DrawLine(gfx::Point(0, 0),
369                   gfx::Point(allocation.width + 1, 0),
370                   kPromoBorderColor);
371
372   // Redraw the rounded corners of the bubble that are hidden by the
373   // background of the promo.
374   SkPaint points_paint;
375   points_paint.setColor(kBubbleBorderColor);
376   points_paint.setStrokeWidth(SkIntToScalar(1));
377   canvas.DrawPoint(gfx::Point(0, allocation.height - 1), points_paint);
378   canvas.DrawPoint(gfx::Point(allocation.width - 1, allocation.height - 1),
379                    points_paint);
380
381   return FALSE; // Propagate expose to children.
382 }
383
384 void BookmarkBubbleGtk::UpdatePromoColors() {
385   if (!promo_)
386     return;
387
388   GdkColor promo_background_color;
389
390   if (!theme_service_->UsingNativeTheme()) {
391     promo_background_color = kPromoBackgroundColor;
392     gtk_widget_set_name(promo_label_, "sign-in-link");
393     gtk_util::SetLabelColor(promo_label_, &kPromoTextColor);
394   } else {
395     promo_background_color = theme_service_->GetGdkColor(
396         ThemeProperties::COLOR_TOOLBAR);
397     gtk_widget_set_name(promo_label_, "sign-in-link-theme-color");
398   }
399
400   gtk_widget_modify_bg(promo_, GTK_STATE_NORMAL, &promo_background_color);
401
402   // No visible highlight color when the mouse is over the link.
403   gtk_widget_modify_base(promo_label_,
404                          GTK_STATE_ACTIVE,
405                          &promo_background_color);
406   gtk_widget_modify_base(promo_label_,
407                          GTK_STATE_PRELIGHT,
408                          &promo_background_color);
409 }
410
411 void BookmarkBubbleGtk::ApplyEdits() {
412   // Set this to make sure we don't attempt to apply edits again.
413   apply_edits_ = false;
414
415   const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_);
416   if (node) {
417     const base::string16 new_title(
418         base::UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(name_entry_))));
419
420     if (new_title != node->GetTitle()) {
421       model_->SetTitle(node, new_title);
422       content::RecordAction(
423           UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"));
424     }
425
426     folder_combo_model_->MaybeChangeParent(
427         node, gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_)));
428   }
429 }
430
431 std::string BookmarkBubbleGtk::GetTitle() {
432   const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_);
433   if (!node) {
434     NOTREACHED();
435     return std::string();
436   }
437
438   return base::UTF16ToUTF8(node->GetTitle());
439 }
440
441 void BookmarkBubbleGtk::ShowEditor() {
442   const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_);
443
444   // Commit any edits now.
445   ApplyEdits();
446
447   // Closing might delete us, so we'll cache what we need on the stack.
448   Profile* profile = profile_;
449   GtkWindow* toplevel = GTK_WINDOW(gtk_widget_get_toplevel(anchor_));
450
451   // Close the bubble, deleting the C++ objects, etc.
452   bubble_->Close();
453
454   if (node) {
455     BookmarkEditor::Show(toplevel, profile,
456                          BookmarkEditor::EditDetails::EditNode(node),
457                          BookmarkEditor::SHOW_TREE);
458   }
459 }
460
461 void BookmarkBubbleGtk::InitFolderComboModel() {
462   const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_);
463   DCHECK(node);
464
465   folder_combo_model_.reset(new RecentlyUsedFoldersComboModel(model_, node));
466
467   GtkListStore* store = gtk_list_store_new(COLUMN_COUNT,
468                                            G_TYPE_STRING, G_TYPE_BOOLEAN);
469
470   // We always have nodes + 1 entries in the combo. The last entry is an entry
471   // that reads 'Choose Another Folder...' and when chosen Bookmark Editor is
472   // opened.
473   for (int i = 0; i < folder_combo_model_->GetItemCount(); ++i) {
474     const bool is_separator = folder_combo_model_->IsItemSeparatorAt(i);
475     const std::string name = is_separator ?
476         std::string() : base::UTF16ToUTF8(folder_combo_model_->GetItemAt(i));
477
478     GtkTreeIter iter;
479     gtk_list_store_append(store, &iter);
480     gtk_list_store_set(store, &iter,
481                        COLUMN_NAME, name.c_str(),
482                        COLUMN_IS_SEPARATOR, is_separator,
483                        -1);
484   }
485
486   folder_combo_ = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
487
488   gtk_combo_box_set_active(GTK_COMBO_BOX(folder_combo_),
489                            folder_combo_model_->GetDefaultIndex());
490   gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(folder_combo_),
491                                        IsSeparator, NULL, NULL);
492   g_object_unref(store);
493
494   GtkCellRenderer* renderer = gtk_cell_renderer_text_new();
495   gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(folder_combo_), renderer, TRUE);
496   gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(folder_combo_), renderer,
497                                  "text", COLUMN_NAME,
498                                  NULL);
499 }