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: 48 explicit UnsizedCombobox(ui::ComboboxModel* model) : views::Combobox(model) {} 49 virtual ~UnsizedCombobox() {} 50 51 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 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 89 bool BookmarkBubbleView::IsShowing() { 90 return bookmark_bubble_ != NULL; 91 } 92 93 void BookmarkBubbleView::Hide() { 94 if (IsShowing()) 95 bookmark_bubble_->GetWidget()->Close(); 96 } 97 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 112 views::View* BookmarkBubbleView::GetInitiallyFocusedView() { 113 return title_tf_; 114 } 115 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 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 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 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 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 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 306 void BookmarkBubbleView::ButtonPressed(views::Button* sender, 307 const ui::Event& event) { 308 HandleButtonPressed(sender); 309 } 310 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 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 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 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