• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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.gle2;
18 
19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_STRING_PREFIX;
20 import static com.android.ide.common.layout.LayoutConstants.SCROLL_VIEW;
21 import static com.android.ide.common.layout.LayoutConstants.STRING_PREFIX;
22 import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG;
23 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage;
24 import static com.android.sdklib.SdkConstants.FD_GEN_SOURCES;
25 
26 import com.android.ide.common.api.Rect;
27 import com.android.ide.common.rendering.LayoutLibrary;
28 import com.android.ide.common.rendering.StaticRenderSession;
29 import com.android.ide.common.rendering.api.Capability;
30 import com.android.ide.common.rendering.api.LayoutLog;
31 import com.android.ide.common.rendering.api.RenderSession;
32 import com.android.ide.common.rendering.api.ResourceValue;
33 import com.android.ide.common.rendering.api.Result;
34 import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
35 import com.android.ide.common.resources.ResourceFile;
36 import com.android.ide.common.resources.ResourceRepository;
37 import com.android.ide.common.resources.ResourceResolver;
38 import com.android.ide.common.resources.configuration.FolderConfiguration;
39 import com.android.ide.common.sdk.LoadStatus;
40 import com.android.ide.eclipse.adt.AdtConstants;
41 import com.android.ide.eclipse.adt.AdtPlugin;
42 import com.android.ide.eclipse.adt.AdtUtils;
43 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
44 import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
45 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
46 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
47 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor;
48 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags;
49 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;
50 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
51 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
52 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener;
53 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;
54 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
55 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
56 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
57 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
58 import com.android.ide.eclipse.adt.internal.editors.ui.DecorComposite;
59 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
60 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
61 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
62 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
63 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
64 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
65 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
66 import com.android.ide.eclipse.adt.io.IFileWrapper;
67 import com.android.resources.Density;
68 import com.android.resources.ResourceFolderType;
69 import com.android.resources.ResourceType;
70 import com.android.sdklib.IAndroidTarget;
71 import com.android.sdklib.SdkConstants;
72 import com.android.util.Pair;
73 
74 import org.eclipse.core.resources.IFile;
75 import org.eclipse.core.resources.IFolder;
76 import org.eclipse.core.resources.IMarker;
77 import org.eclipse.core.resources.IProject;
78 import org.eclipse.core.resources.IResource;
79 import org.eclipse.core.runtime.CoreException;
80 import org.eclipse.core.runtime.IPath;
81 import org.eclipse.core.runtime.IProgressMonitor;
82 import org.eclipse.core.runtime.IStatus;
83 import org.eclipse.core.runtime.NullProgressMonitor;
84 import org.eclipse.core.runtime.Path;
85 import org.eclipse.core.runtime.QualifiedName;
86 import org.eclipse.core.runtime.Status;
87 import org.eclipse.core.runtime.jobs.Job;
88 import org.eclipse.jdt.core.IClasspathEntry;
89 import org.eclipse.jdt.core.IJavaElement;
90 import org.eclipse.jdt.core.IJavaModelMarker;
91 import org.eclipse.jdt.core.IJavaProject;
92 import org.eclipse.jdt.core.IPackageFragment;
93 import org.eclipse.jdt.core.IPackageFragmentRoot;
94 import org.eclipse.jdt.core.JavaCore;
95 import org.eclipse.jdt.core.JavaModelException;
96 import org.eclipse.jdt.internal.ui.preferences.BuildPathsPropertyPage;
97 import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
98 import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
99 import org.eclipse.jface.text.BadLocationException;
100 import org.eclipse.jface.text.IDocument;
101 import org.eclipse.jface.text.source.ISourceViewer;
102 import org.eclipse.jface.viewers.ISelection;
103 import org.eclipse.jface.viewers.ISelectionProvider;
104 import org.eclipse.jface.window.Window;
105 import org.eclipse.swt.SWT;
106 import org.eclipse.swt.custom.SashForm;
107 import org.eclipse.swt.custom.StyleRange;
108 import org.eclipse.swt.custom.StyledText;
109 import org.eclipse.swt.events.MouseAdapter;
110 import org.eclipse.swt.events.MouseEvent;
111 import org.eclipse.swt.graphics.Image;
112 import org.eclipse.swt.layout.GridData;
113 import org.eclipse.swt.layout.GridLayout;
114 import org.eclipse.swt.widgets.Composite;
115 import org.eclipse.swt.widgets.Display;
116 import org.eclipse.text.edits.MalformedTreeException;
117 import org.eclipse.text.edits.MultiTextEdit;
118 import org.eclipse.text.edits.ReplaceEdit;
119 import org.eclipse.ui.IEditorInput;
120 import org.eclipse.ui.IEditorSite;
121 import org.eclipse.ui.INullSelectionListener;
122 import org.eclipse.ui.ISelectionListener;
123 import org.eclipse.ui.IWorkbench;
124 import org.eclipse.ui.IWorkbenchPage;
125 import org.eclipse.ui.IWorkbenchPart;
126 import org.eclipse.ui.IWorkbenchWindow;
127 import org.eclipse.ui.PartInitException;
128 import org.eclipse.ui.PlatformUI;
129 import org.eclipse.ui.dialogs.PreferencesUtil;
130 import org.eclipse.ui.ide.IDE;
131 import org.eclipse.ui.part.EditorPart;
132 import org.eclipse.ui.part.FileEditorInput;
133 import org.eclipse.ui.part.IPage;
134 import org.eclipse.ui.part.PageBookView;
135 import org.w3c.dom.Node;
136 
137 import java.io.File;
138 import java.io.FileOutputStream;
139 import java.io.IOException;
140 import java.io.InputStream;
141 import java.util.ArrayList;
142 import java.util.Collection;
143 import java.util.Collections;
144 import java.util.List;
145 import java.util.Map;
146 import java.util.Set;
147 
148 /**
149  * Graphical layout editor part, version 2.
150  * <p/>
151  * The main component of the editor part is the {@link LayoutCanvasViewer}, which
152  * actually delegates its work to the {@link LayoutCanvas} control.
153  * <p/>
154  * The {@link LayoutCanvasViewer} is set as the site's {@link ISelectionProvider}:
155  * when the selection changes in the canvas, it is thus broadcasted to anyone listening
156  * on the site's selection service.
157  * <p/>
158  * This part is also an {@link ISelectionListener}. It listens to the site's selection
159  * service and thus receives selection changes from itself as well as the associated
160  * outline and property sheet (these are registered by {@link LayoutEditor#getAdapter(Class)}).
161  *
162  * @since GLE2
163  */
164 public class GraphicalEditorPart extends EditorPart
165     implements IPageImageProvider, ISelectionListener, INullSelectionListener {
166 
167     /*
168      * Useful notes:
169      * To understand Drag'n'drop:
170      *   http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html
171      *
172      * To understand the site's selection listener, selection provider, and the
173      * confusion of different-yet-similarly-named interfaces, consult this:
174      *   http://www.eclipse.org/articles/Article-WorkbenchSelections/article.html
175      *
176      * To summarize the selection mechanism:
177      * - The workbench site selection service can be seen as "centralized"
178      *   service that registers selection providers and selection listeners.
179      * - The editor part and the outline are selection providers.
180      * - The editor part, the outline and the property sheet are listeners
181      *   which all listen to each others indirectly.
182      */
183 
184     /**
185      * Session-property on files which specifies the initial config state to be used on
186      * this file
187      */
188     public final static QualifiedName NAME_INITIAL_STATE =
189         new QualifiedName(AdtPlugin.PLUGIN_ID, "initialstate");//$NON-NLS-1$
190 
191     /**
192      * Session-property on files which specifies the inclusion-context (reference to another layout
193      * which should be "including" this layout) when the file is opened
194      */
195     public final static QualifiedName NAME_INCLUDE =
196         new QualifiedName(AdtPlugin.PLUGIN_ID, "includer");//$NON-NLS-1$
197 
198     /** Reference to the layout editor */
199     private final LayoutEditor mLayoutEditor;
200 
201     /** Reference to the file being edited. Can also be used to access the {@link IProject}. */
202     private IFile mEditedFile;
203 
204     /** The configuration composite at the top of the layout editor. */
205     private ConfigurationComposite mConfigComposite;
206 
207     /** The sash that splits the palette from the canvas. */
208     private SashForm mSashPalette;
209 
210     /** The sash that splits the palette from the error view.
211      * The error view is shown only when needed. */
212     private SashForm mSashError;
213 
214     /** The palette displayed on the left of the sash. */
215     private PaletteControl mPalette;
216 
217     /** The layout canvas displayed to the right of the sash. */
218     private LayoutCanvasViewer mCanvasViewer;
219 
220     /** The Rules Engine associated with this editor. It is project-specific. */
221     private RulesEngine mRulesEngine;
222 
223     /** Styled text displaying the most recent error in the error view. */
224     private StyledText mErrorLabel;
225 
226     /**
227      * The resource reference to a file that should surround this file (e.g. include this file
228      * visually), or null if not applicable
229      */
230     private Reference mIncludedWithin;
231 
232     private Map<ResourceType, Map<String, ResourceValue>> mConfiguredFrameworkRes;
233     private Map<ResourceType, Map<String, ResourceValue>> mConfiguredProjectRes;
234     private ProjectCallback mProjectCallback;
235     private boolean mNeedsRecompute = false;
236     private TargetListener mTargetListener;
237     private ConfigListener mConfigListener;
238     private ResourceResolver mResourceResolver;
239     private ReloadListener mReloadListener;
240     private int mMinSdkVersion;
241     private int mTargetSdkVersion;
242     private LayoutActionBar mActionBar;
243 
244     /**
245      * Flags which tracks whether this editor is currently active which is set whenever
246      * {@link #activated()} is called and clear whenever {@link #deactivated()} is called.
247      * This is used to suppress repeated calls to {@link #activate()} to avoid doing
248      * unnecessary work.
249      */
250     private boolean mActive;
251 
GraphicalEditorPart(LayoutEditor layoutEditor)252     public GraphicalEditorPart(LayoutEditor layoutEditor) {
253         mLayoutEditor = layoutEditor;
254         setPartName("Graphical Layout");
255     }
256 
257     // ------------------------------------
258     // Methods overridden from base classes
259     //------------------------------------
260 
261     /**
262      * Initializes the editor part with a site and input.
263      * {@inheritDoc}
264      */
265     @Override
init(IEditorSite site, IEditorInput input)266     public void init(IEditorSite site, IEditorInput input) throws PartInitException {
267         setSite(site);
268         useNewEditorInput(input);
269 
270         if (mTargetListener == null) {
271             mTargetListener = new TargetListener();
272             AdtPlugin.getDefault().addTargetListener(mTargetListener);
273         }
274     }
275 
useNewEditorInput(IEditorInput input)276     private void useNewEditorInput(IEditorInput input) throws PartInitException {
277         // The contract of init() mentions we need to fail if we can't understand the input.
278         if (!(input instanceof FileEditorInput)) {
279             throw new PartInitException("Input is not of type FileEditorInput: " +  //$NON-NLS-1$
280                     input == null ? "null" : input.toString());                     //$NON-NLS-1$
281         }
282     }
283 
getPageImage()284     public Image getPageImage() {
285         return IconFactory.getInstance().getIcon("editor_page_design");  //$NON-NLS-1$
286     }
287 
288     @Override
createPartControl(Composite parent)289     public void createPartControl(Composite parent) {
290 
291         Display d = parent.getDisplay();
292 
293         GridLayout gl = new GridLayout(1, false);
294         parent.setLayout(gl);
295         gl.marginHeight = gl.marginWidth = 0;
296 
297         mConfigListener = new ConfigListener();
298 
299         // Check whether somebody has requested an initial state for the newly opened file.
300         // The initial state is a serialized version of the state compatible with
301         // {@link ConfigurationComposite#CONFIG_STATE}.
302         String initialState = null;
303         IFile file = mEditedFile;
304         if (file == null) {
305             IEditorInput input = mLayoutEditor.getEditorInput();
306             if (input instanceof FileEditorInput) {
307                 file = ((FileEditorInput) input).getFile();
308             }
309         }
310 
311         if (file != null) {
312             try {
313                 initialState = (String) file.getSessionProperty(NAME_INITIAL_STATE);
314                 if (initialState != null) {
315                     // Only use once
316                     file.setSessionProperty(NAME_INITIAL_STATE, null);
317                 }
318             } catch (CoreException e) {
319                 AdtPlugin.log(e, "Can't read session property %1$s", NAME_INITIAL_STATE);
320             }
321         }
322 
323         mConfigComposite = new ConfigurationComposite(mConfigListener, parent,
324                 SWT.BORDER, initialState);
325 
326         mSashPalette = new SashForm(parent, SWT.HORIZONTAL);
327         mSashPalette.setLayoutData(new GridData(GridData.FILL_BOTH));
328 
329         DecorComposite paletteDecor = new DecorComposite(mSashPalette, SWT.BORDER);
330         paletteDecor.setContent(new PaletteControl.PaletteDecor(this));
331         mPalette = (PaletteControl) paletteDecor.getContentControl();
332 
333         Composite layoutBarAndCanvas = new Composite(mSashPalette, SWT.NONE);
334         GridLayout gridLayout = new GridLayout(1, false);
335         gridLayout.horizontalSpacing = 0;
336         gridLayout.verticalSpacing = 0;
337         gridLayout.marginWidth = 0;
338         gridLayout.marginHeight = 0;
339         layoutBarAndCanvas.setLayout(gridLayout);
340         mActionBar = new LayoutActionBar(layoutBarAndCanvas, SWT.NONE, this);
341         GridData detailsData = new GridData(SWT.FILL, SWT.FILL, false, false, 1, 1);
342         mActionBar.setLayoutData(detailsData);
343 
344         mSashError = new SashForm(layoutBarAndCanvas, SWT.VERTICAL | SWT.BORDER);
345         mSashError.setLayoutData(new GridData(GridData.FILL_BOTH));
346 
347         mCanvasViewer = new LayoutCanvasViewer(mLayoutEditor, mRulesEngine, mSashError, SWT.NONE);
348         mSashError.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
349 
350         mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL);
351         mErrorLabel.setEditable(false);
352         mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
353         mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
354         mErrorLabel.addMouseListener(new ErrorLabelListener());
355 
356         mSashPalette.setWeights(new int[] { 20, 80 });
357         mSashError.setWeights(new int[] { 80, 20 });
358         mSashError.setMaximizedControl(mCanvasViewer.getControl());
359 
360         // Initialize the state
361         reloadPalette();
362 
363         getSite().setSelectionProvider(mCanvasViewer);
364         getSite().getPage().addSelectionListener(this);
365     }
366 
367     /**
368      * Listens to workbench selections that does NOT come from {@link LayoutEditor}
369      * (those are generated by ourselves).
370      * <p/>
371      * Selection can be null, as indicated by this class implementing
372      * {@link INullSelectionListener}.
373      */
selectionChanged(IWorkbenchPart part, ISelection selection)374     public void selectionChanged(IWorkbenchPart part, ISelection selection) {
375         if (!(part instanceof LayoutEditor)) {
376             if (part instanceof PageBookView) {
377                 PageBookView pbv = (PageBookView) part;
378                 IPage currentPage = pbv.getCurrentPage();
379                 if (currentPage instanceof OutlinePage) {
380                     LayoutCanvas canvas = getCanvasControl();
381                     if (canvas != null && canvas.getOutlinePage() != currentPage) {
382                         // The notification is not for this view; ignore
383                         // (can happen when there are multiple pages simultaneously
384                         // visible)
385                         return;
386                     }
387                 }
388             }
389             mCanvasViewer.setSelection(selection);
390         }
391     }
392 
393     @Override
dispose()394     public void dispose() {
395         getSite().getPage().removeSelectionListener(this);
396         getSite().setSelectionProvider(null);
397 
398         if (mTargetListener != null) {
399             AdtPlugin.getDefault().removeTargetListener(mTargetListener);
400             mTargetListener = null;
401         }
402 
403         if (mReloadListener != null) {
404             LayoutReloadMonitor.getMonitor().removeListener(mReloadListener);
405             mReloadListener = null;
406         }
407 
408         if (mCanvasViewer != null) {
409             mCanvasViewer.dispose();
410             mCanvasViewer = null;
411         }
412         super.dispose();
413     }
414 
415     /**
416      * Select the visual element corresponding to the given XML node
417      * @param xmlNode The Node whose element we want to select
418      */
select(Node xmlNode)419     public void select(Node xmlNode) {
420         mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode);
421     }
422 
423     /**
424      * Listens to changes from the Configuration UI banner and triggers layout rendering when
425      * changed. Also provide the Configuration UI with the list of resources/layout to display.
426      */
427     private class ConfigListener implements IConfigListener {
428 
429         /**
430          * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it.
431          * <p/>If there is no match, notify the user.
432          */
onConfigurationChange()433         public void onConfigurationChange() {
434             mConfiguredFrameworkRes = mConfiguredProjectRes = null;
435             mResourceResolver = null;
436 
437             if (mEditedFile == null || mConfigComposite.getEditedConfig() == null) {
438                 return;
439             }
440 
441             // Before doing the normal process, test for the following case.
442             // - the editor is being opened (or reset for a new input)
443             // - the file being opened is not the best match for any possible configuration
444             // - another random compatible config was chosen in the config composite.
445             // The result is that 'match' will not be the file being edited, but because this is not
446             // due to a config change, we should not trigger opening the actual best match (also,
447             // because the editor is still opening the MatchingStrategy woudln't answer true
448             // and the best match file would open in a different editor).
449             // So the solution is that if the editor is being created, we just call recomputeLayout
450             // without looking for a better matching layout file.
451             if (mLayoutEditor.isCreatingPages()) {
452                 recomputeLayout();
453             } else {
454                 // get the resources of the file's project.
455                 ProjectResources resources = ResourceManager.getInstance().getProjectResources(
456                         mEditedFile.getProject());
457 
458                 // from the resources, look for a matching file
459                 ResourceFile match = null;
460                 if (resources != null) {
461                     match = resources.getMatchingFile(mEditedFile.getName(),
462                                                       ResourceFolderType.LAYOUT,
463                                                       mConfigComposite.getCurrentConfig());
464                 }
465 
466                 if (match != null) {
467                     // since this is coming from Eclipse, this is always an instance of IFileWrapper
468                     IFileWrapper iFileWrapper = (IFileWrapper) match.getFile();
469                     IFile iFile = iFileWrapper.getIFile();
470                     if (iFile.equals(mEditedFile) == false) {
471                         try {
472                             // tell the editor that the next replacement file is due to a config
473                             // change.
474                             mLayoutEditor.setNewFileOnConfigChange(true);
475 
476                             // ask the IDE to open the replacement file.
477                             IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), iFile);
478 
479                             // we're done!
480                             return;
481                         } catch (PartInitException e) {
482                             // FIXME: do something!
483                         }
484                     }
485 
486                     // at this point, we have not opened a new file.
487 
488                     // Store the state in the current file
489                     mConfigComposite.storeState();
490 
491                     // Even though the layout doesn't change, the config changed, and referenced
492                     // resources need to be updated.
493                     recomputeLayout();
494                 } else {
495                     // display the error.
496                     FolderConfiguration currentConfig = mConfigComposite.getCurrentConfig();
497                     displayError(
498                             "No resources match the configuration\n \n\t%1$s\n \nChange the configuration or create:\n \n\tres/%2$s/%3$s\n \nYou can also click the 'Create' button above.",
499                             currentConfig.toDisplayString(),
500                             currentConfig.getFolderName(ResourceFolderType.LAYOUT),
501                             mEditedFile.getName());
502                 }
503             }
504 
505             reloadPalette();
506         }
507 
onThemeChange()508         public void onThemeChange() {
509             // Store the state in the current file
510             mConfigComposite.storeState();
511             mResourceResolver = null;
512 
513             recomputeLayout();
514 
515             reloadPalette();
516         }
517 
onCreate()518         public void onCreate() {
519             LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigComposite.getShell(),
520                     mEditedFile.getName(), mConfigComposite.getCurrentConfig());
521             if (dialog.open() == Window.OK) {
522                 final FolderConfiguration config = new FolderConfiguration();
523                 dialog.getConfiguration(config);
524 
525                 createAlternateLayout(config);
526             }
527         }
528 
onRenderingTargetPreChange(IAndroidTarget oldTarget)529         public void onRenderingTargetPreChange(IAndroidTarget oldTarget) {
530             preRenderingTargetChangeCleanUp(oldTarget);
531         }
532 
onRenderingTargetPostChange(IAndroidTarget target)533         public void onRenderingTargetPostChange(IAndroidTarget target) {
534             AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target);
535             updateCapabilities(targetData);
536 
537             mPalette.reloadPalette(target);
538         }
539 
getConfiguredFrameworkResources()540         public Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources() {
541             if (mConfiguredFrameworkRes == null && mConfigComposite != null) {
542                 ResourceRepository frameworkRes = getFrameworkResources();
543 
544                 if (frameworkRes == null) {
545                     AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");
546                 } else {
547                     // get the framework resource values based on the current config
548                     mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(
549                             mConfigComposite.getCurrentConfig());
550                 }
551             }
552 
553             return mConfiguredFrameworkRes;
554         }
555 
getConfiguredProjectResources()556         public Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources() {
557             if (mConfiguredProjectRes == null && mConfigComposite != null) {
558                 ProjectResources project = getProjectResources();
559 
560                 // get the project resource values based on the current config
561                 mConfiguredProjectRes = project.getConfiguredResources(
562                         mConfigComposite.getCurrentConfig());
563             }
564 
565             return mConfiguredProjectRes;
566         }
567 
568         /**
569          * Returns a {@link ProjectResources} for the framework resources based on the current
570          * configuration selection.
571          * @return the framework resources or null if not found.
572          */
getFrameworkResources()573         public ResourceRepository getFrameworkResources() {
574             return getFrameworkResources(getRenderingTarget());
575         }
576 
577         /**
578          * Returns a {@link ProjectResources} for the framework resources of a given
579          * target.
580          * @param target the target for which to return the framework resources.
581          * @return the framework resources or null if not found.
582          */
getFrameworkResources(IAndroidTarget target)583         public ResourceRepository getFrameworkResources(IAndroidTarget target) {
584             if (target != null) {
585                 AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
586 
587                 if (data != null) {
588                     return data.getFrameworkResources();
589                 }
590             }
591 
592             return null;
593         }
594 
getProjectResources()595         public ProjectResources getProjectResources() {
596             if (mEditedFile != null) {
597                 ResourceManager manager = ResourceManager.getInstance();
598                 return manager.getProjectResources(mEditedFile.getProject());
599             }
600 
601             return null;
602         }
603 
604         /**
605          * Creates a new layout file from the specified {@link FolderConfiguration}.
606          */
createAlternateLayout(final FolderConfiguration config)607         private void createAlternateLayout(final FolderConfiguration config) {
608             new Job("Create Alternate Resource") {
609                 @Override
610                 protected IStatus run(IProgressMonitor monitor) {
611                     // get the folder name
612                     String folderName = config.getFolderName(ResourceFolderType.LAYOUT);
613                     try {
614 
615                         // look to see if it exists.
616                         // get the res folder
617                         IFolder res = (IFolder)mEditedFile.getParent().getParent();
618                         String path = res.getLocation().toOSString();
619 
620                         File newLayoutFolder = new File(path + File.separator + folderName);
621                         if (newLayoutFolder.isFile()) {
622                             // this should not happen since aapt would have complained
623                             // before, but if one disable the automatic build, this could
624                             // happen.
625                             String message = String.format("File 'res/%1$s' is in the way!",
626                                     folderName);
627 
628                             AdtPlugin.displayError("Layout Creation", message);
629 
630                             return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);
631                         } else if (newLayoutFolder.exists() == false) {
632                             // create it.
633                             newLayoutFolder.mkdir();
634                         }
635 
636                         // now create the file
637                         File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() +
638                                     File.separator + mEditedFile.getName());
639 
640                         newLayoutFile.createNewFile();
641 
642                         InputStream input = mEditedFile.getContents();
643 
644                         FileOutputStream fos = new FileOutputStream(newLayoutFile);
645 
646                         byte[] data = new byte[512];
647                         int count;
648                         while ((count = input.read(data)) != -1) {
649                             fos.write(data, 0, count);
650                         }
651 
652                         input.close();
653                         fos.close();
654 
655                         // refreshes the res folder to show up the new
656                         // layout folder (if needed) and the file.
657                         // We use a progress monitor to catch the end of the refresh
658                         // to trigger the edit of the new file.
659                         res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() {
660                             public void done() {
661                                 mConfigComposite.getDisplay().asyncExec(new Runnable() {
662                                     public void run() {
663                                         onConfigurationChange();
664                                     }
665                                 });
666                             }
667 
668                             public void beginTask(String name, int totalWork) {
669                                 // pass
670                             }
671 
672                             public void internalWorked(double work) {
673                                 // pass
674                             }
675 
676                             public boolean isCanceled() {
677                                 // pass
678                                 return false;
679                             }
680 
681                             public void setCanceled(boolean value) {
682                                 // pass
683                             }
684 
685                             public void setTaskName(String name) {
686                                 // pass
687                             }
688 
689                             public void subTask(String name) {
690                                 // pass
691                             }
692 
693                             public void worked(int work) {
694                                 // pass
695                             }
696                         });
697                     } catch (IOException e2) {
698                         String message = String.format(
699                                 "Failed to create File 'res/%1$s/%2$s' : %3$s",
700                                 folderName, mEditedFile.getName(), e2.getMessage());
701 
702                         AdtPlugin.displayError("Layout Creation", message);
703 
704                         return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
705                                 message, e2);
706                     } catch (CoreException e2) {
707                         String message = String.format(
708                                 "Failed to create File 'res/%1$s/%2$s' : %3$s",
709                                 folderName, mEditedFile.getName(), e2.getMessage());
710 
711                         AdtPlugin.displayError("Layout Creation", message);
712 
713                         return e2.getStatus();
714                     }
715 
716                     return Status.OK_STATUS;
717 
718                 }
719             }.schedule();
720         }
721 
722         /**
723          * When the device changes, zoom the view to fit, but only up to 100% (e.g. zoom
724          * out to fit the content, or zoom back in if we were zoomed out more from the
725          * previous view, but only up to 100% such that we never blow up pixels
726          */
onDevicePostChange()727         public void onDevicePostChange() {
728             if (mActionBar.isZoomingAllowed()) {
729                 getCanvasControl().setFitScale(true);
730             }
731         }
732 
getIncludedWithin()733         public String getIncludedWithin() {
734             return mIncludedWithin != null ? mIncludedWithin.getName() : null;
735         }
736     }
737 
738     /**
739      * Listens to target changed in the current project, to trigger a new layout rendering.
740      */
741     private class TargetListener implements ITargetChangeListener {
742 
onProjectTargetChange(IProject changedProject)743         public void onProjectTargetChange(IProject changedProject) {
744             if (changedProject != null && changedProject.equals(getProject())) {
745                 updateEditor();
746             }
747         }
748 
onTargetLoaded(IAndroidTarget loadedTarget)749         public void onTargetLoaded(IAndroidTarget loadedTarget) {
750             IAndroidTarget target = getRenderingTarget();
751             if (target != null && target.equals(loadedTarget)) {
752                 updateEditor();
753             }
754         }
755 
onSdkLoaded()756         public void onSdkLoaded() {
757             // get the current rendering target to unload it
758             IAndroidTarget oldTarget = getRenderingTarget();
759             preRenderingTargetChangeCleanUp(oldTarget);
760 
761             computeSdkVersion();
762 
763             // get the project target
764             Sdk currentSdk = Sdk.getCurrent();
765             if (currentSdk != null) {
766                 IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
767                 if (target != null) {
768                     mConfigComposite.onSdkLoaded(target);
769                     mConfigListener.onConfigurationChange();
770                 }
771             }
772         }
773 
updateEditor()774         private void updateEditor() {
775             mLayoutEditor.commitPages(false /* onSave */);
776 
777             // because the target changed we must reset the configured resources.
778             mConfiguredFrameworkRes = mConfiguredProjectRes = null;
779             mResourceResolver = null;
780 
781             // make sure we remove the custom view loader, since its parent class loader is the
782             // bridge class loader.
783             mProjectCallback = null;
784 
785             // recreate the ui root node always, this will also call onTargetChange
786             // on the config composite
787             mLayoutEditor.initUiRootNode(true /*force*/);
788         }
789 
getProject()790         private IProject getProject() {
791             return getLayoutEditor().getProject();
792         }
793     }
794 
795     /** Refresh the configured project resources associated with this editor */
refreshProjectResources()796     public void refreshProjectResources() {
797         mConfiguredProjectRes = null;
798         mResourceResolver = null;
799     }
800 
801     /**
802      * Returns the currently edited file
803      *
804      * @return the currently edited file, or null
805      */
getEditedFile()806     public IFile getEditedFile() {
807         return mEditedFile;
808     }
809 
810     /**
811      * Returns the project for the currently edited file, or null
812      *
813      * @return the project containing the edited file, or null
814      */
getProject()815     public IProject getProject() {
816         if (mEditedFile != null) {
817             return mEditedFile.getProject();
818         } else {
819             return null;
820         }
821     }
822 
823     // ----------------
824 
825     /**
826      * Save operation in the Graphical Editor Part.
827      * <p/>
828      * In our workflow, the model is owned by the Structured XML Editor.
829      * The graphical layout editor just displays it -- thus we don't really
830      * save anything here.
831      * <p/>
832      * This must NOT call the parent editor part. At the contrary, the parent editor
833      * part will call this *after* having done the actual save operation.
834      * <p/>
835      * The only action this editor must do is mark the undo command stack as
836      * being no longer dirty.
837      */
838     @Override
doSave(IProgressMonitor monitor)839     public void doSave(IProgressMonitor monitor) {
840         // TODO implement a command stack
841 //        getCommandStack().markSaveLocation();
842 //        firePropertyChange(PROP_DIRTY);
843     }
844 
845     /**
846      * Save operation in the Graphical Editor Part.
847      * <p/>
848      * In our workflow, the model is owned by the Structured XML Editor.
849      * The graphical layout editor just displays it -- thus we don't really
850      * save anything here.
851      */
852     @Override
doSaveAs()853     public void doSaveAs() {
854         // pass
855     }
856 
857     /**
858      * In our workflow, the model is owned by the Structured XML Editor.
859      * The graphical layout editor just displays it -- thus we don't really
860      * save anything here.
861      */
862     @Override
isDirty()863     public boolean isDirty() {
864         return false;
865     }
866 
867     /**
868      * In our workflow, the model is owned by the Structured XML Editor.
869      * The graphical layout editor just displays it -- thus we don't really
870      * save anything here.
871      */
872     @Override
isSaveAsAllowed()873     public boolean isSaveAsAllowed() {
874         return false;
875     }
876 
877     @Override
setFocus()878     public void setFocus() {
879         // TODO Auto-generated method stub
880 
881     }
882 
883     /**
884      * Responds to a page change that made the Graphical editor page the activated page.
885      */
activated()886     public void activated() {
887         if (!mActive) {
888             mActive = true;
889 
890             boolean changed = mConfigComposite.syncRenderState();
891             if (changed) {
892                 // Will also force recomputeLayout()
893                 return;
894             }
895 
896             if (mNeedsRecompute) {
897                 recomputeLayout();
898             }
899         }
900     }
901 
902     /**
903      * Responds to a page change that made the Graphical editor page the deactivated page
904      */
deactivated()905     public void deactivated() {
906         mActive = false;
907     }
908 
909     /**
910      * Opens and initialize the editor with a new file.
911      * @param file the file being edited.
912      */
openFile(IFile file)913     public void openFile(IFile file) {
914         mEditedFile = file;
915         mConfigComposite.setFile(mEditedFile);
916 
917         if (mReloadListener == null) {
918             mReloadListener = new ReloadListener();
919             LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener);
920         }
921 
922         if (mRulesEngine == null) {
923             mRulesEngine = new RulesEngine(this, mEditedFile.getProject());
924             if (mCanvasViewer != null) {
925                 mCanvasViewer.getCanvas().setRulesEngine(mRulesEngine);
926             }
927         }
928 
929         // Pick up hand-off data: somebody requesting this file to be opened may have
930         // requested that it should be opened as included within another file
931         if (mEditedFile != null) {
932             try {
933                 mIncludedWithin = (Reference) mEditedFile.getSessionProperty(NAME_INCLUDE);
934                 if (mIncludedWithin != null) {
935                     // Only use once
936                     mEditedFile.setSessionProperty(NAME_INCLUDE, null);
937                 }
938             } catch (CoreException e) {
939                 AdtPlugin.log(e, "Can't access session property %1$s", NAME_INCLUDE);
940             }
941         }
942 
943         computeSdkVersion();
944     }
945 
946     /**
947      * Resets the editor with a replacement file.
948      * @param file the replacement file.
949      */
replaceFile(IFile file)950     public void replaceFile(IFile file) {
951         mEditedFile = file;
952         mConfigComposite.replaceFile(mEditedFile);
953         computeSdkVersion();
954     }
955 
956     /**
957      * Resets the editor with a replacement file coming from a config change in the config
958      * selector.
959      * @param file the replacement file.
960      */
changeFileOnNewConfig(IFile file)961     public void changeFileOnNewConfig(IFile file) {
962         mEditedFile = file;
963         mConfigComposite.changeFileOnNewConfig(mEditedFile);
964     }
965 
966     /**
967      * Responds to a target change for the project of the edited file
968      */
onTargetChange()969     public void onTargetChange() {
970         AndroidTargetData targetData = mConfigComposite.onXmlModelLoaded();
971         updateCapabilities(targetData);
972 
973         mConfigListener.onConfigurationChange();
974     }
975 
976     /** Updates the capabilities for the given target data (which may be null) */
updateCapabilities(AndroidTargetData targetData)977     private void updateCapabilities(AndroidTargetData targetData) {
978         if (targetData != null) {
979             LayoutLibrary layoutLib = targetData.getLayoutLibrary();
980             if (mIncludedWithin != null &&  !layoutLib.supports(Capability.EMBEDDED_LAYOUT)) {
981                 showIn(null);
982             }
983         }
984     }
985 
getLayoutEditor()986     public LayoutEditor getLayoutEditor() {
987         return mLayoutEditor;
988     }
989 
990     /**
991      * Returns the {@link RulesEngine} associated with this editor
992      *
993      * @return the {@link RulesEngine} associated with this editor, never null
994      */
getRulesEngine()995     public RulesEngine getRulesEngine() {
996         return mRulesEngine;
997     }
998 
999     /**
1000      * Return the {@link LayoutCanvas} associated with this editor
1001      *
1002      * @return the associated {@link LayoutCanvas}
1003      */
getCanvasControl()1004     public LayoutCanvas getCanvasControl() {
1005         if (mCanvasViewer != null) {
1006             return mCanvasViewer.getCanvas();
1007         }
1008         return null;
1009     }
1010 
getModel()1011     public UiDocumentNode getModel() {
1012         return mLayoutEditor.getUiRootNode();
1013     }
1014 
1015     /**
1016      * Callback for XML model changed. Only update/recompute the layout if the editor is visible
1017      */
onXmlModelChanged()1018     public void onXmlModelChanged() {
1019         // To optimize the rendering when the user is editing in the XML pane, we don't
1020         // refresh the editor if it's not the active part.
1021         //
1022         // This behavior is acceptable when the editor is the single "full screen" part
1023         // (as in this case active means visible.)
1024         // Unfortunately this breaks in 2 cases:
1025         // - when performing a drag'n'drop from one editor to another, the target is not
1026         //   properly refreshed before it becomes active.
1027         // - when duplicating the editor window and placing both editors side by side (xml in one
1028         //   and canvas in the other one), the canvas may not be refreshed when the XML is edited.
1029         //
1030         // TODO find a way to really query whether the pane is visible, not just active.
1031 
1032         if (mLayoutEditor.isGraphicalEditorActive()) {
1033             recomputeLayout();
1034         } else {
1035             // Remember we want to recompute as soon as the editor becomes active.
1036             mNeedsRecompute = true;
1037         }
1038     }
1039 
recomputeLayout()1040     public void recomputeLayout() {
1041         try {
1042             if (!ensureFileValid()) {
1043                 return;
1044             }
1045 
1046             UiDocumentNode model = getModel();
1047             if (!ensureModelValid(model)) {
1048                 // Although we display an error, we still treat an empty document as a
1049                 // successful layout result so that we can drop new elements in it.
1050                 //
1051                 // For that purpose, create a special LayoutScene that has no image,
1052                 // no root view yet indicates success and then update the canvas with it.
1053 
1054                 mCanvasViewer.getCanvas().setSession(
1055                         new StaticRenderSession(
1056                                 Result.Status.SUCCESS.createResult(),
1057                                 null /*rootViewInfo*/, null /*image*/),
1058                         null /*explodeNodes*/, true /* layoutlib5 */);
1059                 return;
1060             }
1061 
1062             LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/);
1063 
1064             if (layoutLib != null) {
1065                 // if drawing in real size, (re)set the scaling factor.
1066                 if (mActionBar.isZoomingRealSize()) {
1067                     mActionBar.computeAndSetRealScale(false /* redraw */);
1068                 }
1069 
1070                 IProject project = mEditedFile.getProject();
1071                 renderWithBridge(project, model, layoutLib);
1072             }
1073         } finally {
1074             // no matter the result, we are done doing the recompute based on the latest
1075             // resource/code change.
1076             mNeedsRecompute = false;
1077         }
1078     }
1079 
reloadPalette()1080     public void reloadPalette() {
1081         if (mPalette != null) {
1082             IAndroidTarget renderingTarget = getRenderingTarget();
1083             if (renderingTarget != null) {
1084                 mPalette.reloadPalette(renderingTarget);
1085             }
1086         }
1087     }
1088 
1089     /**
1090      * Returns the {@link LayoutLibrary} associated with this editor, if it has
1091      * been initialized already. May return null if it has not been initialized (or has
1092      * not finished initializing).
1093      *
1094      * @return The {@link LayoutLibrary}, or null
1095      */
getLayoutLibrary()1096     public LayoutLibrary getLayoutLibrary() {
1097         return getReadyLayoutLib(false /*displayError*/);
1098     }
1099 
1100     /**
1101      * Returns the current bounds of the Android device screen, in canvas control pixels.
1102      *
1103      * @return the bounds of the screen, never null
1104      */
getScreenBounds()1105     public Rect getScreenBounds() {
1106         return mConfigComposite.getScreenBounds();
1107     }
1108 
1109     /**
1110      * Returns the scale to multiply pixels in the layout coordinate space with to obtain
1111      * the corresponding dip (device independent pixel)
1112      *
1113      * @return the scale to multiple layout coordinates with to obtain the dip position
1114      */
getDipScale()1115     public float getDipScale() {
1116         return Density.DEFAULT_DENSITY / (float) mConfigComposite.getDensity().getDpiValue();
1117     }
1118 
1119     // --- private methods ---
1120 
1121     /**
1122      * Ensure that the file associated with this editor is valid (exists and is
1123      * synchronized). Any reasons why it is not are displayed in the editor's error area.
1124      *
1125      * @return True if the editor is valid, false otherwise.
1126      */
ensureFileValid()1127     private boolean ensureFileValid() {
1128         // check that the resource exists. If the file is opened but the project is closed
1129         // or deleted for some reason (changed from outside of eclipse), then this will
1130         // return false;
1131         if (mEditedFile.exists() == false) {
1132             displayError("Resource '%1$s' does not exist.",
1133                          mEditedFile.getFullPath().toString());
1134             return false;
1135         }
1136 
1137         if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {
1138             String message = String.format("%1$s is out of sync. Please refresh.",
1139                     mEditedFile.getName());
1140 
1141             displayError(message);
1142 
1143             // also print it in the error console.
1144             IProject iProject = mEditedFile.getProject();
1145             AdtPlugin.printErrorToConsole(iProject.getName(), message);
1146             return false;
1147         }
1148 
1149         return true;
1150     }
1151 
1152     /**
1153      * Returns a {@link LayoutLibrary} that is ready for rendering, or null if the bridge
1154      * is not available or not ready yet (due to SDK loading still being in progress etc).
1155      * If enabled, any reasons preventing the bridge from being returned are displayed to the
1156      * editor's error area.
1157      *
1158      * @param displayError whether to display the loading error or not.
1159      *
1160      * @return LayoutBridge the layout bridge for rendering this editor's scene
1161      */
getReadyLayoutLib(boolean displayError)1162     LayoutLibrary getReadyLayoutLib(boolean displayError) {
1163         Sdk currentSdk = Sdk.getCurrent();
1164         if (currentSdk != null) {
1165             IAndroidTarget target = getRenderingTarget();
1166 
1167             if (target != null) {
1168                 AndroidTargetData data = currentSdk.getTargetData(target);
1169                 if (data != null) {
1170                     LayoutLibrary layoutLib = data.getLayoutLibrary();
1171 
1172                     if (layoutLib.getStatus() == LoadStatus.LOADED) {
1173                         return layoutLib;
1174                     } else if (displayError) { // getBridge() == null
1175                         // SDK is loaded but not the layout library!
1176 
1177                         // check whether the bridge managed to load, or not
1178                         if (layoutLib.getStatus() == LoadStatus.LOADING) {
1179                             displayError("Eclipse is loading framework information and the layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",
1180                                          mEditedFile.getName());
1181                         } else {
1182                             String message = layoutLib.getLoadMessage();
1183                             displayError("Eclipse failed to load the framework information and the layout library!" +
1184                                     message != null ? "\n" + message : "");
1185                         }
1186                     }
1187                 } else { // data == null
1188                     // It can happen that the workspace refreshes while the SDK is loading its
1189                     // data, which could trigger a redraw of the opened layout if some resources
1190                     // changed while Eclipse is closed.
1191                     // In this case data could be null, but this is not an error.
1192                     // We can just silently return, as all the opened editors are automatically
1193                     // refreshed once the SDK finishes loading.
1194                     LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null);
1195 
1196                     // display error is asked.
1197                     if (displayError) {
1198                         String targetName = target.getName();
1199                         switch (targetLoadStatus) {
1200                             case LOADING:
1201                                 String s;
1202                                 if (currentSdk.getTarget(getProject()) == target) {
1203                                     s = String.format(
1204                                             "The project target (%1$s) is still loading.",
1205                                             targetName);
1206                                 } else {
1207                                     s = String.format(
1208                                             "The rendering target (%1$s) is still loading.",
1209                                             targetName);
1210                                 }
1211                                 s += "\nThe layout will refresh automatically once the process is finished.";
1212                                 displayError(s);
1213 
1214                                 break;
1215                             case FAILED: // known failure
1216                             case LOADED: // success but data isn't loaded?!?!
1217                                 displayError("The project target (%s) was not properly loaded.",
1218                                         targetName);
1219                                 break;
1220                         }
1221                     }
1222                 }
1223 
1224             } else if (displayError) { // target == null
1225                 displayError("The project target is not set.");
1226             }
1227         } else if (displayError) { // currentSdk == null
1228             displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",
1229                          mEditedFile.getName());
1230         }
1231 
1232         return null;
1233     }
1234 
1235     /**
1236      * Returns the {@link IAndroidTarget} used for the rendering.
1237      * <p/>
1238      * This first looks for the rendering target setup in the config UI, and if nothing has
1239      * been setup yet, returns the target of the project.
1240      *
1241      * @return an IAndroidTarget object or null if no target is setup and the project has no
1242      * target set.
1243      *
1244      */
getRenderingTarget()1245     public IAndroidTarget getRenderingTarget() {
1246         // if the SDK is null no targets are loaded.
1247         Sdk currentSdk = Sdk.getCurrent();
1248         if (currentSdk == null) {
1249             return null;
1250         }
1251 
1252         assert mConfigComposite.getDisplay().getThread() == Thread.currentThread();
1253 
1254         // attempt to get a target from the configuration selector.
1255         IAndroidTarget renderingTarget = mConfigComposite.getRenderingTarget();
1256         if (renderingTarget != null) {
1257             return renderingTarget;
1258         }
1259 
1260         // fall back to the project target
1261         if (mEditedFile != null) {
1262             return currentSdk.getTarget(mEditedFile.getProject());
1263         }
1264 
1265         return null;
1266     }
1267 
1268     /**
1269      * Returns whether the current rendering target supports the given capability
1270      *
1271      * @param capability the capability to be looked up
1272      * @return true if the current rendering target supports the given capability
1273      */
renderingSupports(Capability capability)1274     public boolean renderingSupports(Capability capability) {
1275         IAndroidTarget target = getRenderingTarget();
1276         if (target != null) {
1277             AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target);
1278             LayoutLibrary layoutLib = targetData.getLayoutLibrary();
1279             return layoutLib.supports(capability);
1280         }
1281 
1282         return false;
1283     }
1284 
ensureModelValid(UiDocumentNode model)1285     private boolean ensureModelValid(UiDocumentNode model) {
1286         // check there is actually a model (maybe the file is empty).
1287         if (model.getUiChildren().size() == 0) {
1288             displayError(
1289                     "No XML content. Please add a root view or layout to your document.");
1290             return false;
1291         }
1292 
1293         return true;
1294     }
1295 
renderWithBridge(IProject iProject, UiDocumentNode model, LayoutLibrary layoutLib)1296     private void renderWithBridge(IProject iProject, UiDocumentNode model,
1297             LayoutLibrary layoutLib) {
1298         LayoutCanvas canvas = getCanvasControl();
1299         Set<UiElementNode> explodeNodes = canvas.getNodesToExplode();
1300         Rect rect = getScreenBounds();
1301         RenderLogger logger = new RenderLogger(mEditedFile.getName());
1302         RenderingMode renderingMode = RenderingMode.NORMAL;
1303         // FIXME set the rendering mode using ViewRule or something.
1304         List<UiElementNode> children = model.getUiChildren();
1305         if (children.size() > 0 &&
1306                 children.get(0).getDescriptor().getXmlLocalName().equals(SCROLL_VIEW)) {
1307             renderingMode = RenderingMode.V_SCROLL;
1308         }
1309 
1310         RenderSession session = RenderService.create(this)
1311             .setModel(model)
1312             .setSize(rect.w, rect.h)
1313             .setLog(logger)
1314             .setRenderingMode(renderingMode)
1315             .setIncludedWithin(mIncludedWithin)
1316             .setNodesToExpand(explodeNodes)
1317             .createRenderSession();
1318 
1319         boolean layoutlib5 = layoutLib.supports(Capability.EMBEDDED_LAYOUT);
1320         canvas.setSession(session, explodeNodes, layoutlib5);
1321 
1322         // update the UiElementNode with the layout info.
1323         if (session != null && session.getResult().isSuccess() == false) {
1324             // An error was generated. Print it (and any other accumulated warnings)
1325             String errorMessage = session.getResult().getErrorMessage();
1326             Throwable exception = session.getResult().getException();
1327             if (exception != null && errorMessage == null) {
1328                 errorMessage = exception.toString();
1329             }
1330             if (exception != null || (errorMessage != null && errorMessage.length() > 0)) {
1331                 logger.error(null, errorMessage, exception, null /*data*/);
1332             } else if (!logger.hasProblems()) {
1333                 logger.error(null, "Unexpected error in rendering, no details given",
1334                         null /*data*/);
1335             }
1336             // These errors will be included in the log warnings which are
1337             // displayed regardless of render success status below
1338         }
1339 
1340         // We might have detected some missing classes and swapped them by a mock view,
1341         // or run into fidelity warnings or missing resources, so emit all these
1342         // warnings
1343         Set<String> missingClasses = mProjectCallback.getMissingClasses();
1344         Set<String> brokenClasses = mProjectCallback.getUninstantiatableClasses();
1345         if (logger.hasProblems()) {
1346             displayLoggerProblems(iProject, logger);
1347             displayFailingClasses(missingClasses, brokenClasses, true);
1348         } else if (missingClasses.size() > 0 || brokenClasses.size() > 0) {
1349             displayFailingClasses(missingClasses, brokenClasses, false);
1350         } else {
1351             // Nope, no missing or broken classes. Clear success, congrats!
1352             hideError();
1353         }
1354 
1355         model.refreshUi();
1356     }
1357 
1358     /**
1359      * Returns the {@link ResourceResolver} for this editor
1360      *
1361      * @return the resolver used to resolve resources for the current configuration of
1362      *         this editor, or null
1363      */
getResourceResolver()1364     public ResourceResolver getResourceResolver() {
1365         if (mResourceResolver == null) {
1366             String theme = mConfigComposite.getTheme();
1367             if (theme == null) {
1368                 displayError("Missing theme.");
1369                 return null;
1370             }
1371             boolean isProjectTheme = mConfigComposite.isProjectTheme();
1372 
1373             Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
1374                 mConfigListener.getConfiguredProjectResources();
1375 
1376             // Get the framework resources
1377             Map<ResourceType, Map<String, ResourceValue>> frameworkResources =
1378                 mConfigListener.getConfiguredFrameworkResources();
1379 
1380             if (configuredProjectRes == null) {
1381                 displayError("Missing project resources for current configuration.");
1382                 return null;
1383             }
1384 
1385             if (frameworkResources == null) {
1386                 displayError("Missing framework resources.");
1387                 return null;
1388             }
1389 
1390             mResourceResolver = ResourceResolver.create(
1391                     configuredProjectRes, frameworkResources,
1392                     theme, isProjectTheme);
1393         }
1394 
1395         return mResourceResolver;
1396     }
1397 
1398     /** Returns a project callback, and optionally resets it */
getProjectCallback(boolean reset, LayoutLibrary layoutLibrary)1399     ProjectCallback getProjectCallback(boolean reset, LayoutLibrary layoutLibrary) {
1400         // Lazily create the project callback the first time we need it
1401         if (mProjectCallback == null) {
1402             ResourceManager resManager = ResourceManager.getInstance();
1403             IProject project = getProject();
1404             ProjectResources projectRes = resManager.getProjectResources(project);
1405             mProjectCallback = new ProjectCallback(layoutLibrary, projectRes, project);
1406         } else if (reset) {
1407             // Also clears the set of missing/broken classes prior to rendering
1408             mProjectCallback.getMissingClasses().clear();
1409             mProjectCallback.getUninstantiatableClasses().clear();
1410         }
1411 
1412         return mProjectCallback;
1413     }
1414 
1415     /**
1416      * Returns the resource name of this layout, NOT including the @layout/ prefix
1417      *
1418      * @return the resource name of this layout, NOT including the @layout/ prefix
1419      */
getLayoutResourceName()1420     public String getLayoutResourceName() {
1421         String name = mEditedFile.getName();
1422         int dotIndex = name.indexOf('.');
1423         if (dotIndex != -1) {
1424             name = name.substring(0, dotIndex);
1425         }
1426         return name;
1427     }
1428 
1429     /**
1430      * Cleans up when the rendering target is about to change
1431      * @param oldTarget the old rendering target.
1432      */
preRenderingTargetChangeCleanUp(IAndroidTarget oldTarget)1433     private void preRenderingTargetChangeCleanUp(IAndroidTarget oldTarget) {
1434         // first clear the caches related to this file in the old target
1435         Sdk currentSdk = Sdk.getCurrent();
1436         if (currentSdk != null) {
1437             AndroidTargetData data = currentSdk.getTargetData(oldTarget);
1438             if (data != null) {
1439                 LayoutLibrary layoutLib = data.getLayoutLibrary();
1440 
1441                 // layoutLib can never be null.
1442                 layoutLib.clearCaches(mEditedFile.getProject());
1443             }
1444         }
1445 
1446         // Also remove the ProjectCallback as it caches custom views which must be reloaded
1447         // with the classloader of the new LayoutLib. We also have to clear it out
1448         // because it stores a reference to the layout library which could have changed.
1449         mProjectCallback = null;
1450 
1451         // FIXME: get rid of the current LayoutScene if any.
1452     }
1453 
1454     private class ReloadListener implements ILayoutReloadListener {
1455         /**
1456          * Called when the file changes triggered a redraw of the layout
1457          */
reloadLayout(final ChangeFlags flags, final boolean libraryChanged)1458         public void reloadLayout(final ChangeFlags flags, final boolean libraryChanged) {
1459             if (mConfigComposite.isDisposed()) {
1460                 return;
1461             }
1462             Display display = mConfigComposite.getDisplay();
1463             display.asyncExec(new Runnable() {
1464                 public void run() {
1465                     reloadLayoutSwt(flags, libraryChanged);
1466                 }
1467             });
1468         }
1469 
1470         /** Reload layout. <b>Must be called on the SWT thread</b> */
reloadLayoutSwt(ChangeFlags flags, boolean libraryChanged)1471         private void reloadLayoutSwt(ChangeFlags flags, boolean libraryChanged) {
1472             if (mConfigComposite.isDisposed()) {
1473                 return;
1474             }
1475             assert mConfigComposite.getDisplay().getThread() == Thread.currentThread();
1476 
1477             boolean recompute = false;
1478             // we only care about the r class of the main project.
1479             if (flags.rClass && libraryChanged == false) {
1480                 recompute = true;
1481                 if (mEditedFile != null) {
1482                     ResourceManager manager = ResourceManager.getInstance();
1483                     ProjectResources projectRes = manager.getProjectResources(
1484                             mEditedFile.getProject());
1485 
1486                     if (projectRes != null) {
1487                         projectRes.resetDynamicIds();
1488                     }
1489                 }
1490             }
1491 
1492             if (flags.localeList) {
1493                 // the locale list *potentially* changed so we update the locale in the
1494                 // config composite.
1495                 // However there's no recompute, as it could not be needed
1496                 // (for instance a new layout)
1497                 // If a resource that's not a layout changed this will trigger a recompute anyway.
1498                 mConfigComposite.updateLocales();
1499             }
1500 
1501             // if a resources was modified.
1502             if (flags.resources) {
1503                 recompute = true;
1504 
1505                 // TODO: differentiate between single and multi resource file changed, and whether
1506                 // the resource change affects the cache.
1507 
1508                 // force a reparse in case a value XML file changed.
1509                 mConfiguredProjectRes = null;
1510                 mResourceResolver = null;
1511 
1512                 // clear the cache in the bridge in case a bitmap/9-patch changed.
1513                 LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/);
1514                 if (layoutLib != null) {
1515                     layoutLib.clearCaches(mEditedFile.getProject());
1516                 }
1517             }
1518 
1519             if (flags.code) {
1520                 // only recompute if the custom view loader was used to load some code.
1521                 if (mProjectCallback != null && mProjectCallback.isUsed()) {
1522                     mProjectCallback = null;
1523                     recompute = true;
1524                 }
1525             }
1526 
1527             if (flags.manifest) {
1528                 recompute |= computeSdkVersion();
1529             }
1530 
1531             if (recompute) {
1532                 if (mLayoutEditor.isGraphicalEditorActive()) {
1533                     recomputeLayout();
1534                 } else {
1535                     mNeedsRecompute = true;
1536                 }
1537             }
1538         }
1539     }
1540 
1541     // ---- Error handling ----
1542 
1543     /**
1544      * Switches the sash to display the error label.
1545      *
1546      * @param errorFormat The new error to display if not null.
1547      * @param parameters String.format parameters for the error format.
1548      */
displayError(String errorFormat, Object...parameters)1549     private void displayError(String errorFormat, Object...parameters) {
1550         if (errorFormat != null) {
1551             mErrorLabel.setText(String.format(errorFormat, parameters));
1552         } else {
1553             mErrorLabel.setText("");
1554         }
1555         mSashError.setMaximizedControl(null);
1556     }
1557 
1558     /** Displays the canvas and hides the error label. */
hideError()1559     private void hideError() {
1560         mErrorLabel.setText("");
1561         mSashError.setMaximizedControl(mCanvasViewer.getControl());
1562     }
1563 
1564     /**
1565      * Switches the sash to display the error label to show a list of
1566      * missing classes and give options to create them.
1567      */
displayFailingClasses(Set<String> missingClasses, Set<String> brokenClasses, boolean append)1568     private void displayFailingClasses(Set<String> missingClasses, Set<String> brokenClasses,
1569             boolean append) {
1570         if (missingClasses.size() == 0 && brokenClasses.size() == 0) {
1571             return;
1572         }
1573 
1574         if (!append) {
1575             mErrorLabel.setText("");    //$NON-NLS-1$
1576         } else {
1577             addText(mErrorLabel, "\n"); //$NON-NLS-1$
1578         }
1579 
1580         if (missingClasses.size() > 0) {
1581             addText(mErrorLabel, "The following classes could not be found:\n");
1582             for (String clazz : missingClasses) {
1583                 addText(mErrorLabel, "- ");
1584                 addText(mErrorLabel, clazz);
1585                 addText(mErrorLabel, " (");
1586 
1587                 IProject project = getProject();
1588                 Collection<String> customViews = getCustomViewClassNames(project);
1589                 addTypoSuggestions(clazz, customViews, false);
1590                 addTypoSuggestions(clazz, customViews, true);
1591                 addTypoSuggestions(clazz, getAndroidViewClassNames(project), false);
1592 
1593                 addActionLink(mErrorLabel,
1594                         ActionLinkStyleRange.LINK_FIX_BUILD_PATH, "Fix Build Path", clazz, null);
1595                 addText(mErrorLabel, ", ");
1596                 addActionLink(mErrorLabel,
1597                         ActionLinkStyleRange.LINK_EDIT_XML, "Edit XML", clazz, null);
1598                 if (clazz.indexOf('.') != -1) {
1599                     // Add "Create Class" link, but only for custom views
1600                     addText(mErrorLabel, ", ");
1601                     addActionLink(mErrorLabel,
1602                             ActionLinkStyleRange.LINK_CREATE_CLASS, "Create Class", clazz, null);
1603                 }
1604                 addText(mErrorLabel, ")\n");
1605             }
1606         }
1607         if (brokenClasses.size() > 0) {
1608             addText(mErrorLabel, "The following classes could not be instantiated:\n");
1609 
1610             // Do we have a custom class (not an Android or add-ons class)
1611             boolean haveCustomClass = false;
1612 
1613             for (String clazz : brokenClasses) {
1614                 addText(mErrorLabel, "- ");
1615                 addText(mErrorLabel, " (");
1616                 addActionLink(mErrorLabel,
1617                         ActionLinkStyleRange.LINK_OPEN_CLASS, "Open Class", clazz, null);
1618                 addText(mErrorLabel, ", ");
1619                 addActionLink(mErrorLabel,
1620                         ActionLinkStyleRange.LINK_SHOW_LOG, "Show Error Log", clazz, null);
1621                 addText(mErrorLabel, ")\n");
1622 
1623                 if (!(clazz.startsWith("android.") || //$NON-NLS-1$
1624                         clazz.startsWith("com.google."))) { //$NON-NLS-1$
1625                     haveCustomClass = true;
1626                 }
1627             }
1628 
1629             addText(mErrorLabel, "See the Error Log (Window > Show View) for more details.\n");
1630 
1631             if (haveCustomClass) {
1632                 addText(mErrorLabel, "Tip: Use View.isInEditMode() in your custom views "
1633                         + "to skip code when shown in Eclipse");
1634             }
1635         }
1636 
1637         mSashError.setMaximizedControl(null);
1638     }
1639 
addTypoSuggestions(String actual, Collection<String> views, boolean compareWithPackage)1640     private void addTypoSuggestions(String actual, Collection<String> views,
1641             boolean compareWithPackage) {
1642         if (views.size() == 0) {
1643             return;
1644         }
1645 
1646         // Look for typos and try to match with custom views and android views
1647         String actualBase = actual.substring(actual.lastIndexOf('.') + 1);
1648         if (views.size() > 0) {
1649             for (String suggested : views) {
1650                 String suggestedBase = suggested.substring(suggested.lastIndexOf('.') + 1);
1651 
1652                 String matchWith = compareWithPackage ? suggested : suggestedBase;
1653                 int maxDistance = actualBase.length() >= 4 ? 2 : 1;
1654                 if (Math.abs(actualBase.length() - matchWith.length()) > maxDistance) {
1655                     // The string lengths differ more than the allowed edit distance;
1656                     // no point in even attempting to compute the edit distance (requires
1657                     // O(n*m) storage and O(n*m) speed, where n and m are the string lengths)
1658                     continue;
1659                 }
1660                 if (AdtUtils.editDistance(actualBase, matchWith) <= maxDistance) {
1661                     // Suggest this class as a typo for the given class
1662                     String labelClass = (suggestedBase.equals(actual) || actual.indexOf('.') != -1)
1663                         ? suggested : suggestedBase;
1664                     addActionLink(mErrorLabel,
1665                             ActionLinkStyleRange.LINK_CHANGE_CLASS_TO,
1666                             String.format("Change to %1$s",
1667                                     // Only show full package name if class name
1668                                     // is the same
1669                                     labelClass),
1670                                     actual,
1671                                     viewNeedsPackage(suggested) ? suggested : suggestedBase
1672                     );
1673                     addText(mErrorLabel, ", ");
1674                 }
1675             }
1676         }
1677     }
1678 
getCustomViewClassNames(IProject project)1679     private static Collection<String> getCustomViewClassNames(IProject project) {
1680         CustomViewFinder finder = CustomViewFinder.get(project);
1681         Collection<String> views = finder.getAllViews();
1682         if (views == null) {
1683             finder.refresh();
1684             views = finder.getAllViews();
1685         }
1686 
1687         return views;
1688     }
1689 
getAndroidViewClassNames(IProject project)1690     private static Collection<String> getAndroidViewClassNames(IProject project) {
1691         Sdk currentSdk = Sdk.getCurrent();
1692         IAndroidTarget target = currentSdk.getTarget(project);
1693         if (target != null) {
1694             AndroidTargetData targetData = currentSdk.getTargetData(target);
1695             LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors();
1696             return layoutDescriptors.getAllViewClassNames();
1697         }
1698 
1699         return Collections.emptyList();
1700     }
1701 
1702     /** Add a normal line of text to the styled text widget. */
addText(StyledText styledText, String...string)1703     private void addText(StyledText styledText, String...string) {
1704         for (String s : string) {
1705             styledText.append(s);
1706         }
1707     }
1708 
1709     /** Display the problem list encountered during a render */
displayLoggerProblems(IProject project, RenderLogger logger)1710     private void displayLoggerProblems(IProject project, RenderLogger logger) {
1711         if (logger.hasProblems()) {
1712             mErrorLabel.setText("");
1713             // A common source of problems is attempting to open a layout when there are
1714             // compilation errors. In this case, may not have run (or may not be up to date)
1715             // so resources cannot be looked up etc. Explain this situation to the user.
1716 
1717             boolean hasAaptErrors = false;
1718             boolean hasJavaErrors = false;
1719             try {
1720                 IMarker[] markers;
1721                 markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
1722                 if (markers.length > 0) {
1723                     for (IMarker marker : markers) {
1724                         String markerType = marker.getType();
1725                         if (markerType.equals(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER)) {
1726                             int severity = marker.getAttribute(IMarker.SEVERITY, -1);
1727                             if (severity == IMarker.SEVERITY_ERROR) {
1728                                 hasJavaErrors = true;
1729                             }
1730                         } else if (markerType.equals(AdtConstants.MARKER_AAPT_COMPILE)) {
1731                             int severity = marker.getAttribute(IMarker.SEVERITY, -1);
1732                             if (severity == IMarker.SEVERITY_ERROR) {
1733                                 hasAaptErrors = true;
1734                             }
1735                         }
1736                     }
1737                 }
1738             } catch (CoreException e) {
1739                 AdtPlugin.log(e, null);
1740             }
1741 
1742             if (logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR)) {
1743                 addBoldText(mErrorLabel,
1744                         "Missing styles. Is the correct theme chosen for this layout?\n");
1745                 addText(mErrorLabel,
1746                         "Use the Theme combo box above the layout to choose a different layout, " +
1747                         "or fix the theme style references.\n\n");
1748             }
1749 
1750             if (hasAaptErrors && logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_PREFIX)) {
1751                 // Text will automatically be wrapped by the error widget so no reason
1752                 // to insert linebreaks in this error message:
1753                 String message =
1754                     "NOTE: This project contains resource errors, so aapt did not succeed, "
1755                      + "which can cause rendering failures. "
1756                      + "Fix resource problems first.\n\n";
1757                  addBoldText(mErrorLabel, message);
1758             } else if (hasJavaErrors && mProjectCallback != null && mProjectCallback.isUsed()) {
1759                 // Text will automatically be wrapped by the error widget so no reason
1760                 // to insert linebreaks in this error message:
1761                 String message =
1762                    "NOTE: This project contains Java compilation errors, "
1763                     + "which can cause rendering failures for custom views. "
1764                     + "Fix compilation problems first.\n\n";
1765                 addBoldText(mErrorLabel, message);
1766             }
1767 
1768             String problems = logger.getProblems(false /*includeFidelityWarnings*/);
1769             addText(mErrorLabel, problems);
1770 
1771             List<String> fidelityWarnings = logger.getFidelityWarnings();
1772             if (fidelityWarnings != null && fidelityWarnings.size() > 0) {
1773                 addText(mErrorLabel,
1774                         "The graphics preview in the layout editor may not be accurate:\n");
1775                 for (String warning : fidelityWarnings) {
1776                     addText(mErrorLabel, warning + ' ');
1777                     addActionLink(mErrorLabel,
1778                             ActionLinkStyleRange.IGNORE_FIDELITY_WARNING,
1779                             "(Ignore for this session)\n", warning, null);
1780                 }
1781             }
1782 
1783             mSashError.setMaximizedControl(null);
1784         } else {
1785             mSashError.setMaximizedControl(mCanvasViewer.getControl());
1786         }
1787     }
1788 
1789     /** Appends the given text as a bold string in the given text widget */
addBoldText(StyledText styledText, String text)1790     private void addBoldText(StyledText styledText, String text) {
1791         String s = styledText.getText();
1792         int start = (s == null ? 0 : s.length());
1793 
1794         styledText.append(text);
1795         StyleRange sr = new StyleRange();
1796         sr.start = start;
1797         sr.length = text.length();
1798         sr.fontStyle = SWT.BOLD;
1799         styledText.setStyleRange(sr);
1800     }
1801 
1802     /**
1803      * Add a URL-looking link to the styled text widget.
1804      * <p/>
1805      * A mouse-click listener is setup and it interprets the link based on the
1806      * action, corresponding to the value fields in {@link ActionLinkStyleRange}.
1807      */
addActionLink(StyledText styledText, int action, String label, String data1, String data2)1808     private void addActionLink(StyledText styledText, int action, String label,
1809             String data1, String data2) {
1810         String s = styledText.getText();
1811         int start = (s == null ? 0 : s.length());
1812         styledText.append(label);
1813 
1814         StyleRange sr = new ActionLinkStyleRange(action, data1, data2);
1815         sr.start = start;
1816         sr.length = label.length();
1817         sr.fontStyle = SWT.NORMAL;
1818         sr.underlineStyle = SWT.UNDERLINE_LINK;
1819         sr.underline = true;
1820         styledText.setStyleRange(sr);
1821     }
1822 
1823     /**
1824      * Looks up the resource file corresponding to the given type
1825      *
1826      * @param type The type of resource to look up, such as {@link ResourceType#LAYOUT}
1827      * @param name The name of the resource (not including ".xml")
1828      * @param isFrameworkResource if true, the resource is a framework resource, otherwise
1829      *            it's a project resource
1830      * @return the resource file defining the named resource, or null if not found
1831      */
findResourceFile(ResourceType type, String name, boolean isFrameworkResource)1832     public IPath findResourceFile(ResourceType type, String name, boolean isFrameworkResource) {
1833         // FIXME: This code does not handle theme value resolution.
1834         // There is code to handle this, but it's in layoutlib; we should
1835         // expose that and use it here.
1836 
1837         Map<ResourceType, Map<String, ResourceValue>> map;
1838         map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes;
1839         if (map == null) {
1840             // Not yet configured
1841             return null;
1842         }
1843 
1844         Map<String, ResourceValue> layoutMap = map.get(type);
1845         if (layoutMap != null) {
1846             ResourceValue value = layoutMap.get(name);
1847             if (value != null) {
1848                 String valueStr = value.getValue();
1849                 if (valueStr.startsWith("?")) { //$NON-NLS-1$
1850                     // FIXME: It's a reference. We should resolve this properly.
1851                     return null;
1852                 }
1853                 return new Path(valueStr);
1854             }
1855         }
1856 
1857         return null;
1858     }
1859 
1860     /**
1861      * Looks up the path to the file corresponding to the given attribute value, such as
1862      * @layout/foo, which will return the foo.xml file in res/layout/. (The general format
1863      * of the resource url is {@literal @[<package_name>:]<resource_type>/<resource_name>}.
1864      *
1865      * @param url the attribute url
1866      * @return the path to the file defining this attribute, or null if not found
1867      */
findResourceFile(String url)1868     public IPath findResourceFile(String url) {
1869         if (!url.startsWith("@")) { //$NON-NLS-1$
1870             return null;
1871         }
1872         int typeEnd = url.indexOf('/', 1);
1873         if (typeEnd == -1) {
1874             return null;
1875         }
1876         int nameBegin = typeEnd + 1;
1877         int typeBegin = 1;
1878         int colon = url.lastIndexOf(':', typeEnd);
1879         boolean isFrameworkResource = false;
1880         if (colon != -1) {
1881             // The URL contains a package name.
1882             // While the url format technically allows other package names,
1883             // the platform apparently only supports @android for now (or if it does,
1884             // there are no usages in the current code base so this is not common).
1885             String packageName = url.substring(typeBegin, colon);
1886             if (ANDROID_PKG.equals(packageName)) {
1887                 isFrameworkResource = true;
1888             }
1889 
1890             typeBegin = colon + 1;
1891         }
1892 
1893         String typeName = url.substring(typeBegin, typeEnd);
1894         ResourceType type = ResourceType.getEnum(typeName);
1895         if (type == null) {
1896             return null;
1897         }
1898 
1899         String name = url.substring(nameBegin);
1900         return findResourceFile(type, name, isFrameworkResource);
1901     }
1902 
1903     /**
1904      * Resolve the given @string reference into a literal String using the current project
1905      * configuration
1906      *
1907      * @param text the text resource reference to resolve
1908      * @return the resolved string, or null
1909      */
findString(String text)1910     public String findString(String text) {
1911         if (text.startsWith(STRING_PREFIX)) {
1912             return findString(text.substring(STRING_PREFIX.length()), false);
1913         } else if (text.startsWith(ANDROID_STRING_PREFIX)) {
1914             return findString(text.substring(ANDROID_STRING_PREFIX.length()), true);
1915         } else {
1916             return text;
1917         }
1918     }
1919 
findString(String name, boolean isFrameworkResource)1920     private String findString(String name, boolean isFrameworkResource) {
1921         Map<ResourceType, Map<String, ResourceValue>> map;
1922         map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes;
1923         if (map == null) {
1924             // Not yet configured
1925             return null;
1926         }
1927 
1928         Map<String, ResourceValue> layoutMap = map.get(ResourceType.STRING);
1929         if (layoutMap != null) {
1930             ResourceValue value = layoutMap.get(name);
1931             if (value != null) {
1932                 // FIXME: This code does not handle theme value resolution.
1933                 // There is code to handle this, but it's in layoutlib; we should
1934                 // expose that and use it here.
1935                 return value.getValue();
1936             }
1937         }
1938 
1939         return null;
1940     }
1941 
1942     /**
1943      * This StyleRange represents a clickable link in the render output, where various
1944      * actions can be taken such as creating a class, opening the project chooser to
1945      * adjust the build path, etc.
1946      */
1947     private class ActionLinkStyleRange extends StyleRange {
1948         /** Create a view class */
1949         private static final int LINK_CREATE_CLASS = 1;
1950         /** Edit the build path for the current project */
1951         private static final int LINK_FIX_BUILD_PATH = 2;
1952         /** Show the XML tab */
1953         private static final int LINK_EDIT_XML = 3;
1954         /** Open the given class */
1955         private static final int LINK_OPEN_CLASS = 4;
1956         /** Show the error log */
1957         private static final int LINK_SHOW_LOG = 5;
1958         /** Change the class reference to the given fully qualified name */
1959         private static final int LINK_CHANGE_CLASS_TO = 6;
1960         /** Ignore the given fidelity warning */
1961         private static final int IGNORE_FIDELITY_WARNING = 7;
1962 
1963         /** Client data 1 - usually the class name */
1964         private final String mData1;
1965         /** Client data 2 - such as the suggested new name */
1966         private final String mData2;
1967         /** The action to be taken when the link is clicked */
1968         private final int mAction;
1969 
ActionLinkStyleRange(int action, String data1, String data2)1970         private ActionLinkStyleRange(int action, String data1, String data2) {
1971             super();
1972             mAction = action;
1973             mData1 = data1;
1974             mData2 = data2;
1975         }
1976 
1977         /** Performs the click action */
onClick()1978         public void onClick() {
1979             switch (mAction) {
1980                 case LINK_CREATE_CLASS:
1981                     createNewClass(mData1);
1982                     break;
1983                 case LINK_EDIT_XML:
1984                     mLayoutEditor.setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
1985                     break;
1986                 case LINK_FIX_BUILD_PATH:
1987                     @SuppressWarnings("restriction")
1988                     String id = BuildPathsPropertyPage.PROP_ID;
1989                     PreferencesUtil.createPropertyDialogOn(
1990                             AdtPlugin.getDisplay().getActiveShell(),
1991                             getProject(), id, null, null).open();
1992                     break;
1993                 case LINK_OPEN_CLASS:
1994                     AdtPlugin.openJavaClass(getProject(), mData1);
1995                     break;
1996                 case LINK_SHOW_LOG:
1997                     IWorkbench workbench = PlatformUI.getWorkbench();
1998                     IWorkbenchWindow workbenchWindow = workbench.getActiveWorkbenchWindow();
1999                     try {
2000                         IWorkbenchPage page = workbenchWindow.getActivePage();
2001                         page.showView("org.eclipse.pde.runtime.LogView"); //$NON-NLS-1$
2002                     } catch (PartInitException e) {
2003                         AdtPlugin.log(e, null);
2004                     }
2005                     break;
2006                 case LINK_CHANGE_CLASS_TO:
2007                     // Change class reference of mData1 to mData2
2008                     // TODO: run under undo lock
2009                     MultiTextEdit edits = new MultiTextEdit();
2010                     ISourceViewer textViewer = mLayoutEditor.getStructuredSourceViewer();
2011                     IDocument document = textViewer.getDocument();
2012                     String xml = document.get();
2013                     int index = 0;
2014                     // Replace <old with <new and </old with </new
2015                     String prefix = "<"; //$NON-NLS-1$
2016                     String find = prefix + mData1;
2017                     String replaceWith = prefix + mData2;
2018                     while (true) {
2019                         index = xml.indexOf(find, index);
2020                         if (index == -1) {
2021                             break;
2022                         }
2023                         edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
2024                         index += find.length();
2025                     }
2026                     index = 0;
2027                     prefix = "</"; //$NON-NLS-1$
2028                     find = prefix + mData1;
2029                     replaceWith = prefix + mData2;
2030                     while (true) {
2031                         index = xml.indexOf(find, index);
2032                         if (index == -1) {
2033                             break;
2034                         }
2035                         edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
2036                         index += find.length();
2037                     }
2038                     // Handle <view class="old">
2039                     index = 0;
2040                     prefix = "\""; //$NON-NLS-1$
2041                     String suffix = "\""; //$NON-NLS-1$
2042                     find = prefix + mData1 + suffix;
2043                     replaceWith = prefix + mData2 + suffix;
2044                     while (true) {
2045                         index = xml.indexOf(find, index);
2046                         if (index == -1) {
2047                             break;
2048                         }
2049                         edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
2050                         index += find.length();
2051                     }
2052                     try {
2053                         edits.apply(document);
2054                     } catch (MalformedTreeException e) {
2055                         AdtPlugin.log(e, null);
2056                     } catch (BadLocationException e) {
2057                         AdtPlugin.log(e, null);
2058                     }
2059                     break;
2060                 case IGNORE_FIDELITY_WARNING:
2061                     RenderLogger.ignoreFidelityWarning(mData1);
2062                     recomputeLayout();
2063                     break;
2064                 default:
2065                     break;
2066             }
2067         }
2068 
2069         @Override
similarTo(StyleRange style)2070         public boolean similarTo(StyleRange style) {
2071             // Prevent adjacent link ranges from getting merged
2072             return false;
2073         }
2074     }
2075 
2076     /**
2077      * Returns the error label for the graphical editor (which may not be visible
2078      * or showing errors)
2079      *
2080      * @return the error label, never null
2081      */
getErrorLabel()2082     StyledText getErrorLabel() {
2083         return mErrorLabel;
2084     }
2085 
2086     /**
2087      * Monitor clicks on the error label.
2088      * If the click happens on a style range created by
2089      * {@link GraphicalEditorPart#addClassLink(StyledText, String)}, we assume it's about
2090      * a missing class and we then proceed to display the standard Eclipse class creator wizard.
2091      */
2092     private class ErrorLabelListener extends MouseAdapter {
2093 
2094         @Override
mouseUp(MouseEvent event)2095         public void mouseUp(MouseEvent event) {
2096             super.mouseUp(event);
2097 
2098             if (event.widget != mErrorLabel) {
2099                 return;
2100             }
2101 
2102             int offset = mErrorLabel.getCaretOffset();
2103 
2104             StyleRange r = null;
2105             StyleRange[] ranges = mErrorLabel.getStyleRanges();
2106             if (ranges != null && ranges.length > 0) {
2107                 for (StyleRange sr : ranges) {
2108                     if (sr.start <= offset && sr.start + sr.length > offset) {
2109                         r = sr;
2110                         break;
2111                     }
2112                 }
2113             }
2114 
2115             if (r instanceof ActionLinkStyleRange) {
2116                 ActionLinkStyleRange range = (ActionLinkStyleRange) r;
2117                 range.onClick();
2118             }
2119 
2120             LayoutCanvas canvas = getCanvasControl();
2121             canvas.updateMenuActionState();
2122         }
2123     }
2124 
createNewClass(String fqcn)2125     private void createNewClass(String fqcn) {
2126 
2127         int pos = fqcn.lastIndexOf('.');
2128         String packageName = pos < 0 ? "" : fqcn.substring(0, pos);  //$NON-NLS-1$
2129         String className = pos <= 0 || pos >= fqcn.length() ? "" : fqcn.substring(pos + 1); //$NON-NLS-1$
2130 
2131         // create the wizard page for the class creation, and configure it
2132         NewClassWizardPage page = new NewClassWizardPage();
2133 
2134         // set the parent class
2135         page.setSuperClass(SdkConstants.CLASS_VIEW, true /* canBeModified */);
2136 
2137         // get the source folders as java elements.
2138         IPackageFragmentRoot[] roots = getPackageFragmentRoots(mLayoutEditor.getProject(),
2139                 false /*includeContainers*/, true /*skipGenFolder*/);
2140 
2141         IPackageFragmentRoot currentRoot = null;
2142         IPackageFragment currentFragment = null;
2143         int packageMatchCount = -1;
2144 
2145         for (IPackageFragmentRoot root : roots) {
2146             // Get the java element for the package.
2147             // This method is said to always return a IPackageFragment even if the
2148             // underlying folder doesn't exist...
2149             IPackageFragment fragment = root.getPackageFragment(packageName);
2150             if (fragment != null && fragment.exists()) {
2151                 // we have a perfect match! we use it.
2152                 currentRoot = root;
2153                 currentFragment = fragment;
2154                 packageMatchCount = -1;
2155                 break;
2156             } else {
2157                 // we don't have a match. we look for the fragment with the best match
2158                 // (ie the closest parent package we can find)
2159                 try {
2160                     IJavaElement[] children;
2161                     children = root.getChildren();
2162                     for (IJavaElement child : children) {
2163                         if (child instanceof IPackageFragment) {
2164                             fragment = (IPackageFragment)child;
2165                             if (packageName.startsWith(fragment.getElementName())) {
2166                                 // its a match. get the number of segments
2167                                 String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
2168                                 if (segments.length > packageMatchCount) {
2169                                     packageMatchCount = segments.length;
2170                                     currentFragment = fragment;
2171                                     currentRoot = root;
2172                                 }
2173                             }
2174                         }
2175                     }
2176                 } catch (JavaModelException e) {
2177                     // Couldn't get the children: we just ignore this package root.
2178                 }
2179             }
2180         }
2181 
2182         ArrayList<IPackageFragment> createdFragments = null;
2183 
2184         if (currentRoot != null) {
2185             // if we have a perfect match, we set it and we're done.
2186             if (packageMatchCount == -1) {
2187                 page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
2188                 page.setPackageFragment(currentFragment, true /* canBeModified */);
2189             } else {
2190                 // we have a partial match.
2191                 // create the package. We have to start with the first segment so that we
2192                 // know what to delete in case of a cancel.
2193                 try {
2194                     createdFragments = new ArrayList<IPackageFragment>();
2195 
2196                     int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
2197                     int count = 0;
2198                     int index = -1;
2199                     // skip the matching packages
2200                     while (count < packageMatchCount) {
2201                         index = packageName.indexOf('.', index+1);
2202                         count++;
2203                     }
2204 
2205                     // create the rest of the segments, except for the last one as indexOf will
2206                     // return -1;
2207                     while (count < totalCount - 1) {
2208                         index = packageName.indexOf('.', index+1);
2209                         count++;
2210                         createdFragments.add(currentRoot.createPackageFragment(
2211                                 packageName.substring(0, index),
2212                                 true /* force*/, new NullProgressMonitor()));
2213                     }
2214 
2215                     // create the last package
2216                     createdFragments.add(currentRoot.createPackageFragment(
2217                             packageName, true /* force*/, new NullProgressMonitor()));
2218 
2219                     // set the root and fragment in the Wizard page
2220                     page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
2221                     page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
2222                             true /* canBeModified */);
2223                 } catch (JavaModelException e) {
2224                     // If we can't create the packages, there's a problem.
2225                     // We revert to the default package
2226                     for (IPackageFragmentRoot root : roots) {
2227                         // Get the java element for the package.
2228                         // This method is said to always return a IPackageFragment even if the
2229                         // underlying folder doesn't exist...
2230                         IPackageFragment fragment = root.getPackageFragment(packageName);
2231                         if (fragment != null && fragment.exists()) {
2232                             page.setPackageFragmentRoot(root, true /* canBeModified*/);
2233                             page.setPackageFragment(fragment, true /* canBeModified */);
2234                             break;
2235                         }
2236                     }
2237                 }
2238             }
2239         } else if (roots.length > 0) {
2240             // if we haven't found a valid fragment, we set the root to the first source folder.
2241             page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
2242         }
2243 
2244         // if we have a starting class name we use it
2245         if (className != null) {
2246             page.setTypeName(className, true /* canBeModified*/);
2247         }
2248 
2249         // create the action that will open it the wizard.
2250         OpenNewClassWizardAction action = new OpenNewClassWizardAction();
2251         action.setConfiguredWizardPage(page);
2252         action.run();
2253         IJavaElement element = action.getCreatedElement();
2254 
2255         if (element == null) {
2256             // lets delete the packages we created just for this.
2257             // we need to start with the leaf and go up
2258             if (createdFragments != null) {
2259                 try {
2260                     for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
2261                         createdFragments.get(i).delete(true /* force*/,
2262                                                        new NullProgressMonitor());
2263                     }
2264                 } catch (JavaModelException e) {
2265                     e.printStackTrace();
2266                 }
2267             }
2268         }
2269     }
2270 
2271     /**
2272      * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source
2273      * folders of the specified project.
2274      *
2275      * @param project the project
2276      * @param includeContainers True to include containers
2277      * @param skipGenFolder True to skip the "gen" folder
2278      * @return an array of IPackageFragmentRoot.
2279      */
getPackageFragmentRoots(IProject project, boolean includeContainers, boolean skipGenFolder)2280     private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project,
2281             boolean includeContainers, boolean skipGenFolder) {
2282         ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
2283         try {
2284             IJavaProject javaProject = JavaCore.create(project);
2285             IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
2286             for (int i = 0; i < roots.length; i++) {
2287                 if (skipGenFolder) {
2288                     IResource resource = roots[i].getResource();
2289                     if (resource != null && resource.getName().equals(FD_GEN_SOURCES)) {
2290                         continue;
2291                     }
2292                 }
2293                 IClasspathEntry entry = roots[i].getRawClasspathEntry();
2294                 if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE ||
2295                         (includeContainers &&
2296                                 entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) {
2297                     result.add(roots[i]);
2298                 }
2299             }
2300         } catch (JavaModelException e) {
2301         }
2302 
2303         return result.toArray(new IPackageFragmentRoot[result.size()]);
2304     }
2305 
2306     /**
2307      * Reopens this file as included within the given file (this assumes that the given
2308      * file has an include tag referencing this view, and the set of views that have this
2309      * property can be found using the {@link IncludeFinder}.
2310      *
2311      * @param includeWithin reference to a file to include as a surrounding context,
2312      *   or null to show the file standalone
2313      */
showIn(Reference includeWithin)2314     public void showIn(Reference includeWithin) {
2315         mIncludedWithin = includeWithin;
2316 
2317         if (includeWithin != null) {
2318             IFile file = includeWithin.getFile();
2319 
2320             // Update configuration
2321             if (file != null) {
2322                 mConfigComposite.resetConfigFor(file);
2323             }
2324         }
2325         recomputeLayout();
2326     }
2327 
2328     /**
2329      * Returns the resource name of the file that is including this current layout, if any
2330      * (may be null)
2331      *
2332      * @return the resource name of an including layout, or null
2333      */
getIncludedWithin()2334     public Reference getIncludedWithin() {
2335         return mIncludedWithin;
2336     }
2337 
2338     /**
2339      * Return all resource names of a given type, either in the project or in the
2340      * framework.
2341      *
2342      * @param framework if true, return all the framework resource names, otherwise return
2343      *            all the project resource names
2344      * @param type the type of resource to look up
2345      * @return a collection of resource names, never null but possibly empty
2346      */
getResourceNames(boolean framework, ResourceType type)2347     public Collection<String> getResourceNames(boolean framework, ResourceType type) {
2348         Map<ResourceType, Map<String, ResourceValue>> map =
2349             framework ? mConfiguredFrameworkRes : mConfiguredProjectRes;
2350         Map<String, ResourceValue> animations = map.get(type);
2351         if (animations != null) {
2352             return animations.keySet();
2353         } else {
2354             return Collections.emptyList();
2355         }
2356     }
2357 
2358     /**
2359      * Return this editor's current configuration
2360      *
2361      * @return the current configuration
2362      */
getConfiguration()2363     public FolderConfiguration getConfiguration() {
2364         return mConfigComposite.getCurrentConfig();
2365     }
2366 
2367     /**
2368      * Figures out the project's minSdkVersion and targetSdkVersion and return whether the values
2369      * have changed.
2370      */
computeSdkVersion()2371     private boolean computeSdkVersion() {
2372         int oldMinSdkVersion = mMinSdkVersion;
2373         int oldTargetSdkVersion = mTargetSdkVersion;
2374 
2375         Pair<Integer, Integer> v = ManifestInfo.computeSdkVersions(mEditedFile.getProject());
2376         mMinSdkVersion = v.getFirst();
2377         mTargetSdkVersion = v.getSecond();
2378 
2379         return oldMinSdkVersion != mMinSdkVersion || oldTargetSdkVersion != mTargetSdkVersion;
2380     }
2381 
getConfigurationComposite()2382     public ConfigurationComposite getConfigurationComposite() {
2383         return mConfigComposite;
2384     }
2385 
getLayoutActionBar()2386     public LayoutActionBar getLayoutActionBar() {
2387         return mActionBar;
2388     }
2389 
2390     /**
2391      * Returns the target SDK version
2392      *
2393      * @return the target SDK version
2394      */
getTargetSdkVersion()2395     public int getTargetSdkVersion() {
2396         return mTargetSdkVersion;
2397     }
2398 
2399     /**
2400      * Returns the minimum SDK version
2401      *
2402      * @return the minimum SDK version
2403      */
getMinSdkVersion()2404     public int getMinSdkVersion() {
2405         return mMinSdkVersion;
2406     }
2407 }
2408