diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java | 2937 |
1 files changed, 2937 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java new file mode 100644 index 000000000..0f5762da6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java @@ -0,0 +1,2937 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.gle2; + +import static com.android.SdkConstants.ANDROID_PKG; +import static com.android.SdkConstants.ANDROID_STRING_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.FD_GEN_SOURCES; +import static com.android.SdkConstants.GRID_LAYOUT; +import static com.android.SdkConstants.SCROLL_VIEW; +import static com.android.SdkConstants.STRING_PREFIX; +import static com.android.SdkConstants.VALUE_FALSE; +import static com.android.SdkConstants.VALUE_FILL_PARENT; +import static com.android.SdkConstants.VALUE_MATCH_PARENT; +import static com.android.SdkConstants.VALUE_WRAP_CONTENT; +import static com.android.ide.common.rendering.RenderSecurityManager.ENABLED_PROPERTY; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET; +import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage; +import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_EAST; +import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_WEST; +import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_COLLAPSED; +import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_OPEN; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.layout.BaseLayoutRule; +import com.android.ide.common.rendering.LayoutLibrary; +import com.android.ide.common.rendering.RenderSecurityException; +import com.android.ide.common.rendering.RenderSecurityManager; +import com.android.ide.common.rendering.StaticRenderSession; +import com.android.ide.common.rendering.api.Capability; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.SessionParams.RenderingMode; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.sdk.LoadStatus; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener; +import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationMatcher; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PaletteControl.PalettePage; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; +import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFactory; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; +import com.android.resources.Density; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.sdklib.IAndroidTarget; +import com.android.tools.lint.detector.api.LintUtils; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaModelMarker; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.ui.preferences.BuildPathsPropertyPage; +import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction; +import org.eclipse.jdt.ui.wizards.NewClassWizardPage; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.text.edits.MalformedTreeException; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorSite; +import org.eclipse.ui.INullSelectionListener; +import org.eclipse.ui.ISelectionListener; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchPartSite; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.PreferencesUtil; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.EditorPart; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.part.IPageSite; +import org.eclipse.ui.part.PageBookView; +import org.eclipse.wb.core.controls.flyout.FlyoutControlComposite; +import org.eclipse.wb.core.controls.flyout.IFlyoutListener; +import org.eclipse.wb.core.controls.flyout.PluginFlyoutPreferences; +import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Graphical layout editor part, version 2. + * <p/> + * The main component of the editor part is the {@link LayoutCanvasViewer}, which + * actually delegates its work to the {@link LayoutCanvas} control. + * <p/> + * The {@link LayoutCanvasViewer} is set as the site's {@link ISelectionProvider}: + * when the selection changes in the canvas, it is thus broadcasted to anyone listening + * on the site's selection service. + * <p/> + * This part is also an {@link ISelectionListener}. It listens to the site's selection + * service and thus receives selection changes from itself as well as the associated + * outline and property sheet (these are registered by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}). + * + * @since GLE2 + */ +public class GraphicalEditorPart extends EditorPart + implements IPageImageProvider, INullSelectionListener, IFlyoutListener, + ConfigurationClient { + + /* + * Useful notes: + * To understand Drag & drop: + * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html + * + * To understand the site's selection listener, selection provider, and the + * confusion of different-yet-similarly-named interfaces, consult this: + * http://www.eclipse.org/articles/Article-WorkbenchSelections/article.html + * + * To summarize the selection mechanism: + * - The workbench site selection service can be seen as "centralized" + * service that registers selection providers and selection listeners. + * - The editor part and the outline are selection providers. + * - The editor part, the outline and the property sheet are listeners + * which all listen to each others indirectly. + */ + + /** Property key for the window preferences for the structure flyout */ + private static final String PREF_STRUCTURE = "design.structure"; //$NON-NLS-1$ + + /** Property key for the window preferences for the palette flyout */ + private static final String PREF_PALETTE = "design.palette"; //$NON-NLS-1$ + + /** + * Session-property on files which specifies the initial config state to be used on + * this file + */ + public final static QualifiedName NAME_INITIAL_STATE = + new QualifiedName(AdtPlugin.PLUGIN_ID, "initialstate");//$NON-NLS-1$ + + /** + * Session-property on files which specifies the inclusion-context (reference to another layout + * which should be "including" this layout) when the file is opened + */ + public final static QualifiedName NAME_INCLUDE = + new QualifiedName(AdtPlugin.PLUGIN_ID, "includer");//$NON-NLS-1$ + + /** Reference to the layout editor */ + private final LayoutEditorDelegate mEditorDelegate; + + /** Reference to the file being edited. Can also be used to access the {@link IProject}. */ + private IFile mEditedFile; + + /** The configuration chooser at the top of the layout editor. */ + private ConfigurationChooser mConfigChooser; + + /** The sash that splits the palette from the error view. + * The error view is shown only when needed. */ + private SashForm mSashError; + + /** The palette displayed on the left of the sash. */ + private PaletteControl mPalette; + + /** The layout canvas displayed to the right of the sash. */ + private LayoutCanvasViewer mCanvasViewer; + + /** The Rules Engine associated with this editor. It is project-specific. */ + private RulesEngine mRulesEngine; + + /** Styled text displaying the most recent error in the error view. */ + private StyledText mErrorLabel; + + /** + * The resource reference to a file that should surround this file (e.g. include this file + * visually), or null if not applicable + */ + private Reference mIncludedWithin; + + private Map<ResourceType, Map<String, ResourceValue>> mConfiguredFrameworkRes; + private Map<ResourceType, Map<String, ResourceValue>> mConfiguredProjectRes; + private ProjectCallback mProjectCallback; + private boolean mNeedsRecompute = false; + private TargetListener mTargetListener; + private ResourceResolver mResourceResolver; + private ReloadListener mReloadListener; + private int mMinSdkVersion; + private int mTargetSdkVersion; + private LayoutActionBar mActionBar; + private OutlinePage mOutlinePage; + private FlyoutControlComposite mStructureFlyout; + private FlyoutControlComposite mPaletteComposite; + private PropertyFactory mPropertyFactory; + private boolean mRenderedOnce; + private final Object mCredential = new Object(); + + /** + * Flags which tracks whether this editor is currently active which is set whenever + * {@link #activated()} is called and clear whenever {@link #deactivated()} is called. + * This is used to suppress repeated calls to {@link #activate()} to avoid doing + * unnecessary work. + */ + private boolean mActive; + + /** + * Constructs a new {@link GraphicalEditorPart} + * + * @param editorDelegate the associated XML editor delegate + */ + public GraphicalEditorPart(@NonNull LayoutEditorDelegate editorDelegate) { + mEditorDelegate = editorDelegate; + setPartName("Graphical Layout"); + } + + // ------------------------------------ + // Methods overridden from base classes + //------------------------------------ + + /** + * Initializes the editor part with a site and input. + * {@inheritDoc} + */ + @Override + public void init(IEditorSite site, IEditorInput input) throws PartInitException { + setSite(site); + useNewEditorInput(input); + + if (mTargetListener == null) { + mTargetListener = new TargetListener(); + AdtPlugin.getDefault().addTargetListener(mTargetListener); + + // Trigger a check to see if the SDK needs to be reloaded (which will + // invoke onSdkLoaded asynchronously as needed). + AdtPlugin.getDefault().refreshSdk(); + } + } + + private void useNewEditorInput(IEditorInput input) throws PartInitException { + // The contract of init() mentions we need to fail if we can't understand the input. + if (!(input instanceof FileEditorInput)) { + throw new PartInitException("Input is not of type FileEditorInput: " + //$NON-NLS-1$ + input == null ? "null" : input.toString()); //$NON-NLS-1$ + } + } + + @Override + public Image getPageImage() { + return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$ + } + + @Override + public void createPartControl(Composite parent) { + + Display d = parent.getDisplay(); + + GridLayout gl = new GridLayout(1, false); + parent.setLayout(gl); + gl.marginHeight = gl.marginWidth = 0; + + // Check whether somebody has requested an initial state for the newly opened file. + // The initial state is a serialized version of the state compatible with + // {@link ConfigurationComposite#CONFIG_STATE}. + String initialState = null; + IFile file = mEditedFile; + if (file == null) { + IEditorInput input = mEditorDelegate.getEditor().getEditorInput(); + if (input instanceof FileEditorInput) { + file = ((FileEditorInput) input).getFile(); + } + } + + if (file != null) { + try { + initialState = (String) file.getSessionProperty(NAME_INITIAL_STATE); + if (initialState != null) { + // Only use once + file.setSessionProperty(NAME_INITIAL_STATE, null); + } + } catch (CoreException e) { + AdtPlugin.log(e, "Can't read session property %1$s", NAME_INITIAL_STATE); + } + } + + IPreferenceStore preferenceStore = AdtPlugin.getDefault().getPreferenceStore(); + PluginFlyoutPreferences preferences; + preferences = new PluginFlyoutPreferences(preferenceStore, PREF_PALETTE); + preferences.initializeDefaults(DOCK_WEST, STATE_OPEN, 200); + mPaletteComposite = new FlyoutControlComposite(parent, SWT.NONE, preferences); + mPaletteComposite.setTitleText("Palette"); + mPaletteComposite.setMinWidth(100); + Composite paletteParent = mPaletteComposite.getFlyoutParent(); + Composite editorParent = mPaletteComposite.getClientParent(); + mPaletteComposite.setListener(this); + + mPaletteComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + PageSiteComposite paletteComposite = new PageSiteComposite(paletteParent, SWT.BORDER); + paletteComposite.setTitleText("Palette"); + paletteComposite.setTitleImage(IconFactory.getInstance().getIcon("palette")); + PalettePage decor = new PalettePage(this); + paletteComposite.setPage(decor); + mPalette = (PaletteControl) decor.getControl(); + decor.createToolbarItems(paletteComposite.getToolBar()); + + // Create the shared structure+editor area + preferences = new PluginFlyoutPreferences(preferenceStore, PREF_STRUCTURE); + preferences.initializeDefaults(DOCK_EAST, STATE_OPEN, 300); + mStructureFlyout = new FlyoutControlComposite(editorParent, SWT.NONE, preferences); + mStructureFlyout.setTitleText("Structure"); + mStructureFlyout.setMinWidth(150); + mStructureFlyout.setListener(this); + + Composite layoutBarAndCanvas = new Composite(mStructureFlyout.getClientParent(), SWT.NONE); + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + layoutBarAndCanvas.setLayout(gridLayout); + + mConfigChooser = new ConfigurationChooser(this, layoutBarAndCanvas, initialState); + mConfigChooser.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mActionBar = new LayoutActionBar(layoutBarAndCanvas, SWT.NONE, this); + GridData detailsData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1); + mActionBar.setLayoutData(detailsData); + if (file != null) { + mActionBar.updateErrorIndicator(file); + } + + mSashError = new SashForm(layoutBarAndCanvas, SWT.VERTICAL | SWT.BORDER); + mSashError.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mCanvasViewer = new LayoutCanvasViewer(mEditorDelegate, mRulesEngine, mSashError, SWT.NONE); + mSashError.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); + mErrorLabel.setEditable(false); + mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + mErrorLabel.addMouseListener(new ErrorLabelListener()); + + mSashError.setWeights(new int[] { 80, 20 }); + mSashError.setMaximizedControl(mCanvasViewer.getControl()); + + // Create the structure views. We really should do this *lazily*, but that + // seems to cause a bug: property sheet won't update. Track this down later. + createStructureViews(mStructureFlyout.getFlyoutParent(), false); + showStructureViews(false, false, false); + + // Initialize the state + reloadPalette(); + + IWorkbenchPartSite site = getSite(); + site.setSelectionProvider(mCanvasViewer); + site.getPage().addSelectionListener(this); + } + + private void createStructureViews(Composite parent, boolean createPropertySheet) { + mOutlinePage = new OutlinePage(this); + mOutlinePage.setShowPropertySheet(createPropertySheet); + mOutlinePage.setShowHeader(true); + + IPageSite pageSite = new IPageSite() { + + @Override + public IWorkbenchPage getPage() { + return getSite().getPage(); + } + + @Override + public ISelectionProvider getSelectionProvider() { + return getSite().getSelectionProvider(); + } + + @Override + public Shell getShell() { + return getSite().getShell(); + } + + @Override + public IWorkbenchWindow getWorkbenchWindow() { + return getSite().getWorkbenchWindow(); + } + + @Override + public void setSelectionProvider(ISelectionProvider provider) { + getSite().setSelectionProvider(provider); + } + + @Override + public Object getAdapter(Class adapter) { + return getSite().getAdapter(adapter); + } + + @Override + public Object getService(Class api) { + return getSite().getService(api); + } + + @Override + public boolean hasService(Class api) { + return getSite().hasService(api); + } + + @Override + public void registerContextMenu(String menuId, MenuManager menuManager, + ISelectionProvider selectionProvider) { + } + + @Override + public IActionBars getActionBars() { + return null; + } + }; + mOutlinePage.init(pageSite); + mOutlinePage.createControl(parent); + mOutlinePage.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + getCanvasControl().getSelectionManager().setSelection(event.getSelection()); + } + }); + } + + /** Shows the embedded (within the layout editor) outline and or properties */ + void showStructureViews(final boolean showOutline, final boolean showProperties, + final boolean updateLayout) { + Display display = mConfigChooser.getDisplay(); + if (display.getThread() != Thread.currentThread()) { + display.asyncExec(new Runnable() { + @Override + public void run() { + if (!mConfigChooser.isDisposed()) { + showStructureViews(showOutline, showProperties, updateLayout); + } + } + + }); + return; + } + + boolean show = showOutline || showProperties; + + Control[] children = mStructureFlyout.getFlyoutParent().getChildren(); + if (children.length == 0) { + if (show) { + createStructureViews(mStructureFlyout.getFlyoutParent(), showProperties); + } + return; + } + + mOutlinePage.setShowPropertySheet(showProperties); + + Control control = children[0]; + if (show != control.getVisible()) { + control.setVisible(show); + mOutlinePage.setActive(show); // disable/re-enable listeners etc + if (show) { + ISelection selection = getCanvasControl().getSelectionManager().getSelection(); + mOutlinePage.selectionChanged(getEditorDelegate().getEditor(), selection); + } + if (updateLayout) { + mStructureFlyout.layout(); + } + // TODO: *dispose* the non-showing widgets to save memory? + } + } + + /** + * Returns the property factory associated with this editor + * + * @return the factory + */ + @NonNull + public PropertyFactory getPropertyFactory() { + if (mPropertyFactory == null) { + mPropertyFactory = new PropertyFactory(this); + } + + return mPropertyFactory; + } + + /** + * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info). + * + * @param rootViewInfo The root of the view info hierarchy. Can be null. + */ + public void setModel(CanvasViewInfo rootViewInfo) { + if (mOutlinePage != null) { + mOutlinePage.setModel(rootViewInfo); + } + } + + /** + * Listens to workbench selections that does NOT come from {@link LayoutEditorDelegate} + * (those are generated by ourselves). + * <p/> + * Selection can be null, as indicated by this class implementing + * {@link INullSelectionListener}. + */ + @Override + public void selectionChanged(IWorkbenchPart part, ISelection selection) { + Object delegate = part instanceof IEditorPart ? + LayoutEditorDelegate.fromEditor((IEditorPart) part) : null; + if (delegate == null) { + if (part instanceof PageBookView) { + PageBookView pbv = (PageBookView) part; + org.eclipse.ui.part.IPage currentPage = pbv.getCurrentPage(); + if (currentPage instanceof OutlinePage) { + LayoutCanvas canvas = getCanvasControl(); + if (canvas != null && canvas.getOutlinePage() != currentPage) { + // The notification is not for this view; ignore + // (can happen when there are multiple pages simultaneously + // visible) + return; + } + } + } + mCanvasViewer.setSelection(selection); + } + } + + @Override + public void dispose() { + getSite().getPage().removeSelectionListener(this); + getSite().setSelectionProvider(null); + + if (mTargetListener != null) { + AdtPlugin.getDefault().removeTargetListener(mTargetListener); + mTargetListener = null; + } + + if (mReloadListener != null) { + LayoutReloadMonitor.getMonitor().removeListener(mReloadListener); + mReloadListener = null; + } + + if (mCanvasViewer != null) { + mCanvasViewer.dispose(); + mCanvasViewer = null; + } + super.dispose(); + } + + /** + * Select the visual element corresponding to the given XML node + * @param xmlNode The Node whose element we want to select + */ + public void select(Node xmlNode) { + mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode); + } + + // ---- Implements ConfigurationClient ---- + @Override + public void aboutToChange(int flags) { + if ((flags & CFG_TARGET) != 0) { + IAndroidTarget oldTarget = mConfigChooser.getConfiguration().getTarget(); + preRenderingTargetChangeCleanUp(oldTarget); + } + } + + @Override + public boolean changed(int flags) { + mConfiguredFrameworkRes = mConfiguredProjectRes = null; + mResourceResolver = null; + + if (mEditedFile == null) { + return true; + } + + // Before doing the normal process, test for the following case. + // - the editor is being opened (or reset for a new input) + // - the file being opened is not the best match for any possible configuration + // - another random compatible config was chosen in the config composite. + // The result is that 'match' will not be the file being edited, but because this is not + // due to a config change, we should not trigger opening the actual best match (also, + // because the editor is still opening the MatchingStrategy woudln't answer true + // and the best match file would open in a different editor). + // So the solution is that if the editor is being created, we just call recomputeLayout + // without looking for a better matching layout file. + if (mEditorDelegate.getEditor().isCreatingPages()) { + recomputeLayout(); + } else { + boolean affectsFileSelection = (flags & Configuration.MASK_FILE_ATTRS) != 0; + IFile best = null; + // get the resources of the file's project. + if (affectsFileSelection) { + best = ConfigurationMatcher.getBestFileMatch(mConfigChooser); + } + if (best != null) { + if (!best.equals(mEditedFile)) { + try { + // tell the editor that the next replacement file is due to a config + // change. + mEditorDelegate.setNewFileOnConfigChange(true); + + boolean reuseEditor = AdtPrefs.getPrefs().isSharedLayoutEditor(); + if (!reuseEditor) { + String data = ConfigurationDescription.getDescription(best); + if (data == null) { + // Not previously opened: duplicate the current state as + // much as possible + data = mConfigChooser.getConfiguration().toPersistentString(); + ConfigurationDescription.setDescription(best, data); + } + } + + // ask the IDE to open the replacement file. + IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), best, + CommonXmlEditor.ID); + + // we're done! + return reuseEditor; + } catch (PartInitException e) { + // FIXME: do something! + } + } + + // at this point, we have not opened a new file. + + // Store the state in the current file + mConfigChooser.saveConstraints(); + + // Even though the layout doesn't change, the config changed, and referenced + // resources need to be updated. + recomputeLayout(); + } else if (affectsFileSelection) { + // display the error. + Configuration configuration = mConfigChooser.getConfiguration(); + FolderConfiguration currentConfig = configuration.getFullConfig(); + displayError( + "No resources match the configuration\n" + + " \n" + + "\t%1$s\n" + + " \n" + + "Change the configuration or create:\n" + + " \n" + + "\tres/%2$s/%3$s\n" + + " \n" + + "You can also click the 'Create New...' item in the configuration " + + "dropdown menu above.", + currentConfig.toDisplayString(), + currentConfig.getFolderName(ResourceFolderType.LAYOUT), + mEditedFile.getName()); + } else { + // Something else changed, such as the theme - just recompute existing + // layout + mConfigChooser.saveConstraints(); + recomputeLayout(); + } + } + + if ((flags & CFG_TARGET) != 0) { + Configuration configuration = mConfigChooser.getConfiguration(); + IAndroidTarget target = configuration.getTarget(); + Sdk current = Sdk.getCurrent(); + if (current != null) { + AndroidTargetData targetData = current.getTargetData(target); + updateCapabilities(targetData); + } + } + + if ((flags & (CFG_DEVICE | CFG_DEVICE_STATE)) != 0) { + // When the device changes, zoom the view to fit, but only up to 100% (e.g. zoom + // out to fit the content, or zoom back in if we were zoomed out more from the + // previous view, but only up to 100% such that we never blow up pixels + if (mActionBar.isZoomingAllowed()) { + getCanvasControl().setFitScale(true, true /*allowZoomIn*/); + } + } + + reloadPalette(); + + getCanvasControl().getPreviewManager().configurationChanged(flags); + + return true; + } + + @Override + public void setActivity(@NonNull String activity) { + ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject()); + String pkg = manifest.getPackage(); + if (activity.startsWith(pkg) && activity.length() > pkg.length() + && activity.charAt(pkg.length()) == '.') { + activity = activity.substring(pkg.length()); + } + CommonXmlEditor editor = getEditorDelegate().getEditor(); + Element element = editor.getUiRootNode().getXmlDocument().getDocumentElement(); + AdtUtils.setToolsAttribute(editor, + element, "Choose Activity", ATTR_CONTEXT, + activity, false /*reveal*/, false /*append*/); + } + + /** + * Returns a {@link ProjectResources} for the framework resources based on the current + * configuration selection. + * @return the framework resources or null if not found. + */ + @Override + @Nullable + public ResourceRepository getFrameworkResources() { + return getFrameworkResources(getRenderingTarget()); + } + + /** + * Returns a {@link ProjectResources} for the framework resources of a given + * target. + * @param target the target for which to return the framework resources. + * @return the framework resources or null if not found. + */ + @Override + @Nullable + public ResourceRepository getFrameworkResources(@Nullable IAndroidTarget target) { + if (target != null) { + AndroidTargetData data = Sdk.getCurrent().getTargetData(target); + + if (data != null) { + return data.getFrameworkResources(); + } + } + + return null; + } + + @Override + @Nullable + public ProjectResources getProjectResources() { + if (mEditedFile != null) { + ResourceManager manager = ResourceManager.getInstance(); + return manager.getProjectResources(mEditedFile.getProject()); + } + + return null; + } + + + @Override + @NonNull + public Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources() { + if (mConfiguredFrameworkRes == null && mConfigChooser != null) { + ResourceRepository frameworkRes = getFrameworkResources(); + + if (frameworkRes == null) { + AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework"); + } else { + // get the framework resource values based on the current config + mConfiguredFrameworkRes = frameworkRes.getConfiguredResources( + mConfigChooser.getConfiguration().getFullConfig()); + } + } + + return mConfiguredFrameworkRes; + } + + @Override + @NonNull + public Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources() { + if (mConfiguredProjectRes == null && mConfigChooser != null) { + ProjectResources project = getProjectResources(); + + // get the project resource values based on the current config + mConfiguredProjectRes = project.getConfiguredResources( + mConfigChooser.getConfiguration().getFullConfig()); + } + + return mConfiguredProjectRes; + } + + @Override + public void createConfigFile() { + LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigChooser.getShell(), + mEditedFile.getName(), mConfigChooser.getConfiguration().getFullConfig()); + if (dialog.open() != Window.OK) { + return; + } + + FolderConfiguration config = new FolderConfiguration(); + dialog.getConfiguration(config); + + // Creates a new layout file from the specified {@link FolderConfiguration}. + CreateNewConfigJob job = new CreateNewConfigJob(this, mEditedFile, config); + job.schedule(); + } + + /** + * Returns the resource name of the file that is including this current layout, if any + * (may be null) + * + * @return the resource name of an including layout, or null + */ + @Override + public Reference getIncludedWithin() { + return mIncludedWithin; + } + + @Override + @Nullable + public LayoutCanvas getCanvas() { + return getCanvasControl(); + } + + /** + * Listens to target changed in the current project, to trigger a new layout rendering. + */ + private class TargetListener implements ITargetChangeListener { + + @Override + public void onProjectTargetChange(IProject changedProject) { + if (changedProject != null && changedProject.equals(getProject())) { + updateEditor(); + } + } + + @Override + public void onTargetLoaded(IAndroidTarget loadedTarget) { + IAndroidTarget target = getRenderingTarget(); + if (target != null && target.equals(loadedTarget)) { + updateEditor(); + } + } + + @Override + public void onSdkLoaded() { + // get the current rendering target to unload it + IAndroidTarget oldTarget = getRenderingTarget(); + preRenderingTargetChangeCleanUp(oldTarget); + + computeSdkVersion(); + + // get the project target + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); + if (target != null) { + mConfigChooser.onSdkLoaded(target); + changed(CFG_FOLDER | CFG_TARGET); + } + } + } + + private void updateEditor() { + mEditorDelegate.getEditor().commitPages(false /* onSave */); + + // because the target changed we must reset the configured resources. + mConfiguredFrameworkRes = mConfiguredProjectRes = null; + mResourceResolver = null; + + // make sure we remove the custom view loader, since its parent class loader is the + // bridge class loader. + mProjectCallback = null; + + // recreate the ui root node always, this will also call onTargetChange + // on the config composite + mEditorDelegate.delegateInitUiRootNode(true /*force*/); + } + + private IProject getProject() { + return getEditorDelegate().getEditor().getProject(); + } + } + + /** Refresh the configured project resources associated with this editor */ + public void refreshProjectResources() { + mConfiguredProjectRes = null; + mResourceResolver = null; + } + + /** + * Returns the currently edited file + * + * @return the currently edited file, or null + */ + public IFile getEditedFile() { + return mEditedFile; + } + + /** + * Returns the project for the currently edited file, or null + * + * @return the project containing the edited file, or null + */ + public IProject getProject() { + if (mEditedFile != null) { + return mEditedFile.getProject(); + } else { + return null; + } + } + + // ---------------- + + /** + * Save operation in the Graphical Editor Part. + * <p/> + * In our workflow, the model is owned by the Structured XML Editor. + * The graphical layout editor just displays it -- thus we don't really + * save anything here. + * <p/> + * This must NOT call the parent editor part. At the contrary, the parent editor + * part will call this *after* having done the actual save operation. + * <p/> + * The only action this editor must do is mark the undo command stack as + * being no longer dirty. + */ + @Override + public void doSave(IProgressMonitor monitor) { + // TODO implement a command stack +// getCommandStack().markSaveLocation(); +// firePropertyChange(PROP_DIRTY); + } + + /** + * Save operation in the Graphical Editor Part. + * <p/> + * In our workflow, the model is owned by the Structured XML Editor. + * The graphical layout editor just displays it -- thus we don't really + * save anything here. + */ + @Override + public void doSaveAs() { + // pass + } + + /** + * In our workflow, the model is owned by the Structured XML Editor. + * The graphical layout editor just displays it -- thus we don't really + * save anything here. + */ + @Override + public boolean isDirty() { + return false; + } + + /** + * In our workflow, the model is owned by the Structured XML Editor. + * The graphical layout editor just displays it -- thus we don't really + * save anything here. + */ + @Override + public boolean isSaveAsAllowed() { + return false; + } + + @Override + public void setFocus() { + // TODO Auto-generated method stub + + } + + /** + * Responds to a page change that made the Graphical editor page the activated page. + */ + public void activated() { + if (!mActive) { + mActive = true; + + syncDockingState(); + mActionBar.updateErrorIndicator(); + + boolean changed = mConfigChooser.syncRenderState(); + if (changed) { + // Will also force recomputeLayout() + return; + } + + if (mNeedsRecompute) { + recomputeLayout(); + } + + mCanvasViewer.getCanvas().syncPreviewMode(); + } + } + + /** + * The global docking state version. This number is incremented each time + * the user customizes the window layout in any layout. + */ + private static int sDockingStateVersion; + + /** + * The window docking state version that this window is currently showing; + * when a different window is reconfigured, the global version number is + * incremented, and when this window is shown, and the current version is + * less than the global version, the window layout will be synced. + */ + private int mDockingStateVersion; + + /** + * Syncs the window docking state. + * <p> + * The layout editor lets you change the docking state -- e.g. you can minimize the + * palette, and drag the structure view to the bottom, and so on. When you restart + * the IDE, the window comes back up with your customized state. + * <p> + * <b>However</b>, when you have multiple editor files open, if you minimize the palette + * in one editor and then switch to another, the other editor will have the old window + * state. That's because each editor has its own set of windows. + * <p> + * This method fixes this. Whenever a window is shown, this method is called, and the + * docking state is synced such that the editor will match the current persistent docking + * state. + */ + private void syncDockingState() { + if (mDockingStateVersion == sDockingStateVersion) { + // No changes to apply + return; + } + mDockingStateVersion = sDockingStateVersion; + + IPreferenceStore preferenceStore = AdtPlugin.getDefault().getPreferenceStore(); + PluginFlyoutPreferences preferences; + preferences = new PluginFlyoutPreferences(preferenceStore, PREF_PALETTE); + mPaletteComposite.apply(preferences); + preferences = new PluginFlyoutPreferences(preferenceStore, PREF_STRUCTURE); + mStructureFlyout.apply(preferences); + mPaletteComposite.layout(); + mStructureFlyout.layout(); + mPaletteComposite.redraw(); // the structure view is nested within the palette + } + + /** + * Responds to a page change that made the Graphical editor page the deactivated page + */ + public void deactivated() { + mActive = false; + + LayoutCanvas canvas = getCanvasControl(); + if (canvas != null) { + canvas.deactivated(); + } + } + + /** + * Opens and initialize the editor with a new file. + * @param file the file being edited. + */ + public void openFile(IFile file) { + mEditedFile = file; + mConfigChooser.setFile(mEditedFile); + + if (mReloadListener == null) { + mReloadListener = new ReloadListener(); + LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener); + } + + if (mRulesEngine == null) { + mRulesEngine = new RulesEngine(this, mEditedFile.getProject()); + if (mCanvasViewer != null) { + mCanvasViewer.getCanvas().setRulesEngine(mRulesEngine); + } + } + + // Pick up hand-off data: somebody requesting this file to be opened may have + // requested that it should be opened as included within another file + if (mEditedFile != null) { + try { + mIncludedWithin = (Reference) mEditedFile.getSessionProperty(NAME_INCLUDE); + if (mIncludedWithin != null) { + // Only use once + mEditedFile.setSessionProperty(NAME_INCLUDE, null); + } + } catch (CoreException e) { + AdtPlugin.log(e, "Can't access session property %1$s", NAME_INCLUDE); + } + } + + computeSdkVersion(); + } + + /** + * Resets the editor with a replacement file. + * @param file the replacement file. + */ + public void replaceFile(IFile file) { + mEditedFile = file; + mConfigChooser.replaceFile(mEditedFile); + computeSdkVersion(); + } + + /** + * Resets the editor with a replacement file coming from a config change in the config + * selector. + * @param file the replacement file. + */ + public void changeFileOnNewConfig(IFile file) { + mEditedFile = file; + mConfigChooser.changeFileOnNewConfig(mEditedFile); + } + + /** + * Responds to a target change for the project of the edited file + */ + public void onTargetChange() { + AndroidTargetData targetData = mConfigChooser.onXmlModelLoaded(); + updateCapabilities(targetData); + + changed(CFG_FOLDER | CFG_TARGET); + } + + /** Updates the capabilities for the given target data (which may be null) */ + private void updateCapabilities(AndroidTargetData targetData) { + if (targetData != null) { + LayoutLibrary layoutLib = targetData.getLayoutLibrary(); + if (mIncludedWithin != null && !layoutLib.supports(Capability.EMBEDDED_LAYOUT)) { + showIn(null); + } + } + } + + /** + * Returns the {@link CommonXmlDelegate} for this editor + * + * @return the {@link CommonXmlDelegate} for this editor + */ + @NonNull + public LayoutEditorDelegate getEditorDelegate() { + return mEditorDelegate; + } + + /** + * Returns the {@link RulesEngine} associated with this editor + * + * @return the {@link RulesEngine} associated with this editor, never null + */ + public RulesEngine getRulesEngine() { + return mRulesEngine; + } + + /** + * Return the {@link LayoutCanvas} associated with this editor + * + * @return the associated {@link LayoutCanvas} + */ + public LayoutCanvas getCanvasControl() { + if (mCanvasViewer != null) { + return mCanvasViewer.getCanvas(); + } + return null; + } + + /** + * Returns the {@link UiDocumentNode} for the XML model edited by this editor + * + * @return the associated model + */ + public UiDocumentNode getModel() { + return mEditorDelegate.getUiRootNode(); + } + + /** + * Callback for XML model changed. Only update/recompute the layout if the editor is visible + */ + public void onXmlModelChanged() { + // To optimize the rendering when the user is editing in the XML pane, we don't + // refresh the editor if it's not the active part. + // + // This behavior is acceptable when the editor is the single "full screen" part + // (as in this case active means visible.) + // Unfortunately this breaks in 2 cases: + // - when performing a drag'n'drop from one editor to another, the target is not + // properly refreshed before it becomes active. + // - when duplicating the editor window and placing both editors side by side (xml in one + // and canvas in the other one), the canvas may not be refreshed when the XML is edited. + // + // TODO find a way to really query whether the pane is visible, not just active. + + if (mEditorDelegate.isGraphicalEditorActive()) { + recomputeLayout(); + } else { + // Remember we want to recompute as soon as the editor becomes active. + mNeedsRecompute = true; + } + } + + /** + * Recomputes the layout + */ + public void recomputeLayout() { + try { + if (!ensureFileValid()) { + return; + } + + UiDocumentNode model = getModel(); + LayoutCanvas canvas = mCanvasViewer.getCanvas(); + if (!ensureModelValid(model)) { + // Although we display an error, we still treat an empty document as a + // successful layout result so that we can drop new elements in it. + // + // For that purpose, create a special LayoutScene that has no image, + // no root view yet indicates success and then update the canvas with it. + + canvas.setSession( + new StaticRenderSession( + Result.Status.SUCCESS.createResult(), + null /*rootViewInfo*/, null /*image*/), + null /*explodeNodes*/, true /* layoutlib5 */); + return; + } + + LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/); + + if (layoutLib != null) { + // if drawing in real size, (re)set the scaling factor. + if (mActionBar.isZoomingRealSize()) { + mActionBar.computeAndSetRealScale(false /* redraw */); + } + + IProject project = mEditedFile.getProject(); + renderWithBridge(project, model, layoutLib); + + canvas.getPreviewManager().renderPreviews(); + } + } finally { + // no matter the result, we are done doing the recompute based on the latest + // resource/code change. + mNeedsRecompute = false; + } + } + + /** + * Reloads the palette + */ + public void reloadPalette() { + if (mPalette != null) { + IAndroidTarget renderingTarget = getRenderingTarget(); + if (renderingTarget != null) { + mPalette.reloadPalette(renderingTarget); + } + } + } + + /** + * Returns the {@link LayoutLibrary} associated with this editor, if it has + * been initialized already. May return null if it has not been initialized (or has + * not finished initializing). + * + * @return The {@link LayoutLibrary}, or null + */ + public LayoutLibrary getLayoutLibrary() { + return getReadyLayoutLib(false /*displayError*/); + } + + /** + * Returns the scale to multiply pixels in the layout coordinate space with to obtain + * the corresponding dip (device independent pixel) + * + * @return the scale to multiple layout coordinates with to obtain the dip position + */ + public float getDipScale() { + float dpi = mConfigChooser.getConfiguration().getDensity().getDpiValue(); + return Density.DEFAULT_DENSITY / dpi; + } + + // --- private methods --- + + /** + * Ensure that the file associated with this editor is valid (exists and is + * synchronized). Any reasons why it is not are displayed in the editor's error area. + * + * @return True if the editor is valid, false otherwise. + */ + private boolean ensureFileValid() { + // check that the resource exists. If the file is opened but the project is closed + // or deleted for some reason (changed from outside of eclipse), then this will + // return false; + if (mEditedFile.exists() == false) { + displayError("Resource '%1$s' does not exist.", + mEditedFile.getFullPath().toString()); + return false; + } + + if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) { + String message = String.format("%1$s is out of sync. Please refresh.", + mEditedFile.getName()); + + displayError(message); + + // also print it in the error console. + IProject iProject = mEditedFile.getProject(); + AdtPlugin.printErrorToConsole(iProject.getName(), message); + return false; + } + + return true; + } + + /** + * Returns a {@link LayoutLibrary} that is ready for rendering, or null if the bridge + * is not available or not ready yet (due to SDK loading still being in progress etc). + * If enabled, any reasons preventing the bridge from being returned are displayed to the + * editor's error area. + * + * @param displayError whether to display the loading error or not. + * + * @return LayoutBridge the layout bridge for rendering this editor's scene + */ + LayoutLibrary getReadyLayoutLib(boolean displayError) { + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = getRenderingTarget(); + + if (target != null) { + AndroidTargetData data = currentSdk.getTargetData(target); + if (data != null) { + LayoutLibrary layoutLib = data.getLayoutLibrary(); + + if (layoutLib.getStatus() == LoadStatus.LOADED) { + return layoutLib; + } else if (displayError) { // getBridge() == null + // SDK is loaded but not the layout library! + + // check whether the bridge managed to load, or not + if (layoutLib.getStatus() == LoadStatus.LOADING) { + 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.", + mEditedFile.getName()); + } else { + String message = layoutLib.getLoadMessage(); + displayError("Eclipse failed to load the framework information and the layout library!" + + message != null ? "\n" + message : ""); + } + } + } else { // data == null + // It can happen that the workspace refreshes while the SDK is loading its + // data, which could trigger a redraw of the opened layout if some resources + // changed while Eclipse is closed. + // In this case data could be null, but this is not an error. + // We can just silently return, as all the opened editors are automatically + // refreshed once the SDK finishes loading. + LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null); + + // display error is asked. + if (displayError) { + String targetName = target.getName(); + switch (targetLoadStatus) { + case LOADING: + String s; + if (currentSdk.getTarget(getProject()) == target) { + s = String.format( + "The project target (%1$s) is still loading.", + targetName); + } else { + s = String.format( + "The rendering target (%1$s) is still loading.", + targetName); + } + s += "\nThe layout will refresh automatically once the process is finished."; + displayError(s); + + break; + case FAILED: // known failure + case LOADED: // success but data isn't loaded?!?! + displayError("The project target (%s) was not properly loaded.", + targetName); + break; + } + } + } + + } else if (displayError) { // target == null + displayError("The project target is not set. Right click project, choose Properties | Android."); + } + } else if (displayError) { // currentSdk == null + displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.", + mEditedFile.getName()); + } + + return null; + } + + /** + * Returns the {@link IAndroidTarget} used for the rendering. + * <p/> + * This first looks for the rendering target setup in the config UI, and if nothing has + * been setup yet, returns the target of the project. + * + * @return an IAndroidTarget object or null if no target is setup and the project has no + * target set. + * + */ + public IAndroidTarget getRenderingTarget() { + // if the SDK is null no targets are loaded. + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk == null) { + return null; + } + + // attempt to get a target from the configuration selector. + IAndroidTarget renderingTarget = mConfigChooser.getConfiguration().getTarget(); + if (renderingTarget != null) { + return renderingTarget; + } + + // fall back to the project target + if (mEditedFile != null) { + return currentSdk.getTarget(mEditedFile.getProject()); + } + + return null; + } + + /** + * Returns whether the current rendering target supports the given capability + * + * @param capability the capability to be looked up + * @return true if the current rendering target supports the given capability + */ + public boolean renderingSupports(Capability capability) { + IAndroidTarget target = getRenderingTarget(); + if (target != null) { + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + LayoutLibrary layoutLib = targetData.getLayoutLibrary(); + return layoutLib.supports(capability); + } + + return false; + } + + private boolean ensureModelValid(UiDocumentNode model) { + // check there is actually a model (maybe the file is empty). + if (model.getUiChildren().size() == 0) { + if (mEditorDelegate.getEditor().isCreatingPages()) { + displayError("Loading editor"); + return false; + } + displayError( + "No XML content. Please add a root view or layout to your document."); + return false; + } + + return true; + } + + /** + * Creates a {@link RenderService} associated with this editor + * @return the render service + */ + @NonNull + public RenderService createRenderService() { + return RenderService.create(this, mCredential); + } + + /** + * Creates a {@link RenderLogger} associated with this editor + * @param name the name of the logger + * @return the new logger + */ + @NonNull + public RenderLogger createRenderLogger(String name) { + return new RenderLogger(name, mCredential); + } + + /** + * Creates a {@link RenderService} associated with this editor + * + * @param configuration the configuration to use (and fallback to editor for the rest) + * @param resolver a resource resolver to use to look up resources + * @return the render service + */ + @NonNull + public RenderService createRenderService(Configuration configuration, + ResourceResolver resolver) { + return RenderService.create(this, configuration, resolver, mCredential); + } + + private void renderWithBridge(IProject iProject, UiDocumentNode model, + LayoutLibrary layoutLib) { + LayoutCanvas canvas = getCanvasControl(); + Set<UiElementNode> explodeNodes = canvas.getNodesToExplode(); + RenderLogger logger = createRenderLogger(mEditedFile.getName()); + RenderingMode renderingMode = RenderingMode.NORMAL; + // FIXME set the rendering mode using ViewRule or something. + List<UiElementNode> children = model.getUiChildren(); + if (children.size() > 0 && + children.get(0).getDescriptor().getXmlLocalName().equals(SCROLL_VIEW)) { + renderingMode = RenderingMode.V_SCROLL; + } + + RenderSession session = RenderService.create(this, mCredential) + .setModel(model) + .setLog(logger) + .setRenderingMode(renderingMode) + .setIncludedWithin(mIncludedWithin) + .setNodesToExpand(explodeNodes) + .createRenderSession(); + + boolean layoutlib5 = layoutLib.supports(Capability.EMBEDDED_LAYOUT); + canvas.setSession(session, explodeNodes, layoutlib5); + + // update the UiElementNode with the layout info. + if (session != null && session.getResult().isSuccess() == false) { + // An error was generated. Print it (and any other accumulated warnings) + String errorMessage = session.getResult().getErrorMessage(); + Throwable exception = session.getResult().getException(); + if (exception != null && errorMessage == null) { + errorMessage = exception.toString(); + } + if (exception != null || (errorMessage != null && errorMessage.length() > 0)) { + logger.error(null, errorMessage, exception, null /*data*/); + } else if (!logger.hasProblems()) { + logger.error(null, "Unexpected error in rendering, no details given", + null /*data*/); + } + // These errors will be included in the log warnings which are + // displayed regardless of render success status below + } + + // We might have detected some missing classes and swapped them by a mock view, + // or run into fidelity warnings or missing resources, so emit all these + // warnings + Set<String> missingClasses = mProjectCallback.getMissingClasses(); + Set<String> brokenClasses = mProjectCallback.getUninstantiatableClasses(); + if (logger.hasProblems()) { + displayLoggerProblems(iProject, logger); + displayFailingClasses(missingClasses, brokenClasses, true); + displayUserStackTrace(logger, true); + } else if (missingClasses.size() > 0 || brokenClasses.size() > 0) { + displayFailingClasses(missingClasses, brokenClasses, false); + displayUserStackTrace(logger, true); + } else if (session != null) { + // Nope, no missing or broken classes. Clear success, congrats! + hideError(); + + // First time this layout is opened, run lint on the file (after a delay) + if (!mRenderedOnce) { + mRenderedOnce = true; + Job job = new Job("Run Lint") { + @Override + protected IStatus run(IProgressMonitor monitor) { + getEditorDelegate().delegateRunLint(); + return Status.OK_STATUS; + } + + }; + job.setSystem(true); + job.schedule(3000); // 3 seconds + } + + mConfigChooser.ensureInitialized(); + } + + model.refreshUi(); + } + + /** + * Returns the {@link ResourceResolver} for this editor + * + * @return the resolver used to resolve resources for the current configuration of + * this editor, or null + */ + public ResourceResolver getResourceResolver() { + if (mResourceResolver == null) { + String theme = mConfigChooser.getThemeName(); + if (theme == null) { + displayError("Missing theme."); + return null; + } + boolean isProjectTheme = mConfigChooser.getConfiguration().isProjectTheme(); + + Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = + getConfiguredProjectResources(); + + // Get the framework resources + Map<ResourceType, Map<String, ResourceValue>> frameworkResources = + getConfiguredFrameworkResources(); + + if (configuredProjectRes == null) { + displayError("Missing project resources for current configuration."); + return null; + } + + if (frameworkResources == null) { + displayError("Missing framework resources."); + return null; + } + + mResourceResolver = ResourceResolver.create( + configuredProjectRes, frameworkResources, + theme, isProjectTheme); + } + + return mResourceResolver; + } + + /** Returns a project callback, and optionally resets it */ + ProjectCallback getProjectCallback(boolean reset, LayoutLibrary layoutLibrary) { + // Lazily create the project callback the first time we need it + if (mProjectCallback == null) { + ResourceManager resManager = ResourceManager.getInstance(); + IProject project = getProject(); + ProjectResources projectRes = resManager.getProjectResources(project); + mProjectCallback = new ProjectCallback(layoutLibrary, projectRes, project, + mCredential, this); + } else if (reset) { + // Also clears the set of missing/broken classes prior to rendering + mProjectCallback.getMissingClasses().clear(); + mProjectCallback.getUninstantiatableClasses().clear(); + } + + return mProjectCallback; + } + + /** + * Returns the resource name of this layout, NOT including the @layout/ prefix + * + * @return the resource name of this layout, NOT including the @layout/ prefix + */ + public String getLayoutResourceName() { + return ResourceHelper.getLayoutName(mEditedFile); + } + + /** + * Cleans up when the rendering target is about to change + * @param oldTarget the old rendering target. + */ + private void preRenderingTargetChangeCleanUp(IAndroidTarget oldTarget) { + // first clear the caches related to this file in the old target + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + AndroidTargetData data = currentSdk.getTargetData(oldTarget); + if (data != null) { + LayoutLibrary layoutLib = data.getLayoutLibrary(); + + // layoutLib can never be null. + layoutLib.clearCaches(mEditedFile.getProject()); + } + } + + // Also remove the ProjectCallback as it caches custom views which must be reloaded + // with the classloader of the new LayoutLib. We also have to clear it out + // because it stores a reference to the layout library which could have changed. + mProjectCallback = null; + + // FIXME: get rid of the current LayoutScene if any. + } + + private class ReloadListener implements ILayoutReloadListener { + /** + * Called when the file changes triggered a redraw of the layout + */ + @Override + public void reloadLayout(final ChangeFlags flags, final boolean libraryChanged) { + if (mConfigChooser.isDisposed()) { + return; + } + Display display = mConfigChooser.getDisplay(); + display.asyncExec(new Runnable() { + @Override + public void run() { + reloadLayoutSwt(flags, libraryChanged); + } + }); + } + + /** Reload layout. <b>Must be called on the SWT thread</b> */ + private void reloadLayoutSwt(ChangeFlags flags, boolean libraryChanged) { + if (mConfigChooser.isDisposed()) { + return; + } + assert mConfigChooser.getDisplay().getThread() == Thread.currentThread(); + + boolean recompute = false; + // we only care about the r class of the main project. + if (flags.rClass && libraryChanged == false) { + recompute = true; + if (mEditedFile != null) { + ResourceManager manager = ResourceManager.getInstance(); + ProjectResources projectRes = manager.getProjectResources( + mEditedFile.getProject()); + + if (projectRes != null) { + projectRes.resetDynamicIds(); + } + } + } + + if (flags.localeList) { + // the locale list *potentially* changed so we update the locale in the + // config composite. + // However there's no recompute, as it could not be needed + // (for instance a new layout) + // If a resource that's not a layout changed this will trigger a recompute anyway. + mConfigChooser.updateLocales(); + } + + // if a resources was modified. + if (flags.resources) { + recompute = true; + + // TODO: differentiate between single and multi resource file changed, and whether + // the resource change affects the cache. + + // force a reparse in case a value XML file changed. + mConfiguredProjectRes = null; + mResourceResolver = null; + + // clear the cache in the bridge in case a bitmap/9-patch changed. + LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/); + if (layoutLib != null) { + layoutLib.clearCaches(mEditedFile.getProject()); + } + } + + if (flags.code) { + // only recompute if the custom view loader was used to load some code. + if (mProjectCallback != null && mProjectCallback.isUsed()) { + mProjectCallback = null; + recompute = true; + } + } + + if (flags.manifest) { + recompute |= computeSdkVersion(); + } + + if (recompute) { + if (mEditorDelegate.isGraphicalEditorActive()) { + recomputeLayout(); + } else { + mNeedsRecompute = true; + } + } + } + } + + // ---- Error handling ---- + + /** + * Switches the sash to display the error label. + * + * @param errorFormat The new error to display if not null. + * @param parameters String.format parameters for the error format. + */ + private void displayError(String errorFormat, Object...parameters) { + if (errorFormat != null) { + mErrorLabel.setText(String.format(errorFormat, parameters)); + } else { + mErrorLabel.setText(""); + } + mSashError.setMaximizedControl(null); + } + + /** Displays the canvas and hides the error label. */ + private void hideError() { + mErrorLabel.setText(""); + mSashError.setMaximizedControl(mCanvasViewer.getControl()); + } + + /** Display the problem list encountered during a render */ + private void displayUserStackTrace(RenderLogger logger, boolean append) { + List<Throwable> throwables = logger.getFirstTrace(); + if (throwables == null || throwables.isEmpty()) { + return; + } + + Throwable throwable = throwables.get(0); + + if (throwable instanceof RenderSecurityException) { + addActionLink(mErrorLabel, ActionLinkStyleRange.LINK_DISABLE_SANDBOX, + "\nTurn off custom view rendering sandbox\n"); + + StringBuilder builder = new StringBuilder(200); + String lastFailedPath = RenderSecurityManager.getLastFailedPath(); + if (lastFailedPath != null) { + builder.append("Diagnostic info for ADT bug report:\n"); + builder.append("Failed path: ").append(lastFailedPath).append('\n'); + String tempDir = System.getProperty("java.io.tmpdir"); + builder.append("Normal temp dir: ").append(tempDir).append('\n'); + File normalized = new File(tempDir); + builder.append("Normalized temp dir: ").append(normalized.getPath()).append('\n'); + try { + builder.append("Canonical temp dir: ").append(normalized.getCanonicalPath()) + .append('\n'); + } catch (IOException e) { + // ignore + } + builder.append("os.name: ").append(System.getProperty("os.name")).append('\n'); + builder.append("os.version: ").append(System.getProperty("os.version")); + builder.append('\n'); + builder.append("java.runtime.version: "); + builder.append(System.getProperty("java.runtime.version")); + } + if (throwable.getMessage().equals("Unable to create temporary file")) { + String javaVersion = System.getProperty("java.version"); + if (javaVersion.startsWith("1.7.0_")) { + int version = Integer + .parseInt(javaVersion.substring(javaVersion.indexOf('_') + 1)); + if (version > 0 && version < 45) { + builder.append('\n'); + builder.append("Tip: This may be caused by using an older version " + + "of JDK 1.7.0; try using at least 1.7.0_45 (you are using " + + javaVersion + ")"); + } + } + } + if (builder.length() > 0) { + addText(mErrorLabel, builder.toString()); + } + } + + StackTraceElement[] frames = throwable.getStackTrace(); + int end = -1; + boolean haveInterestingFrame = false; + for (int i = 0; i < frames.length; i++) { + StackTraceElement frame = frames[i]; + if (isInterestingFrame(frame)) { + haveInterestingFrame = true; + } + String className = frame.getClassName(); + if (className.equals( + "com.android.layoutlib.bridge.impl.RenderSessionImpl")) { //$NON-NLS-1$ + end = i; + break; + } + } + + if (end == -1 || !haveInterestingFrame) { + // Not a recognized stack trace range: just skip it + return; + } + + if (!append) { + mErrorLabel.setText("\n"); //$NON-NLS-1$ + } else { + addText(mErrorLabel, "\n\n"); //$NON-NLS-1$ + } + + addText(mErrorLabel, throwable.toString() + '\n'); + for (int i = 0; i < end; i++) { + StackTraceElement frame = frames[i]; + String className = frame.getClassName(); + String methodName = frame.getMethodName(); + addText(mErrorLabel, " at " + className + '.' + methodName + '('); + String fileName = frame.getFileName(); + if (fileName != null && !fileName.isEmpty()) { + int lineNumber = frame.getLineNumber(); + String location = fileName + ':' + lineNumber; + if (isInterestingFrame(frame)) { + addActionLink(mErrorLabel, ActionLinkStyleRange.LINK_OPEN_LINE, + location, className, methodName, fileName, lineNumber); + } else { + addText(mErrorLabel, location); + } + addText(mErrorLabel, ")\n"); //$NON-NLS-1$ + } + } + } + + private static boolean isInterestingFrame(StackTraceElement frame) { + String className = frame.getClassName(); + return !(className.startsWith("android.") //$NON-NLS-1$ + || className.startsWith("com.android.") //$NON-NLS-1$ + || className.startsWith("java.") //$NON-NLS-1$ + || className.startsWith("javax.") //$NON-NLS-1$ + || className.startsWith("sun.")); //$NON-NLS-1$ + } + + /** + * Switches the sash to display the error label to show a list of + * missing classes and give options to create them. + */ + private void displayFailingClasses(Set<String> missingClasses, Set<String> brokenClasses, + boolean append) { + if (missingClasses.size() == 0 && brokenClasses.size() == 0) { + return; + } + + if (!append) { + mErrorLabel.setText(""); //$NON-NLS-1$ + } else { + addText(mErrorLabel, "\n"); //$NON-NLS-1$ + } + + if (missingClasses.size() > 0) { + addText(mErrorLabel, "The following classes could not be found:\n"); + for (String clazz : missingClasses) { + addText(mErrorLabel, "- "); + addText(mErrorLabel, clazz); + addText(mErrorLabel, " ("); + + IProject project = getProject(); + Collection<String> customViews = getCustomViewClassNames(project); + addTypoSuggestions(clazz, customViews, false); + addTypoSuggestions(clazz, customViews, true); + addTypoSuggestions(clazz, getAndroidViewClassNames(project), false); + + addActionLink(mErrorLabel, + ActionLinkStyleRange.LINK_FIX_BUILD_PATH, "Fix Build Path", clazz); + addText(mErrorLabel, ", "); + addActionLink(mErrorLabel, + ActionLinkStyleRange.LINK_EDIT_XML, "Edit XML", clazz); + if (clazz.indexOf('.') != -1) { + // Add "Create Class" link, but only for custom views + addText(mErrorLabel, ", "); + addActionLink(mErrorLabel, + ActionLinkStyleRange.LINK_CREATE_CLASS, "Create Class", clazz); + } + addText(mErrorLabel, ")\n"); + } + } + if (brokenClasses.size() > 0) { + addText(mErrorLabel, "The following classes could not be instantiated:\n"); + + // Do we have a custom class (not an Android or add-ons class) + boolean haveCustomClass = false; + + for (String clazz : brokenClasses) { + addText(mErrorLabel, "- "); + addText(mErrorLabel, clazz); + addText(mErrorLabel, " ("); + addActionLink(mErrorLabel, + ActionLinkStyleRange.LINK_OPEN_CLASS, "Open Class", clazz); + addText(mErrorLabel, ", "); + addActionLink(mErrorLabel, + ActionLinkStyleRange.LINK_SHOW_LOG, "Show Error Log", clazz); + addText(mErrorLabel, ")\n"); + + if (!(clazz.startsWith("android.") || //$NON-NLS-1$ + clazz.startsWith("com.google."))) { //$NON-NLS-1$ + haveCustomClass = true; + } + } + + addText(mErrorLabel, "See the Error Log (Window > Show View) for more details.\n"); + + if (haveCustomClass) { + addBoldText(mErrorLabel, "Tip: Use View.isInEditMode() in your custom views " + + "to skip code when shown in Eclipse"); + } + } + + mSashError.setMaximizedControl(null); + } + + private void addTypoSuggestions(String actual, Collection<String> views, + boolean compareWithPackage) { + if (views.size() == 0) { + return; + } + + // Look for typos and try to match with custom views and android views + String actualBase = actual.substring(actual.lastIndexOf('.') + 1); + int maxDistance = actualBase.length() >= 4 ? 2 : 1; + + if (views.size() > 0) { + for (String suggested : views) { + String suggestedBase = suggested.substring(suggested.lastIndexOf('.') + 1); + + String matchWith = compareWithPackage ? suggested : suggestedBase; + if (Math.abs(actualBase.length() - matchWith.length()) > maxDistance) { + // The string lengths differ more than the allowed edit distance; + // no point in even attempting to compute the edit distance (requires + // O(n*m) storage and O(n*m) speed, where n and m are the string lengths) + continue; + } + if (LintUtils.editDistance(actualBase, matchWith) <= maxDistance) { + // Suggest this class as a typo for the given class + String labelClass = (suggestedBase.equals(actual) || actual.indexOf('.') != -1) + ? suggested : suggestedBase; + addActionLink(mErrorLabel, + ActionLinkStyleRange.LINK_CHANGE_CLASS_TO, + String.format("Change to %1$s", + // Only show full package name if class name + // is the same + labelClass), + actual, + viewNeedsPackage(suggested) ? suggested : suggestedBase); + addText(mErrorLabel, ", "); + } + } + } + } + + private static Collection<String> getCustomViewClassNames(IProject project) { + CustomViewFinder finder = CustomViewFinder.get(project); + Collection<String> views = finder.getAllViews(); + if (views == null) { + finder.refresh(); + views = finder.getAllViews(); + } + + return views; + } + + private static Collection<String> getAndroidViewClassNames(IProject project) { + Sdk currentSdk = Sdk.getCurrent(); + IAndroidTarget target = currentSdk.getTarget(project); + if (target != null) { + AndroidTargetData targetData = currentSdk.getTargetData(target); + if (targetData != null) { + LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors(); + return layoutDescriptors.getAllViewClassNames(); + } + } + + return Collections.emptyList(); + } + + /** Add a normal line of text to the styled text widget. */ + private void addText(StyledText styledText, String...string) { + for (String s : string) { + styledText.append(s); + } + } + + /** Display the problem list encountered during a render */ + private void displayLoggerProblems(IProject project, RenderLogger logger) { + if (logger.hasProblems()) { + mErrorLabel.setText(""); + // A common source of problems is attempting to open a layout when there are + // compilation errors. In this case, may not have run (or may not be up to date) + // so resources cannot be looked up etc. Explain this situation to the user. + + boolean hasAaptErrors = false; + boolean hasJavaErrors = false; + try { + IMarker[] markers; + markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); + if (markers.length > 0) { + for (IMarker marker : markers) { + String markerType = marker.getType(); + if (markerType.equals(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER)) { + int severity = marker.getAttribute(IMarker.SEVERITY, -1); + if (severity == IMarker.SEVERITY_ERROR) { + hasJavaErrors = true; + } + } else if (markerType.equals(AdtConstants.MARKER_AAPT_COMPILE)) { + int severity = marker.getAttribute(IMarker.SEVERITY, -1); + if (severity == IMarker.SEVERITY_ERROR) { + hasAaptErrors = true; + } + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + if (logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR)) { + addBoldText(mErrorLabel, + "Missing styles. Is the correct theme chosen for this layout?\n"); + addText(mErrorLabel, + "Use the Theme combo box above the layout to choose a different layout, " + + "or fix the theme style references.\n\n"); + } + + List<Throwable> trace = logger.getFirstTrace(); + if (trace != null + && trace.toString().contains( + "java.lang.IndexOutOfBoundsException: Index: 2, Size: 2") //$NON-NLS-1$ + && mConfigChooser.getConfiguration().getDensity() == Density.TV) { + addBoldText(mErrorLabel, + "It looks like you are using a render target where the layout library " + + "does not support the tvdpi density.\n\n"); + addText(mErrorLabel, "Please try either updating to " + + "the latest available version (using the SDK manager), or if no updated " + + "version is available for this specific version of Android, try using " + + "a more recent render target version.\n\n"); + + } + + if (hasAaptErrors && logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_PREFIX)) { + // Text will automatically be wrapped by the error widget so no reason + // to insert linebreaks in this error message: + String message = + "NOTE: This project contains resource errors, so aapt did not succeed, " + + "which can cause rendering failures. " + + "Fix resource problems first.\n\n"; + addBoldText(mErrorLabel, message); + } else if (hasJavaErrors && mProjectCallback != null && mProjectCallback.isUsed()) { + // Text will automatically be wrapped by the error widget so no reason + // to insert linebreaks in this error message: + String message = + "NOTE: This project contains Java compilation errors, " + + "which can cause rendering failures for custom views. " + + "Fix compilation problems first.\n\n"; + addBoldText(mErrorLabel, message); + } + + if (logger.seenTag(RenderLogger.TAG_MISSING_DIMENSION)) { + List<UiElementNode> elements = UiDocumentNode.getAllElements(getModel()); + for (UiElementNode element : elements) { + String width = element.getAttributeValue(ATTR_LAYOUT_WIDTH); + if (width == null || width.length() == 0) { + addSetAttributeLink(element, ATTR_LAYOUT_WIDTH); + } + + String height = element.getAttributeValue(ATTR_LAYOUT_HEIGHT); + if (height == null || height.length() == 0) { + addSetAttributeLink(element, ATTR_LAYOUT_HEIGHT); + } + } + } + + String problems = logger.getProblems(false /*includeFidelityWarnings*/); + addText(mErrorLabel, problems); + + List<String> fidelityWarnings = logger.getFidelityWarnings(); + if (fidelityWarnings != null && fidelityWarnings.size() > 0) { + addText(mErrorLabel, + "The graphics preview in the layout editor may not be accurate:\n"); + for (String warning : fidelityWarnings) { + addText(mErrorLabel, warning + ' '); + addActionLink(mErrorLabel, + ActionLinkStyleRange.IGNORE_FIDELITY_WARNING, + "(Ignore for this session)\n", warning); + } + } + + mSashError.setMaximizedControl(null); + } else { + mSashError.setMaximizedControl(mCanvasViewer.getControl()); + } + } + + /** Appends an action link to set the given attribute on the given value */ + private void addSetAttributeLink(UiElementNode element, String attribute) { + if (element.getXmlNode().getNodeName().equals(GRID_LAYOUT)) { + // GridLayout does not require a layout_width or layout_height to be defined + return; + } + + String fill = VALUE_FILL_PARENT; + // See whether we should offer match_parent instead of fill_parent + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(getProject()); + if (target.getVersion().getApiLevel() >= 8) { + fill = VALUE_MATCH_PARENT; + } + } + + String id = element.getAttributeValue(ATTR_ID); + if (id == null || id.length() == 0) { + id = '<' + element.getXmlNode().getNodeName() + '>'; + } else { + id = BaseLayoutRule.stripIdPrefix(id); + } + + addText(mErrorLabel, String.format("\"%1$s\" does not set the required %2$s attribute:\n", + id, attribute)); + addText(mErrorLabel, " (1) "); + addActionLink(mErrorLabel, + ActionLinkStyleRange.SET_ATTRIBUTE, + String.format("Set to \"%1$s\"", VALUE_WRAP_CONTENT), + element, attribute, VALUE_WRAP_CONTENT); + addText(mErrorLabel, "\n (2) "); + addActionLink(mErrorLabel, + ActionLinkStyleRange.SET_ATTRIBUTE, + String.format("Set to \"%1$s\"\n", fill), + element, attribute, fill); + } + + /** Appends the given text as a bold string in the given text widget */ + private void addBoldText(StyledText styledText, String text) { + String s = styledText.getText(); + int start = (s == null ? 0 : s.length()); + + styledText.append(text); + StyleRange sr = new StyleRange(); + sr.start = start; + sr.length = text.length(); + sr.fontStyle = SWT.BOLD; + styledText.setStyleRange(sr); + } + + /** + * Add a URL-looking link to the styled text widget. + * <p/> + * A mouse-click listener is setup and it interprets the link based on the + * action, corresponding to the value fields in {@link ActionLinkStyleRange}. + */ + private void addActionLink(StyledText styledText, int action, String label, + Object... data) { + String s = styledText.getText(); + int start = (s == null ? 0 : s.length()); + styledText.append(label); + + StyleRange sr = new ActionLinkStyleRange(action, data); + sr.start = start; + sr.length = label.length(); + sr.fontStyle = SWT.NORMAL; + sr.underlineStyle = SWT.UNDERLINE_LINK; + sr.underline = true; + styledText.setStyleRange(sr); + } + + /** + * Looks up the resource file corresponding to the given type + * + * @param type The type of resource to look up, such as {@link ResourceType#LAYOUT} + * @param name The name of the resource (not including ".xml") + * @param isFrameworkResource if true, the resource is a framework resource, otherwise + * it's a project resource + * @return the resource file defining the named resource, or null if not found + */ + public IPath findResourceFile(ResourceType type, String name, boolean isFrameworkResource) { + // FIXME: This code does not handle theme value resolution. + // There is code to handle this, but it's in layoutlib; we should + // expose that and use it here. + + Map<ResourceType, Map<String, ResourceValue>> map; + map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes; + if (map == null) { + // Not yet configured + return null; + } + + Map<String, ResourceValue> layoutMap = map.get(type); + if (layoutMap != null) { + ResourceValue value = layoutMap.get(name); + if (value != null) { + String valueStr = value.getValue(); + if (valueStr.startsWith("?")) { //$NON-NLS-1$ + // FIXME: It's a reference. We should resolve this properly. + return null; + } + return new Path(valueStr); + } + } + + return null; + } + + /** + * Looks up the path to the file corresponding to the given attribute value, such as + * @layout/foo, which will return the foo.xml file in res/layout/. (The general format + * of the resource url is {@literal @[<package_name>:]<resource_type>/<resource_name>}. + * + * @param url the attribute url + * @return the path to the file defining this attribute, or null if not found + */ + public IPath findResourceFile(String url) { + if (!url.startsWith("@")) { //$NON-NLS-1$ + return null; + } + int typeEnd = url.indexOf('/', 1); + if (typeEnd == -1) { + return null; + } + int nameBegin = typeEnd + 1; + int typeBegin = 1; + int colon = url.lastIndexOf(':', typeEnd); + boolean isFrameworkResource = false; + if (colon != -1) { + // The URL contains a package name. + // While the url format technically allows other package names, + // the platform apparently only supports @android for now (or if it does, + // there are no usages in the current code base so this is not common). + String packageName = url.substring(typeBegin, colon); + if (ANDROID_PKG.equals(packageName)) { + isFrameworkResource = true; + } + + typeBegin = colon + 1; + } + + String typeName = url.substring(typeBegin, typeEnd); + ResourceType type = ResourceType.getEnum(typeName); + if (type == null) { + return null; + } + + String name = url.substring(nameBegin); + return findResourceFile(type, name, isFrameworkResource); + } + + /** + * Resolve the given @string reference into a literal String using the current project + * configuration + * + * @param text the text resource reference to resolve + * @return the resolved string, or null + */ + public String findString(String text) { + if (text.startsWith(STRING_PREFIX)) { + return findString(text.substring(STRING_PREFIX.length()), false); + } else if (text.startsWith(ANDROID_STRING_PREFIX)) { + return findString(text.substring(ANDROID_STRING_PREFIX.length()), true); + } else { + return text; + } + } + + private String findString(String name, boolean isFrameworkResource) { + Map<ResourceType, Map<String, ResourceValue>> map; + map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes; + if (map == null) { + // Not yet configured + return null; + } + + Map<String, ResourceValue> layoutMap = map.get(ResourceType.STRING); + if (layoutMap != null) { + ResourceValue value = layoutMap.get(name); + if (value != null) { + // FIXME: This code does not handle theme value resolution. + // There is code to handle this, but it's in layoutlib; we should + // expose that and use it here. + return value.getValue(); + } + } + + return null; + } + + /** + * This StyleRange represents a clickable link in the render output, where various + * actions can be taken such as creating a class, opening the project chooser to + * adjust the build path, etc. + */ + private class ActionLinkStyleRange extends StyleRange { + /** Create a view class */ + private static final int LINK_CREATE_CLASS = 1; + /** Edit the build path for the current project */ + private static final int LINK_FIX_BUILD_PATH = 2; + /** Show the XML tab */ + private static final int LINK_EDIT_XML = 3; + /** Open the given class */ + private static final int LINK_OPEN_CLASS = 4; + /** Show the error log */ + private static final int LINK_SHOW_LOG = 5; + /** Change the class reference to the given fully qualified name */ + private static final int LINK_CHANGE_CLASS_TO = 6; + /** Ignore the given fidelity warning */ + private static final int IGNORE_FIDELITY_WARNING = 7; + /** Set an attribute on the given XML element to a given value */ + private static final int SET_ATTRIBUTE = 8; + /** Open the given file and line number */ + private static final int LINK_OPEN_LINE = 9; + /** Disable sandbox */ + private static final int LINK_DISABLE_SANDBOX = 10; + + /** Client data: the contents depend on the specific action */ + private final Object[] mData; + /** The action to be taken when the link is clicked */ + private final int mAction; + + private ActionLinkStyleRange(int action, Object... data) { + super(); + mAction = action; + mData = data; + } + + /** Performs the click action */ + public void onClick() { + switch (mAction) { + case LINK_CREATE_CLASS: + createNewClass((String) mData[0]); + break; + case LINK_EDIT_XML: + mEditorDelegate.getEditor().setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); + break; + case LINK_FIX_BUILD_PATH: + @SuppressWarnings("restriction") + String id = BuildPathsPropertyPage.PROP_ID; + PreferencesUtil.createPropertyDialogOn( + AdtPlugin.getShell(), + getProject(), id, null, null).open(); + break; + case LINK_OPEN_CLASS: + AdtPlugin.openJavaClass(getProject(), (String) mData[0]); + break; + case LINK_OPEN_LINE: + boolean success = AdtPlugin.openStackTraceLine( + (String) mData[0], // class + (String) mData[1], // method + (String) mData[2], // file + (Integer) mData[3]); // line + if (!success) { + MessageDialog.openError(mErrorLabel.getShell(), "Not Found", + String.format("Could not find %1$s.%2$s", mData[0], mData[1])); + } + break; + case LINK_SHOW_LOG: + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow workbenchWindow = workbench.getActiveWorkbenchWindow(); + try { + IWorkbenchPage page = workbenchWindow.getActivePage(); + page.showView("org.eclipse.pde.runtime.LogView"); //$NON-NLS-1$ + } catch (PartInitException e) { + AdtPlugin.log(e, null); + } + break; + case LINK_CHANGE_CLASS_TO: + // Change class reference of mData[0] to mData[1] + // TODO: run under undo lock + MultiTextEdit edits = new MultiTextEdit(); + ISourceViewer textViewer = + mEditorDelegate.getEditor().getStructuredSourceViewer(); + IDocument document = textViewer.getDocument(); + String xml = document.get(); + int index = 0; + // Replace <old with <new and </old with </new + String prefix = "<"; //$NON-NLS-1$ + String find = prefix + mData[0]; + String replaceWith = prefix + mData[1]; + while (true) { + index = xml.indexOf(find, index); + if (index == -1) { + break; + } + edits.addChild(new ReplaceEdit(index, find.length(), replaceWith)); + index += find.length(); + } + index = 0; + prefix = "</"; //$NON-NLS-1$ + find = prefix + mData[0]; + replaceWith = prefix + mData[1]; + while (true) { + index = xml.indexOf(find, index); + if (index == -1) { + break; + } + edits.addChild(new ReplaceEdit(index, find.length(), replaceWith)); + index += find.length(); + } + // Handle <view class="old"> + index = 0; + prefix = "\""; //$NON-NLS-1$ + String suffix = "\""; //$NON-NLS-1$ + find = prefix + mData[0] + suffix; + replaceWith = prefix + mData[1] + suffix; + while (true) { + index = xml.indexOf(find, index); + if (index == -1) { + break; + } + edits.addChild(new ReplaceEdit(index, find.length(), replaceWith)); + index += find.length(); + } + try { + edits.apply(document); + } catch (MalformedTreeException e) { + AdtPlugin.log(e, null); + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + break; + case IGNORE_FIDELITY_WARNING: + RenderLogger.ignoreFidelityWarning((String) mData[0]); + recomputeLayout(); + break; + case SET_ATTRIBUTE: { + final UiElementNode element = (UiElementNode) mData[0]; + final String attribute = (String) mData[1]; + final String value = (String) mData[2]; + mEditorDelegate.getEditor().wrapUndoEditXmlModel( + String.format("Set \"%1$s\" to \"%2$s\"", attribute, value), + new Runnable() { + @Override + public void run() { + element.setAttributeValue(attribute, ANDROID_URI, value, true); + element.commitDirtyAttributesToXml(); + } + }); + break; + } + case LINK_DISABLE_SANDBOX: { + RenderSecurityManager.sEnabled = false; + recomputeLayout(); + + MessageDialog.openInformation(AdtPlugin.getShell(), + "Disabled Rendering Sandbox", + "The custom view rendering sandbox was disabled for this session.\n\n" + + "You can turn it off permanently by adding\n" + + "-D" + ENABLED_PROPERTY + "=" + VALUE_FALSE + "\n" + + "as a new line in eclipse.ini."); + + break; + } + default: + assert false : mAction; + break; + } + } + + @Override + public boolean similarTo(StyleRange style) { + // Prevent adjacent link ranges from getting merged + return false; + } + } + + /** + * Returns the error label for the graphical editor (which may not be visible + * or showing errors) + * + * @return the error label, never null + */ + StyledText getErrorLabel() { + return mErrorLabel; + } + + /** + * Monitor clicks on the error label. + * If the click happens on a style range created by + * {@link GraphicalEditorPart#addClassLink(StyledText, String)}, we assume it's about + * a missing class and we then proceed to display the standard Eclipse class creator wizard. + */ + private class ErrorLabelListener extends MouseAdapter { + + @Override + public void mouseUp(MouseEvent event) { + super.mouseUp(event); + + if (event.widget != mErrorLabel) { + return; + } + + int offset = mErrorLabel.getCaretOffset(); + + StyleRange r = null; + StyleRange[] ranges = mErrorLabel.getStyleRanges(); + if (ranges != null && ranges.length > 0) { + for (StyleRange sr : ranges) { + if (sr.start <= offset && sr.start + sr.length > offset) { + r = sr; + break; + } + } + } + + if (r instanceof ActionLinkStyleRange) { + ActionLinkStyleRange range = (ActionLinkStyleRange) r; + range.onClick(); + } + + LayoutCanvas canvas = getCanvasControl(); + canvas.updateMenuActionState(); + } + } + + private void createNewClass(String fqcn) { + + int pos = fqcn.lastIndexOf('.'); + String packageName = pos < 0 ? "" : fqcn.substring(0, pos); //$NON-NLS-1$ + String className = pos <= 0 || pos >= fqcn.length() ? "" : fqcn.substring(pos + 1); //$NON-NLS-1$ + + // create the wizard page for the class creation, and configure it + NewClassWizardPage page = new NewClassWizardPage(); + + // set the parent class + page.setSuperClass(SdkConstants.CLASS_VIEW, true /* canBeModified */); + + // get the source folders as java elements. + IPackageFragmentRoot[] roots = getPackageFragmentRoots( + mEditorDelegate.getEditor().getProject(), + false /*includeContainers*/, true /*skipGenFolder*/); + + IPackageFragmentRoot currentRoot = null; + IPackageFragment currentFragment = null; + int packageMatchCount = -1; + + for (IPackageFragmentRoot root : roots) { + // Get the java element for the package. + // This method is said to always return a IPackageFragment even if the + // underlying folder doesn't exist... + IPackageFragment fragment = root.getPackageFragment(packageName); + if (fragment != null && fragment.exists()) { + // we have a perfect match! we use it. + currentRoot = root; + currentFragment = fragment; + packageMatchCount = -1; + break; + } else { + // we don't have a match. we look for the fragment with the best match + // (ie the closest parent package we can find) + try { + IJavaElement[] children; + children = root.getChildren(); + for (IJavaElement child : children) { + if (child instanceof IPackageFragment) { + fragment = (IPackageFragment)child; + if (packageName.startsWith(fragment.getElementName())) { + // its a match. get the number of segments + String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$ + if (segments.length > packageMatchCount) { + packageMatchCount = segments.length; + currentFragment = fragment; + currentRoot = root; + } + } + } + } + } catch (JavaModelException e) { + // Couldn't get the children: we just ignore this package root. + } + } + } + + ArrayList<IPackageFragment> createdFragments = null; + + if (currentRoot != null) { + // if we have a perfect match, we set it and we're done. + if (packageMatchCount == -1) { + page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/); + page.setPackageFragment(currentFragment, true /* canBeModified */); + } else { + // we have a partial match. + // create the package. We have to start with the first segment so that we + // know what to delete in case of a cancel. + try { + createdFragments = new ArrayList<IPackageFragment>(); + + int totalCount = packageName.split("\\.").length; //$NON-NLS-1$ + int count = 0; + int index = -1; + // skip the matching packages + while (count < packageMatchCount) { + index = packageName.indexOf('.', index+1); + count++; + } + + // create the rest of the segments, except for the last one as indexOf will + // return -1; + while (count < totalCount - 1) { + index = packageName.indexOf('.', index+1); + count++; + createdFragments.add(currentRoot.createPackageFragment( + packageName.substring(0, index), + true /* force*/, new NullProgressMonitor())); + } + + // create the last package + createdFragments.add(currentRoot.createPackageFragment( + packageName, true /* force*/, new NullProgressMonitor())); + + // set the root and fragment in the Wizard page + page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/); + page.setPackageFragment(createdFragments.get(createdFragments.size()-1), + true /* canBeModified */); + } catch (JavaModelException e) { + // If we can't create the packages, there's a problem. + // We revert to the default package + for (IPackageFragmentRoot root : roots) { + // Get the java element for the package. + // This method is said to always return a IPackageFragment even if the + // underlying folder doesn't exist... + IPackageFragment fragment = root.getPackageFragment(packageName); + if (fragment != null && fragment.exists()) { + page.setPackageFragmentRoot(root, true /* canBeModified*/); + page.setPackageFragment(fragment, true /* canBeModified */); + break; + } + } + } + } + } else if (roots.length > 0) { + // if we haven't found a valid fragment, we set the root to the first source folder. + page.setPackageFragmentRoot(roots[0], true /* canBeModified*/); + } + + // if we have a starting class name we use it + if (className != null) { + page.setTypeName(className, true /* canBeModified*/); + } + + // create the action that will open it the wizard. + OpenNewClassWizardAction action = new OpenNewClassWizardAction(); + action.setConfiguredWizardPage(page); + action.run(); + IJavaElement element = action.getCreatedElement(); + + if (element == null) { + // lets delete the packages we created just for this. + // we need to start with the leaf and go up + if (createdFragments != null) { + try { + for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) { + createdFragments.get(i).delete(true /* force*/, + new NullProgressMonitor()); + } + } catch (JavaModelException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source + * folders of the specified project. + * + * @param project the project + * @param includeContainers True to include containers + * @param skipGenFolder True to skip the "gen" folder + * @return an array of IPackageFragmentRoot. + */ + private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project, + boolean includeContainers, boolean skipGenFolder) { + ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>(); + try { + IJavaProject javaProject = JavaCore.create(project); + IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots(); + for (int i = 0; i < roots.length; i++) { + if (skipGenFolder) { + IResource resource = roots[i].getResource(); + if (resource != null && resource.getName().equals(FD_GEN_SOURCES)) { + continue; + } + } + IClasspathEntry entry = roots[i].getRawClasspathEntry(); + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE || + (includeContainers && + entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) { + result.add(roots[i]); + } + } + } catch (JavaModelException e) { + } + + return result.toArray(new IPackageFragmentRoot[result.size()]); + } + + /** + * Reopens this file as included within the given file (this assumes that the given + * file has an include tag referencing this view, and the set of views that have this + * property can be found using the {@link IncludeFinder}. + * + * @param includeWithin reference to a file to include as a surrounding context, + * or null to show the file standalone + */ + public void showIn(Reference includeWithin) { + mIncludedWithin = includeWithin; + + if (includeWithin != null) { + IFile file = includeWithin.getFile(); + + // Update configuration + if (file != null) { + mConfigChooser.resetConfigFor(file); + } + } + recomputeLayout(); + } + + /** + * Return all resource names of a given type, either in the project or in the + * framework. + * + * @param framework if true, return all the framework resource names, otherwise return + * all the project resource names + * @param type the type of resource to look up + * @return a collection of resource names, never null but possibly empty + */ + public Collection<String> getResourceNames(boolean framework, ResourceType type) { + Map<ResourceType, Map<String, ResourceValue>> map = + framework ? mConfiguredFrameworkRes : mConfiguredProjectRes; + Map<String, ResourceValue> animations = map.get(type); + if (animations != null) { + return animations.keySet(); + } else { + return Collections.emptyList(); + } + } + + /** + * Return this editor's current configuration + * + * @return the current configuration + */ + public FolderConfiguration getConfiguration() { + return mConfigChooser.getConfiguration().getFullConfig(); + } + + /** + * Figures out the project's minSdkVersion and targetSdkVersion and return whether the values + * have changed. + */ + private boolean computeSdkVersion() { + int oldMinSdkVersion = mMinSdkVersion; + int oldTargetSdkVersion = mTargetSdkVersion; + + Pair<Integer, Integer> v = ManifestInfo.computeSdkVersions(mEditedFile.getProject()); + mMinSdkVersion = v.getFirst(); + mTargetSdkVersion = v.getSecond(); + + return oldMinSdkVersion != mMinSdkVersion || oldTargetSdkVersion != mTargetSdkVersion; + } + + /** + * Returns the associated configuration chooser + * + * @return the configuration chooser + */ + @NonNull + public ConfigurationChooser getConfigurationChooser() { + return mConfigChooser; + } + + /** + * Returns the associated layout actions bar + * + * @return the layout actions bar + */ + @NonNull + public LayoutActionBar getLayoutActionBar() { + return mActionBar; + } + + /** + * Returns the target SDK version + * + * @return the target SDK version + */ + public int getTargetSdkVersion() { + return mTargetSdkVersion; + } + + /** + * Returns the minimum SDK version + * + * @return the minimum SDK version + */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** If the flyout hover is showing, dismiss it */ + public void dismissHoverPalette() { + mPaletteComposite.dismissHover(); + } + + // ---- Implements IFlyoutListener ---- + + @Override + public void stateChanged(int oldState, int newState) { + // Auto zoom the surface if you open or close flyout windows such as the palette + // or the property/outline views + if (newState == STATE_OPEN || newState == STATE_COLLAPSED && oldState == STATE_OPEN) { + getCanvasControl().setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/); + } + + sDockingStateVersion++; + mDockingStateVersion = sDockingStateVersion; + } +} |