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