blob: a86d971015d68c834c8e3efcd3e39f80579d0fe3 [file] [log] [blame]
/*
* Copyright 2000-2010 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.android;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
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.ide.common.resources.configuration.DensityQualifier;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.Density;
import com.android.resources.ResourceType;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.android.tools.idea.rendering.LocalResourceRepository;
import com.android.tools.idea.rendering.ResourceHelper;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
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.fileEditor.FileEditorManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReferenceExpression;
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.psi.xml.XmlTagValue;
import com.intellij.util.ui.ColorIcon;
import com.intellij.util.ui.EmptyIcon;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomManager;
import org.jetbrains.android.dom.resources.ResourceElement;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.uipreview.ColorPicker;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import static com.android.SdkConstants.*;
import static com.android.tools.idea.AndroidPsiUtils.ResourceReferenceType;
/**
* Annotator which puts colors in the editor gutter for both color files, as well
* as any XML resource that references a color attribute (\@color) or color literal (#AARRGGBBB),
* or references it from Java code (R.color.name). It also previews small icons.
* <p>
* TODO: Use {@link com.android.ide.common.resources.ResourceItemResolver} when possible!
*
* TODO: Add test. Unfortunately, it looks like none of the existing Annotator classes
* in IntelliJ have unit tests, so there doesn't appear to be fixture support for this.
*/
public class AndroidColorAnnotator implements Annotator {
private static final int ICON_SIZE = 8;
private static final int MAX_ICON_SIZE = 5000;
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
// disable it in test mode temporary, because of failing AndroidLayoutDomTest#testAttrReferences1()
return;
}
if (element instanceof XmlTag) {
XmlTag tag = (XmlTag)element;
String tagName = tag.getName();
if ((ResourceType.COLOR.getName().equals(tagName) || ResourceType.DRAWABLE.getName().equals(tagName)
|| ResourceType.MIPMAP.getName().equals(tagName))) {
DomElement domElement = DomManager.getDomManager(element.getProject()).getDomElement(tag);
if (domElement instanceof ResourceElement) {
String value = tag.getValue().getText().trim();
annotateXml(element, holder, value);
}
} else if (SdkConstants.TAG_ITEM.equals(tagName)) {
XmlTagValue value = tag.getValue();
String text = value.getText();
annotateXml(element, holder, text);
}
} else if (element instanceof XmlAttributeValue) {
XmlAttributeValue v = (XmlAttributeValue)element;
String value = v.getValue();
if (value == null || value.isEmpty()) {
return;
}
annotateXml(element, holder, value);
} else if (element instanceof PsiReferenceExpression) {
ResourceReferenceType referenceType = AndroidPsiUtils.getResourceReferenceType(element);
if (referenceType != ResourceReferenceType.NONE) {
// (isResourceReference will return true for both "R.drawable.foo" and the foo literal leaf in the
// same expression, which would result in both elements getting annotated and the icon showing up
// in the gutter twice. Instead we only count the outer one.
ResourceType type = AndroidPsiUtils.getResourceType(element);
if (type == ResourceType.COLOR || type == ResourceType.DRAWABLE || type == ResourceType.MIPMAP) {
String name = AndroidPsiUtils.getResourceName(element);
annotateResourceReference(type, holder, element, name, referenceType == ResourceReferenceType.FRAMEWORK);
}
}
}
}
private static void annotateXml(PsiElement element, AnnotationHolder holder, String value) {
if (value.startsWith("#")) {
final PsiFile file = element.getContainingFile();
if (file != null && AndroidResourceUtil.isInResourceSubdirectory(file, null)) {
if (element instanceof XmlTag) {
Annotation annotation = holder.createInfoAnnotation(element, null);
annotation.setGutterIconRenderer(new MyRenderer(element, null));
} else {
assert element instanceof XmlAttributeValue;
Color color = ResourceHelper.parseColor(value);
if (color != null) {
Annotation annotation = holder.createInfoAnnotation(element, null);
annotation.setGutterIconRenderer(new MyRenderer(element, null));
}
}
}
} else if (value.startsWith(COLOR_RESOURCE_PREFIX)) {
annotateResourceReference(ResourceType.COLOR, holder, element, value.substring(COLOR_RESOURCE_PREFIX.length()), false);
} else if (value.startsWith(ANDROID_COLOR_RESOURCE_PREFIX)) {
annotateResourceReference(ResourceType.COLOR, holder, element, value.substring(ANDROID_COLOR_RESOURCE_PREFIX.length()), true);
} else if (value.startsWith(DRAWABLE_PREFIX)) {
annotateResourceReference(ResourceType.DRAWABLE, holder, element, value.substring(DRAWABLE_PREFIX.length()), false);
} else if (value.startsWith(ANDROID_DRAWABLE_PREFIX)) {
annotateResourceReference(ResourceType.DRAWABLE, holder, element, value.substring(ANDROID_DRAWABLE_PREFIX.length()), true);
} else if (value.startsWith(MIPMAP_PREFIX)) {
annotateResourceReference(ResourceType.MIPMAP, holder, element, value.substring(MIPMAP_PREFIX.length()), false);
}
}
/**
* When annotating Java files, we need to find an associated layout file to pick the resource
* resolver from (e.g. to for example have a theme association which will drive how colors are
* resolved). This file picks one of the open layout files, and if not found, the first layout
* file found in the resources (if any).
* */
@Nullable
public static VirtualFile pickLayoutFile(@NotNull Module module, @NotNull AndroidFacet facet) {
VirtualFile layout = null;
VirtualFile[] openFiles = FileEditorManager.getInstance(module.getProject()).getOpenFiles();
for (VirtualFile file : openFiles) {
if (file.getName().endsWith(DOT_XML) && file.getParent() != null &&
file.getParent().getName().startsWith(FD_RES_LAYOUT)) {
layout = file;
break;
}
}
if (layout == null) {
// Pick among actual files in the project
for (VirtualFile resourceDir : facet.getAllResourceDirectories()) {
for (VirtualFile folder : resourceDir.getChildren()) {
if (folder.getName().startsWith(FD_RES_LAYOUT) && folder.isDirectory()) {
for (VirtualFile file : folder.getChildren()) {
if (file.getName().endsWith(DOT_XML) && file.getParent() != null &&
file.getParent().getName().startsWith(FD_RES_LAYOUT)) {
layout = file;
break;
}
}
}
}
}
}
return layout;
}
private static void annotateResourceReference(@NotNull ResourceType type,
@NotNull AnnotationHolder holder,
@NotNull PsiElement element,
@NotNull String name,
boolean isFramework) {
Module module = ModuleUtilCore.findModuleForPsiElement(element);
if (module == null) {
return;
}
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null) {
return;
}
PsiFile file = PsiTreeUtil.getParentOfType(element, PsiFile.class);
if (file == null) {
return;
}
Configuration configuration = pickConfiguration(facet, module, file);
if (configuration == null) {
return;
}
ResourceValue value = findResourceValue(type, name, isFramework, module, configuration);
if (value != null) {
// TODO: Use a *shared* fallback resolver for this?
ResourceResolver resourceResolver = configuration.getResourceResolver();
if (resourceResolver != null) {
annotateResourceValue(type, holder, element, value, resourceResolver);
}
}
}
/** Picks a suitable configuration to use for resource resolution */
@Nullable
private static Configuration pickConfiguration(AndroidFacet facet, Module module, PsiFile file) {
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) {
return null;
}
VirtualFile layout;
String parentName = virtualFile.getParent().getName();
if (!parentName.startsWith(FD_RES_LAYOUT)) {
layout = pickLayoutFile(module, facet);
if (layout == null) {
return null;
}
} else {
layout = virtualFile;
}
return facet.getConfigurationManager().getConfiguration(layout);
}
/** Annotates the given element with the resolved value of the given {@link ResourceValue} */
private static void annotateResourceValue(@NotNull ResourceType type,
@NotNull AnnotationHolder holder,
@NotNull PsiElement element,
@NotNull ResourceValue value,
@NotNull ResourceResolver resourceResolver) {
if (type == ResourceType.COLOR) {
Color color = ResourceHelper.resolveColor(resourceResolver, value, element.getProject());
if (color != null) {
Annotation annotation = holder.createInfoAnnotation(element, null);
annotation.setGutterIconRenderer(new MyRenderer(element, color));
}
} else {
assert type == ResourceType.DRAWABLE || type == ResourceType.MIPMAP;
File iconFile = pickBestBitmap(ResourceHelper.resolveDrawable(resourceResolver, value, element.getProject()));
if (iconFile != null) {
Annotation annotation = holder.createInfoAnnotation(element, null);
annotation.setGutterIconRenderer(new com.android.tools.idea.rendering.GutterIconRenderer(element, iconFile));
}
}
}
@Nullable
public static File pickBestBitmap(@Nullable File bitmap) {
if (bitmap != null && bitmap.exists()) {
// Pick the smallest resolution, if possible! E.g. if the theme resolver located
// drawable-hdpi/foo.png, and drawable-mdpi/foo.png pick that one instead (and ditto
// for -ldpi etc)
File smallest = findSmallestDpiVersion(bitmap);
if (smallest != null) {
return smallest;
}
long length = bitmap.length();
if (length < MAX_ICON_SIZE) {
return bitmap;
}
}
return null;
}
@Nullable
private static File findSmallestDpiVersion(@NonNull File bitmap) {
File parentFile = bitmap.getParentFile();
if (parentFile == null) {
return null;
}
File resFolder = parentFile.getParentFile();
if (resFolder == null) {
return null;
}
String parentName = parentFile.getName();
FolderConfiguration config = FolderConfiguration.getConfigForFolder(parentName);
if (config == null) {
return null;
}
DensityQualifier qualifier = config.getDensityQualifier();
if (qualifier == null) {
return null;
}
Density density = qualifier.getValue();
if (density != null && density.isValidValueForDevice()) {
String fileName = bitmap.getName();
Density[] densities = Density.values();
// Iterate in reverse, since the Density enum is in descending order
for (int i = densities.length - 1; i >= 0; i--) {
Density d = densities[i];
if (d.isValidValueForDevice()) {
String folder = parentName.replace(density.getResourceValue(), d.getResourceValue());
bitmap = new File(resFolder, folder + File.separator + fileName);
if (bitmap.exists()) {
if (bitmap.length() > MAX_ICON_SIZE) {
// No point continuing the loop; the other densities will be too big too
return null;
}
return bitmap;
}
}
}
}
return null;
}
/** Looks up the resource item of the given type and name for the given configuration, if any */
@Nullable
private 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());
}
}
private static class MyRenderer extends GutterIconRenderer {
private final PsiElement myElement;
private final Color myColor;
private MyRenderer(@NotNull PsiElement element, @Nullable Color color) {
myElement = element;
myColor = color;
}
@NotNull
@Override
public Icon getIcon() {
final Color color = getCurrentColor();
return 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;
MyRenderer that = (MyRenderer)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;
}
}
}