// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/views/bookmarks/bookmark_bubble_view.h" #include "base/string16.h" #include "base/string_util.h" #include "base/utf_string_conversions.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/bookmarks/bookmark_editor.h" #include "chrome/browser/bookmarks/bookmark_model.h" #include "chrome/browser/bookmarks/bookmark_utils.h" #include "chrome/browser/metrics/user_metrics.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/views/bubble/bubble.h" #include "content/common/notification_service.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "ui/base/keycodes/keyboard_codes.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_utils.h" #include "views/controls/button/native_button.h" #include "views/controls/textfield/textfield.h" #include "views/events/event.h" #include "views/focus/focus_manager.h" #include "views/layout/grid_layout.h" #include "views/layout/layout_constants.h" #include "views/window/client_view.h" #include "views/window/window.h" using views::Combobox; using views::ColumnSet; using views::GridLayout; using views::Label; using views::Link; using views::NativeButton; using views::View; // Padding between "Title:" and the actual title. static const int kTitlePadding = 4; // Minimum width for the fields - they will push out the size of the bubble if // necessary. This should be big enough so that the field pushes the right side // of the bubble far enough so that the edit button's left edge is to the right // of the field's left edge. static const int kMinimumFieldSize = 180; // Bubble close image. static SkBitmap* kCloseImage = NULL; // Declared in browser_dialogs.h so callers don't have to depend on our header. namespace browser { void ShowBookmarkBubbleView(views::Window* parent, const gfx::Rect& bounds, BubbleDelegate* delegate, Profile* profile, const GURL& url, bool newly_bookmarked) { BookmarkBubbleView::Show(parent, bounds, delegate, profile, url, newly_bookmarked); } void HideBookmarkBubbleView() { BookmarkBubbleView::Hide(); } bool IsBookmarkBubbleViewShowing() { return BookmarkBubbleView::IsShowing(); } } // namespace browser // BookmarkBubbleView --------------------------------------------------------- BookmarkBubbleView* BookmarkBubbleView::bookmark_bubble_ = NULL; // static void BookmarkBubbleView::Show(views::Window* parent, const gfx::Rect& bounds, BubbleDelegate* delegate, Profile* profile, const GURL& url, bool newly_bookmarked) { if (IsShowing()) return; bookmark_bubble_ = new BookmarkBubbleView(delegate, profile, url, newly_bookmarked); // TODO(beng): Pass |parent| after V2 is complete. Bubble* bubble = Bubble::Show( parent->client_view()->GetWidget(), bounds, BubbleBorder::TOP_RIGHT, bookmark_bubble_, bookmark_bubble_); // |bubble_| can be set to NULL in BubbleClosing when we close the bubble // asynchronously. However, that can happen during the Show call above if the // window loses activation while we are getting to ready to show the bubble, // so we must check to make sure we still have a valid bubble before // proceeding. if (!bookmark_bubble_) return; bookmark_bubble_->set_bubble(bubble); bubble->SizeToContents(); GURL url_ptr(url); NotificationService::current()->Notify( NotificationType::BOOKMARK_BUBBLE_SHOWN, Source<Profile>(profile->GetOriginalProfile()), Details<GURL>(&url_ptr)); bookmark_bubble_->BubbleShown(); } // static bool BookmarkBubbleView::IsShowing() { return bookmark_bubble_ != NULL; } void BookmarkBubbleView::Hide() { if (IsShowing()) bookmark_bubble_->Close(); } BookmarkBubbleView::~BookmarkBubbleView() { if (apply_edits_) { ApplyEdits(); } else if (remove_bookmark_) { BookmarkModel* model = profile_->GetBookmarkModel(); const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_); if (node) model->Remove(node->parent(), node->parent()->GetIndexOf(node)); } } void BookmarkBubbleView::BubbleShown() { DCHECK(GetWidget()); GetFocusManager()->RegisterAccelerator( views::Accelerator(ui::VKEY_RETURN, false, false, false), this); title_tf_->RequestFocus(); title_tf_->SelectAll(); } bool BookmarkBubbleView::AcceleratorPressed( const views::Accelerator& accelerator) { if (accelerator.GetKeyCode() != ui::VKEY_RETURN) return false; if (edit_button_->HasFocus()) HandleButtonPressed(edit_button_); else HandleButtonPressed(close_button_); return true; } void BookmarkBubbleView::ViewHierarchyChanged(bool is_add, View* parent, View* child) { if (is_add && child == this) Init(); } BookmarkBubbleView::BookmarkBubbleView(BubbleDelegate* delegate, Profile* profile, const GURL& url, bool newly_bookmarked) : delegate_(delegate), profile_(profile), url_(url), newly_bookmarked_(newly_bookmarked), parent_model_( profile_->GetBookmarkModel(), profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url)), remove_bookmark_(false), apply_edits_(true) { } void BookmarkBubbleView::Init() { static SkColor kTitleColor; static bool initialized = false; if (!initialized) { kTitleColor = color_utils::GetReadableColor(SkColorSetRGB(6, 45, 117), Bubble::kBackgroundColor); kCloseImage = ResourceBundle::GetSharedInstance().GetBitmapNamed( IDR_INFO_BUBBLE_CLOSE); initialized = true; } remove_link_ = new Link(UTF16ToWide(l10n_util::GetStringUTF16( IDS_BOOMARK_BUBBLE_REMOVE_BOOKMARK))); remove_link_->SetController(this); edit_button_ = new NativeButton( this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_OPTIONS))); close_button_ = new NativeButton(this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_DONE))); close_button_->SetIsDefault(true); Label* combobox_label = new Label( UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_FOLDER_TEXT))); parent_combobox_ = new Combobox(&parent_model_); parent_combobox_->SetSelectedItem(parent_model_.node_parent_index()); parent_combobox_->set_listener(this); parent_combobox_->SetAccessibleName( WideToUTF16Hack(combobox_label->GetText())); #if defined(TOUCH_UI) // TODO(saintlou): This is a short term workaround for touch parent_combobox_->SetEnabled(false); #endif Label* title_label = new Label(UTF16ToWide(l10n_util::GetStringUTF16( newly_bookmarked_ ? IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED : IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK))); title_label->SetFont( ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont)); title_label->SetColor(kTitleColor); GridLayout* layout = new GridLayout(this); SetLayoutManager(layout); ColumnSet* cs = layout->AddColumnSet(0); // Top (title) row. cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF, 0, 0); cs->AddPaddingColumn(1, views::kUnrelatedControlHorizontalSpacing); cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF, 0, 0); // Middle (input field) rows. cs = layout->AddColumnSet(2); cs->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0, GridLayout::USE_PREF, 0, 0); cs->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing); cs->AddColumn(GridLayout::FILL, GridLayout::CENTER, 1, GridLayout::USE_PREF, 0, kMinimumFieldSize); // Bottom (buttons) row. cs = layout->AddColumnSet(3); cs->AddPaddingColumn(1, views::kRelatedControlHorizontalSpacing); cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0, GridLayout::USE_PREF, 0, 0); // We subtract 2 to account for the natural button padding, and // to bring the separation visually in line with the row separation // height. cs->AddPaddingColumn(0, views::kRelatedButtonHSpacing - 2); cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0, GridLayout::USE_PREF, 0, 0); layout->StartRow(0, 0); layout->AddView(title_label); layout->AddView(remove_link_); layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing); layout->StartRow(0, 2); layout->AddView(new Label(UTF16ToWide( l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_TITLE_TEXT)))); title_tf_ = new views::Textfield(); title_tf_->SetText(GetTitle()); layout->AddView(title_tf_); layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing); layout->StartRow(0, 2); layout->AddView(combobox_label); layout->AddView(parent_combobox_); layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing); layout->StartRow(0, 3); layout->AddView(edit_button_); layout->AddView(close_button_); } string16 BookmarkBubbleView::GetTitle() { BookmarkModel* bookmark_model= profile_->GetBookmarkModel(); const BookmarkNode* node = bookmark_model->GetMostRecentlyAddedNodeForURL(url_); if (node) return node->GetTitle(); else NOTREACHED(); return string16(); } void BookmarkBubbleView::ButtonPressed( views::Button* sender, const views::Event& event) { HandleButtonPressed(sender); } void BookmarkBubbleView::LinkActivated(Link* source, int event_flags) { DCHECK(source == remove_link_); UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"), profile_); // Set this so we remove the bookmark after the window closes. remove_bookmark_ = true; apply_edits_ = false; bubble_->set_fade_away_on_close(true); Close(); } void BookmarkBubbleView::ItemChanged(Combobox* combobox, int prev_index, int new_index) { if (new_index + 1 == parent_model_.GetItemCount()) { UserMetrics::RecordAction( UserMetricsAction("BookmarkBubble_EditFromCombobox"), profile_); ShowEditor(); return; } } void BookmarkBubbleView::BubbleClosing(Bubble* bubble, bool closed_by_escape) { if (closed_by_escape) { remove_bookmark_ = newly_bookmarked_; apply_edits_ = false; } // We have to reset |bubble_| here, not in our destructor, because we'll be // destroyed asynchronously and the shown state will be checked before then. DCHECK(bookmark_bubble_ == this); bookmark_bubble_ = NULL; if (delegate_) delegate_->BubbleClosing(bubble, closed_by_escape); NotificationService::current()->Notify( NotificationType::BOOKMARK_BUBBLE_HIDDEN, Source<Profile>(profile_->GetOriginalProfile()), NotificationService::NoDetails()); } bool BookmarkBubbleView::CloseOnEscape() { return delegate_ ? delegate_->CloseOnEscape() : true; } bool BookmarkBubbleView::FadeInOnShow() { return false; } std::wstring BookmarkBubbleView::accessible_name() { return UTF16ToWide( l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_ADD_BOOKMARK)); } void BookmarkBubbleView::Close() { ApplyEdits(); static_cast<Bubble*>(GetWidget())->Close(); } void BookmarkBubbleView::HandleButtonPressed(views::Button* sender) { if (sender == edit_button_) { UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"), profile_); bubble_->set_fade_away_on_close(true); ShowEditor(); } else { DCHECK(sender == close_button_); bubble_->set_fade_away_on_close(true); Close(); } // WARNING: we've most likely been deleted when CloseWindow returns. } void BookmarkBubbleView::ShowEditor() { #if defined(TOUCH_UI) // Close the Bubble Close(); // Open the Bookmark Manager Browser* browser = BrowserList::GetLastActiveWithProfile(profile_); DCHECK(browser); if (browser) browser->OpenBookmarkManager(); else NOTREACHED(); #else const BookmarkNode* node = profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_); #if defined(OS_WIN) // Parent the editor to our root ancestor (not the root we're in, as that // is the info bubble and will close shortly). HWND parent = GetAncestor(GetWidget()->GetNativeView(), GA_ROOTOWNER); // We're about to show the bookmark editor. When the bookmark editor closes // we want the browser to become active. WidgetWin::Hide() does a hide in // a such way that activation isn't changed, which means when we close // Windows gets confused as to who it should give active status to. We // explicitly hide the bookmark bubble window in such a way that activation // status changes. That way, when the editor closes, activation is properly // restored to the browser. ShowWindow(GetWidget()->GetNativeView(), SW_HIDE); #else gfx::NativeWindow parent = GTK_WINDOW( static_cast<views::WidgetGtk*>(GetWidget())->GetTransientParent()); #endif // Even though we just hid the window, we need to invoke Close to schedule // the delete and all that. Close(); if (node) { BookmarkEditor::Show(parent, profile_, NULL, BookmarkEditor::EditDetails(node), BookmarkEditor::SHOW_TREE); } #endif } void BookmarkBubbleView::ApplyEdits() { // Set this to make sure we don't attempt to apply edits again. apply_edits_ = false; BookmarkModel* model = profile_->GetBookmarkModel(); const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_); if (node) { const string16 new_title = title_tf_->text(); if (new_title != node->GetTitle()) { model->SetTitle(node, new_title); UserMetrics::RecordAction( UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"), profile_); } // Last index means 'Choose another folder...' if (parent_combobox_->selected_item() < parent_model_.GetItemCount() - 1) { const BookmarkNode* new_parent = parent_model_.GetNodeAt(parent_combobox_->selected_item()); if (new_parent != node->parent()) { UserMetrics::RecordAction( UserMetricsAction("BookmarkBubble_ChangeParent"), profile_); model->Move(node, new_parent, new_parent->child_count()); } } } }