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