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