• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
LayoutEditor()99     public LayoutEditor() {
100         super(false /* addTargetListener */);
101     }
102 
103     /**
104      * @return The root node of the UI element hierarchy
105      */
106     @Override
getUiRootNode()107     public UiDocumentNode getUiRootNode() {
108         return mUiRootNode;
109     }
110 
111     // ---- Base Class Overrides ----
112 
113     @Override
dispose()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
doSave(IProgressMonitor monitor)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
isSaveAsAllowed()147     public boolean isSaveAsAllowed() {
148         return true;
149     }
150 
151     /**
152      * Create the various form pages.
153      */
154     @Override
createFormPages()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
postCreatePages()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
setInput(IEditorInput input)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
setInputWithNotify(IEditorInput input)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      */
showEditorInput(IEditorInput editorInput)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
xmlModelChanged(Document xml_doc)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
getAdapter(Class adapter)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
pageChange(int newPageIndex)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 
partActivated(IWorkbenchPart part)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 
partBroughtToTop(IWorkbenchPart part)376     public void partBroughtToTop(IWorkbenchPart part) {
377         partActivated(part);
378     }
379 
partClosed(IWorkbenchPart part)380     public void partClosed(IWorkbenchPart part) {
381         // pass
382     }
383 
partDeactivated(IWorkbenchPart part)384     public void partDeactivated(IWorkbenchPart part) {
385         if (part == this) {
386             if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) {
387                 mGraphicalEditor.deactivated();
388             }
389         }
390     }
391 
partOpened(IWorkbenchPart part)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
getRootNode()406         protected UiDocumentNode getRootNode() {
407             return mUiRootNode;
408         }
409 
410         // Select the new item
411         @Override
selectUiNode(UiElementNode uiNodeToSelect)412         protected void selectUiNode(UiElementNode uiNodeToSelect) {
413             mGraphicalEditor.selectModel(uiNodeToSelect);
414         }
415 
416         @Override
commitPendingXmlChanges()417         public void commitPendingXmlChanges() {
418             // Pass. There is nothing to commit before the XML is changed here.
419         }
420     }
421 
getUiEditorActions()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      */
isGraphicalEditorActive()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
initUiRootNode(boolean force)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      */
createUnknownDescriptor(String xmlLocalName)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 
onDescriptorsChanged(Document document)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      */
handleNewInput(IEditorInput input)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 
setNewFileOnConfigChange(boolean state)575     public void setNewFileOnConfigChange(boolean state) {
576         mNewFileOnConfigChange = state;
577     }
578 }
579