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.
5 #include "chrome/browser/ui/views/bookmarks/bookmark_editor_view.h"
9 #include "base/basictypes.h"
10 #include "base/logging.h"
11 #include "base/prefs/pref_service.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
15 #include "chrome/browser/history/history_service.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/ui/bookmarks/bookmark_utils.h"
18 #include "chrome/browser/ui/views/constrained_window_views.h"
19 #include "chrome/grit/chromium_strings.h"
20 #include "chrome/grit/generated_resources.h"
21 #include "chrome/grit/locale_settings.h"
22 #include "components/bookmarks/browser/bookmark_model.h"
23 #include "components/bookmarks/browser/bookmark_utils.h"
24 #include "components/url_fixer/url_fixer.h"
25 #include "components/user_prefs/user_prefs.h"
26 #include "ui/accessibility/ax_view_state.h"
27 #include "ui/base/l10n/l10n_util.h"
28 #include "ui/events/event.h"
29 #include "ui/views/background.h"
30 #include "ui/views/controls/button/label_button.h"
31 #include "ui/views/controls/label.h"
32 #include "ui/views/controls/menu/menu_runner.h"
33 #include "ui/views/controls/textfield/textfield.h"
34 #include "ui/views/controls/tree/tree_view.h"
35 #include "ui/views/focus/focus_manager.h"
36 #include "ui/views/layout/grid_layout.h"
37 #include "ui/views/layout/layout_constants.h"
38 #include "ui/views/widget/widget.h"
39 #include "ui/views/window/dialog_client_view.h"
42 using bookmarks::BookmarkExpandedStateTracker;
43 using views::GridLayout;
47 // Background color of text field when URL is invalid.
48 const SkColor kErrorColor = SkColorSetRGB(0xFF, 0xBC, 0xBC);
53 void BookmarkEditor::Show(gfx::NativeWindow parent_window,
55 const EditDetails& details,
56 Configuration configuration) {
58 BookmarkEditorView* editor = new BookmarkEditorView(profile,
59 details.parent_node, details, configuration);
60 editor->Show(parent_window);
63 BookmarkEditorView::BookmarkEditorView(
65 const BookmarkNode* parent,
66 const EditDetails& details,
67 BookmarkEditor::Configuration configuration)
76 bb_model_(BookmarkModelFactory::GetForProfile(profile)),
77 running_menu_for_root_(false),
78 show_tree_(configuration == SHOW_TREE) {
81 DCHECK(bb_model_->client()->CanBeEditedByUser(parent));
85 BookmarkEditorView::~BookmarkEditorView() {
86 // The tree model is deleted before the view. Reset the model otherwise the
87 // tree will reference a deleted model.
89 tree_view_->SetModel(NULL);
90 bb_model_->RemoveObserver(this);
93 base::string16 BookmarkEditorView::GetDialogButtonLabel(
94 ui::DialogButton button) const {
95 if (button == ui::DIALOG_BUTTON_OK)
96 return l10n_util::GetStringUTF16(IDS_SAVE);
97 return views::DialogDelegateView::GetDialogButtonLabel(button);
100 bool BookmarkEditorView::IsDialogButtonEnabled(ui::DialogButton button) const {
101 if (button == ui::DIALOG_BUTTON_OK) {
102 if (!bb_model_->loaded())
105 if (details_.GetNodeType() != BookmarkNode::FOLDER)
106 return GetInputURL().is_valid();
111 views::View* BookmarkEditorView::CreateExtraView() {
112 return new_folder_button_.get();
115 ui::ModalType BookmarkEditorView::GetModalType() const {
116 return ui::MODAL_TYPE_WINDOW;
119 bool BookmarkEditorView::CanResize() const {
123 base::string16 BookmarkEditorView::GetWindowTitle() const {
124 return l10n_util::GetStringUTF16(details_.GetWindowTitleId());
127 bool BookmarkEditorView::Accept() {
128 if (!IsDialogButtonEnabled(ui::DIALOG_BUTTON_OK)) {
129 if (details_.GetNodeType() != BookmarkNode::FOLDER) {
130 // The url is invalid, focus the url field.
131 url_tf_->SelectAll(true);
132 url_tf_->RequestFocus();
136 // Otherwise save changes and close the dialog box.
141 gfx::Size BookmarkEditorView::GetPreferredSize() const {
143 return views::View::GetPreferredSize();
145 return gfx::Size(views::Widget::GetLocalizedContentsSize(
146 IDS_EDITBOOKMARK_DIALOG_WIDTH_CHARS,
147 IDS_EDITBOOKMARK_DIALOG_HEIGHT_LINES));
150 void BookmarkEditorView::OnTreeViewSelectionChanged(
151 views::TreeView* tree_view) {
154 bool BookmarkEditorView::CanEdit(views::TreeView* tree_view,
155 ui::TreeModelNode* node) {
156 // Only allow editting of children of the bookmark bar node and other node.
157 EditorNode* bb_node = tree_model_->AsNode(node);
158 return (bb_node->parent() && bb_node->parent()->parent());
161 void BookmarkEditorView::ContentsChanged(views::Textfield* sender,
162 const base::string16& new_contents) {
166 bool BookmarkEditorView::HandleKeyEvent(views::Textfield* sender,
167 const ui::KeyEvent& key_event) {
171 void BookmarkEditorView::GetAccessibleState(ui::AXViewState* state) {
173 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_TITLE);
174 state->role = ui::AX_ROLE_DIALOG;
177 void BookmarkEditorView::ButtonPressed(views::Button* sender,
178 const ui::Event& event) {
179 DCHECK_EQ(new_folder_button_.get(), sender);
183 bool BookmarkEditorView::IsCommandIdChecked(int command_id) const {
187 bool BookmarkEditorView::IsCommandIdEnabled(int command_id) const {
188 switch (command_id) {
191 return !running_menu_for_root_;
192 case IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM:
200 bool BookmarkEditorView::GetAcceleratorForCommandId(
202 ui::Accelerator* accelerator) {
203 return GetWidget()->GetAccelerator(command_id, accelerator);
206 void BookmarkEditorView::ExecuteCommand(int command_id, int event_flags) {
207 DCHECK(tree_view_->GetSelectedNode());
208 if (command_id == IDS_EDIT) {
209 tree_view_->StartEditing(tree_view_->GetSelectedNode());
210 } else if (command_id == IDS_DELETE) {
211 EditorNode* node = tree_model_->AsNode(tree_view_->GetSelectedNode());
214 if (node->value != 0) {
215 const BookmarkNode* b_node =
216 bookmarks::GetBookmarkNodeByID(bb_model_, node->value);
217 if (!b_node->empty() &&
218 !chrome::ConfirmDeleteBookmarkNode(b_node,
219 GetWidget()->GetNativeWindow())) {
220 // The folder is not empty and the user didn't confirm.
223 deletes_.push_back(node->value);
225 tree_model_->Remove(node->parent(), node);
227 DCHECK_EQ(IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM, command_id);
232 void BookmarkEditorView::Show(gfx::NativeWindow parent) {
233 CreateBrowserModalDialogViews(this, parent);
235 if (show_tree_ && bb_model_->loaded())
238 // Select all the text in the name Textfield.
239 title_tf_->SelectAll(true);
240 // Give focus to the name Textfield.
241 title_tf_->RequestFocus();
244 void BookmarkEditorView::ShowContextMenuForView(
246 const gfx::Point& point,
247 ui::MenuSourceType source_type) {
248 DCHECK_EQ(tree_view_, source);
249 if (!tree_view_->GetSelectedNode())
251 running_menu_for_root_ =
252 (tree_model_->GetParent(tree_view_->GetSelectedNode()) ==
253 tree_model_->GetRoot());
255 context_menu_runner_.reset(new views::MenuRunner(
257 views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU));
259 if (context_menu_runner_->RunMenuAt(source->GetWidget()->GetTopLevelWidget(),
261 gfx::Rect(point, gfx::Size()),
262 views::MENU_ANCHOR_TOPRIGHT,
264 views::MenuRunner::MENU_DELETED) {
269 void BookmarkEditorView::Init() {
270 bb_model_->AddObserver(this);
272 title_label_ = new views::Label(
273 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NAME_LABEL));
275 base::string16 title;
277 if (details_.type == EditDetails::EXISTING_NODE) {
278 title = details_.existing_node->GetTitle();
279 url = details_.existing_node->url();
280 } else if (details_.type == EditDetails::NEW_FOLDER) {
281 title = l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME);
282 } else if (details_.type == EditDetails::NEW_URL) {
284 title = details_.title;
286 title_tf_ = new views::Textfield;
287 title_tf_->SetAccessibleName(
288 l10n_util::GetStringUTF16(IDS_BOOKMARK_AX_EDITOR_NAME_LABEL));
289 title_tf_->SetText(title);
290 title_tf_->set_controller(this);
293 tree_view_ = new views::TreeView;
294 tree_view_->SetRootShown(false);
295 tree_view_->set_context_menu_controller(this);
297 new_folder_button_.reset(new views::LabelButton(this,
298 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NEW_FOLDER_BUTTON)));
299 new_folder_button_->SetStyle(views::Button::STYLE_BUTTON);
300 new_folder_button_->set_owned_by_client();
301 new_folder_button_->SetEnabled(false);
304 GridLayout* layout = GridLayout::CreatePanel(this);
305 SetLayoutManager(layout);
307 const int labels_column_set_id = 0;
308 const int single_column_view_set_id = 1;
309 const int buttons_column_set_id = 2;
311 views::ColumnSet* column_set = layout->AddColumnSet(labels_column_set_id);
312 column_set->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0,
313 GridLayout::USE_PREF, 0, 0);
314 column_set->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing);
315 column_set->AddColumn(GridLayout::FILL, GridLayout::CENTER, 1,
316 GridLayout::USE_PREF, 0, 0);
318 column_set = layout->AddColumnSet(single_column_view_set_id);
319 column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
320 GridLayout::USE_PREF, 0, 0);
322 column_set = layout->AddColumnSet(buttons_column_set_id);
323 column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0,
324 GridLayout::USE_PREF, 0, 0);
325 column_set->AddPaddingColumn(1, views::kRelatedControlHorizontalSpacing);
326 column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0,
327 GridLayout::USE_PREF, 0, 0);
328 column_set->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing);
329 column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0,
330 GridLayout::USE_PREF, 0, 0);
331 column_set->LinkColumnSizes(0, 2, 4, -1);
333 layout->StartRow(0, labels_column_set_id);
334 layout->AddView(title_label_);
335 layout->AddView(title_tf_);
337 if (details_.GetNodeType() != BookmarkNode::FOLDER) {
338 url_label_ = new views::Label(
339 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_URL_LABEL));
341 url_tf_ = new views::Textfield;
343 profile_ ? user_prefs::UserPrefs::Get(profile_) : NULL;
344 url_tf_->SetText(chrome::FormatBookmarkURLForDisplay(url, prefs));
345 url_tf_->set_controller(this);
346 url_tf_->SetAccessibleName(
347 l10n_util::GetStringUTF16(IDS_BOOKMARK_AX_EDITOR_URL_LABEL));
349 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
351 layout->StartRow(0, labels_column_set_id);
352 layout->AddView(url_label_);
353 layout->AddView(url_tf_);
357 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
358 layout->StartRow(1, single_column_view_set_id);
359 layout->AddView(tree_view_->CreateParentIfNecessary());
362 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
364 if (!show_tree_ || bb_model_->loaded())
368 void BookmarkEditorView::BookmarkNodeMoved(BookmarkModel* model,
369 const BookmarkNode* old_parent,
371 const BookmarkNode* new_parent,
376 void BookmarkEditorView::BookmarkNodeAdded(BookmarkModel* model,
377 const BookmarkNode* parent,
382 void BookmarkEditorView::BookmarkNodeRemoved(
383 BookmarkModel* model,
384 const BookmarkNode* parent,
386 const BookmarkNode* node,
387 const std::set<GURL>& removed_urls) {
388 if ((details_.type == EditDetails::EXISTING_NODE &&
389 details_.existing_node->HasAncestor(node)) ||
390 (parent_ && parent_->HasAncestor(node))) {
391 // The node, or its parent was removed. Close the dialog.
392 GetWidget()->Close();
398 void BookmarkEditorView::BookmarkAllUserNodesRemoved(
399 BookmarkModel* model,
400 const std::set<GURL>& removed_urls) {
404 void BookmarkEditorView::BookmarkNodeChildrenReordered(
405 BookmarkModel* model,
406 const BookmarkNode* node) {
410 void BookmarkEditorView::Reset() {
417 new_folder_button_->SetEnabled(true);
419 // Do this first, otherwise when we invoke SetModel with the real one
420 // tree_view will try to invoke something on the model we just deleted.
421 tree_view_->SetModel(NULL);
423 EditorNode* root_node = CreateRootNode();
424 tree_model_.reset(new EditorTreeModel(root_node));
426 tree_view_->SetModel(tree_model_.get());
427 tree_view_->SetController(this);
429 context_menu_runner_.reset();
435 GURL BookmarkEditorView::GetInputURL() const {
436 if (details_.GetNodeType() == BookmarkNode::FOLDER)
438 return url_fixer::FixupURL(base::UTF16ToUTF8(url_tf_->text()), std::string());
441 void BookmarkEditorView::UserInputChanged() {
442 if (details_.GetNodeType() != BookmarkNode::FOLDER) {
443 const GURL url(GetInputURL());
445 url_tf_->SetBackgroundColor(kErrorColor);
447 url_tf_->UseDefaultBackgroundColor();
449 GetDialogClientView()->UpdateDialogButtons();
452 void BookmarkEditorView::NewFolder() {
453 // Create a new entry parented to the selected item, or the bookmark
454 // bar if nothing is selected.
455 EditorNode* parent = tree_model_->AsNode(tree_view_->GetSelectedNode());
461 tree_view_->StartEditing(AddNewFolder(parent));
464 BookmarkEditorView::EditorNode* BookmarkEditorView::AddNewFolder(
465 EditorNode* parent) {
466 EditorNode* new_node = new EditorNode(
467 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME), 0);
468 // |new_node| is now owned by |parent|.
469 tree_model_->Add(parent, new_node, parent->child_count());
473 void BookmarkEditorView::ExpandAndSelect() {
474 BookmarkExpandedStateTracker::Nodes expanded_nodes =
475 bb_model_->expanded_state_tracker()->GetExpandedNodes();
476 for (BookmarkExpandedStateTracker::Nodes::const_iterator i(
477 expanded_nodes.begin()); i != expanded_nodes.end(); ++i) {
478 EditorNode* editor_node =
479 FindNodeWithID(tree_model_->GetRoot(), (*i)->id());
481 tree_view_->Expand(editor_node);
484 const BookmarkNode* to_select = parent_;
485 if (details_.type == EditDetails::EXISTING_NODE)
486 to_select = details_.existing_node->parent();
487 int64 folder_id_to_select = to_select->id();
489 FindNodeWithID(tree_model_->GetRoot(), folder_id_to_select);
491 b_node = tree_model_->GetRoot()->GetChild(0); // Bookmark bar node.
493 tree_view_->SetSelectedNode(b_node);
496 BookmarkEditorView::EditorNode* BookmarkEditorView::CreateRootNode() {
497 EditorNode* root_node = new EditorNode(base::string16(), 0);
498 const BookmarkNode* bb_root_node = bb_model_->root_node();
499 CreateNodes(bb_root_node, root_node);
500 DCHECK(root_node->child_count() >= 2 && root_node->child_count() <= 4);
501 DCHECK_EQ(BookmarkNode::BOOKMARK_BAR, bb_root_node->GetChild(0)->type());
502 DCHECK_EQ(BookmarkNode::OTHER_NODE, bb_root_node->GetChild(1)->type());
503 if (root_node->child_count() >= 3)
504 DCHECK_EQ(BookmarkNode::MOBILE, bb_root_node->GetChild(2)->type());
508 void BookmarkEditorView::CreateNodes(const BookmarkNode* bb_node,
509 BookmarkEditorView::EditorNode* b_node) {
510 for (int i = 0; i < bb_node->child_count(); ++i) {
511 const BookmarkNode* child_bb_node = bb_node->GetChild(i);
512 if (child_bb_node->IsVisible() && child_bb_node->is_folder() &&
513 bb_model_->client()->CanBeEditedByUser(child_bb_node)) {
514 EditorNode* new_b_node = new EditorNode(child_bb_node->GetTitle(),
515 child_bb_node->id());
516 b_node->Add(new_b_node, b_node->child_count());
517 CreateNodes(child_bb_node, new_b_node);
522 BookmarkEditorView::EditorNode* BookmarkEditorView::FindNodeWithID(
523 BookmarkEditorView::EditorNode* node,
525 if (node->value == id)
527 for (int i = 0; i < node->child_count(); ++i) {
528 EditorNode* result = FindNodeWithID(node->GetChild(i), id);
535 void BookmarkEditorView::ApplyEdits() {
536 DCHECK(bb_model_->loaded());
539 tree_view_->CommitEdit();
541 EditorNode* parent = show_tree_ ?
542 tree_model_->AsNode(tree_view_->GetSelectedNode()) : NULL;
543 if (show_tree_ && !parent) {
550 void BookmarkEditorView::ApplyEdits(EditorNode* parent) {
551 DCHECK(!show_tree_ || parent);
553 // We're going to apply edits to the bookmark bar model, which will call us
554 // back. Normally when a structural edit occurs we reset the tree model.
555 // We don't want to do that here, so we remove ourselves as an observer.
556 bb_model_->RemoveObserver(this);
558 GURL new_url(GetInputURL());
559 base::string16 new_title(title_tf_->text());
562 BookmarkEditor::ApplyEditsWithNoFolderChange(
563 bb_model_, parent_, details_, new_title, new_url);
567 // Create the new folders and update the titles.
568 const BookmarkNode* new_parent = NULL;
569 ApplyNameChangesAndCreateNewFolders(
570 bb_model_->root_node(), tree_model_->GetRoot(), parent, &new_parent);
572 BookmarkEditor::ApplyEditsWithPossibleFolderChange(
573 bb_model_, new_parent, details_, new_title, new_url);
575 BookmarkExpandedStateTracker::Nodes expanded_nodes;
576 UpdateExpandedNodes(tree_model_->GetRoot(), &expanded_nodes);
577 bb_model_->expanded_state_tracker()->SetExpandedNodes(expanded_nodes);
579 // Remove the folders that were removed. This has to be done after all the
580 // other changes have been committed.
581 bookmarks::DeleteBookmarkFolders(bb_model_, deletes_);
584 void BookmarkEditorView::ApplyNameChangesAndCreateNewFolders(
585 const BookmarkNode* bb_node,
586 BookmarkEditorView::EditorNode* b_node,
587 BookmarkEditorView::EditorNode* parent_b_node,
588 const BookmarkNode** parent_bb_node) {
589 if (parent_b_node == b_node)
590 *parent_bb_node = bb_node;
591 for (int i = 0; i < b_node->child_count(); ++i) {
592 EditorNode* child_b_node = b_node->GetChild(i);
593 const BookmarkNode* child_bb_node = NULL;
594 if (child_b_node->value == 0) {
596 child_bb_node = bb_model_->AddFolder(bb_node,
597 bb_node->child_count(), child_b_node->GetTitle());
598 child_b_node->value = child_bb_node->id();
600 // Existing node, reset the title (BookmarkModel ignores changes if the
601 // title is the same).
602 for (int j = 0; j < bb_node->child_count(); ++j) {
603 const BookmarkNode* node = bb_node->GetChild(j);
604 if (node->is_folder() && node->id() == child_b_node->value) {
605 child_bb_node = node;
609 DCHECK(child_bb_node);
610 bb_model_->SetTitle(child_bb_node, child_b_node->GetTitle());
612 ApplyNameChangesAndCreateNewFolders(child_bb_node, child_b_node,
613 parent_b_node, parent_bb_node);
617 void BookmarkEditorView::UpdateExpandedNodes(
618 EditorNode* editor_node,
619 BookmarkExpandedStateTracker::Nodes* expanded_nodes) {
620 if (!tree_view_->IsExpanded(editor_node))
624 if (editor_node->value != 0) {
625 expanded_nodes->insert(
626 bookmarks::GetBookmarkNodeByID(bb_model_, editor_node->value));
629 for (int i = 0; i < editor_node->child_count(); ++i)
630 UpdateExpandedNodes(editor_node->GetChild(i), expanded_nodes);
633 ui::SimpleMenuModel* BookmarkEditorView::GetMenuModel() {
634 if (!context_menu_model_.get()) {
635 context_menu_model_.reset(new ui::SimpleMenuModel(this));
636 context_menu_model_->AddItemWithStringId(IDS_EDIT, IDS_EDIT);
637 context_menu_model_->AddItemWithStringId(IDS_DELETE, IDS_DELETE);
638 context_menu_model_->AddItemWithStringId(
639 IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM,
640 IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM);
642 return context_menu_model_.get();
645 void BookmarkEditorView::EditorTreeModel::SetTitle(
646 ui::TreeModelNode* node,
647 const base::string16& title) {
649 ui::TreeNodeModel<EditorNode>::SetTitle(node, title);