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