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_editor_view.h"
6
7#include "base/basictypes.h"
8#include "base/logging.h"
9#include "base/string_util.h"
10#include "base/utf_string_conversions.h"
11#include "chrome/browser/bookmarks/bookmark_model.h"
12#include "chrome/browser/bookmarks/bookmark_utils.h"
13#include "chrome/browser/history/history.h"
14#include "chrome/browser/net/url_fixer_upper.h"
15#include "chrome/browser/prefs/pref_service.h"
16#include "chrome/browser/profiles/profile.h"
17#include "chrome/common/pref_names.h"
18#include "googleurl/src/gurl.h"
19#include "grit/chromium_strings.h"
20#include "grit/generated_resources.h"
21#include "grit/locale_settings.h"
22#include "net/base/net_util.h"
23#include "ui/base/l10n/l10n_util.h"
24#include "views/background.h"
25#include "views/controls/button/native_button.h"
26#include "views/controls/label.h"
27#include "views/controls/menu/menu_2.h"
28#include "views/controls/textfield/textfield.h"
29#include "views/focus/focus_manager.h"
30#include "views/layout/grid_layout.h"
31#include "views/layout/layout_constants.h"
32#include "views/widget/widget.h"
33#include "views/window/window.h"
34
35using views::Button;
36using views::ColumnSet;
37using views::GridLayout;
38using views::Label;
39using views::NativeButton;
40using views::Textfield;
41
42// Background color of text field when URL is invalid.
43static const SkColor kErrorColor = SkColorSetRGB(0xFF, 0xBC, 0xBC);
44
45// Preferred width of the tree.
46static const int kTreeWidth = 300;
47
48// ID for various children.
49static const int kNewFolderButtonID = 1002;
50
51// static
52void BookmarkEditor::Show(HWND parent_hwnd,
53                          Profile* profile,
54                          const BookmarkNode* parent,
55                          const EditDetails& details,
56                          Configuration configuration) {
57  DCHECK(profile);
58  BookmarkEditorView* editor =
59      new BookmarkEditorView(profile, parent, details, configuration);
60  editor->Show(parent_hwnd);
61}
62
63BookmarkEditorView::BookmarkEditorView(
64    Profile* profile,
65    const BookmarkNode* parent,
66    const EditDetails& details,
67    BookmarkEditor::Configuration configuration)
68    : profile_(profile),
69      tree_view_(NULL),
70      new_folder_button_(NULL),
71      url_label_(NULL),
72      title_label_(NULL),
73      parent_(parent),
74      details_(details),
75      running_menu_for_root_(false),
76      show_tree_(configuration == SHOW_TREE) {
77  DCHECK(profile);
78  Init();
79}
80
81BookmarkEditorView::~BookmarkEditorView() {
82  // The tree model is deleted before the view. Reset the model otherwise the
83  // tree will reference a deleted model.
84  if (tree_view_)
85    tree_view_->SetModel(NULL);
86  bb_model_->RemoveObserver(this);
87}
88
89bool BookmarkEditorView::IsDialogButtonEnabled(
90    MessageBoxFlags::DialogButton button) const {
91  if (button == MessageBoxFlags::DIALOGBUTTON_OK) {
92    if (details_.type == EditDetails::NEW_FOLDER)
93      return !title_tf_.text().empty();
94
95    const GURL url(GetInputURL());
96    return bb_model_->IsLoaded() && url.is_valid();
97  }
98  return true;
99}
100
101bool BookmarkEditorView::IsModal() const {
102  return true;
103}
104
105bool BookmarkEditorView::CanResize() const {
106  return true;
107}
108
109std::wstring BookmarkEditorView::GetWindowTitle() const {
110  return UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_EDITOR_TITLE));
111}
112
113bool BookmarkEditorView::Accept() {
114  if (!IsDialogButtonEnabled(MessageBoxFlags::DIALOGBUTTON_OK)) {
115    // The url is invalid, focus the url field.
116    url_tf_.SelectAll();
117    url_tf_.RequestFocus();
118    return false;
119  }
120  // Otherwise save changes and close the dialog box.
121  ApplyEdits();
122  return true;
123}
124
125bool BookmarkEditorView::AreAcceleratorsEnabled(
126    MessageBoxFlags::DialogButton button) {
127  return !show_tree_ || !tree_view_->GetEditingNode();
128}
129
130views::View* BookmarkEditorView::GetContentsView() {
131  return this;
132}
133
134void BookmarkEditorView::Layout() {
135  // Let the grid layout manager lay out most of the dialog...
136  GetLayoutManager()->Layout(this);
137
138  if (!show_tree_)
139    return;
140
141  // Manually lay out the New Folder button in the same row as the OK/Cancel
142  // buttons...
143  gfx::Rect parent_bounds = parent()->GetContentsBounds();
144  gfx::Size prefsize = new_folder_button_->GetPreferredSize();
145  int button_y =
146      parent_bounds.bottom() - prefsize.height() - views::kButtonVEdgeMargin;
147  new_folder_button_->SetBounds(
148      views::kPanelHorizMargin, button_y, prefsize.width(), prefsize.height());
149}
150
151gfx::Size BookmarkEditorView::GetPreferredSize() {
152  if (!show_tree_)
153    return views::View::GetPreferredSize();
154
155  return gfx::Size(views::Window::GetLocalizedContentsSize(
156      IDS_EDITBOOKMARK_DIALOG_WIDTH_CHARS,
157      IDS_EDITBOOKMARK_DIALOG_HEIGHT_LINES));
158}
159
160void BookmarkEditorView::ViewHierarchyChanged(bool is_add,
161                                              views::View* parent,
162                                              views::View* child) {
163  if (show_tree_ && child == this) {
164    // Add and remove the New Folder button from the ClientView's hierarchy.
165    if (is_add) {
166      parent->AddChildView(new_folder_button_.get());
167    } else {
168      parent->RemoveChildView(new_folder_button_.get());
169    }
170  }
171}
172
173void BookmarkEditorView::OnTreeViewSelectionChanged(
174    views::TreeView* tree_view) {
175}
176
177bool BookmarkEditorView::CanEdit(views::TreeView* tree_view,
178                                 ui::TreeModelNode* node) {
179  // Only allow editting of children of the bookmark bar node and other node.
180  EditorNode* bb_node = tree_model_->AsNode(node);
181  return (bb_node->parent() && bb_node->parent()->parent());
182}
183
184void BookmarkEditorView::ContentsChanged(Textfield* sender,
185                                         const std::wstring& new_contents) {
186  UserInputChanged();
187}
188
189void BookmarkEditorView::ButtonPressed(
190    Button* sender, const views::Event& event) {
191  DCHECK(sender);
192  switch (sender->GetID()) {
193    case kNewFolderButtonID:
194      NewFolder();
195      break;
196
197    default:
198      NOTREACHED();
199  }
200}
201
202bool BookmarkEditorView::IsCommandIdChecked(int command_id) const {
203  return false;
204}
205
206bool BookmarkEditorView::IsCommandIdEnabled(int command_id) const {
207  return (command_id != IDS_EDIT || !running_menu_for_root_);
208}
209
210bool BookmarkEditorView::GetAcceleratorForCommandId(
211    int command_id,
212    ui::Accelerator* accelerator) {
213  return GetWidget()->GetAccelerator(command_id, accelerator);
214}
215
216void BookmarkEditorView::ExecuteCommand(int command_id) {
217  DCHECK(tree_view_->GetSelectedNode());
218  if (command_id == IDS_EDIT) {
219    tree_view_->StartEditing(tree_view_->GetSelectedNode());
220  } else {
221    DCHECK(command_id == IDS_BOOMARK_EDITOR_NEW_FOLDER_MENU_ITEM);
222    NewFolder();
223  }
224}
225
226void BookmarkEditorView::Show(HWND parent_hwnd) {
227  views::Window::CreateChromeWindow(parent_hwnd, gfx::Rect(), this);
228  UserInputChanged();
229  if (show_tree_ && bb_model_->IsLoaded())
230    ExpandAndSelect();
231  window()->Show();
232  // Select all the text in the name Textfield.
233  title_tf_.SelectAll();
234  // Give focus to the name Textfield.
235  title_tf_.RequestFocus();
236}
237
238void BookmarkEditorView::Close() {
239  DCHECK(window());
240  window()->CloseWindow();
241}
242
243void BookmarkEditorView::ShowContextMenuForView(View* source,
244                                                const gfx::Point& p,
245                                                bool is_mouse_gesture) {
246  DCHECK(source == tree_view_);
247  if (!tree_view_->GetSelectedNode())
248    return;
249  running_menu_for_root_ =
250      (tree_model_->GetParent(tree_view_->GetSelectedNode()) ==
251       tree_model_->GetRoot());
252  if (!context_menu_contents_.get()) {
253    context_menu_contents_.reset(new ui::SimpleMenuModel(this));
254    context_menu_contents_->AddItemWithStringId(IDS_EDIT, IDS_EDIT);
255    context_menu_contents_->AddItemWithStringId(
256        IDS_BOOMARK_EDITOR_NEW_FOLDER_MENU_ITEM,
257        IDS_BOOMARK_EDITOR_NEW_FOLDER_MENU_ITEM);
258    context_menu_.reset(new views::Menu2(context_menu_contents_.get()));
259  }
260  context_menu_->RunContextMenuAt(p);
261}
262
263void BookmarkEditorView::Init() {
264  bb_model_ = profile_->GetBookmarkModel();
265  DCHECK(bb_model_);
266  bb_model_->AddObserver(this);
267
268  url_tf_.set_parent_owned(false);
269  title_tf_.set_parent_owned(false);
270
271  std::wstring title;
272  if (details_.type == EditDetails::EXISTING_NODE)
273    title = details_.existing_node->GetTitle();
274  else if (details_.type == EditDetails::NEW_FOLDER)
275    title = UTF16ToWide(
276        l10n_util::GetStringUTF16(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME));
277  title_tf_.SetText(title);
278  title_tf_.SetController(this);
279
280  title_label_ = new views::Label(
281      UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_EDITOR_NAME_LABEL)));
282  title_tf_.SetAccessibleName(WideToUTF16Hack(title_label_->GetText()));
283
284  string16 url_text;
285  if (details_.type == EditDetails::EXISTING_NODE) {
286    std::string languages = profile_
287        ? profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)
288        : std::string();
289    // Because this gets parsed by FixupURL(), it's safe to omit the scheme or
290    // trailing slash, and unescape most characters, but we need to not drop any
291    // username/password, or unescape anything that changes the meaning.
292    url_text = net::FormatUrl(details_.existing_node->GetURL(), languages,
293        net::kFormatUrlOmitAll & ~net::kFormatUrlOmitUsernamePassword,
294        UnescapeRule::SPACES, NULL, NULL, NULL);
295  }
296  url_tf_.SetText(UTF16ToWide(url_text));
297  url_tf_.SetController(this);
298
299  url_label_ = new views::Label(
300      UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_EDITOR_URL_LABEL)));
301  url_tf_.SetAccessibleName(WideToUTF16Hack(url_label_->GetText()));
302
303  if (show_tree_) {
304    tree_view_ = new views::TreeView();
305    new_folder_button_.reset(new views::NativeButton(
306        this,
307        UTF16ToWide(l10n_util::GetStringUTF16(
308            IDS_BOOMARK_EDITOR_NEW_FOLDER_BUTTON))));
309    new_folder_button_->set_parent_owned(false);
310    tree_view_->SetContextMenuController(this);
311
312    tree_view_->SetRootShown(false);
313    new_folder_button_->SetEnabled(false);
314    new_folder_button_->SetID(kNewFolderButtonID);
315  }
316
317  // Yummy layout code.
318  GridLayout* layout = GridLayout::CreatePanel(this);
319  SetLayoutManager(layout);
320
321  const int labels_column_set_id = 0;
322  const int single_column_view_set_id = 1;
323  const int buttons_column_set_id = 2;
324
325  ColumnSet* column_set = layout->AddColumnSet(labels_column_set_id);
326  column_set->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0,
327                        GridLayout::USE_PREF, 0, 0);
328  column_set->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing);
329  column_set->AddColumn(GridLayout::FILL, GridLayout::CENTER, 1,
330                        GridLayout::USE_PREF, 0, 0);
331
332  column_set = layout->AddColumnSet(single_column_view_set_id);
333  column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
334                        GridLayout::FIXED, kTreeWidth, 0);
335
336  column_set = layout->AddColumnSet(buttons_column_set_id);
337  column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0,
338                        GridLayout::USE_PREF, 0, 0);
339  column_set->AddPaddingColumn(1, views::kRelatedControlHorizontalSpacing);
340  column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0,
341                        GridLayout::USE_PREF, 0, 0);
342  column_set->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing);
343  column_set->AddColumn(GridLayout::FILL, GridLayout::LEADING, 0,
344                        GridLayout::USE_PREF, 0, 0);
345  column_set->LinkColumnSizes(0, 2, 4, -1);
346
347  layout->StartRow(0, labels_column_set_id);
348
349  layout->AddView(title_label_);
350  layout->AddView(&title_tf_);
351
352  if (details_.type != EditDetails::NEW_FOLDER) {
353    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
354
355    layout->StartRow(0, labels_column_set_id);
356    layout->AddView(url_label_);
357    layout->AddView(&url_tf_);
358  }
359
360  if (show_tree_) {
361    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
362    layout->StartRow(1, single_column_view_set_id);
363    layout->AddView(tree_view_);
364  }
365
366  layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
367
368  if (!show_tree_ || bb_model_->IsLoaded())
369    Reset();
370}
371
372void BookmarkEditorView::BookmarkNodeMoved(BookmarkModel* model,
373                                           const BookmarkNode* old_parent,
374                                           int old_index,
375                                           const BookmarkNode* new_parent,
376                                           int new_index) {
377  Reset();
378}
379
380void BookmarkEditorView::BookmarkNodeAdded(BookmarkModel* model,
381                                           const BookmarkNode* parent,
382                                           int index) {
383  Reset();
384}
385
386void BookmarkEditorView::BookmarkNodeRemoved(BookmarkModel* model,
387                                             const BookmarkNode* parent,
388                                             int index,
389                                             const BookmarkNode* node) {
390  if ((details_.type == EditDetails::EXISTING_NODE &&
391       details_.existing_node->HasAncestor(node)) ||
392      (parent_ && parent_->HasAncestor(node))) {
393    // The node, or its parent was removed. Close the dialog.
394    window()->CloseWindow();
395  } else {
396    Reset();
397  }
398}
399
400void BookmarkEditorView::BookmarkNodeChildrenReordered(
401    BookmarkModel* model, const BookmarkNode* node) {
402  Reset();
403}
404
405void BookmarkEditorView::Reset() {
406  if (!show_tree_) {
407    if (parent())
408      UserInputChanged();
409    return;
410  }
411
412  new_folder_button_->SetEnabled(true);
413
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);
417
418  EditorNode* root_node = CreateRootNode();
419  tree_model_.reset(new EditorTreeModel(root_node));
420
421  tree_view_->SetModel(tree_model_.get());
422  tree_view_->SetController(this);
423
424  context_menu_.reset();
425
426  if (parent())
427    ExpandAndSelect();
428}
429GURL BookmarkEditorView::GetInputURL() const {
430  return URLFixerUpper::FixupURL(UTF16ToUTF8(url_tf_.text()), std::string());
431}
432
433std::wstring BookmarkEditorView::GetInputTitle() const {
434  return title_tf_.text();
435}
436
437void BookmarkEditorView::UserInputChanged() {
438  const GURL url(GetInputURL());
439  if (!url.is_valid())
440    url_tf_.SetBackgroundColor(kErrorColor);
441  else
442    url_tf_.UseDefaultBackgroundColor();
443  GetDialogClientView()->UpdateDialogButtons();
444}
445
446void BookmarkEditorView::NewFolder() {
447  // Create a new entry parented to the selected item, or the bookmark
448  // bar if nothing is selected.
449  EditorNode* parent = tree_model_->AsNode(tree_view_->GetSelectedNode());
450  if (!parent) {
451    NOTREACHED();
452    return;
453  }
454
455  tree_view_->StartEditing(AddNewFolder(parent));
456}
457
458BookmarkEditorView::EditorNode* BookmarkEditorView::AddNewFolder(
459    EditorNode* parent) {
460  EditorNode* new_node = new EditorNode();
461  new_node->set_title(
462      l10n_util::GetStringUTF16(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME));
463  new_node->value = 0;
464  // new_node is now owned by parent.
465  tree_model_->Add(parent, new_node, parent->child_count());
466  return new_node;
467}
468
469void BookmarkEditorView::ExpandAndSelect() {
470  tree_view_->ExpandAll();
471
472  const BookmarkNode* to_select = parent_;
473  if (details_.type == EditDetails::EXISTING_NODE)
474    to_select = details_.existing_node->parent();
475  int64 folder_id_to_select = to_select->id();
476  EditorNode* b_node =
477      FindNodeWithID(tree_model_->GetRoot(), folder_id_to_select);
478  if (!b_node)
479    b_node = tree_model_->GetRoot()->GetChild(0);  // Bookmark bar node.
480
481  tree_view_->SetSelectedNode(b_node);
482}
483
484BookmarkEditorView::EditorNode* BookmarkEditorView::CreateRootNode() {
485  EditorNode* root_node = new EditorNode(std::wstring(), 0);
486  const BookmarkNode* bb_root_node = bb_model_->root_node();
487  CreateNodes(bb_root_node, root_node);
488  DCHECK(root_node->child_count() == 2);
489  DCHECK(bb_root_node->GetChild(0)->type() == BookmarkNode::BOOKMARK_BAR);
490  DCHECK(bb_root_node->GetChild(1)->type() == BookmarkNode::OTHER_NODE);
491  return root_node;
492}
493
494void BookmarkEditorView::CreateNodes(const BookmarkNode* bb_node,
495                                     BookmarkEditorView::EditorNode* b_node) {
496  for (int i = 0; i < bb_node->child_count(); ++i) {
497    const BookmarkNode* child_bb_node = bb_node->GetChild(i);
498    if (child_bb_node->is_folder()) {
499      EditorNode* new_b_node =
500          new EditorNode(WideToUTF16(child_bb_node->GetTitle()),
501                                     child_bb_node->id());
502      b_node->Add(new_b_node, b_node->child_count());
503      CreateNodes(child_bb_node, new_b_node);
504    }
505  }
506}
507
508BookmarkEditorView::EditorNode* BookmarkEditorView::FindNodeWithID(
509    BookmarkEditorView::EditorNode* node,
510    int64 id) {
511  if (node->value == id)
512    return node;
513  for (int i = 0; i < node->child_count(); ++i) {
514    EditorNode* result = FindNodeWithID(node->GetChild(i), id);
515    if (result)
516      return result;
517  }
518  return NULL;
519}
520
521void BookmarkEditorView::ApplyEdits() {
522  DCHECK(bb_model_->IsLoaded());
523
524  EditorNode* parent = show_tree_ ?
525      tree_model_->AsNode(tree_view_->GetSelectedNode()) : NULL;
526  if (show_tree_ && !parent) {
527    NOTREACHED();
528    return;
529  }
530  ApplyEdits(parent);
531}
532
533void BookmarkEditorView::ApplyEdits(EditorNode* parent) {
534  DCHECK(!show_tree_ || parent);
535
536  // We're going to apply edits to the bookmark bar model, which will call us
537  // back. Normally when a structural edit occurs we reset the tree model.
538  // We don't want to do that here, so we remove ourselves as an observer.
539  bb_model_->RemoveObserver(this);
540
541  GURL new_url(GetInputURL());
542  string16 new_title(WideToUTF16Hack(GetInputTitle()));
543
544  if (!show_tree_) {
545    bookmark_utils::ApplyEditsWithNoFolderChange(
546        bb_model_, parent_, details_, new_title, new_url);
547    return;
548  }
549
550  // Create the new folders and update the titles.
551  const BookmarkNode* new_parent = NULL;
552  ApplyNameChangesAndCreateNewFolders(
553      bb_model_->root_node(), tree_model_->GetRoot(), parent, &new_parent);
554
555  bookmark_utils::ApplyEditsWithPossibleFolderChange(
556      bb_model_, new_parent, details_, new_title, new_url);
557}
558
559void BookmarkEditorView::ApplyNameChangesAndCreateNewFolders(
560    const BookmarkNode* bb_node,
561    BookmarkEditorView::EditorNode* b_node,
562    BookmarkEditorView::EditorNode* parent_b_node,
563    const BookmarkNode** parent_bb_node) {
564  if (parent_b_node == b_node)
565    *parent_bb_node = bb_node;
566  for (int i = 0; i < b_node->child_count(); ++i) {
567    EditorNode* child_b_node = b_node->GetChild(i);
568    const BookmarkNode* child_bb_node = NULL;
569    if (child_b_node->value == 0) {
570      // New folder.
571      child_bb_node = bb_model_->AddFolder(bb_node,
572          bb_node->child_count(), child_b_node->GetTitle());
573    } else {
574      // Existing node, reset the title (BookmarkModel ignores changes if the
575      // title is the same).
576      for (int j = 0; j < bb_node->child_count(); ++j) {
577        const BookmarkNode* node = bb_node->GetChild(j);
578        if (node->is_folder() && node->id() == child_b_node->value) {
579          child_bb_node = node;
580          break;
581        }
582      }
583      DCHECK(child_bb_node);
584      bb_model_->SetTitle(child_bb_node, child_b_node->GetTitle());
585    }
586    ApplyNameChangesAndCreateNewFolders(child_bb_node, child_b_node,
587                                        parent_b_node, parent_bb_node);
588  }
589}
590