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