diff options
36 files changed, 1336 insertions, 290 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/error-badge.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/error-badge.png Binary files differnew file mode 100644 index 0000000..2e63c35 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/error-badge.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint1.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint1.png Binary files differnew file mode 100644 index 0000000..aa6f067 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint1.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint2.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint2.png Binary files differnew file mode 100644 index 0000000..8434b78 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint2.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint3.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint3.png Binary files differnew file mode 100644 index 0000000..43ab63e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint3.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint4.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint4.png Binary files differnew file mode 100644 index 0000000..67fee2d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint4.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint5.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint5.png Binary files differnew file mode 100644 index 0000000..3d0c188 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint5.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint6.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint6.png Binary files differnew file mode 100644 index 0000000..42a9767 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint6.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint7.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint7.png Binary files differnew file mode 100644 index 0000000..743aabc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint7.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint8.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint8.png Binary files differnew file mode 100644 index 0000000..927067a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint8.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9.png Binary files differnew file mode 100644 index 0000000..44e66a0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9p.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9p.png Binary files differnew file mode 100644 index 0000000..853ace9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9p.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning-badge.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning-badge.png Binary files differnew file mode 100644 index 0000000..d71d6e3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning-badge.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index 75326fe..507bed5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -16,7 +16,6 @@ package com.android.ide.common.layout; -import static com.android.util.XmlUtils.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; import static com.android.ide.common.layout.LayoutConstants.ATTR_HINT; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; @@ -34,7 +33,10 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT; +import static com.android.util.XmlUtils.ANDROID_URI; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.api.AbstractViewRule; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IAttributeInfo.Format; @@ -971,7 +973,8 @@ public class BaseViewRule extends AbstractViewRule { * @param id attribute to be stripped * @return the id name without the {@code @+id} or {@code @id} prefix */ - public static String stripIdPrefix(String id) { + @NonNull + public static String stripIdPrefix(@Nullable String id) { if (id == null) { return ""; //$NON-NLS-1$ } else if (id.startsWith(NEW_ID_PREFIX)) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java index 0a12ba4..4017cf1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java @@ -20,7 +20,9 @@ package com.android.ide.eclipse.adt.internal.editors; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite; import com.android.sdklib.SdkConstants; +import com.google.common.collect.Maps; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; @@ -36,7 +38,8 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.ui.plugin.AbstractUIPlugin; import java.net.URL; -import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; /** * Factory to generate icons for Android Editors. @@ -55,9 +58,11 @@ public class IconFactory { private static IconFactory sInstance; - private HashMap<String, Image> mIconMap = new HashMap<String, Image>(); - private HashMap<URL, Image> mUrlMap = new HashMap<URL, Image>(); - private HashMap<String, ImageDescriptor> mImageDescMap = new HashMap<String, ImageDescriptor>(); + private Map<String, Image> mIconMap = Maps.newHashMap(); + private Map<URL, Image> mUrlMap = Maps.newHashMap(); + private Map<String, ImageDescriptor> mImageDescMap = Maps.newHashMap(); + private Map<Image, Image> mErrorIcons; + private Map<Image, Image> mWarningIcons; private IconFactory() { } @@ -85,6 +90,24 @@ public class IconFactory { } } mUrlMap.clear(); + if (mErrorIcons != null) { + for (Image icon : mErrorIcons.values()) { + // The map can contain null values + if (icon != null) { + icon.dispose(); + } + } + mErrorIcons = null; + } + if (mWarningIcons != null) { + for (Image icon : mWarningIcons.values()) { + // The map can contain null values + if (icon != null) { + icon.dispose(); + } + } + mWarningIcons = null; + } } /** @@ -253,6 +276,56 @@ public class IconFactory { } /** + * Returns an image with an error icon overlaid on it. The icons are cached, + * so the base image should be cached as well, or this method will keep + * storing new overlays into its cache. + * + * @param image the base image + * @return the combined image + */ + @NonNull + public Image addErrorIcon(@NonNull Image image) { + if (mErrorIcons != null) { + Image combined = mErrorIcons.get(image); + if (combined != null) { + return combined; + } + } else { + mErrorIcons = new IdentityHashMap<Image, Image>(); + } + + Image combined = new ErrorImageComposite(image, false).createImage(); + mErrorIcons.put(image, combined); + + return combined; + } + + /** + * Returns an image with a warning icon overlaid on it. The icons are + * cached, so the base image should be cached as well, or this method will + * keep storing new overlays into its cache. + * + * @param image the base image + * @return the combined image + */ + @NonNull + public Image addWarningIcon(@NonNull Image image) { + if (mWarningIcons != null) { + Image combined = mWarningIcons.get(image); + if (combined != null) { + return combined; + } + } else { + mWarningIcons = new IdentityHashMap<Image, Image>(); + } + + Image combined = new ErrorImageComposite(image, true).createImage(); + mWarningIcons.put(image, combined); + + return combined; + } + + /** * A simple image description that generates a 16x16 image which consists * of a colored letter inside a black & white circle. */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java index 4bc7641..8f50001 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java @@ -39,13 +39,18 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; +import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.ResourceFolderType; import com.android.sdklib.IAndroidTarget; +import com.android.tools.lint.client.api.IssueRegistry; import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; @@ -78,8 +83,11 @@ import org.eclipse.wst.sse.ui.StructuredTextEditor; import org.w3c.dom.Document; import org.w3c.dom.Node; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -138,6 +146,7 @@ public class LayoutEditorDelegate extends CommonXmlDelegate private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap = new HashMap<String, ElementDescriptor>(); + private EclipseLintClient mClient; /** * Flag indicating if the replacement file is due to a config change. @@ -405,27 +414,103 @@ public class LayoutEditorDelegate extends CommonXmlDelegate return true; } + /** + * Returns one of the issues for the given node (there could be more than one) + * + * @param node the node to look up lint issues for + * @return the marker for one of the issues found for the given node + */ + @Nullable + public IMarker getIssueForNode(@Nullable UiViewElementNode node) { + if (node == null) { + return null; + } + + if (mClient != null) { + return mClient.getIssueForNode(node); + } + + return null; + } + + /** + * Returns a collection of nodes that have one or more lint warnings + * associated with them (retrievable via + * {@link #getIssueForNode(UiViewElementNode)}) + * + * @return a collection of nodes, which should <b>not</b> be modified by the + * caller + */ + @Nullable + public Collection<Node> getLintNodes() { + if (mClient != null) { + return mClient.getIssueNodes(); + } + + return null; + } + @Override public Job delegateRunLint() { - Job job = super.delegateRunLint(); + // We want to customize the {@link EclipseLintClient} created to run this + // single file lint, in particular such that we can set the mode which collects + // nodes on that lint job, such that we can quickly look up error nodes + //Job job = super.delegateRunLint(); + + Job job = null; + IFile file = getEditor().getInputFile(); + if (file != null) { + IssueRegistry registry = EclipseLintClient.getRegistry(); + List<IFile> resources = Collections.singletonList(file); + mClient = new EclipseLintClient(registry, + resources, getEditor().getStructuredDocument(), false /*fatal*/); + + mClient.setCollectNodes(true); + + job = EclipseLintRunner.startLint(mClient, resources, file, + false /*show*/); + } if (job != null) { - job.addJobChangeListener(new JobChangeAdapter() { - @Override - public void done(IJobChangeEvent event) { - GraphicalEditorPart graphicalEditor = getGraphicalEditor(); - if (graphicalEditor != null) { - LayoutActionBar bar = graphicalEditor.getLayoutActionBar(); - if (!bar.isDisposed()) { - bar.updateErrorIndicator(); - } - } - } - }); + job.addJobChangeListener(new LintJobListener(getGraphicalEditor())); } return job; } + private class LintJobListener extends JobChangeAdapter implements Runnable { + private final GraphicalEditorPart mEditor; + private final LayoutCanvas mCanvas; + + LintJobListener(GraphicalEditorPart editor) { + mEditor = editor; + mCanvas = editor.getCanvasControl();; + } + + @Override + public void done(IJobChangeEvent event) { + LayoutActionBar bar = mEditor.getLayoutActionBar(); + if (!bar.isDisposed()) { + bar.updateErrorIndicator(); + } + + // Redraw + if (!mCanvas.isDisposed()) { + mCanvas.getDisplay().asyncExec(this); + } + } + + @Override + public void run() { + if (!mCanvas.isDisposed()) { + mCanvas.redraw(); + + OutlinePage outlinePage = mCanvas.getOutlinePage(); + if (outlinePage != null) { + outlinePage.refreshIcons(); + } + } + } + } /** * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java index 0d7fc28..7cbb8f2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java @@ -179,4 +179,8 @@ public class CanvasTransform { public int inverseTranslate(int screenX) { return (int) ((screenX - mMargin + mTranslate) / mScale); } + + public int inverseScale(int canwasW) { + return (int) (canwasW / mScale); + } } 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 index d4f25f8..493f996 100644 --- 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 @@ -17,7 +17,6 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.ide.common.layout.LayoutConstants.ANDROID_STRING_PREFIX; -import static com.android.util.XmlUtils.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; @@ -30,6 +29,7 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage; import static com.android.sdklib.SdkConstants.FD_GEN_SOURCES; +import static com.android.util.XmlUtils.ANDROID_URI; 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; @@ -74,7 +74,6 @@ import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFa 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.lint.EclipseLintClient; 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; @@ -273,6 +272,7 @@ public class GraphicalEditorPart extends EditorPart 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 @@ -400,7 +400,7 @@ public class GraphicalEditorPart extends EditorPart GridData detailsData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1); mActionBar.setLayoutData(detailsData); if (file != null) { - mActionBar.updateErrorIndicator(EclipseLintClient.hasMarkers(file)); + mActionBar.updateErrorIndicator(file); } mSashError = new SashForm(layoutBarAndCanvas, SWT.VERTICAL | SWT.BORDER); @@ -1611,6 +1611,21 @@ public class GraphicalEditorPart extends EditorPart } else { // 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 + } } model.refreshUi(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java index 1c83430..63a36f2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java @@ -15,8 +15,8 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; -import static com.android.util.XmlUtils.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.util.XmlUtils.ANDROID_URI; import com.android.annotations.NonNull; import com.android.ide.common.api.INode; @@ -26,6 +26,7 @@ import com.android.ide.common.api.RuleAction.Separator; import com.android.ide.common.api.RuleAction.Toggle; import com.android.ide.common.layout.BaseViewRule; import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; @@ -35,6 +36,7 @@ import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog; import com.google.common.base.Strings; import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; @@ -553,9 +555,10 @@ public class LayoutActionBar extends Composite { mLintButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { - IFile file = mEditor.getEditorDelegate().getEditor().getInputFile(); + CommonXmlEditor editor = mEditor.getEditorDelegate().getEditor(); + IFile file = editor.getInputFile(); if (file != null) { - EclipseLintClient.showErrors(getShell(), file); + EclipseLintClient.showErrors(getShell(), file, editor); } } }); @@ -567,7 +570,17 @@ public class LayoutActionBar extends Composite { * Updates the lint indicator state in the given layout editor */ public void updateErrorIndicator() { - updateErrorIndicator(EclipseLintClient.hasMarkers(mEditor.getEditedFile())); + updateErrorIndicator(mEditor.getEditedFile()); + } + + /** + * Updates the lint indicator state for the given file + * + * @param file the file to show the indicator status for + */ + public void updateErrorIndicator(IFile file) { + IMarker[] markers = EclipseLintClient.getMarkers(file); + updateErrorIndicator(markers.length); } /** @@ -575,14 +588,14 @@ public class LayoutActionBar extends Composite { * * @param hasLintWarnings whether there are lint errors to be shown */ - void updateErrorIndicator(final boolean hasLintWarnings) { + private void updateErrorIndicator(final int markerCount) { Display display = getDisplay(); if (display.getThread() != Thread.currentThread()) { display.asyncExec(new Runnable() { @Override public void run() { if (!isDisposed()) { - updateErrorIndicator(hasLintWarnings); + updateErrorIndicator(markerCount); } } }); @@ -590,10 +603,37 @@ public class LayoutActionBar extends Composite { } GridData layoutData = (GridData) mLintToolBar.getLayoutData(); - if (layoutData.exclude == hasLintWarnings) { - layoutData.exclude = !hasLintWarnings; - mLintToolBar.setVisible(hasLintWarnings); - layout(); + Integer existing = (Integer) mLintToolBar.getData(); + Integer current = Integer.valueOf(markerCount); + if (!current.equals(existing)) { + mLintToolBar.setData(current); + boolean layout = false; + boolean hasLintWarnings = markerCount > 0; + if (layoutData.exclude == hasLintWarnings) { + layoutData.exclude = !hasLintWarnings; + mLintToolBar.setVisible(hasLintWarnings); + layout = true; + } + if (markerCount > 0) { + String iconName = ""; + switch (markerCount) { + case 1: iconName = "lint1"; break; //$NON-NLS-1$ + case 2: iconName = "lint2"; break; //$NON-NLS-1$ + case 3: iconName = "lint3"; break; //$NON-NLS-1$ + case 4: iconName = "lint4"; break; //$NON-NLS-1$ + case 5: iconName = "lint5"; break; //$NON-NLS-1$ + case 6: iconName = "lint6"; break; //$NON-NLS-1$ + case 7: iconName = "lint7"; break; //$NON-NLS-1$ + case 8: iconName = "lint8"; break; //$NON-NLS-1$ + case 9: iconName = "lint9"; break; //$NON-NLS-1$ + default: iconName = "lint9p"; break;//$NON-NLS-1$ + } + mLintButton.setImage(IconFactory.getInstance().getIcon(iconName)); + } + if (layout) { + layout(); + } + redraw(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java index 481c215..954305e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java @@ -197,6 +197,9 @@ public class LayoutCanvas extends Canvas { /** The overlay which paints the mouse hover. */ private HoverOverlay mHoverOverlay; + /** The overlay which paints the lint warnings */ + private LintOverlay mLintOverlay; + /** The overlay which paints the selection. */ private SelectionOverlay mSelectionOverlay; @@ -267,6 +270,8 @@ public class LayoutCanvas extends Canvas { mImageOverlay = new ImageOverlay(this, mHScale, mVScale); mIncludeOverlay = new IncludeOverlay(this); mImageOverlay.create(display); + mLintOverlay = new LintOverlay(this); + mLintOverlay.create(display); // --- Set up listeners addPaintListener(new PaintListener() { @@ -310,6 +315,8 @@ public class LayoutCanvas extends Canvas { if (editorDelegate != null) { mOutlinePage = editorDelegate.getGraphicalOutline(); } + + new LintTooltipManager(this).register(); } private Runnable mZoomCheck = new Runnable() { @@ -423,6 +430,11 @@ public class LayoutCanvas extends Canvas { mIncludeOverlay = null; } + if (mLintOverlay != null) { + mLintOverlay.dispose(); + mLintOverlay = null; + } + mViewHierarchy.dispose(); } @@ -774,6 +786,11 @@ public class LayoutCanvas extends Canvas { if (!mHoverOverlay.isHiding()) { mHoverOverlay.paint(gc); } + + if (!mLintOverlay.isHiding()) { + mLintOverlay.paint(gc); + } + if (!mIncludeOverlay.isHiding()) { mIncludeOverlay.paint(gc); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java new file mode 100644 index 0000000..d5d5ce7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2010 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 com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Rectangle; +import org.w3c.dom.Node; + +import java.util.Collection; + +import lombok.ast.libs.org.parboiled.google.collect.Lists; + +/** + * The {@link LintOverlay} paints an icon over each view that contains at least one + * lint error (unless the view is smaller than the icon) + */ +public class LintOverlay extends Overlay { + /** Approximate size of lint overlay icons */ + static final int ICON_SIZE = 8; + /** Alpha to draw lint overlay icons with */ + private static final int ALPHA = 192; + + private final LayoutCanvas mCanvas; + private Image mWarningImage; + private Image mErrorImage; + + /** + * Constructs a new {@link LintOverlay} + * + * @param canvas the associated canvas + */ + public LintOverlay(LayoutCanvas canvas) { + mCanvas = canvas; + } + + @Override + public void paint(GC gc) { + LayoutEditorDelegate editor = mCanvas.getEditorDelegate(); + Collection<Node> nodes = editor.getLintNodes(); + if (nodes != null && !nodes.isEmpty()) { + // Copy list before iterating through it to avoid a concurrent list modification + // in case lint runs in the background while painting and updates this list + nodes = Lists.newArrayList(nodes); + ViewHierarchy hierarchy = mCanvas.getViewHierarchy(); + Image icon = getWarningIcon(); + ImageData imageData = icon.getImageData(); + int iconWidth = imageData.width; + int iconHeight = imageData.height; + CanvasTransform mHScale = mCanvas.getHorizontalTransform(); + CanvasTransform mVScale = mCanvas.getVerticalTransform(); + + int oldAlpha = gc.getAlpha(); + try { + gc.setAlpha(ALPHA); + for (Node node : nodes) { + CanvasViewInfo vi = hierarchy.findViewInfoFor(node); + if (vi != null) { + Rectangle bounds = vi.getAbsRect(); + int x = mHScale.translate(bounds.x); + int y = mVScale.translate(bounds.y); + int w = mHScale.scale(bounds.width); + int h = mVScale.scale(bounds.height); + if (w < iconWidth || h < iconHeight) { + // Don't draw badges on tiny widgets (including those + // that aren't tiny but are zoomed out too far) + continue; + } + + x += w - iconWidth; + y += h - iconHeight; + + boolean isError = false; + IMarker marker = editor.getIssueForNode(vi.getUiViewNode()); + if (marker != null) { + int severity = marker.getAttribute(IMarker.SEVERITY, 0); + isError = severity == IMarker.SEVERITY_ERROR; + } + + icon = isError ? getErrorIcon() : getWarningIcon(); + + gc.drawImage(icon, x, y); + } + } + } finally { + gc.setAlpha(oldAlpha); + } + } + } + + private Image getWarningIcon() { + if (mWarningImage == null) { + mWarningImage = IconFactory.getInstance().getIcon("warning-badge"); //$NON-NLS-1$ + } + + return mWarningImage; + } + + private Image getErrorIcon() { + if (mErrorImage == null) { + mErrorImage = IconFactory.getInstance().getIcon("error-badge"); //$NON-NLS-1$ + } + + return mErrorImage; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java new file mode 100644 index 0000000..bc2f390 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2012 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.ide.common.layout.LayoutConstants.ATTR_ID; + +import com.android.ide.common.layout.BaseLayoutRule; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.util.List; + +/** Actual tooltip showing multiple lines for various widgets that have lint errors */ +class LintTooltip extends Shell { + private final LayoutCanvas mCanvas; + private final List<UiViewElementNode> mNodes; + + LintTooltip(LayoutCanvas canvas, List<UiViewElementNode> nodes) { + super(canvas.getDisplay(), SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); + mCanvas = canvas; + mNodes = nodes; + + createContents(); + } + + protected void createContents() { + Display display = getDisplay(); + Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND); + Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); + setBackground(bg); + GridLayout gridLayout = new GridLayout(2, false); + setLayout(gridLayout); + + LayoutEditorDelegate delegate = mCanvas.getEditorDelegate(); + + boolean first = true; + for (UiViewElementNode node : mNodes) { + IMarker marker = delegate.getIssueForNode(node); + if (marker != null) { + String message = marker.getAttribute(IMarker.MESSAGE, null); + if (message != null) { + Label icon = new Label(this, SWT.NONE); + icon.setForeground(fg); + icon.setBackground(bg); + icon.setImage(node.getIcon()); + + Label label = new Label(this, SWT.WRAP); + if (first) { + label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1)); + first = false; + } + + String id = BaseLayoutRule.stripIdPrefix(node.getAttributeValue(ATTR_ID)); + if (id.isEmpty()) { + if (node.getXmlNode() != null) { + id = node.getXmlNode().getNodeName(); + } else { + id = node.getDescriptor().getUiName(); + } + } + + label.setText(String.format("%1$s: %2$s", id, message)); + } + } + } + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java new file mode 100644 index 0000000..a2bbdf4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2012 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.ide.eclipse.adt.internal.editors.layout.gle2.LintOverlay.ICON_SIZE; + +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** Tooltip in the layout editor showing lint errors under the cursor */ +class LintTooltipManager implements Listener { + private final LayoutCanvas mCanvas; + private Shell mTip = null; + private List<UiViewElementNode> mShowingNodes; + + /** + * Sets up a custom tooltip when hovering over tree items. It currently displays the error + * message for the lint warning associated with each node, if any (and only if the hover + * is over the icon portion). + */ + LintTooltipManager(LayoutCanvas canvas) { + mCanvas = canvas; + } + + void register() { + mCanvas.addListener(SWT.Dispose, this); + mCanvas.addListener(SWT.KeyDown, this); + mCanvas.addListener(SWT.MouseMove, this); + mCanvas.addListener(SWT.MouseHover, this); + } + + void unregister() { + mCanvas.removeListener(SWT.Dispose, this); + mCanvas.removeListener(SWT.KeyDown, this); + mCanvas.removeListener(SWT.MouseMove, this); + mCanvas.removeListener(SWT.MouseHover, this); + } + + @Override + public void handleEvent(Event event) { + switch(event.type) { + case SWT.MouseMove: + // See if we're still overlapping this or *other* errors; if so, keep the + // tip up (or update it). + if (mShowingNodes != null) { + List<UiViewElementNode> nodes = computeNodes(event); + if (nodes != null && !nodes.isEmpty()) { + if (nodes.equals(mShowingNodes)) { + return; + } else { + show(nodes); + } + break; + } + } + + // If not, fall through and hide the tooltip + + //$FALL-THROUGH$ + case SWT.Dispose: + case SWT.FocusOut: + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + hide(); + break; + case SWT.MouseHover: + hide(); + show(event); + break; + } + } + + private void hide() { + if (mTip != null) { + mTip.dispose(); + mTip = null; + } + mShowingNodes = null; + } + + private void show(Event event) { + List<UiViewElementNode> nodes = computeNodes(event); + if (nodes != null && !nodes.isEmpty()) { + show(nodes); + } + } + + /** Show a tooltip listing the lint errors for the given nodes */ + private void show(List<UiViewElementNode> nodes) { + hide(); + + mTip = new LintTooltip(mCanvas, nodes); + Rectangle rect = mCanvas.getBounds(); + Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT); + Point pos = mCanvas.toDisplay(rect.x, rect.y + rect.height); + if (size.x > rect.width) { + size = mTip.computeSize(rect.width, SWT.DEFAULT); + } + mTip.setBounds(pos.x, pos.y, size.x, size.y); + + mShowingNodes = nodes; + mTip.setVisible(true); + } + + /** + * Compute the list of nodes which have lint warnings near the given mouse + * coordinates + * + * @param event the mouse cursor event + * @return a list of nodes, possibly empty + */ + @Nullable + private List<UiViewElementNode> computeNodes(Event event) { + LayoutPoint p = ControlPoint.create(mCanvas, event.x, event.y).toLayout(); + LayoutEditorDelegate delegate = mCanvas.getEditorDelegate(); + ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy(); + CanvasTransform mHScale = mCanvas.getHorizontalTransform(); + CanvasTransform mVScale = mCanvas.getVerticalTransform(); + + int layoutIconSize = mHScale.inverseScale(ICON_SIZE); + int slop = mVScale.inverseScale(10); // extra space around icon where tip triggers + + Collection<Node> xmlNodes = delegate.getLintNodes(); + if (xmlNodes == null) { + return null; + } + List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(); + for (Node xmlNode : xmlNodes) { + CanvasViewInfo v = viewHierarchy.findViewInfoFor(xmlNode); + if (v != null) { + Rectangle b = v.getAbsRect(); + int x2 = b.x + b.width; + int y2 = b.y + b.height; + if (p.x < x2 - layoutIconSize - slop + || p.x > x2 + slop + || p.y < y2 - layoutIconSize - slop + || p.y > y2 + slop) { + continue; + } + + nodes.add(v.getUiViewNode()); + } + } + + return nodes; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java index eca47ab..8b017f2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java @@ -16,25 +16,20 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; -import static com.android.util.XmlUtils.ANDROID_URI; -import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN; -import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; import static com.android.ide.common.layout.LayoutConstants.ATTR_ROW_COUNT; import static com.android.ide.common.layout.LayoutConstants.ATTR_SRC; import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; import static com.android.ide.common.layout.LayoutConstants.DRAWABLE_PREFIX; import static com.android.ide.common.layout.LayoutConstants.GRID_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.LAYOUT_PREFIX; -import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; -import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; -import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_VIEWTAG; import static com.android.tools.lint.detector.api.LintConstants.AUTO_URI; import static com.android.tools.lint.detector.api.LintConstants.URI_PREFIX; +import static com.android.util.XmlUtils.ANDROID_URI; import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER; import com.android.annotations.VisibleForTesting; @@ -45,7 +40,6 @@ import com.android.ide.common.layout.GridLayoutRule; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; -import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; @@ -53,12 +47,12 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; -import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.util.Pair; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; @@ -94,9 +88,18 @@ import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MenuDetectEvent; import org.eclipse.swt.events.MenuDetectListener; +import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.IActionBars; @@ -256,6 +259,11 @@ public class OutlinePage extends ContentOutlinePage } } + /** Refresh all the icon state */ + public void refreshIcons() { + getTreeViewer().refresh(); + } + /** * Set whether the outline should be shown in the header * @@ -442,6 +450,8 @@ public class OutlinePage extends ContentOutlinePage public void keyReleased(KeyEvent e) { } }); + + setupTooltip(); } private void createPropertySheet() { @@ -686,45 +696,9 @@ public class OutlinePage extends ContentOutlinePage element = ((CanvasViewInfo) element).getUiViewNode(); } - if (element instanceof UiElementNode) { - UiElementNode node = (UiElementNode) element; - ElementDescriptor desc = node.getDescriptor(); - if (desc != null) { - Image img = null; - // Special case for the common case of vertical linear layouts: - // show vertical linear icon (the default icon shows horizontal orientation) - String uiName = desc.getUiName(); - if (uiName.equals(LINEAR_LAYOUT)) { - Element e = (Element) node.getXmlNode(); - if (VALUE_VERTICAL.equals(e.getAttributeNS(ANDROID_URI, - ATTR_ORIENTATION))) { - IconFactory factory = IconFactory.getInstance(); - img = factory.getIcon("VerticalLinearLayout"); //$NON-NLS-1$ - } - } else if (uiName.equals(VIEW_VIEWTAG)) { - Node xmlNode = node.getXmlNode(); - if (xmlNode instanceof Element) { - String className = ((Element) xmlNode).getAttribute(ATTR_CLASS); - if (className != null && className.length() > 0) { - int index = className.lastIndexOf('.'); - if (index != -1) { - className = className.substring(index + 1); - } - img = IconFactory.getInstance().getIcon(className); - } - } - } - if (img == null) { - img = desc.getGenericIcon(); - } - if (img != null) { - if (node.hasError()) { - return new ErrorImageComposite(img).createImage(); - } else { - return img; - } - } - } + if (element instanceof UiViewElementNode) { + UiViewElementNode v = (UiViewElementNode) element; + return v.getIcon(); } return AdtPlugin.getAndroidLogo(); @@ -1275,4 +1249,103 @@ public class OutlinePage extends ContentOutlinePage makeContributions(null, toolBarManager, null); toolBarManager.update(false); } + + /** + * Sets up a custom tooltip when hovering over tree items. It currently displays the error + * message for the lint warning associated with each node, if any (and only if the hover + * is over the icon portion). + */ + private void setupTooltip() { + final Tree tree = getTreeViewer().getTree(); + + // This is based on SWT Snippet 125 + final Listener listener = new Listener() { + Shell mTip = null; + Label mLabel = null; + + @Override + public void handleEvent(Event event) { + switch(event.type) { + case SWT.Dispose: + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + case SWT.MouseMove: + if (mTip != null) { + mTip.dispose(); + mTip = null; + mLabel = null; + } + break; + case SWT.MouseHover: + if (mTip != null) { + mTip.dispose(); + mTip = null; + mLabel = null; + } + + String tooltip = null; + + TreeItem item = tree.getItem(new Point(event.x, event.y)); + if (item != null) { + Rectangle rect = item.getBounds(0); + if (event.x - rect.x > 16) { // 16: Standard width of our outline icons + return; + } + + Object data = item.getData(); + if (data != null && data instanceof CanvasViewInfo) { + LayoutEditorDelegate editor = mGraphicalEditorPart.getEditorDelegate(); + CanvasViewInfo vi = (CanvasViewInfo) data; + IMarker marker = editor.getIssueForNode(vi.getUiViewNode()); + if (marker != null) { + tooltip = marker.getAttribute(IMarker.MESSAGE, null); + } + } + + if (tooltip != null) { + Shell shell = tree.getShell(); + Display display = tree.getDisplay(); + + Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND); + Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); + mTip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); + mTip.setBackground(bg); + FillLayout layout = new FillLayout(); + layout.marginWidth = 1; + layout.marginHeight = 1; + mTip.setLayout(layout); + mLabel = new Label(mTip, SWT.WRAP); + mLabel.setForeground(fg); + mLabel.setBackground(bg); + mLabel.setText(tooltip); + mLabel.addListener(SWT.MouseExit, this); + mLabel.addListener(SWT.MouseDown, this); + + Point pt = tree.toDisplay(rect.x, rect.y + rect.height); + Rectangle displayBounds = display.getBounds(); + // -10: Don't extend -all- the way to the edge of the screen + // which would make it look like it has been cropped + int availableWidth = displayBounds.x + displayBounds.width - pt.x - 10; + if (availableWidth < 80) { + availableWidth = 80; + } + Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT); + if (size.x > availableWidth) { + size = mTip.computeSize(availableWidth, SWT.DEFAULT); + } + mTip.setBounds(pt.x, pt.y, size.x, size.y); + + mTip.setVisible(true); + } + } + } + } + }; + + tree.addListener(SWT.Dispose, listener); + tree.addListener(SWT.KeyDown, listener); + tree.addListener(SWT.MouseMove, listener); + tree.addListener(SWT.MouseHover, listener); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java index 579ef44..23e42ef 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java @@ -30,6 +30,7 @@ import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.util.Pair; import org.eclipse.swt.graphics.Rectangle; +import org.w3c.dom.Attr; import org.w3c.dom.Node; import java.awt.image.BufferedImage; @@ -117,6 +118,9 @@ public class ViewHierarchy { /** Map from nodes to canvas view infos */ private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap(); + /** Map from DOM nodes to canvas view infos */ + private Map<Node, CanvasViewInfo> mDomNodeToView = Collections.emptyMap(); + /** * Disposes the view hierarchy content. */ @@ -220,11 +224,17 @@ public class ViewHierarchy { mInvisibleParents.clear(); addInvisibleParents(mLastValidViewInfoRoot, explodedNodes); + mDomNodeToView = new HashMap<Node, CanvasViewInfo>(mNodeToView.size()); + for (Map.Entry<UiViewElementNode, CanvasViewInfo> entry : mNodeToView.entrySet()) { + mDomNodeToView.put(entry.getKey().getXmlNode(), entry.getValue()); + } + // Update the selection mCanvas.getSelectionManager().sync(); } else { mIncludedBounds = null; mInvisibleParents.clear(); + mDomNodeToView = Collections.emptyMap(); } } @@ -424,37 +434,25 @@ public class ViewHierarchy { * @return The {@link CanvasViewInfo} corresponding to the given node, or * null if no match was found. */ - public CanvasViewInfo findViewInfoFor(Node node) { - if (mLastValidViewInfoRoot != null) { - return findViewInfoForNode(node, mLastValidViewInfoRoot); - } - return null; - } - - /** - * Tries to find a child with the same view XML node in the view info sub-tree. - * Returns null if not found. - */ - private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) { - if (canvasViewInfo == null) { - return null; - } - if (canvasViewInfo.getXmlNode() == xmlNode) { - return canvasViewInfo; - } + @Nullable + public CanvasViewInfo findViewInfoFor(@Nullable Node node) { + CanvasViewInfo vi = mDomNodeToView.get(node); - // Try to find a matching child - for (CanvasViewInfo child : canvasViewInfo.getChildren()) { - CanvasViewInfo v = findViewInfoForNode(xmlNode, child); - if (v != null) { - return v; + if (vi == null) { + if (node == null) { + return null; + } else if (node.getNodeType() == Node.TEXT_NODE) { + return mDomNodeToView.get(node.getParentNode()); + } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) { + return mDomNodeToView.get(((Attr) node).getOwnerElement()); + } else if (node.getNodeType() == Node.DOCUMENT_NODE) { + return mDomNodeToView.get(node.getOwnerDocument().getDocumentElement()); } } - return null; + return vi; } - /** * Tries to find the inner most child matching the given x,y coordinates in * the view info sub-tree, starting at the last know view info root. This diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java index b87435c..ec19ea7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java @@ -16,10 +16,21 @@ package com.android.ide.eclipse.adt.internal.editors.layout.uimodel; +import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; import static com.android.ide.common.layout.LayoutConstants.FQCN_FRAME_LAYOUT; - +import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; +import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; +import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_VIEWTAG; +import static com.android.util.XmlUtils.ANDROID_URI; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; @@ -29,7 +40,11 @@ import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.IAndroidTarget; import com.android.util.XmlUtils; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; +import org.eclipse.swt.graphics.Image; +import org.w3c.dom.Element; +import org.w3c.dom.Node; /** * Specialized version of {@link UiElementNode} for the {@link ViewElementDescriptor}s. @@ -116,6 +131,64 @@ public class UiViewElementNode extends UiElementNode { return mCachedAttributeDescriptors; } + public Image getIcon() { + ElementDescriptor desc = getDescriptor(); + if (desc != null) { + Image img = null; + // Special case for the common case of vertical linear layouts: + // show vertical linear icon (the default icon shows horizontal orientation) + String uiName = desc.getUiName(); + IconFactory icons = IconFactory.getInstance(); + if (uiName.equals(LINEAR_LAYOUT)) { + Element e = (Element) getXmlNode(); + if (VALUE_VERTICAL.equals(e.getAttributeNS(ANDROID_URI, + ATTR_ORIENTATION))) { + IconFactory factory = icons; + img = factory.getIcon("VerticalLinearLayout"); //$NON-NLS-1$ + } + } else if (uiName.equals(VIEW_VIEWTAG)) { + Node xmlNode = getXmlNode(); + if (xmlNode instanceof Element) { + String className = ((Element) xmlNode).getAttribute(ATTR_CLASS); + if (className != null && className.length() > 0) { + int index = className.lastIndexOf('.'); + if (index != -1) { + className = className.substring(index + 1); + } + img = icons.getIcon(className); + } + } + } + if (img == null) { + img = desc.getGenericIcon(); + } + + if (img != null) { + AndroidXmlEditor editor = getEditor(); + if (editor != null) { + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); + if (delegate != null) { + IMarker marker = delegate.getIssueForNode(this); + if (marker != null) { + int severity = marker.getAttribute(IMarker.SEVERITY, 0); + if (severity == IMarker.SEVERITY_ERROR) { + return icons.addErrorIcon(img); + } else { + return icons.addWarningIcon(img); + } + } + } + } + + return img; + } + + return img; + } + + return AdtPlugin.getAndroidLogo(); + } + /** * Sets the parent of this UI node. * <p/> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java index f6ec863..7085e5d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java @@ -1,5 +1,10 @@ package com.android.ide.eclipse.adt.internal.editors.ui; +import static org.eclipse.ui.ISharedImages.IMG_DEC_FIELD_ERROR; +import static org.eclipse.ui.ISharedImages.IMG_DEC_FIELD_WARNING; +import static org.eclipse.ui.ISharedImages.IMG_OBJS_ERROR_TSK; +import static org.eclipse.ui.ISharedImages.IMG_OBJS_WARN_TSK; + import org.eclipse.jface.resource.CompositeImageDescriptor; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.DecorationOverlayIcon; @@ -19,10 +24,30 @@ public class ErrorImageComposite extends CompositeImageDescriptor { private ImageDescriptor mErrorImageDescriptor; private Point mSize; + /** + * Creates a new {@link ErrorImageComposite} + * + * @param baseImage the base image to overlay an icon on top of + */ public ErrorImageComposite(Image baseImage) { + this(baseImage, false); + } + + /** + * Creates a new {@link ErrorImageComposite} + * + * @param baseImage the base image to overlay an icon on top of + * @param warning if true, add a warning icon, otherwise an error icon + */ + public ErrorImageComposite(Image baseImage, boolean warning) { mBaseImage = baseImage; - mErrorImageDescriptor = PlatformUI.getWorkbench().getSharedImages().getImageDescriptor( - ISharedImages.IMG_OBJS_ERROR_TSK); + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + mErrorImageDescriptor = sharedImages.getImageDescriptor( + warning ? IMG_DEC_FIELD_WARNING : IMG_DEC_FIELD_ERROR); + if (mErrorImageDescriptor == null) { + mErrorImageDescriptor = sharedImages.getImageDescriptor( + warning ? IMG_OBJS_WARN_TSK : IMG_OBJS_ERROR_TSK); + } mSize = new Point(baseImage.getBounds().width, baseImage.getBounds().height); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java index 1d80c8c..e7037ff 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java @@ -41,7 +41,6 @@ import org.w3c.dom.Node; /** * Fix for adding {@code tools:ignore="id"} attributes in XML files. */ -@SuppressWarnings("restriction") // DOM model class AddSuppressAttribute implements ICompletionProposal { private final AndroidXmlEditor mEditor; private final String mId; @@ -98,7 +97,8 @@ class AddSuppressAttribute implements ICompletionProposal { } /** - * Adds any applicable suppress lint fix resolutions into the given list + * Returns a quickfix to suppress a specific lint issue id on the node corresponding to + * the given marker. * * @param editor the associated editor containing the marker * @param marker the marker to create fixes for @@ -144,4 +144,35 @@ class AddSuppressAttribute implements ICompletionProposal { Element element = (Element) node; return new AddSuppressAttribute(editor, id, marker, element, desc); } + + /** + * Returns a quickfix to suppress a given issue type on the <b>root element</b> + * of the given editor. + * + * @param editor the associated editor containing the marker + * @param marker the marker to create fixes for + * @param id the issue id + * @return a fix for this marker, or null if unable + */ + @Nullable + public static AddSuppressAttribute createFixForAll( + @NonNull AndroidXmlEditor editor, + @NonNull IMarker marker, + @NonNull String id) { + // This only applies to XML files: + String fileName = marker.getResource().getName(); + if (!fileName.endsWith(DOT_XML)) { + return null; + } + + Node node = DomUtilities.getNode(editor.getStructuredDocument(), 0); + if (node != null) { + node = node.getOwnerDocument().getDocumentElement(); + String desc = String.format("Add ignore '%1$s\' to element", id); + Element element = (Element) node; + return new AddSuppressAttribute(editor, id, marker, element, desc); + } + + return null; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java index 68b3407..703be80 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java @@ -23,6 +23,7 @@ import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.tools.lint.checks.BuiltinIssueRegistry; @@ -78,13 +79,17 @@ import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Node; import java.io.File; import java.io.IOException; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; import lombok.ast.ecj.EcjTreeConverter; import lombok.ast.grammar.ParseProblem; @@ -102,6 +107,8 @@ public class EclipseLintClient extends LintClient implements IDomParser { private boolean mWasFatal; private boolean mFatalOnly; private EclipseJavaParser mJavaParser; + private boolean mCollectNodes; + private Map<Node, IMarker> mNodeMap; /** * Creates a new {@link EclipseLintClient}. @@ -119,6 +126,58 @@ public class EclipseLintClient extends LintClient implements IDomParser { mFatalOnly = fatalOnly; } + /** + * Returns true if lint should only check fatal issues + * + * @return true if lint should only check fatal issues + */ + public boolean isFatalOnly() { + return mFatalOnly; + } + + /** + * Sets whether the lint client should store associated XML nodes for each + * reported issue + * + * @param collectNodes if true, collect node positions for errors in XML + * files, retrievable via the {@link #getIssueForNode} method + */ + public void setCollectNodes(boolean collectNodes) { + mCollectNodes = collectNodes; + } + + /** + * Returns one of the issues for the given node (there could be more than one) + * + * @param node the node to look up lint issues for + * @return the marker for one of the issues found for the given node + */ + @Nullable + public IMarker getIssueForNode(@NonNull UiViewElementNode node) { + if (mNodeMap != null) { + return mNodeMap.get(node.getXmlNode()); + } + + return null; + } + + /** + * Returns a collection of nodes that have one or more lint warnings + * associated with them (retrievable via + * {@link #getIssueForNode(UiViewElementNode)}) + * + * @return a collection of nodes, which should <b>not</b> be modified by the + * caller + */ + @Nullable + public Collection<Node> getIssueNodes() { + if (mNodeMap != null) { + return mNodeMap.keySet(); + } + + return null; + } + // ----- Extends LintClient ----- @Override @@ -190,6 +249,7 @@ public class EclipseLintClient extends LintClient implements IDomParser { return null; } + @NonNull @Override public Configuration getConfiguration(Project project) { if (project != null) { @@ -249,6 +309,32 @@ public class EclipseLintClient extends LintClient implements IDomParser { if (s == Severity.FATAL) { mWasFatal = true; } + + if (mCollectNodes && location != null && marker != null) { + if (location instanceof LazyLocation) { + LazyLocation l = (LazyLocation) location; + IndexedRegion region = l.mRegion; + if (region instanceof Node) { + Node node = (Node) region; + if (node instanceof Attr) { + node = ((Attr) node).getOwnerElement(); + } + if (mNodeMap == null) { + mNodeMap = new WeakHashMap<Node, IMarker>(); + } + IMarker prev = mNodeMap.get(node); + if (prev != null) { + // Only replace the node if this node has higher priority + int prevSeverity = prev.getAttribute(IMarker.SEVERITY, 0); + if (prevSeverity < severity) { + mNodeMap.put(node, marker); + } + } else { + mNodeMap.put(node, marker); + } + } + } + } } @Override @@ -325,16 +411,6 @@ public class EclipseLintClient extends LintClient implements IDomParser { } /** - * Returns whether the given resource has one or more lint markers - * - * @param resource the resource to be checked, typically a source file - * @return true if the given resource has one or more lint markers - */ - public static boolean hasMarkers(IResource resource) { - return getMarkers(resource).length > 0; - } - - /** * Returns the lint marker for the given resource (which may be a project, folder or file) * * @param resource the resource to be checked, typically a source file @@ -532,9 +608,13 @@ public class EclipseLintClient extends LintClient implements IDomParser { * * @param shell the parent shell to attach the dialog to * @param file the file to show the errors for + * @param editor the editor for the file, if known */ - public static void showErrors(Shell shell, final IFile file) { - LintListDialog dialog = new LintListDialog(shell, file); + public static void showErrors( + @NonNull Shell shell, + @NonNull IFile file, + @Nullable IEditorPart editor) { + LintListDialog dialog = new LintListDialog(shell, file, editor); dialog.open(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java index 25e80fc..d69412b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java @@ -15,40 +15,24 @@ */ package com.android.ide.eclipse.adt.internal.lint; -import static com.android.ide.eclipse.adt.AdtConstants.DOT_CLASS; -import static com.android.ide.eclipse.adt.AdtConstants.DOT_JAVA; -import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; -import com.android.sdklib.SdkConstants; import com.android.tools.lint.client.api.IssueRegistry; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Scope; -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.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.IJobManager; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.text.IDocument; import org.eclipse.swt.widgets.Shell; -import java.io.File; import java.util.ArrayList; import java.util.Collections; -import java.util.EnumSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -75,7 +59,7 @@ public class EclipseLintRunner { @Nullable IDocument doc, boolean fatalOnly) { resources = addLibraries(resources); - CheckFileJob job = (CheckFileJob) startLint(resources, source, doc, fatalOnly, + LintJob job = (LintJob) startLint(resources, source, doc, fatalOnly, false /*show*/); try { job.join(); @@ -116,12 +100,38 @@ public class EclipseLintRunner { @Nullable IDocument doc, boolean fatalOnly, boolean show) { + IssueRegistry registry = EclipseLintClient.getRegistry(); + EclipseLintClient client = new EclipseLintClient(registry, resources, doc, fatalOnly); + return startLint(client, resources, source, show); + } + + /** + * Runs lint and updates the markers. Does not wait for the job to finish - + * just returns immediately. + * + * @param client the lint client receiving issue reports etc + * @param resources the resources (project, folder or file) to be analyzed + * @param source if checking a single source file, the source file. When + * single checking an XML file, this is typically the same as the + * file passed in the list in the first parameter, but when + * checking the .class files of a Java file for example, the + * .class file and all the inner classes of the Java file are + * passed in the first parameter, and the corresponding .java + * source file is passed here. + * @param show if true, show the results in a {@link LintViewPart} + * @return the job running lint in the background. + */ + public static Job startLint( + @NonNull EclipseLintClient client, + @NonNull List<? extends IResource> resources, + @Nullable IResource source, + boolean show) { if (resources != null && !resources.isEmpty()) { resources = addLibraries(resources); cancelCurrentJobs(false); - CheckFileJob job = new CheckFileJob(resources, source, doc, fatalOnly); + LintJob job = new LintJob(client, resources, source); job.schedule(); if (show) { @@ -162,16 +172,10 @@ public class EclipseLintRunner { return true; } - /** Returns the current lint jobs, if any (never returns null but array may be empty) */ - static Job[] getCurrentJobs() { - IJobManager jobManager = Job.getJobManager(); - return jobManager.find(CheckFileJob.FAMILY_RUN_LINT); - } - /** Cancels the current lint jobs, if any, and optionally waits for them to finish */ static void cancelCurrentJobs(boolean wait) { // Cancel any current running jobs first - Job[] currentJobs = getCurrentJobs(); + Job[] currentJobs = LintJob.getCurrentJobs(); for (Job job : currentJobs) { job.cancel(); } @@ -229,132 +233,4 @@ public class EclipseLintRunner { return resources; } - - private static final class CheckFileJob extends Job { - /** Job family */ - private static final Object FAMILY_RUN_LINT = new Object(); - private final List<? extends IResource> mResources; - private final IResource mSource; - private final IDocument mDocument; - private LintDriver mLint; - private boolean mFatal; - private boolean mFatalOnly; - - private CheckFileJob( - @NonNull List<? extends IResource> resources, - @Nullable IResource source, - @Nullable IDocument doc, - boolean fatalOnly) { - super("Running Android Lint"); - mResources = resources; - mSource = source; - mDocument = doc; - mFatalOnly = fatalOnly; - } - - @Override - public boolean belongsTo(Object family) { - return family == FAMILY_RUN_LINT; - } - - @Override - protected void canceling() { - super.canceling(); - if (mLint != null) { - mLint.cancel(); - } - } - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - monitor.beginTask("Looking for errors", IProgressMonitor.UNKNOWN); - IssueRegistry registry = EclipseLintClient.getRegistry(); - EnumSet<Scope> scope = null; - List<File> files = new ArrayList<File>(mResources.size()); - for (IResource resource : mResources) { - File file = AdtUtils.getAbsolutePath(resource).toFile(); - files.add(file); - - if (resource instanceof IProject && mSource == null) { - scope = Scope.ALL; - } else { - String name = resource.getName(); - if (AdtUtils.endsWithIgnoreCase(name, DOT_XML)) { - if (name.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { - scope = EnumSet.of(Scope.MANIFEST); - } else { - scope = Scope.RESOURCE_FILE_SCOPE; - } - } else if (name.endsWith(DOT_JAVA) && resource instanceof IFile) { - if (scope != null) { - if (!scope.contains(Scope.JAVA_FILE)) { - scope = EnumSet.copyOf(scope); - scope.add(Scope.JAVA_FILE); - } - } else { - scope = Scope.JAVA_FILE_SCOPE; - } - } else if (name.endsWith(DOT_CLASS) && resource instanceof IFile) { - if (scope != null) { - if (!scope.contains(Scope.CLASS_FILE)) { - scope = EnumSet.copyOf(scope); - scope.add(Scope.CLASS_FILE); - } - } else { - scope = Scope.CLASS_FILE_SCOPE; - } - } else { - return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, - "Only XML files are supported for single file lint", null); //$NON-NLS-1$ - } - } - } - if (scope == null) { - scope = Scope.ALL; - } - if (mSource == null) { - assert !Scope.checkSingleFile(scope) : scope + " with " + mResources; - } - // Check single file? - if (mSource != null) { - // Delete specific markers - IMarker[] markers = EclipseLintClient.getMarkers(mSource); - for (IMarker marker : markers) { - String id = marker.getAttribute(MARKER_CHECKID_PROPERTY, ""); //$NON-NLS-1$ - Issue issue = registry.getIssue(id); - if (issue == null) { - continue; - } - if (issue.isAdequate(scope)) { - marker.delete(); - } - } - } else { - EclipseLintClient.clearMarkers(mResources); - } - - EclipseLintClient client = new EclipseLintClient(registry, mResources, - mDocument, mFatalOnly); - mLint = new LintDriver(registry, client); - mLint.analyze(files, scope); - mFatal = client.hasFatalErrors(); - return Status.OK_STATUS; - } catch (Exception e) { - return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, - "Failed", e); //$NON-NLS-1$ - } finally { - if (monitor != null) { - monitor.done(); - } - } - } - - /** - * Returns true if a fatal error was encountered - */ - boolean isFatal() { - return mFatal; - } - } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java index 8da3465..646d752 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java @@ -15,6 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.lint; +import com.android.annotations.NonNull; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.tools.lint.client.api.Configuration; @@ -47,6 +48,7 @@ class GlobalLintConfiguration extends Configuration { * * @return the singleton configuration */ + @NonNull public static GlobalLintConfiguration get() { return sInstance; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java new file mode 100644 index 0000000..aaf55c6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2012 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.lint; + +import static com.android.ide.eclipse.adt.AdtConstants.DOT_CLASS; +import static com.android.ide.eclipse.adt.AdtConstants.DOT_JAVA; +import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.sdklib.SdkConstants; +import com.android.tools.lint.client.api.IssueRegistry; +import com.android.tools.lint.client.api.LintDriver; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Scope; + +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.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.IJobManager; +import org.eclipse.core.runtime.jobs.Job; + +import java.io.File; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** Job to check lint on a set of resources */ +final class LintJob extends Job { + /** Job family */ + private static final Object FAMILY_RUN_LINT = new Object(); + private final EclipseLintClient mClient; + private final List<? extends IResource> mResources; + private final IResource mSource; + private LintDriver mLint; + private boolean mFatal; + + LintJob( + @NonNull EclipseLintClient client, + @NonNull List<? extends IResource> resources, + @Nullable IResource source) { + super("Running Android Lint"); + mClient = client; + mResources = resources; + mSource = source; + } + + @Override + public boolean belongsTo(Object family) { + return family == FAMILY_RUN_LINT; + } + + @Override + protected void canceling() { + super.canceling(); + if (mLint != null) { + mLint.cancel(); + } + } + + @Override + @NonNull + protected IStatus run(IProgressMonitor monitor) { + try { + monitor.beginTask("Looking for errors", IProgressMonitor.UNKNOWN); + IssueRegistry registry = EclipseLintClient.getRegistry(); + EnumSet<Scope> scope = null; + List<File> files = new ArrayList<File>(mResources.size()); + for (IResource resource : mResources) { + File file = AdtUtils.getAbsolutePath(resource).toFile(); + files.add(file); + + if (resource instanceof IProject && mSource == null) { + scope = Scope.ALL; + } else { + String name = resource.getName(); + if (AdtUtils.endsWithIgnoreCase(name, DOT_XML)) { + if (name.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { + scope = EnumSet.of(Scope.MANIFEST); + } else { + scope = Scope.RESOURCE_FILE_SCOPE; + } + } else if (name.endsWith(DOT_JAVA) && resource instanceof IFile) { + if (scope != null) { + if (!scope.contains(Scope.JAVA_FILE)) { + scope = EnumSet.copyOf(scope); + scope.add(Scope.JAVA_FILE); + } + } else { + scope = Scope.JAVA_FILE_SCOPE; + } + } else if (name.endsWith(DOT_CLASS) && resource instanceof IFile) { + if (scope != null) { + if (!scope.contains(Scope.CLASS_FILE)) { + scope = EnumSet.copyOf(scope); + scope.add(Scope.CLASS_FILE); + } + } else { + scope = Scope.CLASS_FILE_SCOPE; + } + } else { + return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, + "Only XML & Java files are supported for single file lint", null); //$NON-NLS-1$ + } + } + } + if (scope == null) { + scope = Scope.ALL; + } + if (mSource == null) { + assert !Scope.checkSingleFile(scope) : scope + " with " + mResources; + } + // Check single file? + if (mSource != null) { + // Delete specific markers + IMarker[] markers = EclipseLintClient.getMarkers(mSource); + for (IMarker marker : markers) { + String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, ""); + Issue issue = registry.getIssue(id); + if (issue == null) { + continue; + } + if (issue.isAdequate(scope)) { + marker.delete(); + } + } + } else { + EclipseLintClient.clearMarkers(mResources); + } + + mLint = new LintDriver(registry, mClient); + mLint.analyze(files, scope); + mFatal = mClient.hasFatalErrors(); + return Status.OK_STATUS; + } catch (Exception e) { + return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, + "Failed", e); //$NON-NLS-1$ + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + + /** + * Returns true if a fatal error was encountered + * + * @return true if a fatal error was encountered + */ + public boolean isFatal() { + return mFatal; + } + + /** + * Returns the associated lint client + * + * @return the associated lint client + */ + @NonNull + public EclipseLintClient getLintClient() { + return mClient; + } + + /** Returns the current lint jobs, if any (never returns null but array may be empty) */ + @NonNull + static Job[] getCurrentJobs() { + IJobManager jobManager = Job.getJobManager(); + return jobManager.find(LintJob.FAMILY_RUN_LINT); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java index 561c5fd..3b01e05 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java @@ -261,6 +261,13 @@ class LintList extends Composite implements IResourceChangeListener, ControlList updateColumnWidths(); // in case mSingleFile changed } + /** Select the first item */ + public void selectFirst() { + if (mTree.getItemCount() > 0) { + mTree.select(mTree.getItem(0)); + } + } + private List<IMarker> getMarkers() { mErrorCount = mWarningCount = 0; List<IMarker> markerList = new ArrayList<IMarker>(); @@ -585,11 +592,25 @@ class LintList extends Composite implements IResourceChangeListener, ControlList /** Expands all nodes */ public void expandAll() { mTreeViewer.expandAll(); + + if (mExpandedIds == null) { + mExpandedIds = new HashSet<String>(); + } + IMarker[] topMarkers = mContentProvider.getTopMarkers(); + if (topMarkers != null) { + for (IMarker marker : topMarkers) { + String id = EclipseLintClient.getId(marker); + if (id != null) { + mExpandedIds.add(id); + } + } + } } /** Collapses all nodes */ public void collapseAll() { mTreeViewer.collapseAll(); + mExpandedIds = null; } // ---- Column Persistence ---- diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java index d39916f..bd42906 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java @@ -15,6 +15,8 @@ */ package com.android.ide.eclipse.adt.internal.lint; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; @@ -45,22 +47,30 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; @SuppressWarnings("restriction") // WST DOM access class LintListDialog extends TitleAreaDialog implements SelectionListener { private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$ - private IFile mFile; + private final IFile mFile; + private final IEditorPart mEditor; private Button mFixButton; private Button mIgnoreButton; + private Button mIgnoreAllButton; private Button mShowButton; private Text mDetailsText; private Button mIgnoreTypeButton; private LintList mList; - LintListDialog(Shell parentShell, IFile file) { + LintListDialog( + @NonNull Shell parentShell, + @NonNull IFile file, + @Nullable IEditorPart editor) { super(parentShell); - this.mFile = file; + mFile = file; + mEditor = editor; } @Override @@ -97,31 +107,42 @@ class LintListDialog extends TitleAreaDialog implements SelectionListener { if (page.getActivePart() != null) { site = page.getActivePart().getSite(); } + mList = new LintList(site, container, null /*memento*/, true /*singleFile*/); - mList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 5)); + mList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 6)); mShowButton = new Button(container, SWT.NONE); mShowButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); mShowButton.setText("Show"); + mShowButton.setToolTipText("Opens the editor to reveal the XML with the issue"); mShowButton.addSelectionListener(this); + mFixButton = new Button(container, SWT.NONE); + mFixButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mFixButton.setText("Fix"); + mFixButton.setToolTipText("Automatically corrects the problem, if possible"); + mFixButton.setEnabled(false); + mFixButton.addSelectionListener(this); + mIgnoreButton = new Button(container, SWT.NONE); mIgnoreButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); - mIgnoreButton.setText("Ignore"); - mIgnoreButton.setEnabled(false); + mIgnoreButton.setText("Suppress Issue"); + mIgnoreButton.setToolTipText("Adds a special attribute in the layout to suppress this specific warning"); mIgnoreButton.addSelectionListener(this); + mIgnoreAllButton = new Button(container, SWT.NONE); + mIgnoreAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mIgnoreAllButton.setText("Suppress in Layout"); + mIgnoreAllButton.setEnabled(mEditor instanceof AndroidXmlEditor); + mIgnoreAllButton.setToolTipText("Adds an attribute on the root element to suppress all issues of this type in this layout"); + mIgnoreAllButton.addSelectionListener(this); + mIgnoreTypeButton = new Button(container, SWT.NONE); mIgnoreTypeButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); - mIgnoreTypeButton.setText("Ignore Type"); + mIgnoreTypeButton.setText("Disable Issue Type"); + mIgnoreTypeButton.setToolTipText("Turns off checking for this type of error everywhere"); mIgnoreTypeButton.addSelectionListener(this); - mFixButton = new Button(container, SWT.NONE); - mFixButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); - mFixButton.setText("Fix"); - mFixButton.setEnabled(false); - mFixButton.addSelectionListener(this); - new Label(container, SWT.NONE); mDetailsText = new Text(container, SWT.BORDER | SWT.READ_ONLY | SWT.WRAP @@ -138,6 +159,7 @@ class LintListDialog extends TitleAreaDialog implements SelectionListener { mList.addSelectionListener(this); mList.setResources(Collections.<IResource>singletonList(mFile)); + mList.selectFirst(); if (mList.getSelectedMarkers().size() > 0) { updateSelectionState(); } @@ -205,6 +227,28 @@ class LintListDialog extends TitleAreaDialog implements SelectionListener { LintFixGenerator.suppressDetector(id, true, mFile, true /*all*/); } } + } else if (source == mIgnoreButton) { + for (IMarker marker : mList.getSelectedMarkers()) { + LintFixGenerator.addSuppressAnnotation(marker); + } + } else if (source == mIgnoreAllButton) { + Set<String> ids = new HashSet<String>(); + for (IMarker marker : mList.getSelectedMarkers()) { + String id = EclipseLintClient.getId(marker); + if (id != null && !ids.contains(id)) { + ids.add(id); + if (mEditor instanceof AndroidXmlEditor) { + AndroidXmlEditor editor = (AndroidXmlEditor) mEditor; + AddSuppressAttribute fix = AddSuppressAttribute.createFixForAll(editor, + marker, id); + if (fix != null) { + IStructuredDocument document = editor.getStructuredDocument(); + fix.apply(document); + } + } + } + } + mList.refresh(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java index 39c6d25..3761fde 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java @@ -291,7 +291,7 @@ public class LintViewPart extends ViewPart implements SelectionListener, IJobCha } private void refreshStopIcon() { - Job[] currentJobs = EclipseLintRunner.getCurrentJobs(); + Job[] currentJobs = LintJob.getCurrentJobs(); if (currentJobs.length > 0) { ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); mRefreshAction.setImageDescriptor(sharedImages.getImageDescriptor( @@ -466,7 +466,7 @@ public class LintViewPart extends ViewPart implements SelectionListener, IJobCha workbench.saveAllEditors(false /*confirm*/); } - Job[] jobs = EclipseLintRunner.getCurrentJobs(); + Job[] jobs = LintJob.getCurrentJobs(); if (jobs.length > 0) { EclipseLintRunner.cancelCurrentJobs(false); } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java index 827f8a4..b2d7361 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java @@ -338,6 +338,10 @@ public class LintPreferencePage extends PropertyPage implements IWorkbenchPrefer prefs.setLintOnSave(mCheckFileCheckbox.getSelection()); } + if (mConfiguration == null) { + return; + } + mConfiguration.startBulkEditing(); try { // Severities diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java index 8f6de3a..45590b7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java @@ -163,7 +163,7 @@ public class CompatibilityLibraryHelper { for (IJavaProject javaProject : AdtUtils.getOpenAndroidProjects()) { IProject project = javaProject.getProject(); ProjectState state = Sdk.getProjectState(project); - if (state.isLibrary()) { + if (state != null && state.isLibrary()) { ManifestInfo manifestInfo = ManifestInfo.get(project); if (manifestInfo.getPackage().equals("android.support.v7.gridlayout")) { //$NON-NLS-1$ return project; |