Home | History | Annotate | Download | only in bookmarks
      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/views/bookmarks/bookmark_bubble_view.h"
      6 
      7 #include "base/strings/string16.h"
      8 #include "base/strings/string_util.h"
      9 #include "base/strings/utf_string_conversions.h"
     10 #include "chrome/app/chrome_command_ids.h"
     11 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
     12 #include "chrome/browser/profiles/profile.h"
     13 #include "chrome/browser/ui/bookmarks/bookmark_editor.h"
     14 #include "chrome/browser/ui/sync/sync_promo_ui.h"
     15 #include "chrome/browser/ui/views/bookmarks/bookmark_bubble_view_observer.h"
     16 #include "chrome/browser/ui/views/bookmarks/bookmark_sync_promo_view.h"
     17 #include "components/bookmarks/browser/bookmark_model.h"
     18 #include "components/bookmarks/browser/bookmark_utils.h"
     19 #include "content/public/browser/user_metrics.h"
     20 #include "grit/generated_resources.h"
     21 #include "grit/theme_resources.h"
     22 #include "ui/accessibility/ax_view_state.h"
     23 #include "ui/base/l10n/l10n_util.h"
     24 #include "ui/base/resource/resource_bundle.h"
     25 #include "ui/events/keycodes/keyboard_codes.h"
     26 #include "ui/views/bubble/bubble_frame_view.h"
     27 #include "ui/views/controls/button/label_button.h"
     28 #include "ui/views/controls/combobox/combobox.h"
     29 #include "ui/views/controls/label.h"
     30 #include "ui/views/controls/link.h"
     31 #include "ui/views/controls/textfield/textfield.h"
     32 #include "ui/views/layout/grid_layout.h"
     33 #include "ui/views/layout/layout_constants.h"
     34 #include "ui/views/widget/widget.h"
     35 
     36 using base::UserMetricsAction;
     37 using views::ColumnSet;
     38 using views::GridLayout;
     39 
     40 namespace {
     41 
     42 // Width of the border of a button.
     43 const int kControlBorderWidth = 2;
     44 
     45 // This combobox prevents any lengthy content from stretching the bubble view.
     46 class UnsizedCombobox : public views::Combobox {
     47  public:
     48   explicit UnsizedCombobox(ui::ComboboxModel* model) : views::Combobox(model) {}
     49   virtual ~UnsizedCombobox() {}
     50 
     51   virtual gfx::Size GetPreferredSize() const OVERRIDE {
     52     return gfx::Size(0, views::Combobox::GetPreferredSize().height());
     53   }
     54 
     55  private:
     56   DISALLOW_COPY_AND_ASSIGN(UnsizedCombobox);
     57 };
     58 
     59 }  // namespace
     60 
     61 BookmarkBubbleView* BookmarkBubbleView::bookmark_bubble_ = NULL;
     62 
     63 // static
     64 void BookmarkBubbleView::ShowBubble(views::View* anchor_view,
     65                                     BookmarkBubbleViewObserver* observer,
     66                                     scoped_ptr<BookmarkBubbleDelegate> delegate,
     67                                     Profile* profile,
     68                                     const GURL& url,
     69                                     bool newly_bookmarked) {
     70   if (IsShowing())
     71     return;
     72 
     73   bookmark_bubble_ = new BookmarkBubbleView(anchor_view,
     74                                             observer,
     75                                             delegate.Pass(),
     76                                             profile,
     77                                             url,
     78                                             newly_bookmarked);
     79   views::BubbleDelegateView::CreateBubble(bookmark_bubble_)->Show();
     80   // Select the entire title textfield contents when the bubble is first shown.
     81   bookmark_bubble_->title_tf_->SelectAll(true);
     82   bookmark_bubble_->SetArrowPaintType(views::BubbleBorder::PAINT_NONE);
     83 
     84   if (bookmark_bubble_->observer_)
     85     bookmark_bubble_->observer_->OnBookmarkBubbleShown(url);
     86 }
     87 
     88 // static
     89 bool BookmarkBubbleView::IsShowing() {
     90   return bookmark_bubble_ != NULL;
     91 }
     92 
     93 void BookmarkBubbleView::Hide() {
     94   if (IsShowing())
     95     bookmark_bubble_->GetWidget()->Close();
     96 }
     97 
     98 BookmarkBubbleView::~BookmarkBubbleView() {
     99   if (apply_edits_) {
    100     ApplyEdits();
    101   } else if (remove_bookmark_) {
    102     BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
    103     const BookmarkNode* node = model->GetMostRecentlyAddedUserNodeForURL(url_);
    104     if (node)
    105       model->Remove(node->parent(), node->parent()->GetIndexOf(node));
    106   }
    107   // |parent_combobox_| needs to be destroyed before |parent_model_| as it
    108   // uses |parent_model_| in its destructor.
    109   delete parent_combobox_;
    110 }
    111 
    112 views::View* BookmarkBubbleView::GetInitiallyFocusedView() {
    113   return title_tf_;
    114 }
    115 
    116 void BookmarkBubbleView::WindowClosing() {
    117   // We have to reset |bubble_| here, not in our destructor, because we'll be
    118   // destroyed asynchronously and the shown state will be checked before then.
    119   DCHECK_EQ(bookmark_bubble_, this);
    120   bookmark_bubble_ = NULL;
    121 
    122   if (observer_)
    123     observer_->OnBookmarkBubbleHidden();
    124 }
    125 
    126 bool BookmarkBubbleView::AcceleratorPressed(
    127     const ui::Accelerator& accelerator) {
    128   if (accelerator.key_code() == ui::VKEY_RETURN) {
    129      if (edit_button_->HasFocus())
    130        HandleButtonPressed(edit_button_);
    131      else
    132        HandleButtonPressed(close_button_);
    133      return true;
    134   } else if (accelerator.key_code() == ui::VKEY_ESCAPE) {
    135     remove_bookmark_ = newly_bookmarked_;
    136     apply_edits_ = false;
    137   }
    138 
    139   return BubbleDelegateView::AcceleratorPressed(accelerator);
    140 }
    141 
    142 void BookmarkBubbleView::Init() {
    143   views::Label* title_label = new views::Label(
    144       l10n_util::GetStringUTF16(
    145           newly_bookmarked_ ? IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARKED :
    146                               IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARK));
    147   ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
    148   title_label->SetFontList(rb->GetFontList(ui::ResourceBundle::MediumFont));
    149 
    150   remove_button_ = new views::LabelButton(this, l10n_util::GetStringUTF16(
    151       IDS_BOOKMARK_BUBBLE_REMOVE_BOOKMARK));
    152   remove_button_->SetStyle(views::Button::STYLE_BUTTON);
    153 
    154   edit_button_ = new views::LabelButton(
    155       this, l10n_util::GetStringUTF16(IDS_BOOKMARK_BUBBLE_OPTIONS));
    156   edit_button_->SetStyle(views::Button::STYLE_BUTTON);
    157 
    158   close_button_ = new views::LabelButton(
    159       this, l10n_util::GetStringUTF16(IDS_DONE));
    160   close_button_->SetStyle(views::Button::STYLE_BUTTON);
    161   close_button_->SetIsDefault(true);
    162 
    163   views::Label* combobox_label = new views::Label(
    164       l10n_util::GetStringUTF16(IDS_BOOKMARK_BUBBLE_FOLDER_TEXT));
    165 
    166   parent_combobox_ = new UnsizedCombobox(&parent_model_);
    167   parent_combobox_->set_listener(this);
    168   parent_combobox_->SetAccessibleName(
    169       l10n_util::GetStringUTF16(IDS_BOOKMARK_AX_BUBBLE_FOLDER_TEXT));
    170 
    171   GridLayout* layout = new GridLayout(this);
    172   SetLayoutManager(layout);
    173 
    174   // Column sets used in the layout of the bubble.
    175   enum ColumnSetID {
    176     TITLE_COLUMN_SET_ID,
    177     CONTENT_COLUMN_SET_ID,
    178     SYNC_PROMO_COLUMN_SET_ID
    179   };
    180 
    181   ColumnSet* cs = layout->AddColumnSet(TITLE_COLUMN_SET_ID);
    182   cs->AddPaddingColumn(0, views::kButtonHEdgeMarginNew);
    183   cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF,
    184                 0, 0);
    185   cs->AddPaddingColumn(0, views::kButtonHEdgeMarginNew);
    186 
    187   // The column layout used for middle and bottom rows.
    188   cs = layout->AddColumnSet(CONTENT_COLUMN_SET_ID);
    189   cs->AddPaddingColumn(0, views::kButtonHEdgeMarginNew);
    190   cs->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0,
    191                 GridLayout::USE_PREF, 0, 0);
    192   cs->AddPaddingColumn(0, views::kUnrelatedControlHorizontalSpacing);
    193 
    194   cs->AddColumn(GridLayout::FILL, GridLayout::CENTER, 0,
    195                 GridLayout::USE_PREF, 0, 0);
    196   cs->AddPaddingColumn(1, views::kUnrelatedControlLargeHorizontalSpacing);
    197 
    198   cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0,
    199                 GridLayout::USE_PREF, 0, 0);
    200   cs->AddPaddingColumn(0, views::kRelatedButtonHSpacing);
    201   cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0,
    202                 GridLayout::USE_PREF, 0, 0);
    203   cs->AddPaddingColumn(0, views::kButtonHEdgeMarginNew);
    204 
    205   layout->StartRow(0, TITLE_COLUMN_SET_ID);
    206   layout->AddView(title_label);
    207   layout->AddPaddingRow(0, views::kUnrelatedControlHorizontalSpacing);
    208 
    209   layout->StartRow(0, CONTENT_COLUMN_SET_ID);
    210   views::Label* label = new views::Label(
    211       l10n_util::GetStringUTF16(IDS_BOOKMARK_BUBBLE_TITLE_TEXT));
    212   layout->AddView(label);
    213   title_tf_ = new views::Textfield();
    214   title_tf_->SetText(GetTitle());
    215   title_tf_->SetAccessibleName(
    216       l10n_util::GetStringUTF16(IDS_BOOKMARK_AX_BUBBLE_TITLE_TEXT));
    217 
    218   layout->AddView(title_tf_, 5, 1);
    219 
    220   layout->AddPaddingRow(0, views::kUnrelatedControlHorizontalSpacing);
    221 
    222   layout->StartRow(0, CONTENT_COLUMN_SET_ID);
    223   layout->AddView(combobox_label);
    224   layout->AddView(parent_combobox_, 5, 1);
    225 
    226   layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    227 
    228   layout->StartRow(0, CONTENT_COLUMN_SET_ID);
    229   layout->SkipColumns(2);
    230   layout->AddView(remove_button_);
    231   layout->AddView(edit_button_);
    232   layout->AddView(close_button_);
    233 
    234   layout->AddPaddingRow(
    235       0,
    236       views::kUnrelatedControlVerticalSpacing - kControlBorderWidth);
    237 
    238   if (SyncPromoUI::ShouldShowSyncPromo(profile_)) {
    239     // The column layout used for the sync promo.
    240     cs = layout->AddColumnSet(SYNC_PROMO_COLUMN_SET_ID);
    241     cs->AddColumn(GridLayout::FILL,
    242                   GridLayout::FILL,
    243                   1,
    244                   GridLayout::USE_PREF,
    245                   0,
    246                   0);
    247     layout->StartRow(0, SYNC_PROMO_COLUMN_SET_ID);
    248 
    249     sync_promo_view_ = new BookmarkSyncPromoView(delegate_.get());
    250     layout->AddView(sync_promo_view_);
    251   }
    252 
    253   AddAccelerator(ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE));
    254 }
    255 
    256 BookmarkBubbleView::BookmarkBubbleView(
    257     views::View* anchor_view,
    258     BookmarkBubbleViewObserver* observer,
    259     scoped_ptr<BookmarkBubbleDelegate> delegate,
    260     Profile* profile,
    261     const GURL& url,
    262     bool newly_bookmarked)
    263     : BubbleDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT),
    264       observer_(observer),
    265       delegate_(delegate.Pass()),
    266       profile_(profile),
    267       url_(url),
    268       newly_bookmarked_(newly_bookmarked),
    269       parent_model_(
    270           BookmarkModelFactory::GetForProfile(profile_),
    271           BookmarkModelFactory::GetForProfile(profile_)->
    272               GetMostRecentlyAddedUserNodeForURL(url)),
    273       remove_button_(NULL),
    274       edit_button_(NULL),
    275       close_button_(NULL),
    276       title_tf_(NULL),
    277       parent_combobox_(NULL),
    278       sync_promo_view_(NULL),
    279       remove_bookmark_(false),
    280       apply_edits_(true) {
    281   set_margins(gfx::Insets(views::kPanelVertMargin, 0, 0, 0));
    282   // Compensate for built-in vertical padding in the anchor view's image.
    283   set_anchor_view_insets(gfx::Insets(2, 0, 2, 0));
    284 }
    285 
    286 base::string16 BookmarkBubbleView::GetTitle() {
    287   BookmarkModel* bookmark_model =
    288       BookmarkModelFactory::GetForProfile(profile_);
    289   const BookmarkNode* node =
    290       bookmark_model->GetMostRecentlyAddedUserNodeForURL(url_);
    291   if (node)
    292     return node->GetTitle();
    293   else
    294     NOTREACHED();
    295   return base::string16();
    296 }
    297 
    298 void BookmarkBubbleView::GetAccessibleState(ui::AXViewState* state) {
    299   BubbleDelegateView::GetAccessibleState(state);
    300   state->name =
    301       l10n_util::GetStringUTF16(
    302           newly_bookmarked_ ? IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARKED :
    303                               IDS_BOOKMARK_AX_BUBBLE_PAGE_BOOKMARK);
    304 }
    305 
    306 void BookmarkBubbleView::ButtonPressed(views::Button* sender,
    307                                        const ui::Event& event) {
    308   HandleButtonPressed(sender);
    309 }
    310 
    311 void BookmarkBubbleView::OnPerformAction(views::Combobox* combobox) {
    312   if (combobox->selected_index() + 1 == parent_model_.GetItemCount()) {
    313     content::RecordAction(UserMetricsAction("BookmarkBubble_EditFromCombobox"));
    314     ShowEditor();
    315   }
    316 }
    317 
    318 void BookmarkBubbleView::HandleButtonPressed(views::Button* sender) {
    319   if (sender == remove_button_) {
    320     content::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"));
    321     // Set this so we remove the bookmark after the window closes.
    322     remove_bookmark_ = true;
    323     apply_edits_ = false;
    324     GetWidget()->Close();
    325   } else if (sender == edit_button_) {
    326     content::RecordAction(UserMetricsAction("BookmarkBubble_Edit"));
    327     ShowEditor();
    328   } else {
    329     DCHECK_EQ(close_button_, sender);
    330     GetWidget()->Close();
    331   }
    332 }
    333 
    334 void BookmarkBubbleView::ShowEditor() {
    335   const BookmarkNode* node = BookmarkModelFactory::GetForProfile(
    336       profile_)->GetMostRecentlyAddedUserNodeForURL(url_);
    337   views::Widget* parent = anchor_widget();
    338   DCHECK(parent);
    339 
    340   Profile* profile = profile_;
    341   ApplyEdits();
    342   GetWidget()->Close();
    343 
    344   if (node && parent)
    345     BookmarkEditor::Show(parent->GetNativeWindow(), profile,
    346                          BookmarkEditor::EditDetails::EditNode(node),
    347                          BookmarkEditor::SHOW_TREE);
    348 }
    349 
    350 void BookmarkBubbleView::ApplyEdits() {
    351   // Set this to make sure we don't attempt to apply edits again.
    352   apply_edits_ = false;
    353 
    354   BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
    355   const BookmarkNode* node = model->GetMostRecentlyAddedUserNodeForURL(url_);
    356   if (node) {
    357     const base::string16 new_title = title_tf_->text();
    358     if (new_title != node->GetTitle()) {
    359       model->SetTitle(node, new_title);
    360       content::RecordAction(
    361           UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"));
    362     }
    363     parent_model_.MaybeChangeParent(node, parent_combobox_->selected_index());
    364   }
    365 }
    366