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
36 using views::Combobox;
37 using views::ColumnSet;
38 using views::GridLayout;
39 using views::Label;
40 using views::Link;
41 using views::NativeButton;
42 using views::View;
43
44 // Padding between "Title:" and the actual title.
45 static 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.
51 static const int kMinimumFieldSize = 180;
52
53 // Bubble close image.
54 static SkBitmap* kCloseImage = NULL;
55
56 // Declared in browser_dialogs.h so callers don't have to depend on our header.
57
58 namespace browser {
59
ShowBookmarkBubbleView(views::Window * parent,const gfx::Rect & bounds,BubbleDelegate * delegate,Profile * profile,const GURL & url,bool newly_bookmarked)60 void 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
HideBookmarkBubbleView()70 void HideBookmarkBubbleView() {
71 BookmarkBubbleView::Hide();
72 }
73
IsBookmarkBubbleViewShowing()74 bool IsBookmarkBubbleViewShowing() {
75 return BookmarkBubbleView::IsShowing();
76 }
77
78 } // namespace browser
79
80 // BookmarkBubbleView ---------------------------------------------------------
81
82 BookmarkBubbleView* BookmarkBubbleView::bookmark_bubble_ = NULL;
83
84 // static
Show(views::Window * parent,const gfx::Rect & bounds,BubbleDelegate * delegate,Profile * profile,const GURL & url,bool newly_bookmarked)85 void 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
IsShowing()118 bool BookmarkBubbleView::IsShowing() {
119 return bookmark_bubble_ != NULL;
120 }
121
Hide()122 void BookmarkBubbleView::Hide() {
123 if (IsShowing())
124 bookmark_bubble_->Close();
125 }
126
~BookmarkBubbleView()127 BookmarkBubbleView::~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
BubbleShown()138 void 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
AcceleratorPressed(const views::Accelerator & accelerator)147 bool 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
ViewHierarchyChanged(bool is_add,View * parent,View * child)159 void BookmarkBubbleView::ViewHierarchyChanged(bool is_add, View* parent,
160 View* child) {
161 if (is_add && child == this)
162 Init();
163 }
164
BookmarkBubbleView(BubbleDelegate * delegate,Profile * profile,const GURL & url,bool newly_bookmarked)165 BookmarkBubbleView::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
Init()180 void 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
GetTitle()279 string16 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
ButtonPressed(views::Button * sender,const views::Event & event)290 void BookmarkBubbleView::ButtonPressed(
291 views::Button* sender, const views::Event& event) {
292 HandleButtonPressed(sender);
293 }
294
LinkActivated(Link * source,int event_flags)295 void 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
ItemChanged(Combobox * combobox,int prev_index,int new_index)308 void 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
BubbleClosing(Bubble * bubble,bool closed_by_escape)320 void 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
CloseOnEscape()340 bool BookmarkBubbleView::CloseOnEscape() {
341 return delegate_ ? delegate_->CloseOnEscape() : true;
342 }
343
FadeInOnShow()344 bool BookmarkBubbleView::FadeInOnShow() {
345 return false;
346 }
347
accessible_name()348 std::wstring BookmarkBubbleView::accessible_name() {
349 return UTF16ToWide(
350 l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_ADD_BOOKMARK));
351 }
352
Close()353 void BookmarkBubbleView::Close() {
354 ApplyEdits();
355 static_cast<Bubble*>(GetWidget())->Close();
356 }
357
HandleButtonPressed(views::Button * sender)358 void 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
ShowEditor()372 void 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
ApplyEdits()419 void 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