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.annotations.NonNull;
     20 import com.android.annotations.Nullable;
     21 import com.android.annotations.VisibleForTesting;
     22 import com.android.annotations.VisibleForTesting.Visibility;
     23 import com.android.ide.eclipse.adt.AdtConstants;
     24 import com.android.ide.eclipse.adt.AdtPlugin;
     25 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     26 import com.android.ide.eclipse.adt.internal.editors.XmlEditorMultiOutline;
     27 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
     28 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
     29 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
     30 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     31 import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
     32 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
     33 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
     34 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
     35 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     36 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
     37 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutActionBar;
     38 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
     39 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage;
     40 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager;
     41 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
     42 import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
     43 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     44 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
     45 import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
     46 import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner;
     47 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     48 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     49 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     50 import com.android.resources.ResourceFolderType;
     51 import com.android.sdklib.IAndroidTarget;
     52 import com.android.tools.lint.client.api.IssueRegistry;
     53 
     54 import org.eclipse.core.resources.IContainer;
     55 import org.eclipse.core.resources.IFile;
     56 import org.eclipse.core.resources.IMarker;
     57 import org.eclipse.core.resources.IProject;
     58 import org.eclipse.core.runtime.IProgressMonitor;
     59 import org.eclipse.core.runtime.IStatus;
     60 import org.eclipse.core.runtime.NullProgressMonitor;
     61 import org.eclipse.core.runtime.jobs.IJobChangeEvent;
     62 import org.eclipse.core.runtime.jobs.Job;
     63 import org.eclipse.core.runtime.jobs.JobChangeAdapter;
     64 import org.eclipse.jface.text.source.ISourceViewer;
     65 import org.eclipse.jface.viewers.ISelection;
     66 import org.eclipse.jface.viewers.ISelectionChangedListener;
     67 import org.eclipse.jface.viewers.SelectionChangedEvent;
     68 import org.eclipse.ui.IActionBars;
     69 import org.eclipse.ui.IEditorInput;
     70 import org.eclipse.ui.IEditorPart;
     71 import org.eclipse.ui.IFileEditorInput;
     72 import org.eclipse.ui.ISelectionListener;
     73 import org.eclipse.ui.ISelectionService;
     74 import org.eclipse.ui.IShowEditorInput;
     75 import org.eclipse.ui.IWorkbenchPage;
     76 import org.eclipse.ui.IWorkbenchPart;
     77 import org.eclipse.ui.IWorkbenchPartSite;
     78 import org.eclipse.ui.IWorkbenchWindow;
     79 import org.eclipse.ui.PartInitException;
     80 import org.eclipse.ui.forms.editor.IFormPage;
     81 import org.eclipse.ui.part.FileEditorInput;
     82 import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
     83 import org.eclipse.ui.views.properties.IPropertySheetPage;
     84 import org.eclipse.wst.sse.ui.StructuredTextEditor;
     85 import org.w3c.dom.Document;
     86 import org.w3c.dom.Node;
     87 
     88 import java.io.File;
     89 import java.util.Collection;
     90 import java.util.Collections;
     91 import java.util.HashMap;
     92 import java.util.HashSet;
     93 import java.util.List;
     94 import java.util.Set;
     95 
     96 /**
     97  * Multi-page form editor for /res/layout XML files.
     98  */
     99 public class LayoutEditorDelegate extends CommonXmlDelegate
    100          implements IShowEditorInput, CommonXmlDelegate.IActionContributorDelegate {
    101 
    102     /** The prefix for layout folders that are not the default layout folder */
    103     private static final String LAYOUT_FOLDER_PREFIX = "layout-"; //$NON-NLS-1$
    104 
    105     public static class Creator implements IDelegateCreator {
    106         @Override
    107         @SuppressWarnings("unchecked")
    108         public LayoutEditorDelegate createForFile(
    109                 @NonNull CommonXmlEditor delegator,
    110                 @Nullable ResourceFolderType type) {
    111             if (ResourceFolderType.LAYOUT == type) {
    112                 return new LayoutEditorDelegate(delegator);
    113             }
    114 
    115             return null;
    116         }
    117     }
    118 
    119     /**
    120      * Old standalone-editor ID.
    121      * Use {@link CommonXmlEditor#ID} instead.
    122      */
    123     public static final String LEGACY_EDITOR_ID =
    124         AdtConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$
    125 
    126     /** Root node of the UI element hierarchy */
    127     private UiDocumentNode mUiDocRootNode;
    128 
    129     private GraphicalEditorPart mGraphicalEditor;
    130     private int mGraphicalEditorIndex;
    131 
    132     /** Implementation of the {@link IContentOutlinePage} for this editor */
    133     private OutlinePage mLayoutOutline;
    134 
    135     /** The XML editor outline */
    136     private IContentOutlinePage mEditorOutline;
    137 
    138     /** Multiplexing outline, used for multi-page editors that have their own outline */
    139     private XmlEditorMultiOutline mMultiOutline;
    140 
    141     /**
    142      * Temporary flag set by the editor caret listener which is used to cause
    143      * the next getAdapter(IContentOutlinePage.class) call to return the editor
    144      * outline rather than the multi-outline. See the {@link #delegateGetAdapter}
    145      * method for details.
    146      */
    147     private boolean mCheckOutlineAdapter;
    148 
    149     /** Custom implementation of {@link IPropertySheetPage} for this editor */
    150     private IPropertySheetPage mPropertyPage;
    151 
    152     private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap =
    153         new HashMap<String, ElementDescriptor>();
    154 
    155     private EclipseLintClient mClient;
    156 
    157     /**
    158      * Flag indicating if the replacement file is due to a config change.
    159      * If false, it means the new file is due to an "open action" from the user.
    160      */
    161     private boolean mNewFileOnConfigChange = false;
    162 
    163     /**
    164      * Checks whether an editor part is an instance of {@link CommonXmlEditor}
    165      * with an associated {@link LayoutEditorDelegate} delegate.
    166      *
    167      * @param editorPart An editor part. Can be null.
    168      * @return The {@link LayoutEditorDelegate} delegate associated with the editor or null.
    169      */
    170     public static @Nullable LayoutEditorDelegate fromEditor(@Nullable IEditorPart editorPart) {
    171         if (editorPart instanceof CommonXmlEditor) {
    172             CommonXmlDelegate delegate = ((CommonXmlEditor) editorPart).getDelegate();
    173             if (delegate instanceof LayoutEditorDelegate) {
    174                 return ((LayoutEditorDelegate) delegate);
    175             }
    176         } else if (editorPart instanceof GraphicalEditorPart) {
    177             GraphicalEditorPart part = (GraphicalEditorPart) editorPart;
    178             return part.getEditorDelegate();
    179         }
    180         return null;
    181     }
    182 
    183     /**
    184      * Creates the form editor for resources XML files.
    185      */
    186     @VisibleForTesting(visibility=Visibility.PRIVATE)
    187     protected LayoutEditorDelegate(CommonXmlEditor editor) {
    188         super(editor, new LayoutContentAssist());
    189         // Note that LayoutEditor has its own listeners and does not
    190         // need to call editor.addDefaultTargetListener().
    191     }
    192 
    193     /**
    194      * Returns the {@link RulesEngine} associated with this editor
    195      *
    196      * @return the {@link RulesEngine} associated with this editor.
    197      */
    198     public RulesEngine getRulesEngine() {
    199         return mGraphicalEditor.getRulesEngine();
    200     }
    201 
    202     /**
    203      * Returns the {@link GraphicalEditorPart} associated with this editor
    204      *
    205      * @return the {@link GraphicalEditorPart} associated with this editor
    206      */
    207     public GraphicalEditorPart getGraphicalEditor() {
    208         return mGraphicalEditor;
    209     }
    210 
    211     /**
    212      * @return The root node of the UI element hierarchy
    213      */
    214     @Override
    215     public UiDocumentNode getUiRootNode() {
    216         return mUiDocRootNode;
    217     }
    218 
    219     public void setNewFileOnConfigChange(boolean state) {
    220         mNewFileOnConfigChange = state;
    221     }
    222 
    223     // ---- Base Class Overrides ----
    224 
    225     @Override
    226     public void dispose() {
    227         super.dispose();
    228         if (mGraphicalEditor != null) {
    229             mGraphicalEditor.dispose();
    230             mGraphicalEditor = null;
    231         }
    232     }
    233 
    234     /**
    235      * Save the XML.
    236      * <p/>
    237      * Clients must NOT call this directly. Instead they should always
    238      * call {@link CommonXmlEditor#doSave(IProgressMonitor)} so that th
    239      * editor super class can commit the data properly.
    240      * <p/>
    241      * Here we just need to tell the graphical editor that the model has
    242      * been saved.
    243      */
    244     @Override
    245     public void delegateDoSave(IProgressMonitor monitor) {
    246         super.delegateDoSave(monitor);
    247         if (mGraphicalEditor != null) {
    248             mGraphicalEditor.doSave(monitor);
    249         }
    250     }
    251 
    252     /**
    253      * Create the various form pages.
    254      */
    255     @Override
    256     public void delegateCreateFormPages() {
    257         try {
    258             // get the file being edited so that it can be passed to the layout editor.
    259             IFile editedFile = null;
    260             IEditorInput input = getEditor().getEditorInput();
    261             if (input instanceof FileEditorInput) {
    262                 FileEditorInput fileInput = (FileEditorInput)input;
    263                 editedFile = fileInput.getFile();
    264                 if (!editedFile.isAccessible()) {
    265                     return;
    266                 }
    267             } else {
    268                 AdtPlugin.log(IStatus.ERROR,
    269                         "Input is not of type FileEditorInput: %1$s",  //$NON-NLS-1$
    270                         input.toString());
    271             }
    272 
    273             // It is possible that the Layout Editor already exits if a different version
    274             // of the same layout is being opened (either through "open" action from
    275             // the user, or through a configuration change in the configuration selector.)
    276             if (mGraphicalEditor == null) {
    277 
    278                 // Instantiate GLE v2
    279                 mGraphicalEditor = new GraphicalEditorPart(this);
    280 
    281                 mGraphicalEditorIndex = getEditor().addPage(mGraphicalEditor,
    282                                                             getEditor().getEditorInput());
    283                 getEditor().setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle());
    284 
    285                 mGraphicalEditor.openFile(editedFile);
    286             } else {
    287                 if (mNewFileOnConfigChange) {
    288                     mGraphicalEditor.changeFileOnNewConfig(editedFile);
    289                     mNewFileOnConfigChange = false;
    290                 } else {
    291                     mGraphicalEditor.replaceFile(editedFile);
    292                 }
    293             }
    294         } catch (PartInitException e) {
    295             AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
    296         }
    297     }
    298 
    299     @Override
    300     public void delegatePostCreatePages() {
    301         // Optional: set the default page. Eventually a default page might be
    302         // restored by selectDefaultPage() later based on the last page used by the user.
    303         // For example, to make the last page the default one (rather than the first page),
    304         // uncomment this line:
    305         //   setActivePage(getPageCount() - 1);
    306     }
    307 
    308     /* (non-java doc)
    309      * Change the tab/title name to include the name of the layout.
    310      */
    311     @Override
    312     public void delegateSetInput(IEditorInput input) {
    313         handleNewInput(input);
    314     }
    315 
    316     /*
    317      * (non-Javadoc)
    318      * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput)
    319      */
    320     public void delegateSetInputWithNotify(IEditorInput input) {
    321         handleNewInput(input);
    322     }
    323 
    324     /**
    325      * Called to replace the current {@link IEditorInput} with another one.
    326      * <p/>
    327      * This is used when {@link LayoutEditorMatchingStrategy} returned
    328      * <code>true</code> which means we're opening a different configuration of
    329      * the same layout.
    330      */
    331     @Override
    332     public void showEditorInput(IEditorInput editorInput) {
    333         if (getEditor().getEditorInput().equals(editorInput)) {
    334             return;
    335         }
    336 
    337         // Save the current editor input. This must be called on the editor itself
    338         // since it's the base editor that commits pending changes.
    339         getEditor().doSave(new NullProgressMonitor());
    340 
    341         // Get the current page
    342         int currentPage = getEditor().getActivePage();
    343 
    344         // Remove the pages, except for the graphical editor, which will be dynamically adapted
    345         // to the new model.
    346         // page after the graphical editor:
    347         int count = getEditor().getPageCount();
    348         for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) {
    349             getEditor().removePage(i);
    350         }
    351         // Pages before the graphical editor
    352         for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) {
    353             getEditor().removePage(i);
    354         }
    355 
    356         // Set the current input. We're in the delegate, the input must
    357         // be set into the actual editor instance.
    358         getEditor().setInputWithNotify(editorInput);
    359 
    360         // Re-create or reload the pages with the default page shown as the previous active page.
    361         getEditor().createAndroidPages();
    362         getEditor().selectDefaultPage(Integer.toString(currentPage));
    363 
    364         // When changing an input file of an the editor, the titlebar is not refreshed to
    365         // show the new path/to/file being edited. So we force a refresh
    366         getEditor().firePropertyChange(IWorkbenchPart.PROP_TITLE);
    367     }
    368 
    369     /** Performs a complete refresh of the XML model */
    370     public void refreshXmlModel() {
    371         Document xmlDoc = mUiDocRootNode.getXmlDocument();
    372 
    373         delegateInitUiRootNode(true /*force*/);
    374         mUiDocRootNode.loadFromXmlNode(xmlDoc);
    375 
    376         // Update the model first, since it is used by the viewers.
    377         // No need to call AndroidXmlEditor.xmlModelChanged(xmlDoc) since it's
    378         // a no-op. Instead call onXmlModelChanged on the graphical editor.
    379 
    380         if (mGraphicalEditor != null) {
    381             mGraphicalEditor.onXmlModelChanged();
    382         }
    383     }
    384 
    385     /**
    386      * Processes the new XML Model, which XML root node is given.
    387      *
    388      * @param xml_doc The XML document, if available, or null if none exists.
    389      */
    390     @Override
    391     public void delegateXmlModelChanged(Document xml_doc) {
    392         // init the ui root on demand
    393         delegateInitUiRootNode(false /*force*/);
    394 
    395         mUiDocRootNode.loadFromXmlNode(xml_doc);
    396 
    397         // Update the model first, since it is used by the viewers.
    398         // No need to call AndroidXmlEditor.xmlModelChanged(xmlDoc) since it's
    399         // a no-op. Instead call onXmlModelChanged on the graphical editor.
    400 
    401         if (mGraphicalEditor != null) {
    402             mGraphicalEditor.onXmlModelChanged();
    403         }
    404     }
    405 
    406     /**
    407      * Tells the graphical editor to recompute its layout.
    408      */
    409     public void recomputeLayout() {
    410         mGraphicalEditor.recomputeLayout();
    411     }
    412 
    413     /**
    414      * Does this editor participate in the "format GUI editor changes" option?
    415      *
    416      * @return true since this editor supports automatically formatting XML
    417      *         affected by GUI changes
    418      */
    419     @Override
    420     public boolean delegateSupportsFormatOnGuiEdit() {
    421         return true;
    422     }
    423 
    424     /**
    425      * Returns one of the issues for the given node (there could be more than one)
    426      *
    427      * @param node the node to look up lint issues for
    428      * @return the marker for one of the issues found for the given node
    429      */
    430     @Nullable
    431     public IMarker getIssueForNode(@Nullable UiViewElementNode node) {
    432         if (node == null) {
    433             return null;
    434         }
    435 
    436         if (mClient != null) {
    437             return mClient.getIssueForNode(node);
    438         }
    439 
    440         return null;
    441     }
    442 
    443     /**
    444      * Returns a collection of nodes that have one or more lint warnings
    445      * associated with them (retrievable via
    446      * {@link #getIssueForNode(UiViewElementNode)})
    447      *
    448      * @return a collection of nodes, which should <b>not</b> be modified by the
    449      *         caller
    450      */
    451     @Nullable
    452     public Collection<Node> getLintNodes() {
    453         if (mClient != null) {
    454             return mClient.getIssueNodes();
    455         }
    456 
    457         return null;
    458     }
    459 
    460     @Override
    461     public Job delegateRunLint() {
    462         // We want to customize the {@link EclipseLintClient} created to run this
    463         // single file lint, in particular such that we can set the mode which collects
    464         // nodes on that lint job, such that we can quickly look up error nodes
    465         //Job job = super.delegateRunLint();
    466 
    467         Job job = null;
    468         IFile file = getEditor().getInputFile();
    469         if (file != null) {
    470             IssueRegistry registry = EclipseLintClient.getRegistry();
    471             List<IFile> resources = Collections.singletonList(file);
    472             mClient = new EclipseLintClient(registry,
    473                     resources, getEditor().getStructuredDocument(), false /*fatal*/);
    474 
    475             mClient.setCollectNodes(true);
    476 
    477             job = EclipseLintRunner.startLint(mClient, resources, file,
    478                     false /*show*/);
    479         }
    480 
    481         if (job != null) {
    482             GraphicalEditorPart graphicalEditor = getGraphicalEditor();
    483             if (graphicalEditor != null) {
    484                 job.addJobChangeListener(new LintJobListener(graphicalEditor));
    485             }
    486         }
    487         return job;
    488     }
    489 
    490     private class LintJobListener extends JobChangeAdapter implements Runnable {
    491         private final GraphicalEditorPart mEditor;
    492         private final LayoutCanvas mCanvas;
    493 
    494         LintJobListener(GraphicalEditorPart editor) {
    495             mEditor = editor;
    496             mCanvas = editor.getCanvasControl();
    497         }
    498 
    499         @Override
    500         public void done(IJobChangeEvent event) {
    501             LayoutActionBar bar = mEditor.getLayoutActionBar();
    502             if (!bar.isDisposed()) {
    503                 bar.updateErrorIndicator();
    504             }
    505 
    506             // Redraw
    507             if (!mCanvas.isDisposed()) {
    508                 mCanvas.getDisplay().asyncExec(this);
    509             }
    510         }
    511 
    512         @Override
    513         public void run() {
    514             if (!mCanvas.isDisposed()) {
    515                 mCanvas.redraw();
    516 
    517                 OutlinePage outlinePage = mCanvas.getOutlinePage();
    518                 if (outlinePage != null) {
    519                     outlinePage.refreshIcons();
    520                 }
    521             }
    522         }
    523     }
    524 
    525     /**
    526      * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
    527      */
    528     @Override
    529     public Object delegateGetAdapter(Class<?> adapter) {
    530         if (adapter == IContentOutlinePage.class) {
    531             // Somebody has requested the outline. Eclipse can only have a single outline page,
    532             // even for a multi-part editor:
    533             //       https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917
    534             // To work around this we use PDE's workaround of having a single multiplexing
    535             // outline which switches its contents between the outline pages we register
    536             // for it, and then on page switch we notify it to update itself.
    537 
    538             // There is one complication: The XML editor outline listens for the editor
    539             // selection and uses this to automatically expand its tree children and show
    540             // the current node containing the caret as selected. Unfortunately, this
    541             // listener code contains this:
    542             //
    543             //     /* Bug 136310, unless this page is that part's
    544             //      * IContentOutlinePage, ignore the selection change */
    545             //     if (part.getAdapter(IContentOutlinePage.class) == this) {
    546             //
    547             // This means that when we return the multiplexing outline from this getAdapter
    548             // method, the outline no longer updates to track the selection.
    549             // To work around this, we use the following hack^H^H^H^H technique:
    550             // - Add a selection listener *before* requesting the editor outline, such
    551             //   that the selection listener is told about the impending selection event
    552             //   right before the editor outline hears about it. Set the flag
    553             //   mCheckOutlineAdapter to true. (We also only set it if the editor view
    554             //   itself is active.)
    555             // - In this getAdapter method, when somebody requests the IContentOutline.class,
    556             //   see if mCheckOutlineAdapter to see if this request is *likely* coming
    557             //   from the XML editor outline. If so, make sure it is by actually looking
    558             //   at the signature of the caller. If it's the editor outline, then return
    559             //   the editor outline instance itself rather than the multiplexing outline.
    560             if (mCheckOutlineAdapter && mEditorOutline != null) {
    561                 mCheckOutlineAdapter = false;
    562                 // Make *sure* this is really the editor outline calling in case
    563                 // future versions of Eclipse changes the sequencing or dispatch of selection
    564                 // events:
    565                 StackTraceElement[] frames = new Throwable().fillInStackTrace().getStackTrace();
    566                 if (frames.length > 2) {
    567                     StackTraceElement frame = frames[2];
    568                     if (frame.getClassName().equals(
    569                             "org.eclipse.wst.sse.ui.internal.contentoutline." + //$NON-NLS-1$
    570                             "ConfigurableContentOutlinePage$PostSelectionServiceListener")) { //$NON-NLS-1$
    571                         return mEditorOutline;
    572                     }
    573                 }
    574             }
    575 
    576             // Use a multiplexing outline: workaround for
    577             // https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917
    578             if (mMultiOutline == null || mMultiOutline.isDisposed()) {
    579                 mMultiOutline = new XmlEditorMultiOutline();
    580                 mMultiOutline.addSelectionChangedListener(new ISelectionChangedListener() {
    581                     @Override
    582                     public void selectionChanged(SelectionChangedEvent event) {
    583                         ISelection selection = event.getSelection();
    584                         getEditor().getSite().getSelectionProvider().setSelection(selection);
    585                         if (getEditor().getIgnoreXmlUpdate()) {
    586                             return;
    587                         }
    588                         SelectionManager manager =
    589                                 mGraphicalEditor.getCanvasControl().getSelectionManager();
    590                         manager.setSelection(selection);
    591                     }
    592                 });
    593                 updateOutline(getEditor().getActivePageInstance());
    594             }
    595 
    596             return mMultiOutline;
    597         }
    598 
    599         if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) {
    600             if (mPropertyPage == null) {
    601                 mPropertyPage = new PropertySheetPage(mGraphicalEditor);
    602             }
    603 
    604             return mPropertyPage;
    605         }
    606 
    607         // return default
    608         return super.delegateGetAdapter(adapter);
    609     }
    610 
    611     /**
    612      * Update the contents of the outline to show either the XML editor outline
    613      * or the layout editor graphical outline depending on which tab is visible
    614      */
    615     private void updateOutline(IFormPage page) {
    616         if (mMultiOutline == null) {
    617             return;
    618         }
    619 
    620         IContentOutlinePage outline;
    621         CommonXmlEditor editor = getEditor();
    622         if (!editor.isEditorPageActive()) {
    623             outline = getGraphicalOutline();
    624         } else {
    625             // Use plain XML editor outline instead
    626             if (mEditorOutline == null) {
    627                 StructuredTextEditor structuredTextEditor = editor.getStructuredTextEditor();
    628                 if (structuredTextEditor != null) {
    629                     IWorkbenchWindow window = editor.getSite().getWorkbenchWindow();
    630                     ISelectionService service = window.getSelectionService();
    631                     service.addPostSelectionListener(new ISelectionListener() {
    632                         @Override
    633                         public void selectionChanged(IWorkbenchPart part, ISelection selection) {
    634                             if (getEditor().isEditorPageActive()) {
    635                                 mCheckOutlineAdapter = true;
    636                             }
    637                         }
    638                     });
    639 
    640                     mEditorOutline = (IContentOutlinePage) structuredTextEditor.getAdapter(
    641                             IContentOutlinePage.class);
    642                 }
    643             }
    644 
    645             outline = mEditorOutline;
    646         }
    647 
    648         mMultiOutline.setPageActive(outline);
    649     }
    650 
    651     /**
    652      * Returns the graphical outline associated with the layout editor
    653      *
    654      * @return the outline page, never null
    655      */
    656     @NonNull
    657     public OutlinePage getGraphicalOutline() {
    658         if (mLayoutOutline == null) {
    659             mLayoutOutline = new OutlinePage(mGraphicalEditor);
    660         }
    661 
    662         return mLayoutOutline;
    663     }
    664 
    665     @Override
    666     public void delegatePageChange(int newPageIndex) {
    667         if (getEditor().getCurrentPage() == getEditor().getTextPageIndex() &&
    668                 newPageIndex == mGraphicalEditorIndex) {
    669             // You're switching from the XML editor to the WYSIWYG editor;
    670             // look at the caret position and figure out which node it corresponds to
    671             // (if any) and if found, select the corresponding visual element.
    672             ISourceViewer textViewer = getEditor().getStructuredSourceViewer();
    673             int caretOffset = textViewer.getTextWidget().getCaretOffset();
    674             if (caretOffset >= 0) {
    675                 Node node = DomUtilities.getNode(textViewer.getDocument(), caretOffset);
    676                 if (node != null && mGraphicalEditor != null) {
    677                     mGraphicalEditor.select(node);
    678                 }
    679             }
    680         }
    681 
    682         super.delegatePageChange(newPageIndex);
    683 
    684         if (mGraphicalEditor != null) {
    685             if (newPageIndex == mGraphicalEditorIndex) {
    686                 mGraphicalEditor.activated();
    687             } else {
    688                 mGraphicalEditor.deactivated();
    689             }
    690         }
    691     }
    692 
    693     @Override
    694     public int delegateGetPersistenceCategory() {
    695         return AndroidXmlEditor.CATEGORY_LAYOUT;
    696     }
    697 
    698     @Override
    699     public void delegatePostPageChange(int newPageIndex) {
    700         super.delegatePostPageChange(newPageIndex);
    701 
    702         if (mGraphicalEditor != null) {
    703             LayoutCanvas canvas = mGraphicalEditor.getCanvasControl();
    704             if (canvas != null) {
    705                 IActionBars bars = getEditor().getEditorSite().getActionBars();
    706                 if (bars != null) {
    707                     canvas.updateGlobalActions(bars);
    708                 }
    709             }
    710         }
    711 
    712         IFormPage page = getEditor().getActivePageInstance();
    713         updateOutline(page);
    714     }
    715 
    716     @Override
    717     public IFormPage delegatePostSetActivePage(IFormPage superReturned, String pageIndex) {
    718         IFormPage page = superReturned;
    719         if (page != null) {
    720             updateOutline(page);
    721         }
    722 
    723         return page;
    724     }
    725 
    726     // ----- IActionContributorDelegate methods ----
    727 
    728     @Override
    729     public void setActiveEditor(IEditorPart part, IActionBars bars) {
    730         if (mGraphicalEditor != null) {
    731             LayoutCanvas canvas = mGraphicalEditor.getCanvasControl();
    732             if (canvas != null) {
    733                 canvas.updateGlobalActions(bars);
    734             }
    735         }
    736     }
    737 
    738 
    739     @Override
    740     public void delegateActivated() {
    741         if (mGraphicalEditor != null) {
    742             if (getEditor().getActivePage() == mGraphicalEditorIndex) {
    743                 mGraphicalEditor.activated();
    744             } else {
    745                 mGraphicalEditor.deactivated();
    746             }
    747         }
    748     }
    749 
    750     @Override
    751     public void delegateDeactivated() {
    752         if (mGraphicalEditor != null && getEditor().getActivePage() == mGraphicalEditorIndex) {
    753             mGraphicalEditor.deactivated();
    754         }
    755     }
    756 
    757     @Override
    758     public String delegateGetPartName() {
    759         IEditorInput editorInput = getEditor().getEditorInput();
    760         if (!AdtPrefs.getPrefs().isSharedLayoutEditor()
    761               && editorInput instanceof IFileEditorInput) {
    762             IFileEditorInput fileInput = (IFileEditorInput) editorInput;
    763             IFile file = fileInput.getFile();
    764             IContainer parent = file.getParent();
    765             if (parent != null) {
    766                 String parentName = parent.getName();
    767                 if  (parentName.startsWith(LAYOUT_FOLDER_PREFIX)) {
    768                     parentName = parentName.substring(LAYOUT_FOLDER_PREFIX.length());
    769                     return parentName + File.separatorChar + file.getName();
    770                 }
    771             }
    772         }
    773 
    774         return super.delegateGetPartName();
    775     }
    776 
    777     // ---- Local Methods ----
    778 
    779     /**
    780      * Returns true if the Graphics editor page is visible. This <b>must</b> be
    781      * called from the UI thread.
    782      */
    783     public boolean isGraphicalEditorActive() {
    784         IWorkbenchPartSite workbenchSite = getEditor().getSite();
    785         IWorkbenchPage workbenchPage = workbenchSite.getPage();
    786 
    787         // check if the editor is visible in the workbench page
    788         if (workbenchPage.isPartVisible(getEditor())
    789                 && workbenchPage.getActiveEditor() == getEditor()) {
    790             // and then if the page of the editor is visible (not to be confused with
    791             // the workbench page)
    792             return mGraphicalEditorIndex == getEditor().getActivePage();
    793         }
    794 
    795         return false;
    796     }
    797 
    798     @Override
    799     public void delegateInitUiRootNode(boolean force) {
    800         // The root UI node is always created, even if there's no corresponding XML node.
    801         if (mUiDocRootNode == null || force) {
    802             // get the target data from the opened file (and its project)
    803             AndroidTargetData data = getEditor().getTargetData();
    804 
    805             Document doc = null;
    806             if (mUiDocRootNode != null) {
    807                 doc = mUiDocRootNode.getXmlDocument();
    808             }
    809 
    810             DocumentDescriptor desc;
    811             if (data == null) {
    812                 desc = new DocumentDescriptor("temp", null /*children*/);
    813             } else {
    814                 desc = data.getLayoutDescriptors().getDescriptor();
    815             }
    816 
    817             // get the descriptors from the data.
    818             mUiDocRootNode = (UiDocumentNode) desc.createUiNode();
    819             super.setUiRootNode(mUiDocRootNode);
    820             mUiDocRootNode.setEditor(getEditor());
    821 
    822             mUiDocRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() {
    823                 @Override
    824                 public ElementDescriptor getDescriptor(String xmlLocalName) {
    825                     ElementDescriptor unknown = mUnknownDescriptorMap.get(xmlLocalName);
    826                     if (unknown == null) {
    827                         unknown = createUnknownDescriptor(xmlLocalName);
    828                         mUnknownDescriptorMap.put(xmlLocalName, unknown);
    829                     }
    830 
    831                     return unknown;
    832                 }
    833             });
    834 
    835             onDescriptorsChanged(doc);
    836         }
    837     }
    838 
    839     /**
    840      * Creates a new {@link ViewElementDescriptor} for an unknown XML local name
    841      * (i.e. one that was not mapped by the current descriptors).
    842      * <p/>
    843      * Since we deal with layouts, we returns either a descriptor for a custom view
    844      * or one for the base View.
    845      *
    846      * @param xmlLocalName The XML local name to match.
    847      * @return A non-null {@link ViewElementDescriptor}.
    848      */
    849     private ViewElementDescriptor createUnknownDescriptor(String xmlLocalName) {
    850         ViewElementDescriptor desc = null;
    851         IEditorInput editorInput = getEditor().getEditorInput();
    852         if (editorInput instanceof IFileEditorInput) {
    853             IFileEditorInput fileInput = (IFileEditorInput)editorInput;
    854             IProject project = fileInput.getFile().getProject();
    855 
    856             // Check if we can find a custom view specific to this project.
    857             // This only works if there's an actual matching custom class in the project.
    858             if (xmlLocalName.indexOf('.') != -1) {
    859                 desc = CustomViewDescriptorService.getInstance().getDescriptor(project,
    860                         xmlLocalName);
    861             }
    862 
    863             if (desc == null) {
    864                 // If we didn't find a custom view, create a synthetic one using the
    865                 // the base View descriptor as a model.
    866                 // This is a layout after all, so every XML node should represent
    867                 // a view.
    868 
    869                 Sdk currentSdk = Sdk.getCurrent();
    870                 if (currentSdk != null) {
    871                     IAndroidTarget target = currentSdk.getTarget(project);
    872                     if (target != null) {
    873                         AndroidTargetData data = currentSdk.getTargetData(target);
    874                         if (data != null) {
    875                             // data can be null when the target is still loading
    876                             ViewElementDescriptor viewDesc =
    877                                 data.getLayoutDescriptors().getBaseViewDescriptor();
    878 
    879                             desc = new ViewElementDescriptor(
    880                                     xmlLocalName, // xml local name
    881                                     xmlLocalName, // ui_name
    882                                     xmlLocalName, // canonical class name
    883                                     null, // tooltip
    884                                     null, // sdk_url
    885                                     viewDesc.getAttributes(),
    886                                     viewDesc.getLayoutAttributes(),
    887                                     null, // children
    888                                     false /* mandatory */);
    889                             desc.setSuperClass(viewDesc);
    890                         }
    891                     }
    892                 }
    893             }
    894         }
    895 
    896         if (desc == null) {
    897             // We can only arrive here if the SDK's android target has not finished
    898             // loading. Just create a dummy descriptor with no attributes to be able
    899             // to continue.
    900             desc = new ViewElementDescriptor(xmlLocalName, xmlLocalName);
    901         }
    902         return desc;
    903     }
    904 
    905     private void onDescriptorsChanged(Document document) {
    906 
    907         mUnknownDescriptorMap.clear();
    908 
    909         if (document != null) {
    910             mUiDocRootNode.loadFromXmlNode(document);
    911         } else {
    912             mUiDocRootNode.reloadFromXmlNode(mUiDocRootNode.getXmlDocument());
    913         }
    914 
    915         if (mGraphicalEditor != null) {
    916             mGraphicalEditor.onTargetChange();
    917             mGraphicalEditor.reloadPalette();
    918             mGraphicalEditor.getCanvasControl().syncPreviewMode();
    919         }
    920     }
    921 
    922     /**
    923      * Handles a new input, and update the part name.
    924      * @param input the new input.
    925      */
    926     private void handleNewInput(IEditorInput input) {
    927         if (input instanceof FileEditorInput) {
    928             FileEditorInput fileInput = (FileEditorInput) input;
    929             IFile file = fileInput.getFile();
    930             getEditor().setPartName(String.format("%1$s", file.getName()));
    931         }
    932     }
    933 
    934     /**
    935      * Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN.
    936      * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info.
    937      */
    938     public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
    939         ViewElementDescriptor desc = null;
    940 
    941         AndroidTargetData data = getEditor().getTargetData();
    942         if (data != null) {
    943             LayoutDescriptors layoutDesc = data.getLayoutDescriptors();
    944             if (layoutDesc != null) {
    945                 DocumentDescriptor docDesc = layoutDesc.getDescriptor();
    946                 if (docDesc != null) {
    947                     desc = internalFindFqcnViewDescriptor(fqcn, docDesc.getChildren(), null);
    948                 }
    949             }
    950         }
    951 
    952         if (desc == null) {
    953             // We failed to find a descriptor for the given FQCN.
    954             // Let's consider custom classes and create one as needed.
    955             desc = createUnknownDescriptor(fqcn);
    956         }
    957 
    958         return desc;
    959     }
    960 
    961     /**
    962      * Internal helper to recursively search for a {@link ViewElementDescriptor} that matches
    963      * the requested FQCN.
    964      *
    965      * @param fqcn The target View FQCN to find.
    966      * @param descriptors A list of children descriptors to iterate through.
    967      * @param visited A set we use to remember which descriptors have already been visited,
    968      *  necessary since the view descriptor hierarchy is cyclic.
    969      * @return Either a matching {@link ViewElementDescriptor} or null.
    970      */
    971     private ViewElementDescriptor internalFindFqcnViewDescriptor(String fqcn,
    972             ElementDescriptor[] descriptors,
    973             Set<ElementDescriptor> visited) {
    974         if (visited == null) {
    975             visited = new HashSet<ElementDescriptor>();
    976         }
    977 
    978         if (descriptors != null) {
    979             for (ElementDescriptor desc : descriptors) {
    980                 if (visited.add(desc)) {
    981                     // Set.add() returns true if this a new element that was added to the set.
    982                     // That means we haven't visited this descriptor yet.
    983                     // We want a ViewElementDescriptor with a matching FQCN.
    984                     if (desc instanceof ViewElementDescriptor &&
    985                             fqcn.equals(((ViewElementDescriptor) desc).getFullClassName())) {
    986                         return (ViewElementDescriptor) desc;
    987                     }
    988 
    989                     // Visit its children
    990                     ViewElementDescriptor vd =
    991                         internalFindFqcnViewDescriptor(fqcn, desc.getChildren(), visited);
    992                     if (vd != null) {
    993                         return vd;
    994                     }
    995                 }
    996             }
    997         }
    998 
    999         return null;
   1000     }
   1001 }
   1002