/* * 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_FILL_PARENT; import static com.android.SdkConstants.VALUE_MATCH_PARENT; import static com.android.SdkConstants.VALUE_WRAP_CONTENT; 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.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.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; /** * 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; } private void renderWithBridge(IProject iProject, UiDocumentNode model, LayoutLibrary layoutLib) { LayoutCanvas canvas = getCanvasControl(); Set<UiElementNode> explodeNodes = canvas.getNodesToExplode(); RenderLogger logger = new RenderLogger(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) .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); } 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); 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); 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; /** 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; } 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; } }