diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index 1a01af39e66..83f15c4a7aa 100755 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.AbstractDataFlowValueRenderingTest import org.jetbrains.kotlin.addImport.AbstractAddImportTest import org.jetbrains.kotlin.allopen.AbstractBytecodeListingTestForAllOpen import org.jetbrains.kotlin.android.* +import org.jetbrains.kotlin.android.annotator.AbstractAndroidGutterIconTest import org.jetbrains.kotlin.android.configure.AbstractConfigureProjectTest import org.jetbrains.kotlin.android.folding.AbstractAndroidResourceFoldingTest import org.jetbrains.kotlin.android.intention.AbstractAndroidIntentionTest @@ -1260,6 +1261,10 @@ fun main(args: Array) { testClass { model("android/folding") } + + testClass { + model("android/gutterIcon") + } } testGroup("plugins/plugins-tests/tests", "plugins/android-extensions/android-extensions-jps/testData") { diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidResourceReferenceAnnotator.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidResourceReferenceAnnotator.kt new file mode 100644 index 00000000000..d96d6a41b33 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidResourceReferenceAnnotator.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * 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 org.jetbrains.kotlin.android + +import com.android.SdkConstants +import com.android.resources.ResourceType.* +import com.android.tools.idea.AndroidPsiUtils.ResourceReferenceType.FRAMEWORK +import com.android.tools.idea.rendering.GutterIconRenderer +import com.android.tools.idea.res.ResourceHelper +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.Annotator +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.psi.PsiElement +import org.jetbrains.android.AndroidColorAnnotator +import org.jetbrains.android.facet.AndroidFacet +import org.jetbrains.kotlin.android.ResourceReferenceAnnotatorUtil.* +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor +import org.jetbrains.kotlin.psi.KtReferenceExpression +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + + +class AndroidResourceReferenceAnnotator : Annotator { + override fun annotate(element: PsiElement, holder: AnnotationHolder) { + val reference = element as? KtReferenceExpression ?: return + val androidFacet = AndroidFacet.getInstance(element) ?: return + val referenceTarget = reference.getResourceReferenceTargetDescriptor() ?: return + val resourceType = referenceTarget.getAndroidResourceType() ?: return + + if (resourceType != COLOR && resourceType != DRAWABLE && resourceType != MIPMAP) { + return + } + + val referenceType = referenceTarget.getResourceReferenceType() + val configuration = pickConfiguration(androidFacet, androidFacet.module, element.containingFile) ?: return + val resourceValue = findResourceValue(resourceType, + reference.text, + referenceType == FRAMEWORK, + androidFacet.module, + configuration) ?: return + + val resourceResolver = configuration.resourceResolver ?: return + + if (resourceType == COLOR) { + val color = ResourceHelper.resolveColor(resourceResolver, resourceValue, element.project) + if (color != null) { + val annotation = holder.createInfoAnnotation(element, null) + annotation.gutterIconRenderer = ColorRenderer(element, color) + } + } + else { + var file = ResourceHelper.resolveDrawable(resourceResolver, resourceValue, element.project) + if (file != null && file.path.endsWith(SdkConstants.DOT_XML)) { + file = pickBitmapFromXml(file, resourceResolver, element.project) + } + val iconFile = AndroidColorAnnotator.pickBestBitmap(file) + if (iconFile != null) { + val annotation = holder.createInfoAnnotation(element, null) + annotation.gutterIconRenderer = GutterIconRenderer(element, iconFile) + } + } + } + + private fun KtReferenceExpression.getResourceReferenceTargetDescriptor(): JavaPropertyDescriptor? = + analyze(BodyResolveMode.PARTIAL)[BindingContext.REFERENCE_TARGET, this] as? JavaPropertyDescriptor +} diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidUtil.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidUtil.kt index 813b5d6cf81..bf8369de4e9 100644 --- a/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidUtil.kt +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidUtil.kt @@ -17,6 +17,11 @@ package org.jetbrains.kotlin.android import com.android.SdkConstants +import com.android.SdkConstants.ANDROID_PKG +import com.android.SdkConstants.R_CLASS +import com.android.resources.ResourceType +import com.android.tools.idea.AndroidPsiUtils +import com.android.tools.idea.AndroidPsiUtils.ResourceReferenceType.* import com.intellij.psi.PsiElement import org.jetbrains.android.augment.AndroidPsiElementFinder import org.jetbrains.android.facet.AndroidFacet @@ -25,13 +30,17 @@ import org.jetbrains.android.util.AndroidResourceUtil.isManifestJavaFile import org.jetbrains.android.util.AndroidResourceUtil.isRJavaFile import org.jetbrains.android.util.AndroidUtils import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor +import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.source.PsiSourceFile @@ -40,49 +49,72 @@ internal fun PsiElement.getAndroidFacetForFile(): AndroidFacet? { return AndroidFacet.getInstance(file) } -internal object KotlinAndroidResourceUtil { - fun getReferredResourceOrManifestField(facet: AndroidFacet, expression: KtSimpleNameExpression, localOnly: Boolean) - = getReferredResourceOrManifestField(facet, expression, null, localOnly) - - fun getReferredResourceOrManifestField(facet: AndroidFacet, expression: KtSimpleNameExpression, - className: String?, localOnly: Boolean): AndroidResourceUtil.MyReferredResourceFieldInfo? { - val resFieldName = expression.getReferencedName() - val resClassReference = expression.getPreviousInQualifiedChain() as? KtSimpleNameExpression ?: return null - val resClassName = resClassReference.getReferencedName() - - if (resClassName.isEmpty() || className != null && className != resClassName) { - return null - } - - val rClassReference = resClassReference.getPreviousInQualifiedChain() as? KtSimpleNameExpression ?: return null - val rClassDescriptor = rClassReference.analyze(BodyResolveMode.PARTIAL) - .get(BindingContext.REFERENCE_TARGET, rClassReference) as? ClassDescriptor ?: return null - - val rClassShortName = rClassDescriptor.name.asString() - val fromManifest = AndroidUtils.MANIFEST_CLASS_NAME == rClassShortName - - if (!fromManifest && AndroidUtils.R_CLASS_NAME != rClassShortName) { - return null - } - - if (!localOnly) { - val qName = rClassDescriptor.fqNameSafe.asString() - - if (SdkConstants.CLASS_R == qName || AndroidPsiElementFinder.INTERNAL_R_CLASS_QNAME == qName) { - return AndroidResourceUtil.MyReferredResourceFieldInfo(resClassName, resFieldName, true, false) - } - } - - val containingFile = (rClassDescriptor.source.containingFile as? PsiSourceFile)?.psiFile ?: return null - if (if (fromManifest) !isManifestJavaFile(facet, containingFile) else !isRJavaFile(facet, containingFile)) { - return null - } - - return AndroidResourceUtil.MyReferredResourceFieldInfo(resClassName, resFieldName, false, false) +internal fun JavaPropertyDescriptor.getAndroidResourceType(): ResourceType? { + if (getResourceReferenceType() == NONE) { + return null } - private fun KtExpression.getPreviousInQualifiedChain(): KtExpression? { - val receiverExpression = getQualifiedExpressionForSelector()?.receiverExpression - return (receiverExpression as? KtQualifiedExpression)?.selectorExpression ?: receiverExpression + val containingClass = containingDeclaration as? JavaClassDescriptor ?: return null + return ResourceType.getEnum(containingClass.name.asString()) +} + +internal fun JavaPropertyDescriptor.getResourceReferenceType(): AndroidPsiUtils.ResourceReferenceType { + val containingClass = containingDeclaration as? JavaClassDescriptor ?: return NONE + val rClass = containingClass.containingDeclaration as? JavaClassDescriptor ?: return NONE + + if (R_CLASS == rClass.name.asString()) { + if ((rClass.containingDeclaration as? PackageFragmentDescriptor)?.fqName?.asString() == ANDROID_PKG) { + return FRAMEWORK + } + else { + return APP + } } + + return NONE +} + +internal fun getReferredResourceOrManifestField(facet: AndroidFacet, expression: KtSimpleNameExpression, localOnly: Boolean) + = getReferredResourceOrManifestField(facet, expression, null, localOnly) + +internal fun getReferredResourceOrManifestField(facet: AndroidFacet, expression: KtSimpleNameExpression, + className: String?, localOnly: Boolean): AndroidResourceUtil.MyReferredResourceFieldInfo? { + val resFieldName = expression.getReferencedName() + val resClassReference = expression.getPreviousInQualifiedChain() as? KtSimpleNameExpression ?: return null + val resClassName = resClassReference.getReferencedName() + + if (resClassName.isEmpty() || className != null && className != resClassName) { + return null + } + + val rClassReference = resClassReference.getPreviousInQualifiedChain() as? KtSimpleNameExpression ?: return null + val rClassDescriptor = rClassReference.analyze(BodyResolveMode.PARTIAL) + .get(BindingContext.REFERENCE_TARGET, rClassReference) as? ClassDescriptor ?: return null + + val rClassShortName = rClassDescriptor.name.asString() + val fromManifest = AndroidUtils.MANIFEST_CLASS_NAME == rClassShortName + + if (!fromManifest && AndroidUtils.R_CLASS_NAME != rClassShortName) { + return null + } + + if (!localOnly) { + val qName = rClassDescriptor.fqNameSafe.asString() + + if (SdkConstants.CLASS_R == qName || AndroidPsiElementFinder.INTERNAL_R_CLASS_QNAME == qName) { + return AndroidResourceUtil.MyReferredResourceFieldInfo(resClassName, resFieldName, true, false) + } + } + + val containingFile = (rClassDescriptor.source.containingFile as? PsiSourceFile)?.psiFile ?: return null + if (if (fromManifest) !isManifestJavaFile(facet, containingFile) else !isRJavaFile(facet, containingFile)) { + return null + } + + return AndroidResourceUtil.MyReferredResourceFieldInfo(resClassName, resFieldName, false, false) +} + +private fun KtExpression.getPreviousInQualifiedChain(): KtExpression? { + val receiverExpression = getQualifiedExpressionForSelector()?.receiverExpression + return (receiverExpression as? KtQualifiedExpression)?.selectorExpression ?: receiverExpression } \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/ResourceReferenceAnnotatorUtil.java b/idea/idea-android/src/org/jetbrains/kotlin/android/ResourceReferenceAnnotatorUtil.java new file mode 100644 index 00000000000..6e1d66d4ba1 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/ResourceReferenceAnnotatorUtil.java @@ -0,0 +1,270 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * 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 org.jetbrains.kotlin.android; + + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ResourceItem; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ResourceResolver; +import com.android.resources.ResourceType; +import com.android.tools.idea.configurations.Configuration; +import com.android.tools.idea.res.AppResourceRepository; +import com.android.tools.idea.res.LocalResourceRepository; +import com.android.tools.idea.res.ResourceHelper; +import com.android.tools.idea.ui.resourcechooser.ColorPicker; +import com.android.utils.XmlUtils; +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlAttributeValue; +import com.intellij.psi.xml.XmlTag; +import com.intellij.util.ui.ColorIcon; +import com.intellij.util.ui.EmptyIcon; +import com.intellij.util.ui.JBUI; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.swing.*; +import java.awt.*; +import java.io.File; + +import static com.android.SdkConstants.*; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_DRAWABLE; +import static com.android.tools.idea.uibuilder.property.renderer.NlDefaultRenderer.ICON_SIZE; +import static org.jetbrains.android.AndroidColorAnnotator.pickLayoutFile; + +/** + * Contains copied privates from AndroidColorAnnotator, so we could use them for Kotlin AndroidResourceReferenceAnnotator + */ +public class ResourceReferenceAnnotatorUtil { + @Nullable + public static File pickBitmapFromXml(@NotNull File file, @NotNull ResourceResolver resourceResolver, @NotNull Project project) { + try { + String xml = Files.toString(file, Charsets.UTF_8); + Document document = XmlUtils.parseDocumentSilently(xml, true); + if (document != null && document.getDocumentElement() != null) { + Element root = document.getDocumentElement(); + String tag = root.getTagName(); + Element target = null; + String attribute = null; + if ("vector".equals(tag)) { + // Vectors are handled in the icon cache + return file; + } + else if ("bitmap".equals(tag) || "nine-patch".equals(tag)) { + target = root; + attribute = ATTR_SRC; + } + else if ("selector".equals(tag) || + "level-list".equals(tag) || + "layer-list".equals(tag) || + "transition".equals(tag)) { + NodeList children = root.getChildNodes(); + for (int i = children.getLength() - 1; i >= 0; i--) { + Node item = children.item(i); + if (item.getNodeType() == Node.ELEMENT_NODE && TAG_ITEM.equals(item.getNodeName())) { + target = (Element)item; + if (target.hasAttributeNS(ANDROID_URI, ATTR_DRAWABLE)) { + attribute = ATTR_DRAWABLE; + break; + } + } + } + } + else if ("clip".equals(tag) || "inset".equals(tag) || "scale".equals(tag)) { + target = root; + attribute = ATTR_DRAWABLE; + } else { + // etc - no bitmap to be found + return null; + } + if (attribute != null && target.hasAttributeNS(ANDROID_URI, attribute)) { + String src = target.getAttributeNS(ANDROID_URI, attribute); + ResourceValue value = resourceResolver.findResValue(src, false); + if (value != null) { + return ResourceHelper.resolveDrawable(resourceResolver, value, project); + + } + } + } + } catch (Throwable ignore) { + // Not logging for now; afraid to risk unexpected crashes in upcoming preview. TODO: Re-enable. + //Logger.getInstance(AndroidColorAnnotator.class).warn(String.format("Could not read/render icon image %1$s", file), e); + } + + return null; + } + + /** Looks up the resource item of the given type and name for the given configuration, if any */ + @Nullable + public static ResourceValue findResourceValue(ResourceType type, + String name, + boolean isFramework, + Module module, + Configuration configuration) { + if (isFramework) { + ResourceRepository frameworkResources = configuration.getFrameworkResources(); + if (frameworkResources == null) { + return null; + } + if (!frameworkResources.hasResourceItem(type, name)) { + return null; + } + ResourceItem item = frameworkResources.getResourceItem(type, name); + return item.getResourceValue(type, configuration.getFullConfig(), false); + } else { + LocalResourceRepository appResources = AppResourceRepository.getAppResources(module, true); + if (appResources == null) { + return null; + } + if (!appResources.hasResourceItem(type, name)) { + return null; + } + return appResources.getConfiguredValue(type, name, configuration.getFullConfig()); + } + } + + /** Picks a suitable configuration to use for resource resolution */ + @Nullable + public static Configuration pickConfiguration(AndroidFacet facet, Module module, PsiFile file) { + VirtualFile virtualFile = file.getVirtualFile(); + if (virtualFile == null) { + return null; + } + + VirtualFile parent = virtualFile.getParent(); + if (parent == null) { + return null; + } + VirtualFile layout; + String parentName = parent.getName(); + if (!parentName.startsWith(FD_RES_LAYOUT)) { + layout = pickLayoutFile(module, facet); + if (layout == null) { + return null; + } + } else { + layout = virtualFile; + } + + return facet.getConfigurationManager().getConfiguration(layout); + } + + public static class ColorRenderer extends GutterIconRenderer { + private final PsiElement myElement; + private final Color myColor; + + ColorRenderer(@NotNull PsiElement element, @Nullable Color color) { + myElement = element; + myColor = color; + } + + @NotNull + @Override + public Icon getIcon() { + Color color = getCurrentColor(); + return JBUI.scale(color == null ? EmptyIcon.create(ICON_SIZE) : new ColorIcon(ICON_SIZE, color)); + } + + @Nullable + private Color getCurrentColor() { + if (myColor != null) { + return myColor; + } else if (myElement instanceof XmlTag) { + return ResourceHelper.parseColor(((XmlTag)myElement).getValue().getText()); + } else if (myElement instanceof XmlAttributeValue) { + return ResourceHelper.parseColor(((XmlAttributeValue)myElement).getValue()); + } else { + return null; + } + } + + @Override + public AnAction getClickAction() { + if (myColor != null) { // Cannot set colors that were derived + return null; + } + return new AnAction() { + @Override + public void actionPerformed(AnActionEvent e) { + final Editor editor = CommonDataKeys.EDITOR.getData(e.getDataContext()); + if (editor != null) { + // Need ARGB support in platform color chooser; see + // https://youtrack.jetbrains.com/issue/IDEA-123498 + //final Color color = + // ColorChooser.chooseColor(editor.getComponent(), AndroidBundle.message("android.choose.color"), getCurrentColor()); + final Color color = ColorPicker.showDialog(editor.getComponent(), "Choose Color", getCurrentColor(), true, null, false); + if (color != null) { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + if (myElement instanceof XmlTag) { + ((XmlTag)myElement).getValue().setText(ResourceHelper.colorToString(color)); + } else if (myElement instanceof XmlAttributeValue) { + XmlAttribute attribute = PsiTreeUtil.getParentOfType(myElement, XmlAttribute.class); + if (attribute != null) { + attribute.setValue(ResourceHelper.colorToString(color)); + } + } + } + }); + } + } + } + }; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ColorRenderer that = (ColorRenderer)o; + // TODO: Compare with modification count in app resources (if not framework) + if (myColor != null ? !myColor.equals(that.myColor) : that.myColor != null) return false; + if (!myElement.equals(that.myElement)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = myElement.hashCode(); + result = 31 * result + (myColor != null ? myColor.hashCode() : 0); + return result; + } + } +} diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/inspection/KotlinAndroidResourceQuickFixProvider.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/inspection/KotlinAndroidResourceQuickFixProvider.kt index 61bfee3fa35..d597d271ce4 100644 --- a/idea/idea-android/src/org/jetbrains/kotlin/android/inspection/KotlinAndroidResourceQuickFixProvider.kt +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/inspection/KotlinAndroidResourceQuickFixProvider.kt @@ -25,7 +25,7 @@ import org.jetbrains.android.facet.AndroidFacet import org.jetbrains.android.inspections.CreateFileResourceQuickFix import org.jetbrains.android.inspections.CreateValueResourceQuickFix import org.jetbrains.android.util.AndroidResourceUtil -import org.jetbrains.kotlin.android.KotlinAndroidResourceUtil +import org.jetbrains.kotlin.android.getReferredResourceOrManifestField import org.jetbrains.kotlin.idea.references.KtSimpleNameReference @@ -39,7 +39,7 @@ class KotlinAndroidResourceQuickFixProvider : UnresolvedReferenceQuickFixProvide manifest.`package`.value ?: return val contextFile = expression.containingFile ?: return - val info = KotlinAndroidResourceUtil.getReferredResourceOrManifestField(facet, expression, true) + val info = getReferredResourceOrManifestField(facet, expression, true) if (info == null || info.isFromManifest) { return } diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AbstractAndroidGutterIconTest.kt b/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AbstractAndroidGutterIconTest.kt new file mode 100644 index 00000000000..bfeba001444 --- /dev/null +++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AbstractAndroidGutterIconTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * 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 org.jetbrains.kotlin.android.annotator + +import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.PathUtil +import com.intellij.util.ui.ColorIcon +import junit.framework.TestCase +import org.jetbrains.kotlin.android.KotlinAndroidTestCase +import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil +import org.jetbrains.kotlin.test.InTextDirectivesUtils +import java.awt.Color +import java.io.File +import javax.swing.ImageIcon + + +abstract class AbstractAndroidGutterIconTest : KotlinAndroidTestCase() { + + fun doTest(path: String) { + val testFile = File(path) + val testFileText = FileUtil.loadFile(testFile) + val withRuntime = InTextDirectivesUtils.isDirectiveDefined(testFileText, "// WITH_RUNTIME") + + val drawable = InTextDirectivesUtils.isDirectiveDefined(testFileText, "// DRAWABLE") + val color = InTextDirectivesUtils.findListWithPrefixes(testFileText, "// COLOR: ").takeIf { it.isNotEmpty() }?.let { + val components = it.map { it.toInt(16) } + Color(components[0], components[1], components[2]) + } + + try { + if (withRuntime) { + ConfigLibraryUtil.configureKotlinRuntime(myFixture.module) + } + + copyResourceDirectoryForTest(path) + myFixture.copyFileToProject(testFile.parent + "/R.java", "gen/${COM_MYAPP_PACKAGE_PATH}R.java") + + val sourceFile = myFixture.copyFileToProject(path, "src/${PathUtil.getFileName(path)}") + myFixture.configureFromExistingVirtualFile(sourceFile) + + val gutter = myFixture.findAllGutters().find { + when { + drawable -> it.icon is ImageIcon + color != null -> (it.icon as? ColorIcon)?.iconColor == color + else -> error("COLOR or DRAWABLE must be defined!") + } + } + + TestCase.assertNotNull(gutter) + } + finally { + if (withRuntime) { + ConfigLibraryUtil.unConfigureKotlinRuntime(myFixture.module) + } + } + } + + companion object { + private val COM_MYAPP_PACKAGE_PATH: String = "com/myapp/" + } +} \ No newline at end of file diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AndroidGutterIconTestGenerated.java b/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AndroidGutterIconTestGenerated.java new file mode 100644 index 00000000000..0d2804d4f30 --- /dev/null +++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AndroidGutterIconTestGenerated.java @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * 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 org.jetbrains.kotlin.android.annotator; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/testData/android/gutterIcon") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class AndroidGutterIconTestGenerated extends AbstractAndroidGutterIconTest { + public void testAllFilesPresentInGutterIcon() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/gutterIcon"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("color.kt") + public void testColor() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/color.kt"); + doTest(fileName); + } + + @TestMetadata("drawable.kt") + public void testDrawable() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/drawable.kt"); + doTest(fileName); + } + + @TestMetadata("mipmap.kt") + public void testMipmap() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/mipmap.kt"); + doTest(fileName); + } + + @TestMetadata("systemColor.kt") + public void testSystemColor() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/systemColor.kt"); + doTest(fileName); + } + + @TestMetadata("systemDrawable.kt") + public void testSystemDrawable() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/systemDrawable.kt"); + doTest(fileName); + } + + @TestMetadata("idea/testData/android/gutterIcon/res") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Res extends AbstractAndroidGutterIconTest { + public void testAllFilesPresentInRes() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/gutterIcon/res"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true); + } + + } +} diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml index c12d4cc1c6c..0678f0f0953 100644 --- a/idea/src/META-INF/android.xml +++ b/idea/src/META-INF/android.xml @@ -57,6 +57,8 @@ + + diff --git a/idea/testData/android/gutterIcon/R.java b/idea/testData/android/gutterIcon/R.java new file mode 100644 index 00000000000..a4750cceafa --- /dev/null +++ b/idea/testData/android/gutterIcon/R.java @@ -0,0 +1,18 @@ +package com.myapp; + +public final class R { + public static final class drawable { + public static final int test_icon=0x7f02005d; + } + public static final class mipmap { + public static final int test_icon=0x7f04005d; + } + public static final class color { + public static final int colorAccent=0x7f0a0013; + public static final int colorPrimary=0x7f0a0014; + public static final int colorPrimaryDark=0x7f0a0015; + } + public static final class layout { + public static final int activity_kotlin=0x7f08005d; + } +} \ No newline at end of file diff --git a/idea/testData/android/gutterIcon/color.kt b/idea/testData/android/gutterIcon/color.kt new file mode 100644 index 00000000000..c57cf7bece4 --- /dev/null +++ b/idea/testData/android/gutterIcon/color.kt @@ -0,0 +1,6 @@ +// COLOR: FF,40,81 +package com.myapp + +import android.content.Context + +fun Context.getPrimaryColor() = resources.getColor(R.color.colorAccent, null) \ No newline at end of file diff --git a/idea/testData/android/gutterIcon/drawable.kt b/idea/testData/android/gutterIcon/drawable.kt new file mode 100644 index 00000000000..d69383f813e --- /dev/null +++ b/idea/testData/android/gutterIcon/drawable.kt @@ -0,0 +1,6 @@ +// DRAWABLE +package com.myapp + +import android.content.Context + +fun Context.getAddIcon() = resources.getDrawable(R.drawable.test_icon, null) \ No newline at end of file diff --git a/idea/testData/android/gutterIcon/mipmap.kt b/idea/testData/android/gutterIcon/mipmap.kt new file mode 100644 index 00000000000..ad893e0dd31 --- /dev/null +++ b/idea/testData/android/gutterIcon/mipmap.kt @@ -0,0 +1,6 @@ +// DRAWABLE +package com.myapp + +import android.content.Context + +fun Context.getAddIcon() = resources.getDrawable(R.mipmap.test_icon, null) \ No newline at end of file diff --git a/idea/testData/android/gutterIcon/res/drawable/test_icon.png b/idea/testData/android/gutterIcon/res/drawable/test_icon.png new file mode 100644 index 00000000000..444e8a5ee85 Binary files /dev/null and b/idea/testData/android/gutterIcon/res/drawable/test_icon.png differ diff --git a/idea/testData/android/gutterIcon/res/layout/layout.xml b/idea/testData/android/gutterIcon/res/layout/layout.xml new file mode 100644 index 00000000000..8affc5030a8 --- /dev/null +++ b/idea/testData/android/gutterIcon/res/layout/layout.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/idea/testData/android/gutterIcon/res/mipmap-mdpi/test_icon.png b/idea/testData/android/gutterIcon/res/mipmap-mdpi/test_icon.png new file mode 100644 index 00000000000..444e8a5ee85 Binary files /dev/null and b/idea/testData/android/gutterIcon/res/mipmap-mdpi/test_icon.png differ diff --git a/idea/testData/android/gutterIcon/res/values/colors.xml b/idea/testData/android/gutterIcon/res/values/colors.xml new file mode 100644 index 00000000000..3ab3e9cbce0 --- /dev/null +++ b/idea/testData/android/gutterIcon/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/idea/testData/android/gutterIcon/systemColor.kt b/idea/testData/android/gutterIcon/systemColor.kt new file mode 100644 index 00000000000..56c8bda8953 --- /dev/null +++ b/idea/testData/android/gutterIcon/systemColor.kt @@ -0,0 +1,4 @@ +// COLOR: 0,0,0 +import android.content.Context + +fun getBlackColor() = android.R.color.black \ No newline at end of file diff --git a/idea/testData/android/gutterIcon/systemDrawable.kt b/idea/testData/android/gutterIcon/systemDrawable.kt new file mode 100644 index 00000000000..116ccd63cf0 --- /dev/null +++ b/idea/testData/android/gutterIcon/systemDrawable.kt @@ -0,0 +1,4 @@ +// DRAWABLE +import android.content.Context + +fun Context.getAddIcon() = resources.getDrawable(android.R.drawable.ic_menu_add, null) \ No newline at end of file