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