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/views/bookmarks/bookmark_bubble_view.h"
      6 
      7 #include "base/string16.h"
      8 #include "base/string_util.h"
      9 #include "base/utf_string_conversions.h"
     10 #include "chrome/app/chrome_command_ids.h"
     11 #include "chrome/browser/bookmarks/bookmark_editor.h"
     12 #include "chrome/browser/bookmarks/bookmark_model.h"
     13 #include "chrome/browser/bookmarks/bookmark_utils.h"
     14 #include "chrome/browser/metrics/user_metrics.h"
     15 #include "chrome/browser/profiles/profile.h"
     16 #include "chrome/browser/ui/browser.h"
     17 #include "chrome/browser/ui/browser_list.h"
     18 #include "chrome/browser/ui/views/bubble/bubble.h"
     19 #include "content/common/notification_service.h"
     20 #include "grit/generated_resources.h"
     21 #include "grit/theme_resources.h"
     22 #include "ui/base/keycodes/keyboard_codes.h"
     23 #include "ui/base/l10n/l10n_util.h"
     24 #include "ui/base/resource/resource_bundle.h"
     25 #include "ui/gfx/canvas.h"
     26 #include "ui/gfx/color_utils.h"
     27 #include "views/controls/button/native_button.h"
     28 #include "views/controls/textfield/textfield.h"
     29 #include "views/events/event.h"
     30 #include "views/focus/focus_manager.h"
     31 #include "views/layout/grid_layout.h"
     32 #include "views/layout/layout_constants.h"
     33 #include "views/window/client_view.h"
     34 #include "views/window/window.h"
     35 
     36 using views::Combobox;
     37 using views::ColumnSet;
     38 using views::GridLayout;
     39 using views::Label;
     40 using views::Link;
     41 using views::NativeButton;
     42 using views::View;
     43 
     44 // Padding between "Title:" and the actual title.
     45 static const int kTitlePadding = 4;
     46 
     47 // Minimum width for the fields - they will push out the size of the bubble if
     48 // necessary. This should be big enough so that the field pushes the right side
     49 // of the bubble far enough so that the edit button's left edge is to the right
     50 // of the field's left edge.
     51 static const int kMinimumFieldSize = 180;
     52 
     53 // Bubble close image.
     54 static SkBitmap* kCloseImage = NULL;
     55 
     56 // Declared in browser_dialogs.h so callers don't have to depend on our header.
     57 
     58 namespace browser {
     59 
     60 void ShowBookmarkBubbleView(views::Window* parent,
     61                             const gfx::Rect& bounds,
     62                             BubbleDelegate* delegate,
     63                             Profile* profile,
     64                             const GURL& url,
     65                             bool newly_bookmarked) {
     66   BookmarkBubbleView::Show(parent, bounds, delegate, profile, url,
     67                            newly_bookmarked);
     68 }
     69 
     70 void HideBookmarkBubbleView() {
     71   BookmarkBubbleView::Hide();
     72 }
     73 
     74 bool IsBookmarkBubbleViewShowing() {
     75   return BookmarkBubbleView::IsShowing();
     76 }
     77 
     78 }  // namespace browser
     79 
     80 // BookmarkBubbleView ---------------------------------------------------------
     81 
     82 BookmarkBubbleView* BookmarkBubbleView::bookmark_bubble_ = NULL;
     83 
     84 // static
     85 void BookmarkBubbleView::Show(views::Window* parent,
     86                               const gfx::Rect& bounds,
     87                               BubbleDelegate* delegate,
     88                               Profile* profile,
     89                               const GURL& url,
     90                               bool newly_bookmarked) {
     91   if (IsShowing())
     92     return;
     93 
     94   bookmark_bubble_ = new BookmarkBubbleView(delegate, profile, url,
     95                                             newly_bookmarked);
     96   // TODO(beng): Pass |parent| after V2 is complete.
     97   Bubble* bubble = Bubble::Show(
     98       parent->client_view()->GetWidget(), bounds, BubbleBorder::TOP_RIGHT,
     99       bookmark_bubble_, bookmark_bubble_);
    100   // |bubble_| can be set to NULL in BubbleClosing when we close the bubble
    101   // asynchronously. However, that can happen during the Show call above if the
    102   // window loses activation while we are getting to ready to show the bubble,
    103   // so we must check to make sure we still have a valid bubble before
    104   // proceeding.
    105   if (!bookmark_bubble_)
    106     return;
    107   bookmark_bubble_->set_bubble(bubble);
    108   bubble->SizeToContents();
    109   GURL url_ptr(url);
    110   NotificationService::current()->Notify(
    111       NotificationType::BOOKMARK_BUBBLE_SHOWN,
    112       Source<Profile>(profile->GetOriginalProfile()),
    113       Details<GURL>(&url_ptr));
    114   bookmark_bubble_->BubbleShown();
    115 }
    116 
    117 // static
    118 bool BookmarkBubbleView::IsShowing() {
    119   return bookmark_bubble_ != NULL;
    120 }
    121 
    122 void BookmarkBubbleView::Hide() {
    123   if (IsShowing())
    124     bookmark_bubble_->Close();
    125 }
    126 
    127 BookmarkBubbleView::~BookmarkBubbleView() {
    128   if (apply_edits_) {
    129     ApplyEdits();
    130   } else if (remove_bookmark_) {
    131     BookmarkModel* model = profile_->GetBookmarkModel();
    132     const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_);
    133     if (node)
    134       model->Remove(node->parent(), node->parent()->GetIndexOf(node));
    135   }
    136 }
    137 
    138 void BookmarkBubbleView::BubbleShown() {
    139   DCHECK(GetWidget());
    140   GetFocusManager()->RegisterAccelerator(
    141       views::Accelerator(ui::VKEY_RETURN, false, false, false), this);
    142 
    143   title_tf_->RequestFocus();
    144   title_tf_->SelectAll();
    145 }
    146 
    147 bool BookmarkBubbleView::AcceleratorPressed(
    148     const views::Accelerator& accelerator) {
    149   if (accelerator.GetKeyCode() != ui::VKEY_RETURN)
    150     return false;
    151 
    152   if (edit_button_->HasFocus())
    153     HandleButtonPressed(edit_button_);
    154   else
    155     HandleButtonPressed(close_button_);
    156   return true;
    157 }
    158 
    159 void BookmarkBubbleView::ViewHierarchyChanged(bool is_add, View* parent,
    160                                               View* child) {
    161   if (is_add && child == this)
    162     Init();
    163 }
    164 
    165 BookmarkBubbleView::BookmarkBubbleView(BubbleDelegate* delegate,
    166                                        Profile* profile,
    167                                        const GURL& url,
    168                                        bool newly_bookmarked)
    169     : delegate_(delegate),
    170       profile_(profile),
    171       url_(url),
    172       newly_bookmarked_(newly_bookmarked),
    173       parent_model_(
    174           profile_->GetBookmarkModel(),
    175           profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url)),
    176       remove_bookmark_(false),
    177       apply_edits_(true) {
    178 }
    179 
    180 void BookmarkBubbleView::Init() {
    181   static SkColor kTitleColor;
    182   static bool initialized = false;
    183   if (!initialized) {
    184     kTitleColor = color_utils::GetReadableColor(SkColorSetRGB(6, 45, 117),
    185                                                 Bubble::kBackgroundColor);
    186     kCloseImage = ResourceBundle::GetSharedInstance().GetBitmapNamed(
    187       IDR_INFO_BUBBLE_CLOSE);
    188 
    189     initialized = true;
    190   }
    191 
    192   remove_link_ = new Link(UTF16ToWide(l10n_util::GetStringUTF16(
    193       IDS_BOOMARK_BUBBLE_REMOVE_BOOKMARK)));
    194   remove_link_->SetController(this);
    195 
    196   edit_button_ = new NativeButton(
    197       this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_OPTIONS)));
    198 
    199   close_button_ =
    200       new NativeButton(this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_DONE)));
    201   close_button_->SetIsDefault(true);
    202 
    203   Label* combobox_label = new Label(
    204       UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_FOLDER_TEXT)));
    205 
    206   parent_combobox_ = new Combobox(&parent_model_);
    207   parent_combobox_->SetSelectedItem(parent_model_.node_parent_index());
    208   parent_combobox_->set_listener(this);
    209   parent_combobox_->SetAccessibleName(
    210       WideToUTF16Hack(combobox_label->GetText()));
    211 #if defined(TOUCH_UI)
    212   // TODO(saintlou): This is a short term workaround for touch
    213   parent_combobox_->SetEnabled(false);
    214 #endif
    215 
    216   Label* title_label = new Label(UTF16ToWide(l10n_util::GetStringUTF16(
    217       newly_bookmarked_ ? IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED :
    218                           IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK)));
    219   title_label->SetFont(
    220       ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont));
    221   title_label->SetColor(kTitleColor);
    222 
    223   GridLayout* layout = new GridLayout(this);
    224   SetLayoutManager(layout);
    225 
    226   ColumnSet* cs = layout->AddColumnSet(0);
    227 
    228   // Top (title) row.
    229   cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF,
    230                 0, 0);
    231   cs->AddPaddingColumn(1, views::kUnrelatedControlHorizontalSpacing);
    232   cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF,
    233                 0, 0);
    234 
    235   // Middle (input field) rows.
    236   cs = layout->AddColumnSet(2);
    237   cs->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0,
    238                 GridLayout::USE_PREF, 0, 0);
    239   cs->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing);
    240   cs->AddColumn(GridLayout::FILL, GridLayout::CENTER, 1,
    241                 GridLayout::USE_PREF, 0, kMinimumFieldSize);
    242 
    243   // Bottom (buttons) row.
    244   cs = layout->AddColumnSet(3);
    245   cs->AddPaddingColumn(1, views::kRelatedControlHorizontalSpacing);
    246   cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0,
    247                 GridLayout::USE_PREF, 0, 0);
    248   // We subtract 2 to account for the natural button padding, and
    249   // to bring the separation visually in line with the row separation
    250   // height.
    251   cs->AddPaddingColumn(0, views::kRelatedButtonHSpacing - 2);
    252   cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0,
    253                 GridLayout::USE_PREF, 0, 0);
    254 
    255   layout->StartRow(0, 0);
    256   layout->AddView(title_label);
    257   layout->AddView(remove_link_);
    258 
    259   layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);
    260   layout->StartRow(0, 2);
    261   layout->AddView(new Label(UTF16ToWide(
    262       l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_TITLE_TEXT))));
    263   title_tf_ = new views::Textfield();
    264   title_tf_->SetText(GetTitle());
    265   layout->AddView(title_tf_);
    266 
    267   layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);
    268 
    269   layout->StartRow(0, 2);
    270   layout->AddView(combobox_label);
    271   layout->AddView(parent_combobox_);
    272   layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);
    273 
    274   layout->StartRow(0, 3);
    275   layout->AddView(edit_button_);
    276   layout->AddView(close_button_);
    277 }
    278 
    279 string16 BookmarkBubbleView::GetTitle() {
    280   BookmarkModel* bookmark_model= profile_->GetBookmarkModel();
    281   const BookmarkNode* node =
    282       bookmark_model->GetMostRecentlyAddedNodeForURL(url_);
    283   if (node)
    284     return node->GetTitle();
    285   else
    286     NOTREACHED();
    287   return string16();
    288 }
    289 
    290 void BookmarkBubbleView::ButtonPressed(
    291     views::Button* sender, const views::Event& event) {
    292   HandleButtonPressed(sender);
    293 }
    294 
    295 void BookmarkBubbleView::LinkActivated(Link* source, int event_flags) {
    296   DCHECK(source == remove_link_);
    297   UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"),
    298                             profile_);
    299 
    300   // Set this so we remove the bookmark after the window closes.
    301   remove_bookmark_ = true;
    302   apply_edits_ = false;
    303 
    304   bubble_->set_fade_away_on_close(true);
    305   Close();
    306 }
    307 
    308 void BookmarkBubbleView::ItemChanged(Combobox* combobox,
    309                                      int prev_index,
    310                                      int new_index) {
    311   if (new_index + 1 == parent_model_.GetItemCount()) {
    312     UserMetrics::RecordAction(
    313               UserMetricsAction("BookmarkBubble_EditFromCombobox"), profile_);
    314 
    315     ShowEditor();
    316     return;
    317   }
    318 }
    319 
    320 void BookmarkBubbleView::BubbleClosing(Bubble* bubble,
    321                                        bool closed_by_escape) {
    322   if (closed_by_escape) {
    323     remove_bookmark_ = newly_bookmarked_;
    324     apply_edits_ = false;
    325   }
    326 
    327   // We have to reset |bubble_| here, not in our destructor, because we'll be
    328   // destroyed asynchronously and the shown state will be checked before then.
    329   DCHECK(bookmark_bubble_ == this);
    330   bookmark_bubble_ = NULL;
    331 
    332   if (delegate_)
    333     delegate_->BubbleClosing(bubble, closed_by_escape);
    334   NotificationService::current()->Notify(
    335       NotificationType::BOOKMARK_BUBBLE_HIDDEN,
    336       Source<Profile>(profile_->GetOriginalProfile()),
    337       NotificationService::NoDetails());
    338 }
    339 
    340 bool BookmarkBubbleView::CloseOnEscape() {
    341   return delegate_ ? delegate_->CloseOnEscape() : true;
    342 }
    343 
    344 bool BookmarkBubbleView::FadeInOnShow() {
    345   return false;
    346 }
    347 
    348 std::wstring BookmarkBubbleView::accessible_name() {
    349   return UTF16ToWide(
    350       l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_ADD_BOOKMARK));
    351 }
    352 
    353 void BookmarkBubbleView::Close() {
    354   ApplyEdits();
    355   static_cast<Bubble*>(GetWidget())->Close();
    356 }
    357 
    358 void BookmarkBubbleView::HandleButtonPressed(views::Button* sender) {
    359   if (sender == edit_button_) {
    360     UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"),
    361                               profile_);
    362     bubble_->set_fade_away_on_close(true);
    363     ShowEditor();
    364   } else {
    365     DCHECK(sender == close_button_);
    366     bubble_->set_fade_away_on_close(true);
    367     Close();
    368   }
    369   // WARNING: we've most likely been deleted when CloseWindow returns.
    370 }
    371 
    372 void BookmarkBubbleView::ShowEditor() {
    373 #if defined(TOUCH_UI)
    374   // Close the Bubble
    375   Close();
    376 
    377   // Open the Bookmark Manager
    378   Browser* browser = BrowserList::GetLastActiveWithProfile(profile_);
    379   DCHECK(browser);
    380   if (browser)
    381     browser->OpenBookmarkManager();
    382   else
    383     NOTREACHED();
    384 
    385 #else
    386   const BookmarkNode* node =
    387       profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_);
    388 
    389 #if defined(OS_WIN)
    390   // Parent the editor to our root ancestor (not the root we're in, as that
    391   // is the info bubble and will close shortly).
    392   HWND parent = GetAncestor(GetWidget()->GetNativeView(), GA_ROOTOWNER);
    393 
    394   // We're about to show the bookmark editor. When the bookmark editor closes
    395   // we want the browser to become active. WidgetWin::Hide() does a hide in
    396   // a such way that activation isn't changed, which means when we close
    397   // Windows gets confused as to who it should give active status to. We
    398   // explicitly hide the bookmark bubble window in such a way that activation
    399   // status changes. That way, when the editor closes, activation is properly
    400   // restored to the browser.
    401   ShowWindow(GetWidget()->GetNativeView(), SW_HIDE);
    402 #else
    403   gfx::NativeWindow parent = GTK_WINDOW(
    404       static_cast<views::WidgetGtk*>(GetWidget())->GetTransientParent());
    405 #endif
    406 
    407   // Even though we just hid the window, we need to invoke Close to schedule
    408   // the delete and all that.
    409   Close();
    410 
    411   if (node) {
    412     BookmarkEditor::Show(parent, profile_, NULL,
    413                          BookmarkEditor::EditDetails(node),
    414                          BookmarkEditor::SHOW_TREE);
    415   }
    416 #endif
    417 }
    418 
    419 void BookmarkBubbleView::ApplyEdits() {
    420   // Set this to make sure we don't attempt to apply edits again.
    421   apply_edits_ = false;
    422 
    423   BookmarkModel* model = profile_->GetBookmarkModel();
    424   const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_);
    425   if (node) {
    426     const string16 new_title = title_tf_->text();
    427     if (new_title != node->GetTitle()) {
    428       model->SetTitle(node, new_title);
    429       UserMetrics::RecordAction(
    430           UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"),
    431           profile_);
    432     }
    433     // Last index means 'Choose another folder...'
    434     if (parent_combobox_->selected_item() <
    435         parent_model_.GetItemCount() - 1) {
    436       const BookmarkNode* new_parent =
    437           parent_model_.GetNodeAt(parent_combobox_->selected_item());
    438       if (new_parent != node->parent()) {
    439         UserMetrics::RecordAction(
    440             UserMetricsAction("BookmarkBubble_ChangeParent"), profile_);
    441         model->Move(node, new_parent, new_parent->child_count());
    442       }
    443     }
    444   }
    445 }
    446