Home | History | Annotate | Download | only in layout
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.layout;
     18 
     19 import com.android.ide.eclipse.adt.AdtConstants;
     20 import com.android.ide.eclipse.adt.AdtPlugin;
     21 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     22 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
     23 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     24 import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
     25 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
     26 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
     27 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
     28 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     29 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
     30 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage;
     31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PropertySheetPage;
     32 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
     33 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
     34 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     35 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     36 import com.android.sdklib.IAndroidTarget;
     37 
     38 import org.eclipse.core.resources.IFile;
     39 import org.eclipse.core.resources.IProject;
     40 import org.eclipse.core.runtime.IProgressMonitor;
     41 import org.eclipse.core.runtime.IStatus;
     42 import org.eclipse.core.runtime.NullProgressMonitor;
     43 import org.eclipse.jface.text.source.ISourceViewer;
     44 import org.eclipse.ui.IEditorInput;
     45 import org.eclipse.ui.IEditorPart;
     46 import org.eclipse.ui.IFileEditorInput;
     47 import org.eclipse.ui.IPartListener;
     48 import org.eclipse.ui.IShowEditorInput;
     49 import org.eclipse.ui.IWorkbenchPage;
     50 import org.eclipse.ui.IWorkbenchPart;
     51 import org.eclipse.ui.IWorkbenchPartSite;
     52 import org.eclipse.ui.PartInitException;
     53 import org.eclipse.ui.part.FileEditorInput;
     54 import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
     55 import org.eclipse.ui.views.properties.IPropertySheetPage;
     56 import org.w3c.dom.Document;
     57 import org.w3c.dom.Node;
     58 
     59 import java.util.HashMap;
     60 import java.util.HashSet;
     61 import java.util.Set;
     62 
     63 /**
     64  * Multi-page form editor for /res/layout XML files.
     65  */
     66 public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, IPartListener {
     67 
     68     public static final String ID = AdtConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$
     69 
     70     /** Root node of the UI element hierarchy */
     71     private UiDocumentNode mUiRootNode;
     72 
     73     private GraphicalEditorPart mGraphicalEditor;
     74     private int mGraphicalEditorIndex;
     75     /** Implementation of the {@link IContentOutlinePage} for this editor */
     76     private IContentOutlinePage mOutline;
     77     /** Custom implementation of {@link IPropertySheetPage} for this editor */
     78     private IPropertySheetPage mPropertyPage;
     79 
     80     private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap =
     81         new HashMap<String, ElementDescriptor>();
     82 
     83 
     84     /**
     85      * Flag indicating if the replacement file is due to a config change.
     86      * If false, it means the new file is due to an "open action" from the user.
     87      */
     88     private boolean mNewFileOnConfigChange = false;
     89 
     90     /**
     91      * Creates the form editor for resources XML files.
     92      */
     93     public LayoutEditor() {
     94         super(false /* addTargetListener */);
     95     }
     96 
     97     /**
     98      * Returns the {@link RulesEngine} associated with this editor
     99      *
    100      * @return the {@link RulesEngine} associated with this editor.
    101      */
    102     public RulesEngine getRulesEngine() {
    103         return mGraphicalEditor.getRulesEngine();
    104     }
    105 
    106     /**
    107      * Returns the {@link GraphicalEditorPart} associated with this editor
    108      *
    109      * @return the {@link GraphicalEditorPart} associated with this editor
    110      */
    111     public GraphicalEditorPart getGraphicalEditor() {
    112         return mGraphicalEditor;
    113     }
    114 
    115     /**
    116      * @return The root node of the UI element hierarchy
    117      */
    118     @Override
    119     public UiDocumentNode getUiRootNode() {
    120         return mUiRootNode;
    121     }
    122 
    123     public void setNewFileOnConfigChange(boolean state) {
    124         mNewFileOnConfigChange = state;
    125     }
    126 
    127     // ---- Base Class Overrides ----
    128 
    129     @Override
    130     public void dispose() {
    131         getSite().getPage().removePartListener(this);
    132 
    133         super.dispose();
    134     }
    135 
    136     /**
    137      * Save the XML.
    138      * <p/>
    139      * The actual save operation is done in the super class by committing
    140      * all data to the XML model and then having the Structured XML Editor
    141      * save the XML.
    142      * <p/>
    143      * Here we just need to tell the graphical editor that the model has
    144      * been saved.
    145      */
    146     @Override
    147     public void doSave(IProgressMonitor monitor) {
    148         super.doSave(monitor);
    149         if (mGraphicalEditor != null) {
    150             mGraphicalEditor.doSave(monitor);
    151         }
    152     }
    153 
    154     /**
    155      * Returns whether the "save as" operation is supported by this editor.
    156      * <p/>
    157      * Save-As is a valid operation for the ManifestEditor since it acts on a
    158      * single source file.
    159      *
    160      * @see IEditorPart
    161      */
    162     @Override
    163     public boolean isSaveAsAllowed() {
    164         return true;
    165     }
    166 
    167     /**
    168      * Create the various form pages.
    169      */
    170     @Override
    171     protected void createFormPages() {
    172         try {
    173             // get the file being edited so that it can be passed to the layout editor.
    174             IFile editedFile = null;
    175             IEditorInput input = getEditorInput();
    176             if (input instanceof FileEditorInput) {
    177                 FileEditorInput fileInput = (FileEditorInput)input;
    178                 editedFile = fileInput.getFile();
    179             } else {
    180                 AdtPlugin.log(IStatus.ERROR,
    181                         "Input is not of type FileEditorInput: %1$s",  //$NON-NLS-1$
    182                         input.toString());
    183             }
    184 
    185             // It is possible that the Layout Editor already exits if a different version
    186             // of the same layout is being opened (either through "open" action from
    187             // the user, or through a configuration change in the configuration selector.)
    188             if (mGraphicalEditor == null) {
    189 
    190                 // Instantiate GLE v2
    191                 mGraphicalEditor = new GraphicalEditorPart(this);
    192 
    193                 mGraphicalEditorIndex = addPage(mGraphicalEditor, getEditorInput());
    194                 setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle());
    195 
    196                 mGraphicalEditor.openFile(editedFile);
    197             } else {
    198                 if (mNewFileOnConfigChange) {
    199                     mGraphicalEditor.changeFileOnNewConfig(editedFile);
    200                     mNewFileOnConfigChange = false;
    201                 } else {
    202                     mGraphicalEditor.replaceFile(editedFile);
    203                 }
    204             }
    205 
    206             // put in place the listener to handle layout recompute only when needed.
    207             getSite().getPage().addPartListener(this);
    208         } catch (PartInitException e) {
    209             AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
    210         }
    211     }
    212 
    213     @Override
    214     protected void postCreatePages() {
    215         super.postCreatePages();
    216 
    217         // Optional: set the default page. Eventually a default page might be
    218         // restored by selectDefaultPage() later based on the last page used by the user.
    219         // For example, to make the last page the default one (rather than the first page),
    220         // uncomment this line:
    221         //   setActivePage(getPageCount() - 1);
    222     }
    223 
    224     /* (non-java doc)
    225      * Change the tab/title name to include the name of the layout.
    226      */
    227     @Override
    228     protected void setInput(IEditorInput input) {
    229         super.setInput(input);
    230         handleNewInput(input);
    231     }
    232 
    233     /*
    234      * (non-Javadoc)
    235      * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput)
    236      */
    237     @Override
    238     protected void setInputWithNotify(IEditorInput input) {
    239         super.setInputWithNotify(input);
    240         handleNewInput(input);
    241     }
    242 
    243     /**
    244      * Called to replace the current {@link IEditorInput} with another one.
    245      * <p/>This is used when {@link MatchingStrategy} returned <code>true</code> which means we're
    246      * opening a different configuration of the same layout.
    247      */
    248     public void showEditorInput(IEditorInput editorInput) {
    249         if (getEditorInput().equals(editorInput)) {
    250             return;
    251         }
    252 
    253         // save the current editor input.
    254         doSave(new NullProgressMonitor());
    255 
    256         // get the current page
    257         int currentPage = getActivePage();
    258 
    259         // remove the pages, except for the graphical editor, which will be dynamically adapted
    260         // to the new model.
    261         // page after the graphical editor:
    262         int count = getPageCount();
    263         for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) {
    264             removePage(i);
    265         }
    266         // pages before the graphical editor
    267         for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) {
    268             removePage(i);
    269         }
    270 
    271         // set the current input.
    272         setInputWithNotify(editorInput);
    273 
    274         // re-create or reload the pages with the default page shown as the previous active page.
    275         createAndroidPages();
    276         selectDefaultPage(Integer.toString(currentPage));
    277 
    278         // When changing an input file of an the editor, the titlebar is not refreshed to
    279         // show the new path/to/file being edited. So we force a refresh
    280         firePropertyChange(IWorkbenchPart.PROP_TITLE);
    281     }
    282 
    283     /** Performs a complete refresh of the XML model */
    284     public void refreshXmlModel() {
    285         Document xmlDoc = mUiRootNode.getXmlDocument();
    286 
    287         initUiRootNode(true /*force*/);
    288         mUiRootNode.loadFromXmlNode(xmlDoc);
    289         // update the model first, since it is used by the viewers.
    290         super.xmlModelChanged(xmlDoc);
    291 
    292         if (mGraphicalEditor != null) {
    293             mGraphicalEditor.onXmlModelChanged();
    294         }
    295     }
    296 
    297     /**
    298      * Processes the new XML Model, which XML root node is given.
    299      *
    300      * @param xml_doc The XML document, if available, or null if none exists.
    301      */
    302     @Override
    303     protected void xmlModelChanged(Document xml_doc) {
    304         if (mIgnoreXmlUpdate) {
    305             return;
    306         }
    307 
    308         // init the ui root on demand
    309         initUiRootNode(false /*force*/);
    310 
    311         mUiRootNode.loadFromXmlNode(xml_doc);
    312 
    313         // update the model first, since it is used by the viewers.
    314         super.xmlModelChanged(xml_doc);
    315 
    316         if (mGraphicalEditor != null) {
    317             mGraphicalEditor.onXmlModelChanged();
    318         }
    319     }
    320 
    321     /**
    322      * Tells the graphical editor to recompute its layout.
    323      */
    324     public void recomputeLayout() {
    325         mGraphicalEditor.recomputeLayout();
    326     }
    327 
    328     @Override
    329     public boolean supportsFormatOnGuiEdit() {
    330         return true;
    331     }
    332 
    333     /**
    334      * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
    335      */
    336     @SuppressWarnings("unchecked")
    337     @Override
    338     public Object getAdapter(Class adapter) {
    339         // For the outline, force it to come from the Graphical Editor.
    340         // This fixes the case where a layout file is opened in XML view first and the outline
    341         // gets stuck in the XML outline.
    342         if (IContentOutlinePage.class == adapter && mGraphicalEditor != null) {
    343 
    344             if (mOutline == null && mGraphicalEditor != null) {
    345                 mOutline = new OutlinePage(mGraphicalEditor);
    346             }
    347 
    348             return mOutline;
    349         }
    350 
    351         if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) {
    352             if (mPropertyPage == null) {
    353                 mPropertyPage = new PropertySheetPage();
    354             }
    355 
    356             return mPropertyPage;
    357         }
    358 
    359         // return default
    360         return super.getAdapter(adapter);
    361     }
    362 
    363     @Override
    364     protected void pageChange(int newPageIndex) {
    365         if (getCurrentPage() == mTextPageIndex &&
    366                 newPageIndex == mGraphicalEditorIndex) {
    367             // You're switching from the XML editor to the WYSIWYG editor;
    368             // look at the caret position and figure out which node it corresponds to
    369             // (if any) and if found, select the corresponding visual element.
    370             ISourceViewer textViewer = getStructuredSourceViewer();
    371             int caretOffset = textViewer.getTextWidget().getCaretOffset();
    372             if (caretOffset >= 0) {
    373                 Node node = DomUtilities.getNode(textViewer.getDocument(), caretOffset);
    374                 if (node != null && mGraphicalEditor != null) {
    375                     mGraphicalEditor.select(node);
    376                 }
    377             }
    378         }
    379 
    380         super.pageChange(newPageIndex);
    381 
    382         if (mGraphicalEditor != null) {
    383             if (newPageIndex == mGraphicalEditorIndex) {
    384                 mGraphicalEditor.activated();
    385             } else {
    386                 mGraphicalEditor.deactivated();
    387             }
    388         }
    389     }
    390 
    391     // ----- IPartListener Methods ----
    392 
    393     public void partActivated(IWorkbenchPart part) {
    394         if (part == this) {
    395             if (mGraphicalEditor != null) {
    396                 if (getActivePage() == mGraphicalEditorIndex) {
    397                     mGraphicalEditor.activated();
    398                 } else {
    399                     mGraphicalEditor.deactivated();
    400                 }
    401             }
    402         }
    403     }
    404 
    405     public void partBroughtToTop(IWorkbenchPart part) {
    406         partActivated(part);
    407     }
    408 
    409     public void partClosed(IWorkbenchPart part) {
    410         // pass
    411     }
    412 
    413     public void partDeactivated(IWorkbenchPart part) {
    414         if (part == this) {
    415             if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) {
    416                 mGraphicalEditor.deactivated();
    417             }
    418         }
    419     }
    420 
    421     public void partOpened(IWorkbenchPart part) {
    422         /*
    423          * We used to automatically bring the outline and the property sheet to view
    424          * when opening the editor. This behavior has always been a mixed bag and not
    425          * exactly satisfactory. GLE1 is being useless/deprecated and GLE2 will need to
    426          * improve on that, so right now let's comment this out.
    427          */
    428         //EclipseUiHelper.showView(EclipseUiHelper.CONTENT_OUTLINE_VIEW_ID, false /* activate */);
    429         //EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, false /* activate */);
    430     }
    431 
    432     // ---- Local Methods ----
    433 
    434     /**
    435      * Returns true if the Graphics editor page is visible. This <b>must</b> be
    436      * called from the UI thread.
    437      */
    438     public boolean isGraphicalEditorActive() {
    439         IWorkbenchPartSite workbenchSite = getSite();
    440         IWorkbenchPage workbenchPage = workbenchSite.getPage();
    441 
    442         // check if the editor is visible in the workbench page
    443         if (workbenchPage.isPartVisible(this) && workbenchPage.getActiveEditor() == this) {
    444             // and then if the page of the editor is visible (not to be confused with
    445             // the workbench page)
    446             return mGraphicalEditorIndex == getActivePage();
    447         }
    448 
    449         return false;
    450     }
    451 
    452     @Override
    453     public void initUiRootNode(boolean force) {
    454         // The root UI node is always created, even if there's no corresponding XML node.
    455         if (mUiRootNode == null || force) {
    456             // get the target data from the opened file (and its project)
    457             AndroidTargetData data = getTargetData();
    458 
    459             Document doc = null;
    460             if (mUiRootNode != null) {
    461                 doc = mUiRootNode.getXmlDocument();
    462             }
    463 
    464             DocumentDescriptor desc;
    465             if (data == null) {
    466                 desc = new DocumentDescriptor("temp", null /*children*/);
    467             } else {
    468                 desc = data.getLayoutDescriptors().getDescriptor();
    469             }
    470 
    471             // get the descriptors from the data.
    472             mUiRootNode = (UiDocumentNode) desc.createUiNode();
    473             mUiRootNode.setEditor(this);
    474 
    475             mUiRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() {
    476 
    477                 public ElementDescriptor getDescriptor(String xmlLocalName) {
    478 
    479                     ElementDescriptor desc = mUnknownDescriptorMap.get(xmlLocalName);
    480 
    481                     if (desc == null) {
    482                         desc = createUnknownDescriptor(xmlLocalName);
    483                         mUnknownDescriptorMap.put(xmlLocalName, desc);
    484                     }
    485 
    486                     return desc;
    487                 }
    488             });
    489 
    490             onDescriptorsChanged(doc);
    491         }
    492     }
    493 
    494     /**
    495      * Creates a new {@link ViewElementDescriptor} for an unknown XML local name
    496      * (i.e. one that was not mapped by the current descriptors).
    497      * <p/>
    498      * Since we deal with layouts, we returns either a descriptor for a custom view
    499      * or one for the base View.
    500      *
    501      * @param xmlLocalName The XML local name to match.
    502      * @return A non-null {@link ViewElementDescriptor}.
    503      */
    504     private ViewElementDescriptor createUnknownDescriptor(String xmlLocalName) {
    505         ViewElementDescriptor desc = null;
    506         IEditorInput editorInput = getEditorInput();
    507         if (editorInput instanceof IFileEditorInput) {
    508             IFileEditorInput fileInput = (IFileEditorInput)editorInput;
    509             IProject project = fileInput.getFile().getProject();
    510 
    511             // Check if we can find a custom view specific to this project.
    512             // This only works if there's an actual matching custom class in the project.
    513             desc = CustomViewDescriptorService.getInstance().getDescriptor(project, xmlLocalName);
    514 
    515             if (desc == null) {
    516                 // If we didn't find a custom view, create a synthetic one using the
    517                 // the base View descriptor as a model.
    518                 // This is a layout after all, so every XML node should represent
    519                 // a view.
    520 
    521                 Sdk currentSdk = Sdk.getCurrent();
    522                 if (currentSdk != null) {
    523                     IAndroidTarget target = currentSdk.getTarget(project);
    524                     if (target != null) {
    525                         AndroidTargetData data = currentSdk.getTargetData(target);
    526                         if (data != null) {
    527                             // data can be null when the target is still loading
    528                             ViewElementDescriptor viewDesc =
    529                                 data.getLayoutDescriptors().getBaseViewDescriptor();
    530 
    531                             desc = new ViewElementDescriptor(
    532                                     xmlLocalName, // xml local name
    533                                     xmlLocalName, // ui_name
    534                                     xmlLocalName, // canonical class name
    535                                     null, // tooltip
    536                                     null, // sdk_url
    537                                     viewDesc.getAttributes(),
    538                                     viewDesc.getLayoutAttributes(),
    539                                     null, // children
    540                                     false /* mandatory */);
    541                             desc.setSuperClass(viewDesc);
    542                         }
    543                     }
    544                 }
    545             }
    546         }
    547 
    548         if (desc == null) {
    549             // We can only arrive here if the SDK's android target has not finished
    550             // loading. Just create a dummy descriptor with no attributes to be able
    551             // to continue.
    552             desc = new ViewElementDescriptor(xmlLocalName, xmlLocalName);
    553         }
    554         return desc;
    555     }
    556 
    557     private void onDescriptorsChanged(Document document) {
    558 
    559         mUnknownDescriptorMap.clear();
    560 
    561         if (document != null) {
    562             mUiRootNode.loadFromXmlNode(document);
    563         } else {
    564             mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlDocument());
    565         }
    566 
    567         if (mGraphicalEditor != null) {
    568             mGraphicalEditor.onTargetChange();
    569             mGraphicalEditor.reloadPalette();
    570         }
    571     }
    572 
    573     /**
    574      * Handles a new input, and update the part name.
    575      * @param input the new input.
    576      */
    577     private void handleNewInput(IEditorInput input) {
    578         if (input instanceof FileEditorInput) {
    579             FileEditorInput fileInput = (FileEditorInput) input;
    580             IFile file = fileInput.getFile();
    581             setPartName(String.format("%1$s",
    582                     file.getName()));
    583         }
    584     }
    585 
    586     /**
    587      * Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN.
    588      * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info.
    589      */
    590     public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
    591         ViewElementDescriptor desc = null;
    592 
    593         AndroidTargetData data = getTargetData();
    594         if (data != null) {
    595             LayoutDescriptors layoutDesc = data.getLayoutDescriptors();
    596             if (layoutDesc != null) {
    597                 DocumentDescriptor docDesc = layoutDesc.getDescriptor();
    598                 if (docDesc != null) {
    599                     desc = internalFindFqcnViewDescriptor(fqcn, docDesc.getChildren(), null);
    600                 }
    601             }
    602         }
    603 
    604         if (desc == null) {
    605             // We failed to find a descriptor for the given FQCN.
    606             // Let's consider custom classes and create one as needed.
    607             desc = createUnknownDescriptor(fqcn);
    608         }
    609 
    610         return desc;
    611     }
    612 
    613     /**
    614      * Internal helper to recursively search for a {@link ViewElementDescriptor} that matches
    615      * the requested FQCN.
    616      *
    617      * @param fqcn The target View FQCN to find.
    618      * @param descriptors A list of children descriptors to iterate through.
    619      * @param visited A set we use to remember which descriptors have already been visited,
    620      *  necessary since the view descriptor hierarchy is cyclic.
    621      * @return Either a matching {@link ViewElementDescriptor} or null.
    622      */
    623     private ViewElementDescriptor internalFindFqcnViewDescriptor(String fqcn,
    624             ElementDescriptor[] descriptors,
    625             Set<ElementDescriptor> visited) {
    626         if (visited == null) {
    627             visited = new HashSet<ElementDescriptor>();
    628         }
    629 
    630         if (descriptors != null) {
    631             for (ElementDescriptor desc : descriptors) {
    632                 if (visited.add(desc)) {
    633                     // Set.add() returns true if this a new element that was added to the set.
    634                     // That means we haven't visited this descriptor yet.
    635                     // We want a ViewElementDescriptor with a matching FQCN.
    636                     if (desc instanceof ViewElementDescriptor &&
    637                             fqcn.equals(((ViewElementDescriptor) desc).getFullClassName())) {
    638                         return (ViewElementDescriptor) desc;
    639                     }
    640 
    641                     // Visit its children
    642                     ViewElementDescriptor vd =
    643                         internalFindFqcnViewDescriptor(fqcn, desc.getChildren(), visited);
    644                     if (vd != null) {
    645                         return vd;
    646                     }
    647                 }
    648             }
    649         }
    650 
    651         return null;
    652     }
    653 }
    654