Home | History | Annotate | Download | only in bookmarks
      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, &gtk_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