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 35 using views::Button; 36 using views::ColumnSet; 37 using views::GridLayout; 38 using views::Label; 39 using views::NativeButton; 40 using views::Textfield; 41 42 // Background color of text field when URL is invalid. 43 static const SkColor kErrorColor = SkColorSetRGB(0xFF, 0xBC, 0xBC); 44 45 // Preferred width of the tree. 46 static const int kTreeWidth = 300; 47 48 // ID for various children. 49 static const int kNewFolderButtonID = 1002; 50 51 // static 52 void 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 63 BookmarkEditorView::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 81 BookmarkEditorView::~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 89 bool 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 101 bool BookmarkEditorView::IsModal() const { 102 return true; 103 } 104 105 bool BookmarkEditorView::CanResize() const { 106 return true; 107 } 108 109 std::wstring BookmarkEditorView::GetWindowTitle() const { 110 return UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_EDITOR_TITLE)); 111 } 112 113 bool 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 125 bool BookmarkEditorView::AreAcceleratorsEnabled( 126 MessageBoxFlags::DialogButton button) { 127 return !show_tree_ || !tree_view_->GetEditingNode(); 128 } 129 130 views::View* BookmarkEditorView::GetContentsView() { 131 return this; 132 } 133 134 void 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 151 gfx::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 160 void 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 173 void BookmarkEditorView::OnTreeViewSelectionChanged( 174 views::TreeView* tree_view) { 175 } 176 177 bool 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 184 void BookmarkEditorView::ContentsChanged(Textfield* sender, 185 const std::wstring& new_contents) { 186 UserInputChanged(); 187 } 188 189 void 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 202 bool BookmarkEditorView::IsCommandIdChecked(int command_id) const { 203 return false; 204 } 205 206 bool BookmarkEditorView::IsCommandIdEnabled(int command_id) const { 207 return (command_id != IDS_EDIT || !running_menu_for_root_); 208 } 209 210 bool BookmarkEditorView::GetAcceleratorForCommandId( 211 int command_id, 212 ui::Accelerator* accelerator) { 213 return GetWidget()->GetAccelerator(command_id, accelerator); 214 } 215 216 void 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 226 void 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 238 void BookmarkEditorView::Close() { 239 DCHECK(window()); 240 window()->CloseWindow(); 241 } 242 243 void 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 263 void 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 372 void 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 380 void BookmarkEditorView::BookmarkNodeAdded(BookmarkModel* model, 381 const BookmarkNode* parent, 382 int index) { 383 Reset(); 384 } 385 386 void 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 400 void BookmarkEditorView::BookmarkNodeChildrenReordered( 401 BookmarkModel* model, const BookmarkNode* node) { 402 Reset(); 403 } 404 405 void 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 } 429 GURL BookmarkEditorView::GetInputURL() const { 430 return URLFixerUpper::FixupURL(UTF16ToUTF8(url_tf_.text()), std::string()); 431 } 432 433 std::wstring BookmarkEditorView::GetInputTitle() const { 434 return title_tf_.text(); 435 } 436 437 void 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 446 void 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 458 BookmarkEditorView::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 469 void 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 484 BookmarkEditorView::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 494 void 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 508 BookmarkEditorView::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 521 void 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 533 void 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 559 void 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