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