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/profiles/profile.h"
16 #include "chrome/browser/ui/bookmarks/bookmark_utils.h"
17 #include "chrome/browser/ui/browser_dialogs.h"
18 #include "chrome/grit/generated_resources.h"
19 #include "chrome/grit/locale_settings.h"
20 #include "components/bookmarks/browser/bookmark_model.h"
21 #include "components/bookmarks/browser/bookmark_utils.h"
22 #include "components/constrained_window/constrained_window_views.h"
23 #include "components/history/core/browser/history_service.h"
24 #include "components/url_formatter/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 bookmarks::BookmarkModel
;
44 using bookmarks::BookmarkNode
;
45 using views::GridLayout
;
49 // Background color of text field when URL is invalid.
50 const SkColor kErrorColor
= SkColorSetRGB(0xFF, 0xBC, 0xBC);
54 BookmarkEditorView::BookmarkEditorView(
56 const BookmarkNode
* parent
,
57 const EditDetails
& details
,
58 BookmarkEditor::Configuration configuration
)
67 bb_model_(BookmarkModelFactory::GetForProfile(profile
)),
68 running_menu_for_root_(false),
69 show_tree_(configuration
== SHOW_TREE
) {
72 DCHECK(bb_model_
->client()->CanBeEditedByUser(parent
));
76 BookmarkEditorView::~BookmarkEditorView() {
77 // The tree model is deleted before the view. Reset the model otherwise the
78 // tree will reference a deleted model.
80 tree_view_
->SetModel(NULL
);
81 bb_model_
->RemoveObserver(this);
84 base::string16
BookmarkEditorView::GetDialogButtonLabel(
85 ui::DialogButton button
) const {
86 if (button
== ui::DIALOG_BUTTON_OK
)
87 return l10n_util::GetStringUTF16(IDS_SAVE
);
88 return views::DialogDelegateView::GetDialogButtonLabel(button
);
91 bool BookmarkEditorView::IsDialogButtonEnabled(ui::DialogButton button
) const {
92 if (button
== ui::DIALOG_BUTTON_OK
) {
93 if (!bb_model_
->loaded())
96 if (details_
.GetNodeType() != BookmarkNode::FOLDER
)
97 return GetInputURL().is_valid();
102 views::View
* BookmarkEditorView::CreateExtraView() {
103 return new_folder_button_
.get();
106 ui::ModalType
BookmarkEditorView::GetModalType() const {
107 return ui::MODAL_TYPE_WINDOW
;
110 bool BookmarkEditorView::CanResize() const {
114 base::string16
BookmarkEditorView::GetWindowTitle() const {
115 return l10n_util::GetStringUTF16(details_
.GetWindowTitleId());
118 bool BookmarkEditorView::Accept() {
119 if (!IsDialogButtonEnabled(ui::DIALOG_BUTTON_OK
)) {
120 if (details_
.GetNodeType() != BookmarkNode::FOLDER
) {
121 // The url is invalid, focus the url field.
122 url_tf_
->SelectAll(true);
123 url_tf_
->RequestFocus();
127 // Otherwise save changes and close the dialog box.
132 gfx::Size
BookmarkEditorView::GetPreferredSize() const {
134 return views::View::GetPreferredSize();
136 return gfx::Size(views::Widget::GetLocalizedContentsSize(
137 IDS_EDITBOOKMARK_DIALOG_WIDTH_CHARS
,
138 IDS_EDITBOOKMARK_DIALOG_HEIGHT_LINES
));
141 void BookmarkEditorView::OnTreeViewSelectionChanged(
142 views::TreeView
* tree_view
) {
145 bool BookmarkEditorView::CanEdit(views::TreeView
* tree_view
,
146 ui::TreeModelNode
* node
) {
147 // Only allow editting of children of the bookmark bar node and other node.
148 EditorNode
* bb_node
= tree_model_
->AsNode(node
);
149 return (bb_node
->parent() && bb_node
->parent()->parent());
152 void BookmarkEditorView::ContentsChanged(views::Textfield
* sender
,
153 const base::string16
& new_contents
) {
157 bool BookmarkEditorView::HandleKeyEvent(views::Textfield
* sender
,
158 const ui::KeyEvent
& key_event
) {
162 void BookmarkEditorView::GetAccessibleState(ui::AXViewState
* state
) {
163 views::DialogDelegateView::GetAccessibleState(state
);
165 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_TITLE
);
168 void BookmarkEditorView::ButtonPressed(views::Button
* sender
,
169 const ui::Event
& event
) {
170 DCHECK_EQ(new_folder_button_
.get(), sender
);
174 bool BookmarkEditorView::IsCommandIdChecked(int command_id
) const {
178 bool BookmarkEditorView::IsCommandIdEnabled(int command_id
) const {
179 switch (command_id
) {
182 return !running_menu_for_root_
;
183 case IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM
:
191 bool BookmarkEditorView::GetAcceleratorForCommandId(
193 ui::Accelerator
* accelerator
) {
194 return GetWidget()->GetAccelerator(command_id
, accelerator
);
197 void BookmarkEditorView::ExecuteCommand(int command_id
, int event_flags
) {
198 DCHECK(tree_view_
->GetSelectedNode());
199 if (command_id
== IDS_EDIT
) {
200 tree_view_
->StartEditing(tree_view_
->GetSelectedNode());
201 } else if (command_id
== IDS_DELETE
) {
202 EditorNode
* node
= tree_model_
->AsNode(tree_view_
->GetSelectedNode());
205 if (node
->value
!= 0) {
206 const BookmarkNode
* b_node
=
207 bookmarks::GetBookmarkNodeByID(bb_model_
, node
->value
);
208 if (!b_node
->empty() &&
209 !chrome::ConfirmDeleteBookmarkNode(b_node
,
210 GetWidget()->GetNativeWindow())) {
211 // The folder is not empty and the user didn't confirm.
214 deletes_
.push_back(node
->value
);
216 tree_model_
->Remove(node
->parent(), node
);
218 DCHECK_EQ(IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM
, command_id
);
223 void BookmarkEditorView::Show(gfx::NativeWindow parent
) {
224 constrained_window::CreateBrowserModalDialogViews(this, parent
);
226 if (show_tree_
&& bb_model_
->loaded())
229 // Select all the text in the name Textfield.
230 title_tf_
->SelectAll(true);
231 // Give focus to the name Textfield.
232 title_tf_
->RequestFocus();
235 void BookmarkEditorView::ShowContextMenuForView(
237 const gfx::Point
& point
,
238 ui::MenuSourceType source_type
) {
239 DCHECK_EQ(tree_view_
, source
);
240 if (!tree_view_
->GetSelectedNode())
242 running_menu_for_root_
=
243 (tree_model_
->GetParent(tree_view_
->GetSelectedNode()) ==
244 tree_model_
->GetRoot());
246 context_menu_runner_
.reset(new views::MenuRunner(
248 views::MenuRunner::HAS_MNEMONICS
| views::MenuRunner::CONTEXT_MENU
));
250 if (context_menu_runner_
->RunMenuAt(source
->GetWidget()->GetTopLevelWidget(),
252 gfx::Rect(point
, gfx::Size()),
253 views::MENU_ANCHOR_TOPRIGHT
,
255 views::MenuRunner::MENU_DELETED
) {
260 const char* BookmarkEditorView::GetClassName() const {
261 return "BookmarkEditorView";
264 void BookmarkEditorView::BookmarkNodeMoved(BookmarkModel
* model
,
265 const BookmarkNode
* old_parent
,
267 const BookmarkNode
* new_parent
,
272 void BookmarkEditorView::BookmarkNodeAdded(BookmarkModel
* model
,
273 const BookmarkNode
* parent
,
278 void BookmarkEditorView::BookmarkNodeRemoved(
279 BookmarkModel
* model
,
280 const BookmarkNode
* parent
,
282 const BookmarkNode
* node
,
283 const std::set
<GURL
>& removed_urls
) {
284 if ((details_
.type
== EditDetails::EXISTING_NODE
&&
285 details_
.existing_node
->HasAncestor(node
)) ||
286 (parent_
&& parent_
->HasAncestor(node
))) {
287 // The node, or its parent was removed. Close the dialog.
288 GetWidget()->Close();
294 void BookmarkEditorView::BookmarkAllUserNodesRemoved(
295 BookmarkModel
* model
,
296 const std::set
<GURL
>& removed_urls
) {
300 void BookmarkEditorView::BookmarkNodeChildrenReordered(
301 BookmarkModel
* model
,
302 const BookmarkNode
* node
) {
306 void BookmarkEditorView::Init() {
307 bb_model_
->AddObserver(this);
309 title_label_
= new views::Label(
310 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NAME_LABEL
));
312 base::string16 title
;
314 if (details_
.type
== EditDetails::EXISTING_NODE
) {
315 title
= details_
.existing_node
->GetTitle();
316 url
= details_
.existing_node
->url();
317 } else if (details_
.type
== EditDetails::NEW_FOLDER
) {
318 title
= l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME
);
319 } else if (details_
.type
== EditDetails::NEW_URL
) {
321 title
= details_
.title
;
323 title_tf_
= new views::Textfield
;
324 title_tf_
->SetAccessibleName(
325 l10n_util::GetStringUTF16(IDS_BOOKMARK_AX_EDITOR_NAME_LABEL
));
326 title_tf_
->SetText(title
);
327 title_tf_
->set_controller(this);
330 tree_view_
= new views::TreeView
;
331 tree_view_
->SetRootShown(false);
332 tree_view_
->set_context_menu_controller(this);
334 new_folder_button_
.reset(new views::LabelButton(this,
335 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NEW_FOLDER_BUTTON
)));
336 new_folder_button_
->SetStyle(views::Button::STYLE_BUTTON
);
337 new_folder_button_
->set_owned_by_client();
338 new_folder_button_
->SetEnabled(false);
341 GridLayout
* layout
= GridLayout::CreatePanel(this);
342 SetLayoutManager(layout
);
344 const int labels_column_set_id
= 0;
345 const int single_column_view_set_id
= 1;
346 const int buttons_column_set_id
= 2;
348 views::ColumnSet
* column_set
= layout
->AddColumnSet(labels_column_set_id
);
349 column_set
->AddColumn(views::kControlLabelGridAlignment
, GridLayout::CENTER
,
350 0, GridLayout::USE_PREF
, 0, 0);
351 column_set
->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing
);
352 column_set
->AddColumn(GridLayout::FILL
, GridLayout::CENTER
, 1,
353 GridLayout::USE_PREF
, 0, 0);
355 column_set
= layout
->AddColumnSet(single_column_view_set_id
);
356 column_set
->AddColumn(GridLayout::FILL
, GridLayout::FILL
, 1,
357 GridLayout::USE_PREF
, 0, 0);
359 column_set
= layout
->AddColumnSet(buttons_column_set_id
);
360 column_set
->AddColumn(GridLayout::FILL
, GridLayout::LEADING
, 0,
361 GridLayout::USE_PREF
, 0, 0);
362 column_set
->AddPaddingColumn(1, views::kRelatedControlHorizontalSpacing
);
363 column_set
->AddColumn(GridLayout::FILL
, GridLayout::LEADING
, 0,
364 GridLayout::USE_PREF
, 0, 0);
365 column_set
->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing
);
366 column_set
->AddColumn(GridLayout::FILL
, GridLayout::LEADING
, 0,
367 GridLayout::USE_PREF
, 0, 0);
368 column_set
->LinkColumnSizes(0, 2, 4, -1);
370 layout
->StartRow(0, labels_column_set_id
);
371 layout
->AddView(title_label_
);
372 layout
->AddView(title_tf_
);
374 if (details_
.GetNodeType() != BookmarkNode::FOLDER
) {
375 url_label_
= new views::Label(
376 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_URL_LABEL
));
378 url_tf_
= new views::Textfield
;
380 profile_
? user_prefs::UserPrefs::Get(profile_
) : NULL
;
381 url_tf_
->SetText(chrome::FormatBookmarkURLForDisplay(url
, prefs
));
382 url_tf_
->set_controller(this);
383 url_tf_
->SetAccessibleName(
384 l10n_util::GetStringUTF16(IDS_BOOKMARK_AX_EDITOR_URL_LABEL
));
386 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
388 layout
->StartRow(0, labels_column_set_id
);
389 layout
->AddView(url_label_
);
390 layout
->AddView(url_tf_
);
394 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
395 layout
->StartRow(1, single_column_view_set_id
);
396 layout
->AddView(tree_view_
->CreateParentIfNecessary());
399 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
401 if (!show_tree_
|| bb_model_
->loaded())
405 void BookmarkEditorView::Reset() {
412 new_folder_button_
->SetEnabled(true);
414 // Do this first, otherwise when we invoke SetModel with the real one
415 // tree_view will try to invoke something on the model we just deleted.
416 tree_view_
->SetModel(NULL
);
418 EditorNode
* root_node
= CreateRootNode();
419 tree_model_
.reset(new EditorTreeModel(root_node
));
421 tree_view_
->SetModel(tree_model_
.get());
422 tree_view_
->SetController(this);
424 context_menu_runner_
.reset();
430 GURL
BookmarkEditorView::GetInputURL() const {
431 if (details_
.GetNodeType() == BookmarkNode::FOLDER
)
433 return url_formatter::FixupURL(base::UTF16ToUTF8(url_tf_
->text()),
437 void BookmarkEditorView::UserInputChanged() {
438 if (details_
.GetNodeType() != BookmarkNode::FOLDER
) {
439 const GURL
url(GetInputURL());
441 url_tf_
->SetBackgroundColor(kErrorColor
);
443 url_tf_
->UseDefaultBackgroundColor();
445 GetDialogClientView()->UpdateDialogButtons();
448 void BookmarkEditorView::NewFolder() {
449 // Create a new entry parented to the selected item, or the bookmark
450 // bar if nothing is selected.
451 EditorNode
* parent
= tree_model_
->AsNode(tree_view_
->GetSelectedNode());
457 tree_view_
->StartEditing(AddNewFolder(parent
));
460 BookmarkEditorView::EditorNode
* BookmarkEditorView::AddNewFolder(
461 EditorNode
* parent
) {
462 EditorNode
* new_node
= new EditorNode(
463 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME
), 0);
464 // |new_node| is now owned by |parent|.
465 tree_model_
->Add(parent
, new_node
, parent
->child_count());
469 void BookmarkEditorView::ExpandAndSelect() {
470 BookmarkExpandedStateTracker::Nodes expanded_nodes
=
471 bb_model_
->expanded_state_tracker()->GetExpandedNodes();
472 for (BookmarkExpandedStateTracker::Nodes::const_iterator
i(
473 expanded_nodes
.begin()); i
!= expanded_nodes
.end(); ++i
) {
474 EditorNode
* editor_node
=
475 FindNodeWithID(tree_model_
->GetRoot(), (*i
)->id());
477 tree_view_
->Expand(editor_node
);
480 const BookmarkNode
* to_select
= parent_
;
481 if (details_
.type
== EditDetails::EXISTING_NODE
)
482 to_select
= details_
.existing_node
->parent();
483 int64 folder_id_to_select
= to_select
->id();
485 FindNodeWithID(tree_model_
->GetRoot(), folder_id_to_select
);
487 b_node
= tree_model_
->GetRoot()->GetChild(0); // Bookmark bar node.
489 tree_view_
->SetSelectedNode(b_node
);
492 BookmarkEditorView::EditorNode
* BookmarkEditorView::CreateRootNode() {
493 EditorNode
* root_node
= new EditorNode(base::string16(), 0);
494 const BookmarkNode
* bb_root_node
= bb_model_
->root_node();
495 CreateNodes(bb_root_node
, root_node
);
496 DCHECK(root_node
->child_count() >= 2 && root_node
->child_count() <= 4);
497 DCHECK_EQ(BookmarkNode::BOOKMARK_BAR
, bb_root_node
->GetChild(0)->type());
498 DCHECK_EQ(BookmarkNode::OTHER_NODE
, bb_root_node
->GetChild(1)->type());
499 if (root_node
->child_count() >= 3)
500 DCHECK_EQ(BookmarkNode::MOBILE
, bb_root_node
->GetChild(2)->type());
504 void BookmarkEditorView::CreateNodes(const BookmarkNode
* bb_node
,
505 BookmarkEditorView::EditorNode
* b_node
) {
506 for (int i
= 0; i
< bb_node
->child_count(); ++i
) {
507 const BookmarkNode
* child_bb_node
= bb_node
->GetChild(i
);
508 if (child_bb_node
->IsVisible() && child_bb_node
->is_folder() &&
509 bb_model_
->client()->CanBeEditedByUser(child_bb_node
)) {
510 EditorNode
* new_b_node
= new EditorNode(child_bb_node
->GetTitle(),
511 child_bb_node
->id());
512 b_node
->Add(new_b_node
, b_node
->child_count());
513 CreateNodes(child_bb_node
, new_b_node
);
518 BookmarkEditorView::EditorNode
* BookmarkEditorView::FindNodeWithID(
519 BookmarkEditorView::EditorNode
* node
,
521 if (node
->value
== id
)
523 for (int i
= 0; i
< node
->child_count(); ++i
) {
524 EditorNode
* result
= FindNodeWithID(node
->GetChild(i
), id
);
531 void BookmarkEditorView::ApplyEdits() {
532 DCHECK(bb_model_
->loaded());
535 tree_view_
->CommitEdit();
537 EditorNode
* parent
= show_tree_
?
538 tree_model_
->AsNode(tree_view_
->GetSelectedNode()) : NULL
;
539 if (show_tree_
&& !parent
) {
546 void BookmarkEditorView::ApplyEdits(EditorNode
* parent
) {
547 DCHECK(!show_tree_
|| parent
);
549 // We're going to apply edits to the bookmark bar model, which will call us
550 // back. Normally when a structural edit occurs we reset the tree model.
551 // We don't want to do that here, so we remove ourselves as an observer.
552 bb_model_
->RemoveObserver(this);
554 GURL
new_url(GetInputURL());
555 base::string16
new_title(title_tf_
->text());
558 BookmarkEditor::ApplyEditsWithNoFolderChange(
559 bb_model_
, parent_
, details_
, new_title
, new_url
);
563 // Create the new folders and update the titles.
564 const BookmarkNode
* new_parent
= NULL
;
565 ApplyNameChangesAndCreateNewFolders(
566 bb_model_
->root_node(), tree_model_
->GetRoot(), parent
, &new_parent
);
568 BookmarkEditor::ApplyEditsWithPossibleFolderChange(
569 bb_model_
, new_parent
, details_
, new_title
, new_url
);
571 BookmarkExpandedStateTracker::Nodes expanded_nodes
;
572 UpdateExpandedNodes(tree_model_
->GetRoot(), &expanded_nodes
);
573 bb_model_
->expanded_state_tracker()->SetExpandedNodes(expanded_nodes
);
575 // Remove the folders that were removed. This has to be done after all the
576 // other changes have been committed.
577 bookmarks::DeleteBookmarkFolders(bb_model_
, deletes_
);
580 void BookmarkEditorView::ApplyNameChangesAndCreateNewFolders(
581 const BookmarkNode
* bb_node
,
582 BookmarkEditorView::EditorNode
* b_node
,
583 BookmarkEditorView::EditorNode
* parent_b_node
,
584 const BookmarkNode
** parent_bb_node
) {
585 if (parent_b_node
== b_node
)
586 *parent_bb_node
= bb_node
;
587 for (int i
= 0; i
< b_node
->child_count(); ++i
) {
588 EditorNode
* child_b_node
= b_node
->GetChild(i
);
589 const BookmarkNode
* child_bb_node
= NULL
;
590 if (child_b_node
->value
== 0) {
592 child_bb_node
= bb_model_
->AddFolder(bb_node
,
593 bb_node
->child_count(), child_b_node
->GetTitle());
594 child_b_node
->value
= child_bb_node
->id();
596 // Existing node, reset the title (BookmarkModel ignores changes if the
597 // title is the same).
598 for (int j
= 0; j
< bb_node
->child_count(); ++j
) {
599 const BookmarkNode
* node
= bb_node
->GetChild(j
);
600 if (node
->is_folder() && node
->id() == child_b_node
->value
) {
601 child_bb_node
= node
;
605 DCHECK(child_bb_node
);
606 bb_model_
->SetTitle(child_bb_node
, child_b_node
->GetTitle());
608 ApplyNameChangesAndCreateNewFolders(child_bb_node
, child_b_node
,
609 parent_b_node
, parent_bb_node
);
613 void BookmarkEditorView::UpdateExpandedNodes(
614 EditorNode
* editor_node
,
615 BookmarkExpandedStateTracker::Nodes
* expanded_nodes
) {
616 if (!tree_view_
->IsExpanded(editor_node
))
620 if (editor_node
->value
!= 0) {
621 expanded_nodes
->insert(
622 bookmarks::GetBookmarkNodeByID(bb_model_
, editor_node
->value
));
625 for (int i
= 0; i
< editor_node
->child_count(); ++i
)
626 UpdateExpandedNodes(editor_node
->GetChild(i
), expanded_nodes
);
629 ui::SimpleMenuModel
* BookmarkEditorView::GetMenuModel() {
630 if (!context_menu_model_
.get()) {
631 context_menu_model_
.reset(new ui::SimpleMenuModel(this));
632 context_menu_model_
->AddItemWithStringId(IDS_EDIT
, IDS_EDIT
);
633 context_menu_model_
->AddItemWithStringId(IDS_DELETE
, IDS_DELETE
);
634 context_menu_model_
->AddItemWithStringId(
635 IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM
,
636 IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM
);
638 return context_menu_model_
.get();
641 void BookmarkEditorView::EditorTreeModel::SetTitle(
642 ui::TreeModelNode
* node
,
643 const base::string16
& title
) {
645 ui::TreeNodeModel
<EditorNode
>::SetTitle(node
, title
);
650 void ShowBookmarkEditorViews(gfx::NativeWindow parent_window
,
652 const BookmarkEditor::EditDetails
& details
,
653 BookmarkEditor::Configuration configuration
) {
655 BookmarkEditorView
* editor
= new BookmarkEditorView(
656 profile
, details
.parent_node
, details
, configuration
);
657 editor
->Show(parent_window
);
660 } // namespace chrome