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.
5 #include "chrome/browser/ui/gtk/bookmarks/bookmark_bubble_gtk.h"
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"
39 using base::UserMetricsAction
;
49 // Thickness of the bubble's border.
50 const int kBubbleBorderThickness
= 1;
52 // Color of the bubble's border.
53 const SkColor kBubbleBorderColor
= SkColorSetRGB(0x63, 0x63, 0x63);
55 // Background color of the sync promo.
56 const GdkColor kPromoBackgroundColor
= GDK_COLOR_RGB(0xf5, 0xf5, 0xf5);
58 // Color of the border of the sync promo.
59 const SkColor kPromoBorderColor
= SkColorSetRGB(0xe5, 0xe5, 0xe5);
61 // Color of the text in the sync promo.
62 const GdkColor kPromoTextColor
= GDK_COLOR_RGB(0x66, 0x66, 0x66);
64 // Vertical padding inside the sync promo.
65 const int kPromoVerticalPadding
= 15;
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>";
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"
76 "widget \"*sign-in-link\" style \"sign-in-link\"\n";
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);
86 BookmarkBubbleGtk
* BookmarkBubbleGtk::bookmark_bubble_
= NULL
;
89 void BookmarkBubbleGtk::Show(GtkWidget
* anchor
,
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.
97 bookmark_bubble_
= new BookmarkBubbleGtk(anchor
,
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;
111 void BookmarkBubbleGtk::Observe(int type
,
112 const content::NotificationSource
& source
,
113 const content::NotificationDetails
& details
) {
114 DCHECK(type
== chrome::NOTIFICATION_BROWSER_THEME_CHANGED
);
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
);
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
);
131 BookmarkBubbleGtk::BookmarkBubbleGtk(GtkWidget
* anchor
,
134 bool newly_bookmarked
)
137 model_(BookmarkModelFactory::GetForProfile(profile
)),
138 theme_service_(GtkThemeService::GetFrom(profile_
)),
145 newly_bookmarked_(newly_bookmarked
),
147 remove_bookmark_(false),
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());
160 GtkWidget
* bubble_container
= gtk_vbox_new(FALSE
, 0);
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
);
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);
176 gtk_misc_set_alignment(GTK_MISC(label
), 0, 1);
177 gtk_box_pack_start(GTK_BOX(top
), label
,
179 gtk_box_pack_start(GTK_BOX(top
), remove_button_
,
182 InitFolderComboModel();
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());
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(
192 l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_TITLE_TEXT
).c_str(),
194 l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_FOLDER_TEXT
).c_str(),
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(""),
203 gtk_box_pack_start(GTK_BOX(bottom
), edit_button
,
205 gtk_box_pack_start(GTK_BOX(bottom
), close_button
,
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
);
214 gtk_box_pack_start(GTK_BOX(bubble_container
), content
, TRUE
, TRUE
, 0);
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
,
221 base::string16 link_markup_utf16
;
222 base::UTF8ToUTF16(link_markup
, strlen(link_markup
), &link_markup_utf16
);
225 std::string promo_markup
= l10n_util::GetStringFUTF8(
226 IDS_BOOKMARK_SYNC_PROMO_MESSAGE
,
229 promo_
= gtk_event_box_new();
230 gtk_widget_set_app_paintable(promo_
, TRUE
);
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
);
239 // Custom link color.
240 gtk_rc_parse_string(kPromoLinkStyle
);
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_
,
248 G_CALLBACK(&OnSyncPromoRealizeThunk
),
250 g_signal_connect(promo_
,
252 G_CALLBACK(&OnSyncPromoExposeThunk
),
254 g_signal_connect(promo_label_
,
256 G_CALLBACK(&OnSignInClickedThunk
),
260 bubble_
= BubbleGtk::Show(anchor_
,
263 BubbleGtk::ANCHOR_TOP_RIGHT
,
264 BubbleGtk::MATCH_SYSTEM_THEME
|
265 BubbleGtk::POPUP_WINDOW
|
266 BubbleGtk::GRAB_INPUT
,
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);
287 registrar_
.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
288 content::Source
<ThemeService
>(theme_service_
));
289 theme_service_
->InitThemesFor(this);
292 BookmarkBubbleGtk::~BookmarkBubbleGtk() {
293 DCHECK(bookmark_bubble_
);
294 bookmark_bubble_
= NULL
;
298 } else if (remove_bookmark_
) {
299 const BookmarkNode
* node
= model_
->GetMostRecentlyAddedNodeForURL(url_
);
301 model_
->Remove(node
->parent(), node
->parent()->GetIndexOf(node
));
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.
311 void BookmarkBubbleGtk::OnNameActivate(GtkWidget
* widget
) {
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(
326 base::Bind(&BookmarkBubbleGtk::ShowEditor
, factory_
.GetWeakPtr()));
330 void BookmarkBubbleGtk::OnEditClicked(GtkWidget
* widget
) {
331 content::RecordAction(UserMetricsAction("BookmarkBubble_Edit"));
335 void BookmarkBubbleGtk::OnCloseClicked(GtkWidget
* widget
) {
339 void BookmarkBubbleGtk::OnRemoveClicked(GtkWidget
* widget
) {
340 content::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"));
342 apply_edits_
= false;
343 remove_bookmark_
= true;
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
);
355 void BookmarkBubbleGtk::OnSyncPromoRealize(GtkWidget
* widget
) {
356 int width
= gtk_util::GetWidgetSize(widget
).width();
357 gtk_util::SetLabelWidth(promo_label_
, width
);
360 gboolean
BookmarkBubbleGtk::OnSyncPromoExpose(GtkWidget
* widget
,
361 GdkEventExpose
* event
) {
362 GtkAllocation allocation
;
363 gtk_widget_get_allocation(widget
, &allocation
);
365 gfx::CanvasSkiaPaint
canvas(event
);
367 // Draw a border on top of the promo.
368 canvas
.DrawLine(gfx::Point(0, 0),
369 gfx::Point(allocation
.width
+ 1, 0),
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),
381 return FALSE
; // Propagate expose to children.
384 void BookmarkBubbleGtk::UpdatePromoColors() {
388 GdkColor promo_background_color
;
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
);
395 promo_background_color
= theme_service_
->GetGdkColor(
396 ThemeProperties::COLOR_TOOLBAR
);
397 gtk_widget_set_name(promo_label_
, "sign-in-link-theme-color");
400 gtk_widget_modify_bg(promo_
, GTK_STATE_NORMAL
, &promo_background_color
);
402 // No visible highlight color when the mouse is over the link.
403 gtk_widget_modify_base(promo_label_
,
405 &promo_background_color
);
406 gtk_widget_modify_base(promo_label_
,
408 &promo_background_color
);
411 void BookmarkBubbleGtk::ApplyEdits() {
412 // Set this to make sure we don't attempt to apply edits again.
413 apply_edits_
= false;
415 const BookmarkNode
* node
= model_
->GetMostRecentlyAddedNodeForURL(url_
);
417 const base::string16
new_title(
418 base::UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(name_entry_
))));
420 if (new_title
!= node
->GetTitle()) {
421 model_
->SetTitle(node
, new_title
);
422 content::RecordAction(
423 UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"));
426 folder_combo_model_
->MaybeChangeParent(
427 node
, gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_
)));
431 std::string
BookmarkBubbleGtk::GetTitle() {
432 const BookmarkNode
* node
= model_
->GetMostRecentlyAddedNodeForURL(url_
);
435 return std::string();
438 return base::UTF16ToUTF8(node
->GetTitle());
441 void BookmarkBubbleGtk::ShowEditor() {
442 const BookmarkNode
* node
= model_
->GetMostRecentlyAddedNodeForURL(url_
);
444 // Commit any edits now.
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_
));
451 // Close the bubble, deleting the C++ objects, etc.
455 BookmarkEditor::Show(toplevel
, profile
,
456 BookmarkEditor::EditDetails::EditNode(node
),
457 BookmarkEditor::SHOW_TREE
);
461 void BookmarkBubbleGtk::InitFolderComboModel() {
462 const BookmarkNode
* node
= model_
->GetMostRecentlyAddedNodeForURL(url_
);
465 folder_combo_model_
.reset(new RecentlyUsedFoldersComboModel(model_
, node
));
467 GtkListStore
* store
= gtk_list_store_new(COLUMN_COUNT
,
468 G_TYPE_STRING
, G_TYPE_BOOLEAN
);
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
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
));
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
,
486 folder_combo_
= gtk_combo_box_new_with_model(GTK_TREE_MODEL(store
));
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
);
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
,