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