1// Copyright (c) 2011 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/string16.h"
8#include "base/string_util.h"
9#include "base/utf_string_conversions.h"
10#include "chrome/app/chrome_command_ids.h"
11#include "chrome/browser/bookmarks/bookmark_editor.h"
12#include "chrome/browser/bookmarks/bookmark_model.h"
13#include "chrome/browser/bookmarks/bookmark_utils.h"
14#include "chrome/browser/metrics/user_metrics.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/ui/browser.h"
17#include "chrome/browser/ui/browser_list.h"
18#include "chrome/browser/ui/views/bubble/bubble.h"
19#include "content/common/notification_service.h"
20#include "grit/generated_resources.h"
21#include "grit/theme_resources.h"
22#include "ui/base/keycodes/keyboard_codes.h"
23#include "ui/base/l10n/l10n_util.h"
24#include "ui/base/resource/resource_bundle.h"
25#include "ui/gfx/canvas.h"
26#include "ui/gfx/color_utils.h"
27#include "views/controls/button/native_button.h"
28#include "views/controls/textfield/textfield.h"
29#include "views/events/event.h"
30#include "views/focus/focus_manager.h"
31#include "views/layout/grid_layout.h"
32#include "views/layout/layout_constants.h"
33#include "views/window/client_view.h"
34#include "views/window/window.h"
35
36using views::Combobox;
37using views::ColumnSet;
38using views::GridLayout;
39using views::Label;
40using views::Link;
41using views::NativeButton;
42using views::View;
43
44// Padding between "Title:" and the actual title.
45static const int kTitlePadding = 4;
46
47// Minimum width for the fields - they will push out the size of the bubble if
48// necessary. This should be big enough so that the field pushes the right side
49// of the bubble far enough so that the edit button's left edge is to the right
50// of the field's left edge.
51static const int kMinimumFieldSize = 180;
52
53// Bubble close image.
54static SkBitmap* kCloseImage = NULL;
55
56// Declared in browser_dialogs.h so callers don't have to depend on our header.
57
58namespace browser {
59
60void ShowBookmarkBubbleView(views::Window* parent,
61                            const gfx::Rect& bounds,
62                            BubbleDelegate* delegate,
63                            Profile* profile,
64                            const GURL& url,
65                            bool newly_bookmarked) {
66  BookmarkBubbleView::Show(parent, bounds, delegate, profile, url,
67                           newly_bookmarked);
68}
69
70void HideBookmarkBubbleView() {
71  BookmarkBubbleView::Hide();
72}
73
74bool IsBookmarkBubbleViewShowing() {
75  return BookmarkBubbleView::IsShowing();
76}
77
78}  // namespace browser
79
80// BookmarkBubbleView ---------------------------------------------------------
81
82BookmarkBubbleView* BookmarkBubbleView::bookmark_bubble_ = NULL;
83
84// static
85void BookmarkBubbleView::Show(views::Window* parent,
86                              const gfx::Rect& bounds,
87                              BubbleDelegate* delegate,
88                              Profile* profile,
89                              const GURL& url,
90                              bool newly_bookmarked) {
91  if (IsShowing())
92    return;
93
94  bookmark_bubble_ = new BookmarkBubbleView(delegate, profile, url,
95                                            newly_bookmarked);
96  // TODO(beng): Pass |parent| after V2 is complete.
97  Bubble* bubble = Bubble::Show(
98      parent->client_view()->GetWidget(), bounds, BubbleBorder::TOP_RIGHT,
99      bookmark_bubble_, bookmark_bubble_);
100  // |bubble_| can be set to NULL in BubbleClosing when we close the bubble
101  // asynchronously. However, that can happen during the Show call above if the
102  // window loses activation while we are getting to ready to show the bubble,
103  // so we must check to make sure we still have a valid bubble before
104  // proceeding.
105  if (!bookmark_bubble_)
106    return;
107  bookmark_bubble_->set_bubble(bubble);
108  bubble->SizeToContents();
109  GURL url_ptr(url);
110  NotificationService::current()->Notify(
111      NotificationType::BOOKMARK_BUBBLE_SHOWN,
112      Source<Profile>(profile->GetOriginalProfile()),
113      Details<GURL>(&url_ptr));
114  bookmark_bubble_->BubbleShown();
115}
116
117// static
118bool BookmarkBubbleView::IsShowing() {
119  return bookmark_bubble_ != NULL;
120}
121
122void BookmarkBubbleView::Hide() {
123  if (IsShowing())
124    bookmark_bubble_->Close();
125}
126
127BookmarkBubbleView::~BookmarkBubbleView() {
128  if (apply_edits_) {
129    ApplyEdits();
130  } else if (remove_bookmark_) {
131    BookmarkModel* model = profile_->GetBookmarkModel();
132    const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_);
133    if (node)
134      model->Remove(node->parent(), node->parent()->GetIndexOf(node));
135  }
136}
137
138void BookmarkBubbleView::BubbleShown() {
139  DCHECK(GetWidget());
140  GetFocusManager()->RegisterAccelerator(
141      views::Accelerator(ui::VKEY_RETURN, false, false, false), this);
142
143  title_tf_->RequestFocus();
144  title_tf_->SelectAll();
145}
146
147bool BookmarkBubbleView::AcceleratorPressed(
148    const views::Accelerator& accelerator) {
149  if (accelerator.GetKeyCode() != ui::VKEY_RETURN)
150    return false;
151
152  if (edit_button_->HasFocus())
153    HandleButtonPressed(edit_button_);
154  else
155    HandleButtonPressed(close_button_);
156  return true;
157}
158
159void BookmarkBubbleView::ViewHierarchyChanged(bool is_add, View* parent,
160                                              View* child) {
161  if (is_add && child == this)
162    Init();
163}
164
165BookmarkBubbleView::BookmarkBubbleView(BubbleDelegate* delegate,
166                                       Profile* profile,
167                                       const GURL& url,
168                                       bool newly_bookmarked)
169    : delegate_(delegate),
170      profile_(profile),
171      url_(url),
172      newly_bookmarked_(newly_bookmarked),
173      parent_model_(
174          profile_->GetBookmarkModel(),
175          profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url)),
176      remove_bookmark_(false),
177      apply_edits_(true) {
178}
179
180void BookmarkBubbleView::Init() {
181  static SkColor kTitleColor;
182  static bool initialized = false;
183  if (!initialized) {
184    kTitleColor = color_utils::GetReadableColor(SkColorSetRGB(6, 45, 117),
185                                                Bubble::kBackgroundColor);
186    kCloseImage = ResourceBundle::GetSharedInstance().GetBitmapNamed(
187      IDR_INFO_BUBBLE_CLOSE);
188
189    initialized = true;
190  }
191
192  remove_link_ = new Link(UTF16ToWide(l10n_util::GetStringUTF16(
193      IDS_BOOMARK_BUBBLE_REMOVE_BOOKMARK)));
194  remove_link_->SetController(this);
195
196  edit_button_ = new NativeButton(
197      this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_OPTIONS)));
198
199  close_button_ =
200      new NativeButton(this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_DONE)));
201  close_button_->SetIsDefault(true);
202
203  Label* combobox_label = new Label(
204      UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_FOLDER_TEXT)));
205
206  parent_combobox_ = new Combobox(&parent_model_);
207  parent_combobox_->SetSelectedItem(parent_model_.node_parent_index());
208  parent_combobox_->set_listener(this);
209  parent_combobox_->SetAccessibleName(
210      WideToUTF16Hack(combobox_label->GetText()));
211#if defined(TOUCH_UI)
212  // TODO(saintlou): This is a short term workaround for touch
213  parent_combobox_->SetEnabled(false);
214#endif
215
216  Label* title_label = new Label(UTF16ToWide(l10n_util::GetStringUTF16(
217      newly_bookmarked_ ? IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED :
218                          IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK)));
219  title_label->SetFont(
220      ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont));
221  title_label->SetColor(kTitleColor);
222
223  GridLayout* layout = new GridLayout(this);
224  SetLayoutManager(layout);
225
226  ColumnSet* cs = layout->AddColumnSet(0);
227
228  // Top (title) row.
229  cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF,
230                0, 0);
231  cs->AddPaddingColumn(1, views::kUnrelatedControlHorizontalSpacing);
232  cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF,
233                0, 0);
234
235  // Middle (input field) rows.
236  cs = layout->AddColumnSet(2);
237  cs->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0,
238                GridLayout::USE_PREF, 0, 0);
239  cs->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing);
240  cs->AddColumn(GridLayout::FILL, GridLayout::CENTER, 1,
241                GridLayout::USE_PREF, 0, kMinimumFieldSize);
242
243  // Bottom (buttons) row.
244  cs = layout->AddColumnSet(3);
245  cs->AddPaddingColumn(1, views::kRelatedControlHorizontalSpacing);
246  cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0,
247                GridLayout::USE_PREF, 0, 0);
248  // We subtract 2 to account for the natural button padding, and
249  // to bring the separation visually in line with the row separation
250  // height.
251  cs->AddPaddingColumn(0, views::kRelatedButtonHSpacing - 2);
252  cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0,
253                GridLayout::USE_PREF, 0, 0);
254
255  layout->StartRow(0, 0);
256  layout->AddView(title_label);
257  layout->AddView(remove_link_);
258
259  layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);
260  layout->StartRow(0, 2);
261  layout->AddView(new Label(UTF16ToWide(
262      l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_TITLE_TEXT))));
263  title_tf_ = new views::Textfield();
264  title_tf_->SetText(GetTitle());
265  layout->AddView(title_tf_);
266
267  layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);
268
269  layout->StartRow(0, 2);
270  layout->AddView(combobox_label);
271  layout->AddView(parent_combobox_);
272  layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);
273
274  layout->StartRow(0, 3);
275  layout->AddView(edit_button_);
276  layout->AddView(close_button_);
277}
278
279string16 BookmarkBubbleView::GetTitle() {
280  BookmarkModel* bookmark_model= profile_->GetBookmarkModel();
281  const BookmarkNode* node =
282      bookmark_model->GetMostRecentlyAddedNodeForURL(url_);
283  if (node)
284    return node->GetTitle();
285  else
286    NOTREACHED();
287  return string16();
288}
289
290void BookmarkBubbleView::ButtonPressed(
291    views::Button* sender, const views::Event& event) {
292  HandleButtonPressed(sender);
293}
294
295void BookmarkBubbleView::LinkActivated(Link* source, int event_flags) {
296  DCHECK(source == remove_link_);
297  UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"),
298                            profile_);
299
300  // Set this so we remove the bookmark after the window closes.
301  remove_bookmark_ = true;
302  apply_edits_ = false;
303
304  bubble_->set_fade_away_on_close(true);
305  Close();
306}
307
308void BookmarkBubbleView::ItemChanged(Combobox* combobox,
309                                     int prev_index,
310                                     int new_index) {
311  if (new_index + 1 == parent_model_.GetItemCount()) {
312    UserMetrics::RecordAction(
313              UserMetricsAction("BookmarkBubble_EditFromCombobox"), profile_);
314
315    ShowEditor();
316    return;
317  }
318}
319
320void BookmarkBubbleView::BubbleClosing(Bubble* bubble,
321                                       bool closed_by_escape) {
322  if (closed_by_escape) {
323    remove_bookmark_ = newly_bookmarked_;
324    apply_edits_ = false;
325  }
326
327  // We have to reset |bubble_| here, not in our destructor, because we'll be
328  // destroyed asynchronously and the shown state will be checked before then.
329  DCHECK(bookmark_bubble_ == this);
330  bookmark_bubble_ = NULL;
331
332  if (delegate_)
333    delegate_->BubbleClosing(bubble, closed_by_escape);
334  NotificationService::current()->Notify(
335      NotificationType::BOOKMARK_BUBBLE_HIDDEN,
336      Source<Profile>(profile_->GetOriginalProfile()),
337      NotificationService::NoDetails());
338}
339
340bool BookmarkBubbleView::CloseOnEscape() {
341  return delegate_ ? delegate_->CloseOnEscape() : true;
342}
343
344bool BookmarkBubbleView::FadeInOnShow() {
345  return false;
346}
347
348std::wstring BookmarkBubbleView::accessible_name() {
349  return UTF16ToWide(
350      l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_ADD_BOOKMARK));
351}
352
353void BookmarkBubbleView::Close() {
354  ApplyEdits();
355  static_cast<Bubble*>(GetWidget())->Close();
356}
357
358void BookmarkBubbleView::HandleButtonPressed(views::Button* sender) {
359  if (sender == edit_button_) {
360    UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"),
361                              profile_);
362    bubble_->set_fade_away_on_close(true);
363    ShowEditor();
364  } else {
365    DCHECK(sender == close_button_);
366    bubble_->set_fade_away_on_close(true);
367    Close();
368  }
369  // WARNING: we've most likely been deleted when CloseWindow returns.
370}
371
372void BookmarkBubbleView::ShowEditor() {
373#if defined(TOUCH_UI)
374  // Close the Bubble
375  Close();
376
377  // Open the Bookmark Manager
378  Browser* browser = BrowserList::GetLastActiveWithProfile(profile_);
379  DCHECK(browser);
380  if (browser)
381    browser->OpenBookmarkManager();
382  else
383    NOTREACHED();
384
385#else
386  const BookmarkNode* node =
387      profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_);
388
389#if defined(OS_WIN)
390  // Parent the editor to our root ancestor (not the root we're in, as that
391  // is the info bubble and will close shortly).
392  HWND parent = GetAncestor(GetWidget()->GetNativeView(), GA_ROOTOWNER);
393
394  // We're about to show the bookmark editor. When the bookmark editor closes
395  // we want the browser to become active. WidgetWin::Hide() does a hide in
396  // a such way that activation isn't changed, which means when we close
397  // Windows gets confused as to who it should give active status to. We
398  // explicitly hide the bookmark bubble window in such a way that activation
399  // status changes. That way, when the editor closes, activation is properly
400  // restored to the browser.
401  ShowWindow(GetWidget()->GetNativeView(), SW_HIDE);
402#else
403  gfx::NativeWindow parent = GTK_WINDOW(
404      static_cast<views::WidgetGtk*>(GetWidget())->GetTransientParent());
405#endif
406
407  // Even though we just hid the window, we need to invoke Close to schedule
408  // the delete and all that.
409  Close();
410
411  if (node) {
412    BookmarkEditor::Show(parent, profile_, NULL,
413                         BookmarkEditor::EditDetails(node),
414                         BookmarkEditor::SHOW_TREE);
415  }
416#endif
417}
418
419void BookmarkBubbleView::ApplyEdits() {
420  // Set this to make sure we don't attempt to apply edits again.
421  apply_edits_ = false;
422
423  BookmarkModel* model = profile_->GetBookmarkModel();
424  const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_);
425  if (node) {
426    const string16 new_title = title_tf_->text();
427    if (new_title != node->GetTitle()) {
428      model->SetTitle(node, new_title);
429      UserMetrics::RecordAction(
430          UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"),
431          profile_);
432    }
433    // Last index means 'Choose another folder...'
434    if (parent_combobox_->selected_item() <
435        parent_model_.GetItemCount() - 1) {
436      const BookmarkNode* new_parent =
437          parent_model_.GetNodeAt(parent_combobox_->selected_item());
438      if (new_parent != node->parent()) {
439        UserMetrics::RecordAction(
440            UserMetricsAction("BookmarkBubble_ChangeParent"), profile_);
441        model->Move(node, new_parent, new_parent->child_count());
442      }
443    }
444  }
445}
446