// Copyright (c) 2010 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/views/bookmark_editor_view.h" #include "app/l10n_util.h" #include "base/basictypes.h" #include "base/logging.h" #include "base/string_util.h" #include "base/utf_string_conversions.h" #include "chrome/browser/bookmarks/bookmark_model.h" #include "chrome/browser/bookmarks/bookmark_utils.h" #include "chrome/browser/history/history.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/profile.h" #include "chrome/browser/net/url_fixer_upper.h" #include "chrome/common/pref_names.h" #include "googleurl/src/gurl.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.h" #include "grit/locale_settings.h" #include "net/base/net_util.h" #include "views/background.h" #include "views/focus/focus_manager.h" #include "views/grid_layout.h" #include "views/controls/button/native_button.h" #include "views/controls/label.h" #include "views/controls/menu/menu_2.h" #include "views/standard_layout.h" #include "views/widget/widget.h" #include "views/window/window.h" using views::Button; using views::ColumnSet; using views::GridLayout; using views::Label; using views::NativeButton; using views::Textfield; // Background color of text field when URL is invalid. static const SkColor kErrorColor = SkColorSetRGB(0xFF, 0xBC, 0xBC); // Preferred width of the tree. static const int kTreeWidth = 300; // ID for various children. static const int kNewGroupButtonID = 1002; // static void BookmarkEditor::Show(HWND parent_hwnd, Profile* profile, const BookmarkNode* parent, const EditDetails& details, Configuration configuration) { DCHECK(profile); BookmarkEditorView* editor = new BookmarkEditorView(profile, parent, details, configuration); editor->Show(parent_hwnd); } BookmarkEditorView::BookmarkEditorView( Profile* profile, const BookmarkNode* parent, const EditDetails& details, BookmarkEditor::Configuration configuration) : profile_(profile), tree_view_(NULL), new_group_button_(NULL), url_label_(NULL), title_label_(NULL), parent_(parent), details_(details), running_menu_for_root_(false), show_tree_(configuration == SHOW_TREE) { DCHECK(profile); Init(); } BookmarkEditorView::~BookmarkEditorView() { // The tree model is deleted before the view. Reset the model otherwise the // tree will reference a deleted model. if (tree_view_) tree_view_->SetModel(NULL); bb_model_->RemoveObserver(this); } bool BookmarkEditorView::IsDialogButtonEnabled( MessageBoxFlags::DialogButton button) const { if (button == MessageBoxFlags::DIALOGBUTTON_OK) { if (details_.type == EditDetails::NEW_FOLDER) return !title_tf_.text().empty(); const GURL url(GetInputURL()); return bb_model_->IsLoaded() && url.is_valid(); } return true; } bool BookmarkEditorView::IsModal() const { return true; } std::wstring BookmarkEditorView::GetWindowTitle() const { return l10n_util::GetString(IDS_BOOMARK_EDITOR_TITLE); } bool BookmarkEditorView::Accept() { if (!IsDialogButtonEnabled(MessageBoxFlags::DIALOGBUTTON_OK)) { // The url is invalid, focus the url field. url_tf_.SelectAll(); url_tf_.RequestFocus(); return false; } // Otherwise save changes and close the dialog box. ApplyEdits(); return true; } bool BookmarkEditorView::AreAcceleratorsEnabled( MessageBoxFlags::DialogButton button) { return !show_tree_ || !tree_view_->GetEditingNode(); } views::View* BookmarkEditorView::GetContentsView() { return this; } void BookmarkEditorView::Layout() { // Let the grid layout manager lay out most of the dialog... GetLayoutManager()->Layout(this); if (!show_tree_) return; // Manually lay out the New Folder button in the same row as the OK/Cancel // buttons... gfx::Rect parent_bounds = GetParent()->GetLocalBounds(false); gfx::Size prefsize = new_group_button_->GetPreferredSize(); int button_y = parent_bounds.bottom() - prefsize.height() - kButtonVEdgeMargin; new_group_button_->SetBounds(kPanelHorizMargin, button_y, prefsize.width(), prefsize.height()); } gfx::Size BookmarkEditorView::GetPreferredSize() { if (!show_tree_) return views::View::GetPreferredSize(); return gfx::Size(views::Window::GetLocalizedContentsSize( IDS_EDITBOOKMARK_DIALOG_WIDTH_CHARS, IDS_EDITBOOKMARK_DIALOG_HEIGHT_LINES)); } void BookmarkEditorView::ViewHierarchyChanged(bool is_add, views::View* parent, views::View* child) { if (show_tree_ && child == this) { // Add and remove the New Folder button from the ClientView's hierarchy. if (is_add) { parent->AddChildView(new_group_button_.get()); } else { parent->RemoveChildView(new_group_button_.get()); } } } void BookmarkEditorView::OnTreeViewSelectionChanged( views::TreeView* tree_view) { } bool BookmarkEditorView::CanEdit(views::TreeView* tree_view, TreeModelNode* node) { // Only allow editting of children of the bookmark bar node and other node. EditorNode* bb_node = tree_model_->AsNode(node); return (bb_node->GetParent() && bb_node->GetParent()->GetParent()); } void BookmarkEditorView::ContentsChanged(Textfield* sender, const std::wstring& new_contents) { UserInputChanged(); } void BookmarkEditorView::ButtonPressed( Button* sender, const views::Event& event) { DCHECK(sender); switch (sender->GetID()) { case kNewGroupButtonID: NewGroup(); break; default: NOTREACHED(); } } bool BookmarkEditorView::IsCommandIdChecked(int command_id) const { return false; } bool BookmarkEditorView::IsCommandIdEnabled(int command_id) const { return (command_id != IDS_EDIT || !running_menu_for_root_); } bool BookmarkEditorView::GetAcceleratorForCommandId( int command_id, menus::Accelerator* accelerator) { return GetWidget()->GetAccelerator(command_id, accelerator); } void BookmarkEditorView::ExecuteCommand(int command_id) { DCHECK(tree_view_->GetSelectedNode()); if (command_id == IDS_EDIT) { tree_view_->StartEditing(tree_view_->GetSelectedNode()); } else { DCHECK(command_id == IDS_BOOMARK_EDITOR_NEW_FOLDER_MENU_ITEM); NewGroup(); } } void BookmarkEditorView::Show(HWND parent_hwnd) { views::Window::CreateChromeWindow(parent_hwnd, gfx::Rect(), this); UserInputChanged(); if (show_tree_ && bb_model_->IsLoaded()) ExpandAndSelect(); window()->Show(); // Select all the text in the name Textfield. title_tf_.SelectAll(); // Give focus to the name Textfield. title_tf_.RequestFocus(); } void BookmarkEditorView::Close() { DCHECK(window()); window()->Close(); } void BookmarkEditorView::ShowContextMenu(View* source, const gfx::Point& p, bool is_mouse_gesture) { DCHECK(source == tree_view_); if (!tree_view_->GetSelectedNode()) return; running_menu_for_root_ = (tree_model_->GetParent(tree_view_->GetSelectedNode()) == tree_model_->GetRoot()); if (!context_menu_contents_.get()) { context_menu_contents_.reset(new menus::SimpleMenuModel(this)); context_menu_contents_->AddItemWithStringId(IDS_EDIT, IDS_EDIT); context_menu_contents_->AddItemWithStringId( IDS_BOOMARK_EDITOR_NEW_FOLDER_MENU_ITEM, IDS_BOOMARK_EDITOR_NEW_FOLDER_MENU_ITEM); context_menu_.reset(new views::Menu2(context_menu_contents_.get())); } context_menu_->RunContextMenuAt(p); } void BookmarkEditorView::Init() { bb_model_ = profile_->GetBookmarkModel(); DCHECK(bb_model_); bb_model_->AddObserver(this); url_tf_.set_parent_owned(false); title_tf_.set_parent_owned(false); std::wstring title; if (details_.type == EditDetails::EXISTING_NODE) title = details_.existing_node->GetTitle(); else if (details_.type == EditDetails::NEW_FOLDER) title = l10n_util::GetString(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME); title_tf_.SetText(title); title_tf_.SetController(this); title_label_ = new views::Label( l10n_util::GetString(IDS_BOOMARK_EDITOR_NAME_LABEL)); title_tf_.SetAccessibleName(title_label_->GetText()); string16 url_text; if (details_.type == EditDetails::EXISTING_NODE) { std::string languages = profile_ ? profile_->GetPrefs()->GetString(prefs::kAcceptLanguages) : std::string(); // Because this gets parsed by FixupURL(), it's safe to omit the scheme or // trailing slash, and unescape most characters, but we need to not drop any // username/password, or unescape anything that changes the meaning. url_text = net::FormatUrl(details_.existing_node->GetURL(), languages, net::kFormatUrlOmitAll & ~net::kFormatUrlOmitUsernamePassword, UnescapeRule::SPACES, NULL, NULL, NULL); } url_tf_.SetText(UTF16ToWide(url_text)); url_tf_.SetController(this); url_label_ = new views::Label( l10n_util::GetString(IDS_BOOMARK_EDITOR_URL_LABEL)); url_tf_.SetAccessibleName(url_label_->GetText()); if (show_tree_) { tree_view_ = new views::TreeView(); new_group_button_.reset(new views::NativeButton( this, l10n_util::GetString(IDS_BOOMARK_EDITOR_NEW_FOLDER_BUTTON))); new_group_button_->set_parent_owned(false); tree_view_->SetContextMenuController(this); tree_view_->SetRootShown(false); new_group_button_->SetEnabled(false); new_group_button_->SetID(kNewGroupButtonID); } // Yummy layout code. GridLayout* layout = CreatePanelGridLayout(this); SetLayoutManager(layout); const int labels_column_set_id = 0; const int single_column_view_set_id = 1; const int buttons_column_set_id = 2; ColumnSet* column_set = layout->AddColumnSet(labels_column_set_id); column_set->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0, GridLayout::USE_PREF, 0, 0); column_set->AddPaddingColumn(0, kRelatedControlHorizontalSpacing); column_set->AddColumn(GridLayout::FILL, GridLayout::CENTER, 1, GridLayout::USE_PREF, 0, 0); column_set = layout->AddColumnSet(single_column_view_set_id); column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1, GridLayout::FIXED, kTreeWidth, 0); column_set = layout->AddColumnSet(buttons_column_set_id); column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0, GridLayout::USE_PREF, 0, 0); column_set->AddPaddingColumn(1, kRelatedControlHorizontalSpacing); column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0, GridLayout::USE_PREF, 0, 0); column_set->AddPaddingColumn(0, kRelatedControlHorizontalSpacing); column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0, GridLayout::USE_PREF, 0, 0); column_set->LinkColumnSizes(0, 2, 4, -1); layout->StartRow(0, labels_column_set_id); layout->AddView(title_label_); layout->AddView(&title_tf_); if (details_.type != EditDetails::NEW_FOLDER) { layout->AddPaddingRow(0, kRelatedControlVerticalSpacing); layout->StartRow(0, labels_column_set_id); layout->AddView(url_label_); layout->AddView(&url_tf_); } if (show_tree_) { layout->AddPaddingRow(0, kRelatedControlVerticalSpacing); layout->StartRow(1, single_column_view_set_id); layout->AddView(tree_view_); } layout->AddPaddingRow(0, kRelatedControlVerticalSpacing); if (!show_tree_ || bb_model_->IsLoaded()) Reset(); } void BookmarkEditorView::BookmarkNodeMoved(BookmarkModel* model, const BookmarkNode* old_parent, int old_index, const BookmarkNode* new_parent, int new_index) { Reset(); } void BookmarkEditorView::BookmarkNodeAdded(BookmarkModel* model, const BookmarkNode* parent, int index) { Reset(); } void BookmarkEditorView::BookmarkNodeRemoved(BookmarkModel* model, const BookmarkNode* parent, int index, const BookmarkNode* node) { if ((details_.type == EditDetails::EXISTING_NODE && details_.existing_node->HasAncestor(node)) || (parent_ && parent_->HasAncestor(node))) { // The node, or its parent was removed. Close the dialog. window()->Close(); } else { Reset(); } } void BookmarkEditorView::BookmarkNodeChildrenReordered( BookmarkModel* model, const BookmarkNode* node) { Reset(); } void BookmarkEditorView::Reset() { if (!show_tree_) { if (GetParent()) UserInputChanged(); return; } new_group_button_->SetEnabled(true); // Do this first, otherwise when we invoke SetModel with the real one // tree_view will try to invoke something on the model we just deleted. tree_view_->SetModel(NULL); EditorNode* root_node = CreateRootNode(); tree_model_.reset(new EditorTreeModel(root_node)); tree_view_->SetModel(tree_model_.get()); tree_view_->SetController(this); context_menu_.reset(); if (GetParent()) ExpandAndSelect(); } GURL BookmarkEditorView::GetInputURL() const { return URLFixerUpper::FixupURL(UTF16ToUTF8(url_tf_.text()), std::string()); } std::wstring BookmarkEditorView::GetInputTitle() const { return title_tf_.text(); } void BookmarkEditorView::UserInputChanged() { const GURL url(GetInputURL()); if (!url.is_valid()) url_tf_.SetBackgroundColor(kErrorColor); else url_tf_.UseDefaultBackgroundColor(); GetDialogClientView()->UpdateDialogButtons(); } void BookmarkEditorView::NewGroup() { // Create a new entry parented to the selected item, or the bookmark // bar if nothing is selected. EditorNode* parent = tree_model_->AsNode(tree_view_->GetSelectedNode()); if (!parent) { NOTREACHED(); return; } tree_view_->StartEditing(AddNewGroup(parent)); } BookmarkEditorView::EditorNode* BookmarkEditorView::AddNewGroup( EditorNode* parent) { EditorNode* new_node = new EditorNode(); new_node->SetTitle( l10n_util::GetStringUTF16(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME)); new_node->value = 0; // new_node is now owned by parent. tree_model_->Add(parent, parent->GetChildCount(), new_node); return new_node; } void BookmarkEditorView::ExpandAndSelect() { tree_view_->ExpandAll(); const BookmarkNode* to_select = parent_; if (details_.type == EditDetails::EXISTING_NODE) to_select = details_.existing_node->GetParent(); int64 group_id_to_select = to_select->id(); EditorNode* b_node = FindNodeWithID(tree_model_->GetRoot(), group_id_to_select); if (!b_node) b_node = tree_model_->GetRoot()->GetChild(0); // Bookmark bar node. tree_view_->SetSelectedNode(b_node); } BookmarkEditorView::EditorNode* BookmarkEditorView::CreateRootNode() { EditorNode* root_node = new EditorNode(std::wstring(), 0); const BookmarkNode* bb_root_node = bb_model_->root_node(); CreateNodes(bb_root_node, root_node); DCHECK(root_node->GetChildCount() == 2); DCHECK(bb_root_node->GetChild(0)->type() == BookmarkNode::BOOKMARK_BAR); DCHECK(bb_root_node->GetChild(1)->type() == BookmarkNode::OTHER_NODE); return root_node; } void BookmarkEditorView::CreateNodes(const BookmarkNode* bb_node, BookmarkEditorView::EditorNode* b_node) { for (int i = 0; i < bb_node->GetChildCount(); ++i) { const BookmarkNode* child_bb_node = bb_node->GetChild(i); if (child_bb_node->is_folder()) { EditorNode* new_b_node = new EditorNode(WideToUTF16(child_bb_node->GetTitle()), child_bb_node->id()); b_node->Add(b_node->GetChildCount(), new_b_node); CreateNodes(child_bb_node, new_b_node); } } } BookmarkEditorView::EditorNode* BookmarkEditorView::FindNodeWithID( BookmarkEditorView::EditorNode* node, int64 id) { if (node->value == id) return node; for (int i = 0; i < node->GetChildCount(); ++i) { EditorNode* result = FindNodeWithID(node->GetChild(i), id); if (result) return result; } return NULL; } void BookmarkEditorView::ApplyEdits() { DCHECK(bb_model_->IsLoaded()); EditorNode* parent = show_tree_ ? tree_model_->AsNode(tree_view_->GetSelectedNode()) : NULL; if (show_tree_ && !parent) { NOTREACHED(); return; } ApplyEdits(parent); } void BookmarkEditorView::ApplyEdits(EditorNode* parent) { DCHECK(!show_tree_ || parent); // We're going to apply edits to the bookmark bar model, which will call us // back. Normally when a structural edit occurs we reset the tree model. // We don't want to do that here, so we remove ourselves as an observer. bb_model_->RemoveObserver(this); GURL new_url(GetInputURL()); string16 new_title(WideToUTF16Hack(GetInputTitle())); if (!show_tree_) { bookmark_utils::ApplyEditsWithNoGroupChange( bb_model_, parent_, details_, new_title, new_url); return; } // Create the new groups and update the titles. const BookmarkNode* new_parent = NULL; ApplyNameChangesAndCreateNewGroups( bb_model_->root_node(), tree_model_->GetRoot(), parent, &new_parent); bookmark_utils::ApplyEditsWithPossibleGroupChange( bb_model_, new_parent, details_, new_title, new_url); } void BookmarkEditorView::ApplyNameChangesAndCreateNewGroups( const BookmarkNode* bb_node, BookmarkEditorView::EditorNode* b_node, BookmarkEditorView::EditorNode* parent_b_node, const BookmarkNode** parent_bb_node) { if (parent_b_node == b_node) *parent_bb_node = bb_node; for (int i = 0; i < b_node->GetChildCount(); ++i) { EditorNode* child_b_node = b_node->GetChild(i); const BookmarkNode* child_bb_node = NULL; if (child_b_node->value == 0) { // New group. child_bb_node = bb_model_->AddGroup(bb_node, bb_node->GetChildCount(), child_b_node->GetTitle()); } else { // Existing node, reset the title (BBModel ignores changes if the title // is the same). for (int j = 0; j < bb_node->GetChildCount(); ++j) { const BookmarkNode* node = bb_node->GetChild(j); if (node->is_folder() && node->id() == child_b_node->value) { child_bb_node = node; break; } } DCHECK(child_bb_node); bb_model_->SetTitle(child_bb_node, child_b_node->GetTitle()); } ApplyNameChangesAndCreateNewGroups(child_bb_node, child_b_node, parent_b_node, parent_bb_node); } }