bookmark_utils.cc revision 201ade2fbba22bfb27ae029f4d23fca6ded109a0
1// Copyright (c) 2010 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/bookmarks/bookmark_utils.h" 6 7#include "app/drag_drop_types.h" 8#include "app/l10n_util.h" 9#include "app/tree_node_iterator.h" 10#include "base/basictypes.h" 11#include "base/file_path.h" 12#include "base/string_number_conversions.h" 13#include "base/string16.h" 14#include "base/time.h" 15#include "base/utf_string_conversions.h" 16#include "chrome/browser/bookmarks/bookmark_node_data.h" 17#include "chrome/browser/bookmarks/bookmark_model.h" 18#if defined(OS_MACOSX) 19#include "chrome/browser/bookmarks/bookmark_pasteboard_helper_mac.h" 20#endif 21#include "chrome/browser/browser_list.h" 22#include "chrome/browser/browser_process.h" 23#include "chrome/browser/browser_window.h" 24#include "chrome/browser/history/query_parser.h" 25#include "chrome/browser/platform_util.h" 26#include "chrome/browser/prefs/pref_service.h" 27#include "chrome/browser/profile.h" 28#include "chrome/browser/tab_contents/page_navigator.h" 29#include "chrome/browser/tab_contents/tab_contents.h" 30#include "chrome/browser/ui/browser.h" 31#include "chrome/common/notification_service.h" 32#include "chrome/common/pref_names.h" 33#include "grit/app_strings.h" 34#include "grit/chromium_strings.h" 35#include "grit/generated_resources.h" 36#include "net/base/net_util.h" 37#include "views/event.h" 38 39#if defined(TOOLKIT_VIEWS) 40#include "app/os_exchange_data.h" 41#include "views/drag_utils.h" 42#include "views/widget/root_view.h" 43#include "views/widget/widget.h" 44#elif defined(TOOLKIT_GTK) 45#include "chrome/browser/gtk/custom_drag.h" 46#endif 47 48using base::Time; 49 50namespace { 51 52// A PageNavigator implementation that creates a new Browser. This is used when 53// opening a url and there is no Browser open. The Browser is created the first 54// time the PageNavigator method is invoked. 55class NewBrowserPageNavigator : public PageNavigator { 56 public: 57 explicit NewBrowserPageNavigator(Profile* profile) 58 : profile_(profile), 59 browser_(NULL) {} 60 61 virtual ~NewBrowserPageNavigator() { 62 if (browser_) 63 browser_->window()->Show(); 64 } 65 66 Browser* browser() const { return browser_; } 67 68 virtual void OpenURL(const GURL& url, 69 const GURL& referrer, 70 WindowOpenDisposition disposition, 71 PageTransition::Type transition) { 72 if (!browser_) { 73 Profile* profile = (disposition == OFF_THE_RECORD) ? 74 profile_->GetOffTheRecordProfile() : profile_; 75 browser_ = Browser::Create(profile); 76 // Always open the first tab in the foreground. 77 disposition = NEW_FOREGROUND_TAB; 78 } 79 browser_->OpenURL(url, referrer, NEW_FOREGROUND_TAB, transition); 80 } 81 82 private: 83 Profile* profile_; 84 Browser* browser_; 85 86 DISALLOW_COPY_AND_ASSIGN(NewBrowserPageNavigator); 87}; 88 89void CloneBookmarkNodeImpl(BookmarkModel* model, 90 const BookmarkNodeData::Element& element, 91 const BookmarkNode* parent, 92 int index_to_add_at) { 93 if (element.is_url) { 94 model->AddURL(parent, index_to_add_at, element.title, element.url); 95 } else { 96 const BookmarkNode* new_folder = model->AddGroup(parent, 97 index_to_add_at, 98 element.title); 99 for (int i = 0; i < static_cast<int>(element.children.size()); ++i) 100 CloneBookmarkNodeImpl(model, element.children[i], new_folder, i); 101 } 102} 103 104// Returns the number of descendants of node that are of type url. 105int DescendantURLCount(const BookmarkNode* node) { 106 int result = 0; 107 for (int i = 0; i < node->GetChildCount(); ++i) { 108 const BookmarkNode* child = node->GetChild(i); 109 if (child->is_url()) 110 result++; 111 else 112 result += DescendantURLCount(child); 113 } 114 return result; 115} 116 117// Implementation of OpenAll. Opens all nodes of type URL and recurses for 118// groups. |navigator| is the PageNavigator used to open URLs. After the first 119// url is opened |opened_url| is set to true and |navigator| is set to the 120// PageNavigator of the last active tab. This is done to handle a window 121// disposition of new window, in which case we want subsequent tabs to open in 122// that window. 123void OpenAllImpl(const BookmarkNode* node, 124 WindowOpenDisposition initial_disposition, 125 PageNavigator** navigator, 126 bool* opened_url) { 127 if (node->is_url()) { 128 WindowOpenDisposition disposition; 129 if (*opened_url) 130 disposition = NEW_BACKGROUND_TAB; 131 else 132 disposition = initial_disposition; 133 (*navigator)->OpenURL(node->GetURL(), GURL(), disposition, 134 PageTransition::AUTO_BOOKMARK); 135 if (!*opened_url) { 136 *opened_url = true; 137 // We opened the first URL which may have opened a new window or clobbered 138 // the current page, reset the navigator just to be sure. 139 Browser* new_browser = BrowserList::GetLastActive(); 140 if (new_browser) { 141 TabContents* current_tab = new_browser->GetSelectedTabContents(); 142 DCHECK(new_browser && current_tab); 143 if (new_browser && current_tab) 144 *navigator = current_tab; 145 } // else, new_browser == NULL, which happens during testing. 146 } 147 } else { 148 // Group, recurse through children. 149 for (int i = 0; i < node->GetChildCount(); ++i) { 150 OpenAllImpl(node->GetChild(i), initial_disposition, navigator, 151 opened_url); 152 } 153 } 154} 155 156bool ShouldOpenAll(gfx::NativeWindow parent, 157 const std::vector<const BookmarkNode*>& nodes) { 158 int descendant_count = 0; 159 for (size_t i = 0; i < nodes.size(); ++i) 160 descendant_count += DescendantURLCount(nodes[i]); 161 if (descendant_count < bookmark_utils::num_urls_before_prompting) 162 return true; 163 164 string16 message = l10n_util::GetStringFUTF16( 165 IDS_BOOKMARK_BAR_SHOULD_OPEN_ALL, 166 base::IntToString16(descendant_count)); 167 string16 title = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); 168 return platform_util::SimpleYesNoBox(parent, title, message); 169} 170 171// Comparison function that compares based on date modified of the two nodes. 172bool MoreRecentlyModified(const BookmarkNode* n1, const BookmarkNode* n2) { 173 return n1->date_group_modified() > n2->date_group_modified(); 174} 175 176// Returns true if |text| contains each string in |words|. This is used when 177// searching for bookmarks. 178bool DoesBookmarkTextContainWords(const string16& text, 179 const std::vector<string16>& words) { 180 for (size_t i = 0; i < words.size(); ++i) { 181 if (text.find(words[i]) == string16::npos) 182 return false; 183 } 184 return true; 185} 186 187// Returns true if |node|s title or url contains the strings in |words|. 188// |languages| argument is user's accept-language setting to decode IDN. 189bool DoesBookmarkContainWords(const BookmarkNode* node, 190 const std::vector<string16>& words, 191 const std::string& languages) { 192 return 193 DoesBookmarkTextContainWords( 194 l10n_util::ToLower(node->GetTitle()), words) || 195 DoesBookmarkTextContainWords( 196 l10n_util::ToLower(UTF8ToUTF16(node->GetURL().spec())), words) || 197 DoesBookmarkTextContainWords(l10n_util::ToLower( 198 net::FormatUrl(node->GetURL(), languages, net::kFormatUrlOmitNothing, 199 UnescapeRule::NORMAL, NULL, NULL, NULL)), words); 200} 201 202} // namespace 203 204namespace bookmark_utils { 205 206int num_urls_before_prompting = 15; 207 208int PreferredDropOperation(int source_operations, int operations) { 209 int common_ops = (source_operations & operations); 210 if (!common_ops) 211 return 0; 212 if (DragDropTypes::DRAG_COPY & common_ops) 213 return DragDropTypes::DRAG_COPY; 214 if (DragDropTypes::DRAG_LINK & common_ops) 215 return DragDropTypes::DRAG_LINK; 216 if (DragDropTypes::DRAG_MOVE & common_ops) 217 return DragDropTypes::DRAG_MOVE; 218 return DragDropTypes::DRAG_NONE; 219} 220 221int BookmarkDragOperation(const BookmarkNode* node) { 222 if (node->is_url()) { 223 return DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_MOVE | 224 DragDropTypes::DRAG_LINK; 225 } 226 return DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_MOVE; 227} 228 229int BookmarkDropOperation(Profile* profile, 230 const views::DropTargetEvent& event, 231 const BookmarkNodeData& data, 232 const BookmarkNode* parent, 233 int index) { 234 if (data.IsFromProfile(profile) && data.size() > 1) 235 // Currently only accept one dragged node at a time. 236 return DragDropTypes::DRAG_NONE; 237 238 if (!bookmark_utils::IsValidDropLocation(profile, data, parent, index)) 239 return DragDropTypes::DRAG_NONE; 240 241 if (data.GetFirstNode(profile)) { 242 // User is dragging from this profile: move. 243 return DragDropTypes::DRAG_MOVE; 244 } 245 // User is dragging from another app, copy. 246 return PreferredDropOperation(event.GetSourceOperations(), 247 DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_LINK); 248} 249 250int PerformBookmarkDrop(Profile* profile, 251 const BookmarkNodeData& data, 252 const BookmarkNode* parent_node, 253 int index) { 254 BookmarkModel* model = profile->GetBookmarkModel(); 255 if (data.IsFromProfile(profile)) { 256 const std::vector<const BookmarkNode*> dragged_nodes = 257 data.GetNodes(profile); 258 if (!dragged_nodes.empty()) { 259 // Drag from same profile. Move nodes. 260 for (size_t i = 0; i < dragged_nodes.size(); ++i) { 261 model->Move(dragged_nodes[i], parent_node, index); 262 index = parent_node->IndexOfChild(dragged_nodes[i]) + 1; 263 } 264 return DragDropTypes::DRAG_MOVE; 265 } 266 return DragDropTypes::DRAG_NONE; 267 } 268 // Dropping a group from different profile. Always accept. 269 bookmark_utils::CloneBookmarkNode(model, data.elements, parent_node, index); 270 return DragDropTypes::DRAG_COPY; 271} 272 273bool IsValidDropLocation(Profile* profile, 274 const BookmarkNodeData& data, 275 const BookmarkNode* drop_parent, 276 int index) { 277 if (!drop_parent->is_folder()) { 278 NOTREACHED(); 279 return false; 280 } 281 282 if (!data.is_valid()) 283 return false; 284 285 if (data.IsFromProfile(profile)) { 286 std::vector<const BookmarkNode*> nodes = data.GetNodes(profile); 287 for (size_t i = 0; i < nodes.size(); ++i) { 288 // Don't allow the drop if the user is attempting to drop on one of the 289 // nodes being dragged. 290 const BookmarkNode* node = nodes[i]; 291 int node_index = (drop_parent == node->GetParent()) ? 292 drop_parent->IndexOfChild(nodes[i]) : -1; 293 if (node_index != -1 && (index == node_index || index == node_index + 1)) 294 return false; 295 296 // drop_parent can't accept a child that is an ancestor. 297 if (drop_parent->HasAncestor(node)) 298 return false; 299 } 300 return true; 301 } 302 // From the same profile, always accept. 303 return true; 304} 305 306void CloneBookmarkNode(BookmarkModel* model, 307 const std::vector<BookmarkNodeData::Element>& elements, 308 const BookmarkNode* parent, 309 int index_to_add_at) { 310 if (!parent->is_folder() || !model) { 311 NOTREACHED(); 312 return; 313 } 314 for (size_t i = 0; i < elements.size(); ++i) 315 CloneBookmarkNodeImpl(model, elements[i], parent, index_to_add_at + i); 316} 317 318 319// Bookmark dragging 320void DragBookmarks(Profile* profile, 321 const std::vector<const BookmarkNode*>& nodes, 322 gfx::NativeView view) { 323 DCHECK(!nodes.empty()); 324 325#if defined(TOOLKIT_VIEWS) 326 // Set up our OLE machinery 327 OSExchangeData data; 328 BookmarkNodeData drag_data(nodes); 329 drag_data.Write(profile, &data); 330 331 views::RootView* root_view = 332 views::Widget::GetWidgetFromNativeView(view)->GetRootView(); 333 334 // Allow nested message loop so we get DnD events as we drag this around. 335 bool was_nested = MessageLoop::current()->IsNested(); 336 MessageLoop::current()->SetNestableTasksAllowed(true); 337 338 root_view->StartDragForViewFromMouseEvent(NULL, data, 339 DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_MOVE | 340 DragDropTypes::DRAG_LINK); 341 342 MessageLoop::current()->SetNestableTasksAllowed(was_nested); 343#elif defined(OS_MACOSX) 344 // Allow nested message loop so we get DnD events as we drag this around. 345 bool was_nested = MessageLoop::current()->IsNested(); 346 MessageLoop::current()->SetNestableTasksAllowed(true); 347 bookmark_pasteboard_helper_mac::StartDrag(profile, nodes, view); 348 MessageLoop::current()->SetNestableTasksAllowed(was_nested); 349#elif defined(TOOLKIT_GTK) 350 BookmarkDrag::BeginDrag(profile, nodes); 351#endif 352} 353 354void OpenAll(gfx::NativeWindow parent, 355 Profile* profile, 356 PageNavigator* navigator, 357 const std::vector<const BookmarkNode*>& nodes, 358 WindowOpenDisposition initial_disposition) { 359 if (!ShouldOpenAll(parent, nodes)) 360 return; 361 362 NewBrowserPageNavigator navigator_impl(profile); 363 if (!navigator) { 364 Browser* browser = 365 BrowserList::FindBrowserWithType(profile, Browser::TYPE_NORMAL, false); 366 if (!browser || !browser->GetSelectedTabContents()) { 367 navigator = &navigator_impl; 368 } else { 369 if (initial_disposition != NEW_WINDOW && 370 initial_disposition != OFF_THE_RECORD) { 371 browser->window()->Activate(); 372 } 373 navigator = browser->GetSelectedTabContents(); 374 } 375 } 376 377 bool opened_url = false; 378 for (size_t i = 0; i < nodes.size(); ++i) 379 OpenAllImpl(nodes[i], initial_disposition, &navigator, &opened_url); 380} 381 382void OpenAll(gfx::NativeWindow parent, 383 Profile* profile, 384 PageNavigator* navigator, 385 const BookmarkNode* node, 386 WindowOpenDisposition initial_disposition) { 387 std::vector<const BookmarkNode*> nodes; 388 nodes.push_back(node); 389 OpenAll(parent, profile, navigator, nodes, initial_disposition); 390} 391 392void CopyToClipboard(BookmarkModel* model, 393 const std::vector<const BookmarkNode*>& nodes, 394 bool remove_nodes) { 395 if (nodes.empty()) 396 return; 397 398 BookmarkNodeData(nodes).WriteToClipboard(NULL); 399 400 if (remove_nodes) { 401 for (size_t i = 0; i < nodes.size(); ++i) { 402 model->Remove(nodes[i]->GetParent(), 403 nodes[i]->GetParent()->IndexOfChild(nodes[i])); 404 } 405 } 406} 407 408void PasteFromClipboard(BookmarkModel* model, 409 const BookmarkNode* parent, 410 int index) { 411 if (!parent) 412 return; 413 414 BookmarkNodeData bookmark_data; 415 if (!bookmark_data.ReadFromClipboard()) 416 return; 417 418 if (index == -1) 419 index = parent->GetChildCount(); 420 bookmark_utils::CloneBookmarkNode( 421 model, bookmark_data.elements, parent, index); 422} 423 424bool CanPasteFromClipboard(const BookmarkNode* node) { 425 if (!node) 426 return false; 427 return BookmarkNodeData::ClipboardContainsBookmarks(); 428} 429 430std::string GetNameForURL(const GURL& url) { 431 if (url.is_valid()) { 432 return WideToUTF8(net::GetSuggestedFilename( 433 url, std::string(), std::string(), FilePath()).ToWStringHack()); 434 } else { 435 return l10n_util::GetStringUTF8(IDS_APP_UNTITLED_SHORTCUT_FILE_NAME); 436 } 437} 438 439std::vector<const BookmarkNode*> GetMostRecentlyModifiedGroups( 440 BookmarkModel* model, 441 size_t max_count) { 442 std::vector<const BookmarkNode*> nodes; 443 TreeNodeIterator<const BookmarkNode> iterator(model->root_node()); 444 while (iterator.has_next()) { 445 const BookmarkNode* parent = iterator.Next(); 446 if (parent->is_folder() && parent->date_group_modified() > base::Time()) { 447 if (max_count == 0) { 448 nodes.push_back(parent); 449 } else { 450 std::vector<const BookmarkNode*>::iterator i = 451 std::upper_bound(nodes.begin(), nodes.end(), parent, 452 &MoreRecentlyModified); 453 if (nodes.size() < max_count || i != nodes.end()) { 454 nodes.insert(i, parent); 455 while (nodes.size() > max_count) 456 nodes.pop_back(); 457 } 458 } 459 } // else case, the root node, which we don't care about or imported nodes 460 // (which have a time of 0). 461 } 462 463 if (nodes.size() < max_count) { 464 // Add the bookmark bar and other nodes if there is space. 465 if (find(nodes.begin(), nodes.end(), model->GetBookmarkBarNode()) == 466 nodes.end()) { 467 nodes.push_back(model->GetBookmarkBarNode()); 468 } 469 470 if (nodes.size() < max_count && 471 find(nodes.begin(), nodes.end(), model->other_node()) == nodes.end()) { 472 nodes.push_back(model->other_node()); 473 } 474 } 475 return nodes; 476} 477 478void GetMostRecentlyAddedEntries(BookmarkModel* model, 479 size_t count, 480 std::vector<const BookmarkNode*>* nodes) { 481 TreeNodeIterator<const BookmarkNode> iterator(model->root_node()); 482 while (iterator.has_next()) { 483 const BookmarkNode* node = iterator.Next(); 484 if (node->is_url()) { 485 std::vector<const BookmarkNode*>::iterator insert_position = 486 std::upper_bound(nodes->begin(), nodes->end(), node, 487 &MoreRecentlyAdded); 488 if (nodes->size() < count || insert_position != nodes->end()) { 489 nodes->insert(insert_position, node); 490 while (nodes->size() > count) 491 nodes->pop_back(); 492 } 493 } 494 } 495} 496 497bool MoreRecentlyAdded(const BookmarkNode* n1, const BookmarkNode* n2) { 498 return n1->date_added() > n2->date_added(); 499} 500 501void GetBookmarksContainingText(BookmarkModel* model, 502 const string16& text, 503 size_t max_count, 504 const std::string& languages, 505 std::vector<const BookmarkNode*>* nodes) { 506 std::vector<string16> words; 507 QueryParser parser; 508 parser.ExtractQueryWords(l10n_util::ToLower(text), &words); 509 if (words.empty()) 510 return; 511 512 TreeNodeIterator<const BookmarkNode> iterator(model->root_node()); 513 while (iterator.has_next()) { 514 const BookmarkNode* node = iterator.Next(); 515 if (node->is_url() && DoesBookmarkContainWords(node, words, languages)) { 516 nodes->push_back(node); 517 if (nodes->size() == max_count) 518 return; 519 } 520 } 521} 522 523bool DoesBookmarkContainText(const BookmarkNode* node, 524 const string16& text, 525 const std::string& languages) { 526 std::vector<string16> words; 527 QueryParser parser; 528 parser.ExtractQueryWords(l10n_util::ToLower(text), &words); 529 if (words.empty()) 530 return false; 531 532 return (node->is_url() && DoesBookmarkContainWords(node, words, languages)); 533} 534 535static const BookmarkNode* CreateNewNode(BookmarkModel* model, 536 const BookmarkNode* parent, const BookmarkEditor::EditDetails& details, 537 const string16& new_title, const GURL& new_url) { 538 const BookmarkNode* node; 539 if (details.type == BookmarkEditor::EditDetails::NEW_URL) { 540 node = model->AddURL(parent, parent->GetChildCount(), new_title, new_url); 541 } else if (details.type == BookmarkEditor::EditDetails::NEW_FOLDER) { 542 node = model->AddGroup(parent, parent->GetChildCount(), new_title); 543 for (size_t i = 0; i < details.urls.size(); ++i) { 544 model->AddURL(node, node->GetChildCount(), details.urls[i].second, 545 details.urls[i].first); 546 } 547 model->SetDateGroupModified(parent, Time::Now()); 548 } else { 549 NOTREACHED(); 550 return NULL; 551 } 552 553 return node; 554} 555 556const BookmarkNode* ApplyEditsWithNoGroupChange(BookmarkModel* model, 557 const BookmarkNode* parent, const BookmarkEditor::EditDetails& details, 558 const string16& new_title, const GURL& new_url) { 559 if (details.type == BookmarkEditor::EditDetails::NEW_URL || 560 details.type == BookmarkEditor::EditDetails::NEW_FOLDER) { 561 return CreateNewNode(model, parent, details, new_title, new_url); 562 } 563 564 const BookmarkNode* node = details.existing_node; 565 DCHECK(node); 566 567 if (node->is_url()) 568 model->SetURL(node, new_url); 569 model->SetTitle(node, new_title); 570 571 return node; 572} 573 574const BookmarkNode* ApplyEditsWithPossibleGroupChange(BookmarkModel* model, 575 const BookmarkNode* new_parent, const BookmarkEditor::EditDetails& details, 576 const string16& new_title, const GURL& new_url) { 577 if (details.type == BookmarkEditor::EditDetails::NEW_URL || 578 details.type == BookmarkEditor::EditDetails::NEW_FOLDER) { 579 return CreateNewNode(model, new_parent, details, new_title, new_url); 580 } 581 582 const BookmarkNode* node = details.existing_node; 583 DCHECK(node); 584 585 if (new_parent != node->GetParent()) 586 model->Move(node, new_parent, new_parent->GetChildCount()); 587 if (node->is_url()) 588 model->SetURL(node, new_url); 589 model->SetTitle(node, new_title); 590 591 return node; 592} 593 594// Formerly in BookmarkBarView 595void ToggleWhenVisible(Profile* profile) { 596 PrefService* prefs = profile->GetPrefs(); 597 const bool always_show = !prefs->GetBoolean(prefs::kShowBookmarkBar); 598 599 // The user changed when the bookmark bar is shown, update the preferences. 600 prefs->SetBoolean(prefs::kShowBookmarkBar, always_show); 601 prefs->ScheduleSavePersistentPrefs(); 602 603 // And notify the notification service. 604 Source<Profile> source(profile); 605 NotificationService::current()->Notify( 606 NotificationType::BOOKMARK_BAR_VISIBILITY_PREF_CHANGED, 607 source, 608 NotificationService::NoDetails()); 609} 610 611void RegisterUserPrefs(PrefService* prefs) { 612 prefs->RegisterBooleanPref(prefs::kShowBookmarkBar, false); 613} 614 615void GetURLAndTitleToBookmark(TabContents* tab_contents, 616 GURL* url, 617 string16* title) { 618 *url = tab_contents->GetURL(); 619 *title = tab_contents->GetTitle(); 620} 621 622void GetURLsForOpenTabs(Browser* browser, 623 std::vector<std::pair<GURL, string16> >* urls) { 624 for (int i = 0; i < browser->tab_count(); ++i) { 625 std::pair<GURL, string16> entry; 626 GetURLAndTitleToBookmark(browser->GetTabContentsAt(i), &(entry.first), 627 &(entry.second)); 628 urls->push_back(entry); 629 } 630} 631 632const BookmarkNode* GetParentForNewNodes( 633 const BookmarkNode* parent, 634 const std::vector<const BookmarkNode*>& selection, 635 int* index) { 636 const BookmarkNode* real_parent = parent; 637 638 if (selection.size() == 1 && selection[0]->is_folder()) 639 real_parent = selection[0]; 640 641 if (index) { 642 if (selection.size() == 1 && selection[0]->is_url()) { 643 *index = real_parent->IndexOfChild(selection[0]) + 1; 644 if (*index == 0) { 645 // Node doesn't exist in parent, add to end. 646 NOTREACHED(); 647 *index = real_parent->GetChildCount(); 648 } 649 } else { 650 *index = real_parent->GetChildCount(); 651 } 652 } 653 654 return real_parent; 655} 656 657bool NodeHasURLs(const BookmarkNode* node) { 658 DCHECK(node); 659 660 if (node->is_url()) 661 return true; 662 663 for (int i = 0; i < node->GetChildCount(); ++i) { 664 if (NodeHasURLs(node->GetChild(i))) 665 return true; 666 } 667 return false; 668} 669 670} // namespace bookmark_utils 671