Home | History | Annotate | Download | only in bookmarks
      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