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