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
36 using base::UserMetricsAction;
37 using views::ColumnSet;
38 using views::GridLayout;
39
40 namespace {
41
42 // Width of the border of a button.
43 const int kControlBorderWidth = 2;
44
45 // This combobox prevents any lengthy content from stretching the bubble view.
46 class UnsizedCombobox : public views::Combobox {
47 public:
UnsizedCombobox(ui::ComboboxModel * model)48 explicit UnsizedCombobox(ui::ComboboxModel* model) : views::Combobox(model) {}
~UnsizedCombobox()49 virtual ~UnsizedCombobox() {}
50
GetPreferredSize() const51 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
61 BookmarkBubbleView* BookmarkBubbleView::bookmark_bubble_ = NULL;
62
63 // static
ShowBubble(views::View * anchor_view,BookmarkBubbleViewObserver * observer,scoped_ptr<BookmarkBubbleDelegate> delegate,Profile * profile,const GURL & url,bool newly_bookmarked)64 void 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
IsShowing()89 bool BookmarkBubbleView::IsShowing() {
90 return bookmark_bubble_ != NULL;
91 }
92
Hide()93 void BookmarkBubbleView::Hide() {
94 if (IsShowing())
95 bookmark_bubble_->GetWidget()->Close();
96 }
97
~BookmarkBubbleView()98 BookmarkBubbleView::~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
GetInitiallyFocusedView()112 views::View* BookmarkBubbleView::GetInitiallyFocusedView() {
113 return title_tf_;
114 }
115
WindowClosing()116 void 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
AcceleratorPressed(const ui::Accelerator & accelerator)126 bool 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
Init()142 void 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
BookmarkBubbleView(views::View * anchor_view,BookmarkBubbleViewObserver * observer,scoped_ptr<BookmarkBubbleDelegate> delegate,Profile * profile,const GURL & url,bool newly_bookmarked)256 BookmarkBubbleView::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
GetTitle()286 base::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
GetAccessibleState(ui::AXViewState * state)298 void 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
ButtonPressed(views::Button * sender,const ui::Event & event)306 void BookmarkBubbleView::ButtonPressed(views::Button* sender,
307 const ui::Event& event) {
308 HandleButtonPressed(sender);
309 }
310
OnPerformAction(views::Combobox * combobox)311 void 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
HandleButtonPressed(views::Button * sender)318 void 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
ShowEditor()334 void 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
ApplyEdits()350 void 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