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.AdtPlugin;
     20 import com.android.ide.eclipse.adt.AndroidConstants;
     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.ViewElementDescriptor;
     27 import com.android.ide.eclipse.adt.internal.editors.layout.gle1.GraphicalLayoutEditor;
     28 import com.android.ide.eclipse.adt.internal.editors.layout.gle1.UiContentOutlinePage;
     29 import com.android.ide.eclipse.adt.internal.editors.layout.gle1.UiPropertySheetPage;
     30 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage2;
     31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
     32 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PropertySheetPage2;
     33 import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiActions;
     34 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
     35 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     36 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     37 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     38 import com.android.sdklib.IAndroidTarget;
     39 
     40 import org.eclipse.core.resources.IFile;
     41 import org.eclipse.core.resources.IProject;
     42 import org.eclipse.core.runtime.IProgressMonitor;
     43 import org.eclipse.core.runtime.IStatus;
     44 import org.eclipse.core.runtime.NullProgressMonitor;
     45 import org.eclipse.gef.ui.parts.TreeViewer;
     46 import org.eclipse.ui.IEditorInput;
     47 import org.eclipse.ui.IEditorPart;
     48 import org.eclipse.ui.IFileEditorInput;
     49 import org.eclipse.ui.IPartListener;
     50 import org.eclipse.ui.IShowEditorInput;
     51 import org.eclipse.ui.IWorkbenchPage;
     52 import org.eclipse.ui.IWorkbenchPart;
     53 import org.eclipse.ui.IWorkbenchPartSite;
     54 import org.eclipse.ui.PartInitException;
     55 import org.eclipse.ui.part.FileEditorInput;
     56 import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
     57 import org.eclipse.ui.views.properties.IPropertySheetPage;
     58 import org.w3c.dom.Document;
     59 
     60 import java.util.HashMap;
     61 
     62 /**
     63  * Multi-page form editor for /res/layout XML files.
     64  */
     65 public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, IPartListener {
     66 
     67     public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$
     68 
     69     /** Root node of the UI element hierarchy */
     70     private UiDocumentNode mUiRootNode;
     71 
     72     private IGraphicalLayoutEditor mGraphicalEditor;
     73     private int mGraphicalEditorIndex;
     74     /**
     75      * Implementation of the {@link IContentOutlinePage} for this editor.
     76      * @deprecated Used for backward compatibility with GLE1.
     77      */
     78     private UiContentOutlinePage mOutlineForGle1;
     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 UiEditorActions mUiEditorActions;
     85 
     86     private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap =
     87         new HashMap<String, ElementDescriptor>();
     88 
     89 
     90     /**
     91      * Flag indicating if the replacement file is due to a config change.
     92      * If false, it means the new file is due to an "open action" from the user.
     93      */
     94     private boolean mNewFileOnConfigChange = false;
     95 
     96     /**
     97      * Creates the form editor for resources XML files.
     98      */
     99     public LayoutEditor() {
    100         super(false /* addTargetListener */);
    101     }
    102 
    103     /**
    104      * @return The root node of the UI element hierarchy
    105      */
    106     @Override
    107     public UiDocumentNode getUiRootNode() {
    108         return mUiRootNode;
    109     }
    110 
    111     // ---- Base Class Overrides ----
    112 
    113     @Override
    114     public void dispose() {
    115         getSite().getPage().removePartListener(this);
    116 
    117         super.dispose();
    118     }
    119 
    120     /**
    121      * Save the XML.
    122      * <p/>
    123      * The actual save operation is done in the super class by committing
    124      * all data to the XML model and then having the Structured XML Editor
    125      * save the XML.
    126      * <p/>
    127      * Here we just need to tell the graphical editor that the model has
    128      * been saved.
    129      */
    130     @Override
    131     public void doSave(IProgressMonitor monitor) {
    132         super.doSave(monitor);
    133         if (mGraphicalEditor != null) {
    134             mGraphicalEditor.doSave(monitor);
    135         }
    136     }
    137 
    138     /**
    139      * Returns whether the "save as" operation is supported by this editor.
    140      * <p/>
    141      * Save-As is a valid operation for the ManifestEditor since it acts on a
    142      * single source file.
    143      *
    144      * @see IEditorPart
    145      */
    146     @Override
    147     public boolean isSaveAsAllowed() {
    148         return true;
    149     }
    150 
    151     /**
    152      * Create the various form pages.
    153      */
    154     @Override
    155     protected void createFormPages() {
    156         try {
    157             // The graphical layout editor is now enabled by default.
    158             // In case there's an issue we provide a way to disable it using an
    159             // env variable.
    160             if (System.getenv("ANDROID_DISABLE_LAYOUT") == null) {      //$NON-NLS-1$
    161                 // get the file being edited so that it can be passed to the layout editor.
    162                 IFile editedFile = null;
    163                 IEditorInput input = getEditorInput();
    164                 if (input instanceof FileEditorInput) {
    165                     FileEditorInput fileInput = (FileEditorInput)input;
    166                     editedFile = fileInput.getFile();
    167                 } else {
    168                     AdtPlugin.log(IStatus.ERROR,
    169                             "Input is not of type FileEditorInput: %1$s",  //$NON-NLS-1$
    170                             input.toString());
    171                 }
    172 
    173                 // It is possible that the Layout Editor already exits if a different version
    174                 // of the same layout is being opened (either through "open" action from
    175                 // the user, or through a configuration change in the configuration selector.)
    176                 if (mGraphicalEditor == null) {
    177 
    178                     String useGle2 = System.getenv("USE_GLE2");     //$NON-NLS-1$
    179 
    180                     if (useGle2 != null && !useGle2.equals("0")) {  //$NON-NLS-1$
    181                         mGraphicalEditor = new GraphicalEditorPart(this);
    182                     } else {
    183                         mGraphicalEditor = new GraphicalLayoutEditor(this);
    184                     }
    185 
    186                     mGraphicalEditorIndex = addPage(mGraphicalEditor, getEditorInput());
    187                     setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle());
    188 
    189                     mGraphicalEditor.openFile(editedFile);
    190                 } else {
    191                     if (mNewFileOnConfigChange) {
    192                         mGraphicalEditor.changeFileOnNewConfig(editedFile);
    193                         mNewFileOnConfigChange = false;
    194                     } else {
    195                         mGraphicalEditor.replaceFile(editedFile);
    196                     }
    197                 }
    198 
    199                 // put in place the listener to handle layout recompute only when needed.
    200                 getSite().getPage().addPartListener(this);
    201             }
    202         } catch (PartInitException e) {
    203             AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
    204         }
    205      }
    206 
    207     @Override
    208     protected void postCreatePages() {
    209         super.postCreatePages();
    210 
    211         // This is called after the createFormPages() and createTextPage() methods have
    212         // been called. Usually we select the first page (e.g. the GLE here) but right
    213         // now we're going to temporarily select the last page (the XML text editor) if
    214         // GLE1 is being used. That's because GLE1 is mostly useless and being deprecated.
    215         //
    216         // Note that this sets the default page. Eventually a default page might be
    217         // restored by selectDefaultPage() later based on the last page used by the user.
    218         //
    219         // TODO revert this once GLE2 becomes useful and is the default.
    220 
    221         if (mGraphicalEditor instanceof GraphicalLayoutEditor) {
    222             setActivePage(getPageCount() - 1);
    223         }
    224     }
    225 
    226     /* (non-java doc)
    227      * Change the tab/title name to include the name of the layout.
    228      */
    229     @Override
    230     protected void setInput(IEditorInput input) {
    231         super.setInput(input);
    232         handleNewInput(input);
    233     }
    234 
    235     /*
    236      * (non-Javadoc)
    237      * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput)
    238      */
    239     @Override
    240     protected void setInputWithNotify(IEditorInput input) {
    241         super.setInputWithNotify(input);
    242         handleNewInput(input);
    243     }
    244 
    245     /**
    246      * Called to replace the current {@link IEditorInput} with another one.
    247      * <p/>This is used when {@link MatchingStrategy} returned <code>true</code> which means we're
    248      * opening a different configuration of the same layout.
    249      */
    250     public void showEditorInput(IEditorInput editorInput) {
    251         // save the current editor input.
    252         doSave(new NullProgressMonitor());
    253 
    254         // get the current page
    255         int currentPage = getActivePage();
    256 
    257         // remove the pages, except for the graphical editor, which will be dynamically adapted
    258         // to the new model.
    259         // page after the graphical editor:
    260         int count = getPageCount();
    261         for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) {
    262             removePage(i);
    263         }
    264         // pages before the graphical editor
    265         for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) {
    266             removePage(i);
    267         }
    268 
    269         // set the current input.
    270         setInputWithNotify(editorInput);
    271 
    272         // re-create or reload the pages with the default page shown as the previous active page.
    273         createAndroidPages();
    274         selectDefaultPage(Integer.toString(currentPage));
    275 
    276         // update the GLE1 outline. The GLE2 outline doesn't need this call anymore.
    277         if (mOutlineForGle1 != null) {
    278             mOutlineForGle1.reloadModel();
    279         }
    280     }
    281 
    282     /**
    283      * Processes the new XML Model, which XML root node is given.
    284      *
    285      * @param xml_doc The XML document, if available, or null if none exists.
    286      */
    287     @Override
    288     protected void xmlModelChanged(Document xml_doc) {
    289         // init the ui root on demand
    290         initUiRootNode(false /*force*/);
    291 
    292         mUiRootNode.loadFromXmlNode(xml_doc);
    293 
    294         // update the model first, since it is used by the viewers.
    295         super.xmlModelChanged(xml_doc);
    296 
    297         if (mGraphicalEditor != null) {
    298             mGraphicalEditor.onXmlModelChanged();
    299         }
    300 
    301         // update the GLE1 outline. The GLE2 outline doesn't need this call anymore.
    302         if (mOutlineForGle1 != null) {
    303             mOutlineForGle1.reloadModel();
    304         }
    305     }
    306 
    307     /**
    308      * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
    309      */
    310     @SuppressWarnings("unchecked")
    311     @Override
    312     public Object getAdapter(Class adapter) {
    313         // for the outline, force it to come from the Graphical Editor.
    314         // This fixes the case where a layout file is opened in XML view first and the outline
    315         // gets stuck in the XML outline.
    316         if (IContentOutlinePage.class == adapter && mGraphicalEditor != null) {
    317 
    318             if (mOutline == null && mGraphicalEditor instanceof GraphicalLayoutEditor) {
    319                 // Create the GLE1 outline. We need to keep a specific reference to it in order
    320                 // to call its reloadModel() method. The GLE2 outline no longer relies on this
    321                 // and can be casted to the base interface.
    322                 mOutlineForGle1 = new UiContentOutlinePage(
    323                         (GraphicalLayoutEditor) mGraphicalEditor,
    324                         new TreeViewer());
    325                 mOutline = mOutlineForGle1;
    326 
    327             } else if (mOutline == null && mGraphicalEditor instanceof GraphicalEditorPart) {
    328                 mOutline = new OutlinePage2();
    329             }
    330 
    331             return mOutline;
    332         }
    333 
    334         if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) {
    335             if (mPropertyPage == null && mGraphicalEditor instanceof GraphicalLayoutEditor) {
    336                 mPropertyPage = new UiPropertySheetPage();
    337 
    338             } else if (mPropertyPage == null && mGraphicalEditor instanceof GraphicalEditorPart) {
    339                 mPropertyPage = new PropertySheetPage2();
    340             }
    341 
    342             return mPropertyPage;
    343         }
    344 
    345         // return default
    346         return super.getAdapter(adapter);
    347     }
    348 
    349     @Override
    350     protected void pageChange(int newPageIndex) {
    351         super.pageChange(newPageIndex);
    352 
    353         if (mGraphicalEditor != null) {
    354             if (newPageIndex == mGraphicalEditorIndex) {
    355                 mGraphicalEditor.activated();
    356             } else {
    357                 mGraphicalEditor.deactivated();
    358             }
    359         }
    360     }
    361 
    362     // ----- IPartListener Methods ----
    363 
    364     public void partActivated(IWorkbenchPart part) {
    365         if (part == this) {
    366             if (mGraphicalEditor != null) {
    367                 if (getActivePage() == mGraphicalEditorIndex) {
    368                     mGraphicalEditor.activated();
    369                 } else {
    370                     mGraphicalEditor.deactivated();
    371                 }
    372             }
    373         }
    374     }
    375 
    376     public void partBroughtToTop(IWorkbenchPart part) {
    377         partActivated(part);
    378     }
    379 
    380     public void partClosed(IWorkbenchPart part) {
    381         // pass
    382     }
    383 
    384     public void partDeactivated(IWorkbenchPart part) {
    385         if (part == this) {
    386             if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) {
    387                 mGraphicalEditor.deactivated();
    388             }
    389         }
    390     }
    391 
    392     public void partOpened(IWorkbenchPart part) {
    393         /*
    394          * We used to automatically bring the outline and the property sheet to view
    395          * when opening the editor. This behavior has always been a mixed bag and not
    396          * exactly satisfactory. GLE1 is being useless/deprecated and GLE2 will need to
    397          * improve on that, so right now let's comment this out.
    398          */
    399         //EclipseUiHelper.showView(EclipseUiHelper.CONTENT_OUTLINE_VIEW_ID, false /* activate */);
    400         //EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, false /* activate */);
    401     }
    402 
    403     public class UiEditorActions extends UiActions {
    404 
    405         @Override
    406         protected UiDocumentNode getRootNode() {
    407             return mUiRootNode;
    408         }
    409 
    410         // Select the new item
    411         @Override
    412         protected void selectUiNode(UiElementNode uiNodeToSelect) {
    413             mGraphicalEditor.selectModel(uiNodeToSelect);
    414         }
    415 
    416         @Override
    417         public void commitPendingXmlChanges() {
    418             // Pass. There is nothing to commit before the XML is changed here.
    419         }
    420     }
    421 
    422     public UiEditorActions getUiEditorActions() {
    423         if (mUiEditorActions == null) {
    424             mUiEditorActions = new UiEditorActions();
    425         }
    426         return mUiEditorActions;
    427     }
    428 
    429     // ---- Local Methods ----
    430 
    431     /**
    432      * Returns true if the Graphics editor page is visible. This <b>must</b> be
    433      * called from the UI thread.
    434      */
    435     public boolean isGraphicalEditorActive() {
    436         IWorkbenchPartSite workbenchSite = getSite();
    437         IWorkbenchPage workbenchPage = workbenchSite.getPage();
    438 
    439         // check if the editor is visible in the workbench page
    440         if (workbenchPage.isPartVisible(this) && workbenchPage.getActiveEditor() == this) {
    441             // and then if the page of the editor is visible (not to be confused with
    442             // the workbench page)
    443             return mGraphicalEditorIndex == getActivePage();
    444         }
    445 
    446         return false;
    447     }
    448 
    449     @Override
    450     public void initUiRootNode(boolean force) {
    451         // The root UI node is always created, even if there's no corresponding XML node.
    452         if (mUiRootNode == null || force) {
    453             // get the target data from the opened file (and its project)
    454             AndroidTargetData data = getTargetData();
    455 
    456             Document doc = null;
    457             if (mUiRootNode != null) {
    458                 doc = mUiRootNode.getXmlDocument();
    459             }
    460 
    461             DocumentDescriptor desc;
    462             if (data == null) {
    463                 desc = new DocumentDescriptor("temp", null /*children*/);
    464             } else {
    465                 desc = data.getLayoutDescriptors().getDescriptor();
    466             }
    467 
    468             // get the descriptors from the data.
    469             mUiRootNode = (UiDocumentNode) desc.createUiNode();
    470             mUiRootNode.setEditor(this);
    471 
    472             mUiRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() {
    473 
    474                 public ElementDescriptor getDescriptor(String xmlLocalName) {
    475 
    476                     ElementDescriptor desc = mUnknownDescriptorMap.get(xmlLocalName);
    477 
    478                     if (desc == null) {
    479                         desc = createUnknownDescriptor(xmlLocalName);
    480                         mUnknownDescriptorMap.put(xmlLocalName, desc);
    481                     }
    482 
    483                     return desc;
    484                 }
    485             });
    486 
    487             onDescriptorsChanged(doc);
    488         }
    489     }
    490 
    491     /**
    492      * Creates a new {@link ElementDescriptor} for an unknown XML local name
    493      * (i.e. one that was not mapped by the current descriptors).
    494      * <p/>
    495      * Since we deal with layouts, we returns either a descriptor for a custom view
    496      * or one for the base View.
    497      *
    498      * @param xmlLocalName The XML local name to match.
    499      * @return A non-null {@link ElementDescriptor}.
    500      */
    501     private ElementDescriptor createUnknownDescriptor(String xmlLocalName) {
    502         IEditorInput editorInput = getEditorInput();
    503         if (editorInput instanceof IFileEditorInput) {
    504             IFileEditorInput fileInput = (IFileEditorInput)editorInput;
    505             IProject project = fileInput.getFile().getProject();
    506 
    507             // Check if we can find a custom view specific to this project.
    508             ElementDescriptor desc = CustomViewDescriptorService.getInstance().getDescriptor(
    509                     project, xmlLocalName);
    510 
    511             if (desc != null) {
    512                 return desc;
    513             }
    514 
    515             // If we didn't find a custom view, reuse the base View descriptor.
    516             // This is a layout after all, so every XML node should represent
    517             // a view.
    518 
    519             Sdk currentSdk = Sdk.getCurrent();
    520             if (currentSdk != null) {
    521                 IAndroidTarget target = currentSdk.getTarget(project);
    522                 if (target != null) {
    523                     AndroidTargetData data = currentSdk.getTargetData(target);
    524                     if (data != null) {
    525                         // data can be null when the target is still loading
    526                         desc = data.getLayoutDescriptors().getBaseViewDescriptor();
    527                     }
    528                 }
    529             }
    530 
    531             if (desc != null) {
    532                 return desc;
    533             }
    534         }
    535 
    536         // We get here if the editor input is not of the right type or if the
    537         // SDK hasn't finished loading. In either case, return something just
    538         // because we should not return null.
    539         return new ViewElementDescriptor(xmlLocalName, xmlLocalName);
    540     }
    541 
    542     private void onDescriptorsChanged(Document document) {
    543 
    544         mUnknownDescriptorMap.clear();
    545 
    546         if (document != null) {
    547             mUiRootNode.loadFromXmlNode(document);
    548         } else {
    549             mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlDocument());
    550         }
    551 
    552         if (mOutlineForGle1 != null) {
    553             mOutlineForGle1.reloadModel();
    554         }
    555 
    556         if (mGraphicalEditor != null) {
    557             mGraphicalEditor.onTargetChange();
    558             mGraphicalEditor.reloadPalette();
    559         }
    560     }
    561 
    562     /**
    563      * Handles a new input, and update the part name.
    564      * @param input the new input.
    565      */
    566     private void handleNewInput(IEditorInput input) {
    567         if (input instanceof FileEditorInput) {
    568             FileEditorInput fileInput = (FileEditorInput) input;
    569             IFile file = fileInput.getFile();
    570             setPartName(String.format("%1$s",
    571                     file.getName()));
    572         }
    573     }
    574 
    575     public void setNewFileOnConfigChange(boolean state) {
    576         mNewFileOnConfigChange = state;
    577     }
    578 }
    579