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