bookmark_bubble_view.cc revision f8ee788a64d60abd8f2d742a5fdedde054ecd910
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
36using base::UserMetricsAction;
37using views::ColumnSet;
38using views::GridLayout;
39
40namespace {
41
42// Width of the border of a button.
43const int kControlBorderWidth = 2;
44
45// This combobox prevents any lengthy content from stretching the bubble view.
46class 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
61BookmarkBubbleView* BookmarkBubbleView::bookmark_bubble_ = NULL;
62
63// static
64void 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
89bool BookmarkBubbleView::IsShowing() {
90  return bookmark_bubble_ != NULL;
91}
92
93void BookmarkBubbleView::Hide() {
94  if (IsShowing())
95    bookmark_bubble_->GetWidget()->Close();
96}
97
98BookmarkBubbleView::~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
112views::View* BookmarkBubbleView::GetInitiallyFocusedView() {
113  return title_tf_;
114}
115
116void 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
126bool 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
142void 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
256BookmarkBubbleView::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
286base::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
298void 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
306void BookmarkBubbleView::ButtonPressed(views::Button* sender,
307                                       const ui::Event& event) {
308  HandleButtonPressed(sender);
309}
310
311void 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
318void 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
334void 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
350void 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