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 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 70 void HideBookmarkBubbleView() { 71 BookmarkBubbleView::Hide(); 72 } 73 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 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 118 bool BookmarkBubbleView::IsShowing() { 119 return bookmark_bubble_ != NULL; 120 } 121 122 void BookmarkBubbleView::Hide() { 123 if (IsShowing()) 124 bookmark_bubble_->Close(); 125 } 126 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 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 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 159 void BookmarkBubbleView::ViewHierarchyChanged(bool is_add, View* parent, 160 View* child) { 161 if (is_add && child == this) 162 Init(); 163 } 164 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 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 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 290 void BookmarkBubbleView::ButtonPressed( 291 views::Button* sender, const views::Event& event) { 292 HandleButtonPressed(sender); 293 } 294 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 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 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 340 bool BookmarkBubbleView::CloseOnEscape() { 341 return delegate_ ? delegate_->CloseOnEscape() : true; 342 } 343 344 bool BookmarkBubbleView::FadeInOnShow() { 345 return false; 346 } 347 348 std::wstring BookmarkBubbleView::accessible_name() { 349 return UTF16ToWide( 350 l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_ADD_BOOKMARK)); 351 } 352 353 void BookmarkBubbleView::Close() { 354 ApplyEdits(); 355 static_cast<Bubble*>(GetWidget())->Close(); 356 } 357 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 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 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