blob: d5d1b72c02145ec2724d6c10606d39f3c2f1fa69 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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 com.android.tools.idea.editors.theme;
import com.android.SdkConstants;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.BuildTypeContainer;
import com.android.builder.model.ProductFlavorContainer;
import com.android.builder.model.SourceProvider;
import com.android.ide.common.rendering.api.ItemResourceValue;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.ide.common.resources.ResourceResolver;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.VersionQualifier;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.idea.actions.OverrideResourceAction;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.configurations.ConfigurationManager;
import com.android.tools.idea.configurations.ResourceResolverCache;
import com.android.tools.idea.editors.theme.datamodels.ConfiguredItemResourceValue;
import com.android.tools.idea.editors.theme.datamodels.EditedStyleItem;
import com.android.tools.idea.editors.theme.datamodels.ThemeEditorStyle;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.javadoc.AndroidJavaDocRenderer;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.android.tools.idea.rendering.LocalResourceRepository;
import com.android.tools.idea.rendering.ResourceFolderRegistry;
import com.android.tools.idea.rendering.ResourceFolderRepository;
import com.android.tools.idea.rendering.ResourceHelper;
import com.android.tools.lint.checks.ApiLookup;
import com.google.common.base.Predicate;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.Processor;
import org.jetbrains.android.dom.attrs.AttributeDefinition;
import org.jetbrains.android.dom.attrs.AttributeFormat;
import org.jetbrains.android.dom.resources.ResourceElement;
import org.jetbrains.android.dom.resources.Style;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.inspections.lint.IntellijLintClient;
import org.jetbrains.android.uipreview.ChooseResourceDialog;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Utility class for static methods which are used in different classes of theme editor
*/
public class ThemeEditorUtils {
private static final Logger LOG = Logger.getInstance(ThemeEditorUtils.class);
private static final Cache<String, String> ourTooltipCache = CacheBuilder.newBuilder().weakValues().maximumSize(30) // To be able to cache roughly one screen of attributes
.build();
private static final Set<String> DEFAULT_THEMES = ImmutableSet.of("Theme.AppCompat.NoActionBar", "Theme.AppCompat.Light.NoActionBar");
private static final Set<String> DEFAULT_THEMES_FALLBACK =
ImmutableSet.of("Theme.Material.NoActionBar", "Theme.Material.Light.NoActionBar");
private static final String[] CUSTOM_WIDGETS_JAR_PATHS = {
// Bundled path
"/plugins/android/lib/androidWidgets/theme-editor-widgets.jar",
// Development path
"/../adt/idea/android/lib/androidWidgets/theme-editor-widgets.jar"
};
public static final Comparator<ThemeEditorStyle> STYLE_COMPARATOR = new Comparator<ThemeEditorStyle>() {
@Override
public int compare(ThemeEditorStyle o1, ThemeEditorStyle o2) {
if (o1.isProjectStyle() == o2.isProjectStyle()) {
return o1.getQualifiedName().compareToIgnoreCase(o2.getQualifiedName());
}
return o1.isProjectStyle() ? -1 : 1;
}
};
private ThemeEditorUtils() {
}
@NotNull
public static String generateToolTipText(@NotNull final ItemResourceValue resValue,
@NotNull final Module module,
@NotNull final Configuration configuration) {
final LocalResourceRepository repository = AppResourceRepository.getAppResources(module, true);
if (repository == null) {
return "";
}
String tooltipKey = resValue.toString() + module.toString() + configuration.toString() + repository.getModificationCount();
String cachedTooltip = ourTooltipCache.getIfPresent(tooltipKey);
if (cachedTooltip != null) {
return cachedTooltip;
}
String tooltipContents = AndroidJavaDocRenderer.renderItemResourceWithDoc(module, configuration, resValue);
ourTooltipCache.put(tooltipKey, tooltipContents);
return tooltipContents;
}
/**
* Returns html that will be displayed in attributes table for a given item.
* For example: deprecated attrs will be with a strike through
*/
@NotNull
public static String getDisplayHtml(EditedStyleItem item) {
return item.isDeprecated() ? "<html><body><strike>" + item.getQualifiedName() + "</strike></body></html>" : item.getQualifiedName();
}
public static void openThemeEditor(@NotNull final Project project) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
ThemeEditorVirtualFile file = null;
final FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
for (final FileEditor editor : fileEditorManager.getAllEditors()) {
if (!(editor instanceof ThemeEditor)) {
continue;
}
ThemeEditor themeEditor = (ThemeEditor)editor;
if (themeEditor.getVirtualFile().getProject() == project) {
file = themeEditor.getVirtualFile();
break;
}
}
// If existing virtual file is found, openEditor with created descriptor is going to
// show existing editor (without creating a new tab). If we haven't found any existing
// virtual file, we're creating one here (new tab with theme editor will be opened).
if (file == null) {
file = ThemeEditorVirtualFile.getThemeEditorFile(project);
}
final OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file);
fileEditorManager.openEditor(descriptor, true);
}
});
}
/**
* Find every attribute in the theme hierarchy and all the possible configurations where it's present.
* @param style the theme to retrieve all the attributes from
* @param attributeConfigurations a {@link HashMultimap} where all the attributes and configurations will be stored
* @param resolver ThemeResolver that would be used to find themes by name.
*/
private static void findAllAttributes(@NotNull final ThemeEditorStyle style,
@NotNull HashMultimap<String, FolderConfiguration> attributeConfigurations,
@NotNull ThemeResolver resolver) {
for (ThemeEditorStyle parent : style.getAllParents(resolver)) {
findAllAttributes(parent, attributeConfigurations, resolver);
}
Multimap<String, ConfiguredItemResourceValue> configuredValues = style.getConfiguredValues();
for (String attributeName : configuredValues.keys()) {
Collection<ConfiguredItemResourceValue> values = configuredValues.get(attributeName);
for (ConfiguredItemResourceValue value : values) {
attributeConfigurations.put(attributeName, value.getConfiguration());
}
}
}
/**
* Set of {@link ConfiguredItemResourceValue} items that allows overwriting elements and uses the folder and the attribute name
* as key.
*/
static class AttributeInheritanceSet implements Iterable<ConfiguredItemResourceValue> {
private HashSet<ConfiguredItemResourceValue> myAttributes = Sets.newHashSet();
// Index by attribute configuration and name.
private Map<String, ConfiguredItemResourceValue> myAttributesIndex = Maps.newHashMap();
private static String getItemKey(@NotNull ConfiguredItemResourceValue item) {
return String.format("%1$s - %2$s", item.getConfiguration(), ResolutionUtils.getQualifiedItemName(item.getItemResourceValue()));
}
public boolean add(@NotNull ConfiguredItemResourceValue value) {
String key = getItemKey(value);
ConfiguredItemResourceValue existingValue = myAttributesIndex.get(key);
if (existingValue != null) {
myAttributes.remove(existingValue);
}
myAttributes.add(value);
myAttributesIndex.put(key, value);
return existingValue != null;
}
@Override
public Iterator<ConfiguredItemResourceValue> iterator() {
return myAttributes.iterator();
}
public void addAll(@NotNull AttributeInheritanceSet existingAttributes) {
for (ConfiguredItemResourceValue value : existingAttributes) {
add(value);
}
}
}
public static List<EditedStyleItem> resolveAllAttributes(@NotNull final ThemeEditorStyle style, @NotNull ThemeResolver themeResolver) {
HashMultimap<String, FolderConfiguration> attributes = HashMultimap.create();
findAllAttributes(style, attributes, themeResolver);
ImmutableSet<FolderConfiguration> allConfigurations = ImmutableSet.copyOf(attributes.values());
Configuration configuration = style.getConfiguration();
ResourceResolverCache resolverCache = ResourceResolverCache.create(configuration.getConfigurationManager());
// Go over all the existing configurations and resolve each attribute
Map<String, AttributeInheritanceSet> configuredAttributes = Maps.newHashMap();
for (FolderConfiguration folderConfiguration : allConfigurations) {
ResourceResolver resolver = resolverCache.getResourceResolver(configuration.getTarget(), style.getQualifiedName(), folderConfiguration);
StyleResourceValue resolvedStyle = resolver.getStyle(style.getName(), style.isFramework());
if (resolvedStyle == null) {
// The style doesn't exist for this configuration
continue;
}
for (String attributeName : attributes.keys()) {
String noPrefixName = StringUtil.trimStart(attributeName, SdkConstants.PREFIX_ANDROID);
ResourceValue value = resolver.findItemInStyle(resolvedStyle, noPrefixName, noPrefixName.length() != attributeName.length());
if (value != null) {
AttributeInheritanceSet inheritanceSet = configuredAttributes.get(attributeName);
if (inheritanceSet == null) {
inheritanceSet = new AttributeInheritanceSet();
configuredAttributes.put(attributeName, inheritanceSet);
}
inheritanceSet.add(new ConfiguredItemResourceValue(folderConfiguration, (ItemResourceValue)value, style));
}
}
}
resolverCache.reset();
// Now build the EditedStyleItems from the resolved attributes
final ImmutableList.Builder<EditedStyleItem> allValues = ImmutableList.builder();
for (String attributeName : configuredAttributes.keySet()) {
Iterable<ConfiguredItemResourceValue> configuredValues = configuredAttributes.get(attributeName);
final ConfiguredItemResourceValue bestMatch =
(ConfiguredItemResourceValue)style.getConfiguration().getFullConfig()
.findMatchingConfigurable(ImmutableList.copyOf(configuredValues));
if (bestMatch == null) {
allValues.add(new EditedStyleItem(configuredValues.iterator().next(), style));
}
else {
allValues.add(new EditedStyleItem(bestMatch, Iterables
.filter(configuredValues, new Predicate<ConfiguredItemResourceValue>() {
@Override
public boolean apply(@Nullable ConfiguredItemResourceValue input) {
return input != bestMatch;
}
}), style));
}
}
return allValues.build();
}
@Nullable
public static Object extractRealValue(@NotNull final EditedStyleItem item, @NotNull final Class<?> desiredClass) {
String value = item.getValue();
if (desiredClass == Boolean.class && ("true".equals(value) || "false".equals(value))) {
return Boolean.valueOf(value);
}
if (desiredClass == Integer.class) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return value;
}
}
return value;
}
public static boolean acceptsFormat(@Nullable AttributeDefinition attrDefByName, @NotNull AttributeFormat want) {
if (attrDefByName == null) {
return false;
}
return attrDefByName.getFormats().contains(want);
}
@NotNull
private static ImmutableCollection<ThemeEditorStyle> findThemes(@NotNull Collection<ThemeEditorStyle> themes, final @NotNull Set<String> names) {
return ImmutableSet.copyOf(Iterables.filter(themes, new Predicate<ThemeEditorStyle>() {
@Override
public boolean apply(@Nullable ThemeEditorStyle theme) {
return theme != null && names.contains(theme.getName());
}
}));
}
@NotNull
public static ImmutableList<Module> findAndroidModules(@NotNull Project project) {
final ModuleManager manager = ModuleManager.getInstance(project);
final ImmutableList.Builder<Module> builder = ImmutableList.builder();
for (Module module : manager.getModules()) {
final AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet != null) {
builder.add(module);
}
}
return builder.build();
}
@NotNull
public static ImmutableList<ThemeEditorStyle> getDefaultThemes(@NotNull ThemeResolver themeResolver) {
Collection<ThemeEditorStyle> readOnlyLibThemes = themeResolver.getExternalLibraryThemes();
Collection<ThemeEditorStyle> foundThemes = new HashSet<ThemeEditorStyle>();
foundThemes.addAll(findThemes(readOnlyLibThemes, DEFAULT_THEMES));
if (foundThemes.isEmpty()) {
Collection<ThemeEditorStyle> readOnlyFrameworkThemes = themeResolver.getFrameworkThemes();
foundThemes = new HashSet<ThemeEditorStyle>();
foundThemes.addAll(findThemes(readOnlyFrameworkThemes, DEFAULT_THEMES_FALLBACK));
if (foundThemes.isEmpty()) {
foundThemes.addAll(readOnlyLibThemes);
foundThemes.addAll(readOnlyFrameworkThemes);
}
}
Set<ThemeEditorStyle> temporarySet = new TreeSet<ThemeEditorStyle>(STYLE_COMPARATOR);
temporarySet.addAll(foundThemes);
return ImmutableList.copyOf(temporarySet);
}
public static int getMinApiLevel(@NotNull Module module) {
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null) {
return 1;
}
AndroidModuleInfo moduleInfo = AndroidModuleInfo.get(facet);
return moduleInfo.getMinSdkVersion().getApiLevel();
}
/**
* Returns the URL for the theme editor custom widgets jar
*/
@Nullable
public static URL getCustomWidgetsJarUrl() {
String homePath = FileUtil.toSystemIndependentName(PathManager.getHomePath());
StringBuilder notFoundPaths = new StringBuilder();
for (String path : CUSTOM_WIDGETS_JAR_PATHS) {
String jarPath = homePath + path;
VirtualFile root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(jarPath));
if (root != null) {
File rootFile = VfsUtilCore.virtualToIoFile(root);
if (rootFile.exists()) {
try {
LOG.debug("Theme editor custom widgets found at " + jarPath);
return rootFile.toURI().toURL();
}
catch (MalformedURLException e) {
LOG.error(e);
}
}
}
else {
notFoundPaths.append(jarPath).append('\n');
}
}
LOG.error("Unable to find theme-editor-widgets.jar in paths:\n" + notFoundPaths.toString());
return null;
}
/**
* Creates a new style by displaying the dialog of the {@link NewStyleDialog}.
* @param parentStyle is used in NewStyleDialog, will be preselected in the parent text field and name will be suggested based on it.
* @param isTheme whether theme or style will be created
* @param message is used in NewStyleDialog to display message to user
* @return the new style name or null if the style wasn't created.
*/
@Nullable
public static String createNewStyle(@Nullable ThemeEditorStyle parentStyle,
@NotNull final ThemeEditorContext myThemeEditorContext,
boolean isTheme,
@Nullable final String message) {
// if isTheme is true, parentStyle shouldn't be null
assert !isTheme || parentStyle != null;
final NewStyleDialog dialog = new NewStyleDialog(isTheme, myThemeEditorContext, (parentStyle == null) ? null : parentStyle.getQualifiedName(),
(parentStyle == null) ? null : parentStyle.getName(), message);
boolean createStyle = dialog.showAndGet();
if (!createStyle) {
return null;
}
int minModuleApi = getMinApiLevel(myThemeEditorContext.getCurrentContextModule());
//int newAttributeApiLevel = getOriginalApiLevel(newAttributeName, myThemeEditorContext.getProject());
//int newValueApiLevel = getOriginalApiLevel(newAttributeValue, myThemeEditorContext.getProject());
int minAcceptableApi = getOriginalApiLevel(dialog.getStyleParentName(), myThemeEditorContext.getProject());
final String fileName = AndroidResourceUtil.getDefaultResourceFileName(ResourceType.STYLE);
FolderConfiguration config = new FolderConfiguration();
if (minModuleApi < minAcceptableApi) {
VersionQualifier qualifier = new VersionQualifier(minAcceptableApi);
config.setVersionQualifier(qualifier);
}
final List<String> dirNames = Collections.singletonList(config.getFolderName(ResourceFolderType.VALUES));
if (fileName == null) {
LOG.error("Couldn't find a default filename for ResourceType.STYLE");
return null;
}
boolean isCreated = new WriteCommandAction<Boolean>(myThemeEditorContext.getProject(), "Create new theme " + dialog.getStyleName()) {
@Override
protected void run(@NotNull Result<Boolean> result) {
CommandProcessor.getInstance().markCurrentCommandAsGlobal(myThemeEditorContext.getProject());
result.setResult(AndroidResourceUtil.
createValueResource(myThemeEditorContext.getCurrentContextModule(), dialog.getStyleName(), null,
ResourceType.STYLE, fileName, dirNames, new Processor<ResourceElement>() {
@Override
public boolean process(ResourceElement element) {
assert element instanceof Style;
final Style style = (Style)element;
style.getParentStyle().setStringValue(dialog.getStyleParentName());
return true;
}
}));
}
}.execute().getResultObject();
return isCreated ? SdkConstants.STYLE_RESOURCE_PREFIX + dialog.getStyleName() : null;
}
/**
* Returns the Api level at which was defined the attribute or value with the name passed as argument.
* Returns -1 if the name argument is null or not the name of a framework attribute or resource,
* or if it is the name of a framework attribute or resource defined in API 1.
*/
public static int getOriginalApiLevel(@Nullable String name, @NotNull Project project) {
if (name == null) {
return -1;
}
boolean isAttribute;
if (name.startsWith(SdkConstants.ANDROID_NS_NAME_PREFIX)) {
isAttribute = true;
}
else if (name.startsWith(SdkConstants.ANDROID_PREFIX)) {
isAttribute = false;
}
else {
// Not a framework attribute or resource
return -1;
}
ApiLookup apiLookup = IntellijLintClient.getApiLookup(project);
assert apiLookup != null;
if (isAttribute) {
return apiLookup.getFieldVersion("android/R$attr", name.substring(SdkConstants.ANDROID_NS_NAME_PREFIX_LEN));
}
String[] namePieces = name.substring(SdkConstants.ANDROID_PREFIX.length()).split("/");
if (namePieces.length == 2) {
// If dealing with a value, it should be of the form "type/value"
return apiLookup.getFieldVersion("android/R$" + namePieces[0], AndroidResourceUtil.getFieldNameByResourceName(namePieces[1]));
}
return -1;
}
/**
* Checks if the theme selected in the configuration is AppCompat based.
*/
public static boolean isAppCompatTheme(@NotNull Configuration configuration) {
ResourceResolver resources = configuration.getResourceResolver();
if (resources == null) {
LOG.error("ResourceResolver is null");
return false;
}
StyleResourceValue theme = resources.getDefaultTheme();
for (int i = 0; (i < ResourceResolver.MAX_RESOURCE_INDIRECTION) && theme != null; i++) {
// for loop ensures that we don't run into cyclic theme inheritance.
if (theme.getName().startsWith("Theme.AppCompat")) {
return true;
}
theme = resources.getParent(theme);
}
return false;
}
/**
* Copies a theme to a values folder with api version apiLevel,
* potentially creating the necessary folder or file.
* @param apiLevel api level of the folder the theme is copied to
* @param toBeCopied theme to be copied
*/
public static void copyTheme(int apiLevel, @NotNull final XmlTag toBeCopied) {
PsiFile file = toBeCopied.getContainingFile();
assert file instanceof XmlFile : file;
ResourceFolderType folderType = ResourceHelper.getFolderType(file);
assert folderType != null : file;
FolderConfiguration config = ResourceHelper.getFolderConfiguration(file);
assert config != null : file;
VersionQualifier qualifier = new VersionQualifier(apiLevel);
config.setVersionQualifier(qualifier);
String folder = config.getFolderName(folderType);
if (folderType != ResourceFolderType.VALUES) {
OverrideResourceAction.forkResourceFile((XmlFile)file, folder, false);
}
else {
XmlTag tag = OverrideResourceAction.getValueTag(PsiTreeUtil.getParentOfType(toBeCopied, XmlTag.class, false));
if (tag != null) {
AndroidFacet facet = AndroidFacet.getInstance(toBeCopied);
if (facet != null) {
PsiDirectory dir = null;
PsiDirectory resFolder = file.getParent();
if (resFolder != null) {
resFolder = resFolder.getParent();
}
if (resFolder != null) {
dir = resFolder.findSubdirectory(folder);
if (dir == null) {
dir = resFolder.createSubdirectory(folder);
}
}
OverrideResourceAction.forkResourceValue(toBeCopied.getProject(), tag, file, facet, dir, false);
}
}
}
}
/**
* Given a {@link SourceProvider}, it returns a list of all the available ResourceFolderRepositories
*/
@NotNull
public static List<ResourceFolderRepository> getResourceFolderRepositoriesFromSourceSet(@NotNull AndroidFacet facet,
@Nullable SourceProvider provider) {
if (provider == null) {
return Collections.emptyList();
}
Collection<File> resDirectories = provider.getResDirectories();
LocalFileSystem fileSystem = LocalFileSystem.getInstance();
List<ResourceFolderRepository> folders = Lists.newArrayListWithExpectedSize(resDirectories.size());
for (File dir : resDirectories) {
VirtualFile virtualFile = fileSystem.findFileByIoFile(dir);
if (virtualFile != null) {
folders.add(ResourceFolderRegistry.get(facet, virtualFile));
}
}
return folders;
}
@NotNull
public static Configuration getConfigurationForModule(@NotNull Module module) {
Project project = module.getProject();
final AndroidFacet facet = AndroidFacet.getInstance(module);
assert facet != null : "moduleComboModel must contain only Android modules";
ConfigurationManager configurationManager = facet.getConfigurationManager();
// Using the project virtual file to set up configuration for the theme editor
// That fact is hard-coded in computeBestDevice() method in Configuration.java
// BEWARE if attempting to modify to use a different virtual file
final VirtualFile projectFile = project.getProjectFile();
assert projectFile != null;
return configurationManager.getConfiguration(projectFile);
}
/**
* Interface to visit all the available {@link LocalResourceRepository}
*/
public interface ResourceFolderVisitor {
/**
* @param resources a repository containing resources
* @param variantName string that identifies the variant used to obtain the resources
* @param isSelected true if the current passed repository is in an active source set
*/
void visitResourceFolder(@NotNull LocalResourceRepository resources, @NotNull String variantName, boolean isSelected);
}
/**
* Visits every ResourceFolderRepository
*/
public static void acceptResourceResolverVisitor(@NotNull AndroidFacet facet, @NotNull ResourceFolderVisitor visitor) {
// Set of the SourceProviders for the current selected configuration
Set<SourceProvider> selectedProviders = Sets.newHashSet();
// Set of the SourceProviders that are not active in the current selected configuration
Set<SourceProvider> inactiveProviders = Sets.newHashSet();
selectedProviders.add(facet.getMainSourceProvider());
IdeaAndroidProject androidModel = facet.getAndroidModel();
if (androidModel != null) {
assert facet.requiresAndroidModel();
selectedProviders.add(facet.getBuildTypeSourceProvider());
selectedProviders.add(facet.getMultiFlavorSourceProvider());
// Add inactive SourceSets
AndroidProject project = androidModel.getAndroidProject();
for (BuildTypeContainer buildType : project.getBuildTypes()) {
inactiveProviders.add(buildType.getSourceProvider());
}
for (ProductFlavorContainer productFlavor : project.getProductFlavors()) {
inactiveProviders.add(productFlavor.getSourceProvider());
}
}
for (SourceProvider provider : selectedProviders) {
for (ResourceFolderRepository resourceRepository : getResourceFolderRepositoriesFromSourceSet(facet, provider)) {
visitor.visitResourceFolder(resourceRepository, provider.getName(), true);
}
}
for (SourceProvider provider : inactiveProviders) {
for (ResourceFolderRepository resourceRepository : getResourceFolderRepositoriesFromSourceSet(facet, provider)) {
visitor.visitResourceFolder(resourceRepository, provider.getName(), false);
}
}
}
public static ChooseResourceDialog getResourceDialog(@NotNull EditedStyleItem item, @NotNull ThemeEditorContext context,
ResourceType[] allowedTypes) {
String itemValue = item.getValue();
String resourceName;
// If it points to an existing resource.
if (!RenderResources.REFERENCE_EMPTY.equals(itemValue) &&
!RenderResources.REFERENCE_NULL.equals(itemValue) &&
itemValue.startsWith(SdkConstants.PREFIX_RESOURCE_REF)) {
// Use the name of that resource.
resourceName = itemValue.substring(itemValue.indexOf('/') + 1);
}
else {
// Otherwise use the name of the attribute.
resourceName = item.getName();
}
resourceName = getDefaultResourceName(context, resourceName);
Module module = context.getModuleForResources();
final Configuration configuration = getConfigurationForModule(module);
ResourceResolver resourceResolver = configuration.getResourceResolver();
assert resourceResolver != null;
ChooseResourceDialog dialog;
ResourceHelper.StateList stateList = ResourceHelper.resolveStateList(resourceResolver, item.getSelectedValue(), context.getProject());
if (stateList != null) {
dialog = new ChooseResourceDialog(context.getModuleForResources(), context.getConfiguration(), allowedTypes, stateList,
ChooseResourceDialog.ResourceNameVisibility.FORCE, resourceName);
}
else {
String resolvedResource;
Color color = ResourceHelper.resolveColor(resourceResolver, item.getSelectedValue(), context.getProject());
if (color != null) {
resolvedResource = ResourceHelper.colorToString(color);
}
else {
resolvedResource = resourceResolver.resolveResValue(item.getSelectedValue()).getName();
}
dialog = new ChooseResourceDialog(context.getModuleForResources(), allowedTypes, resolvedResource, null,
ChooseResourceDialog.ResourceNameVisibility.FORCE, resourceName);
}
return dialog;
}
/**
* Build a name for a new resource based on a provided name.
* @param initialName a name that result should be based on (that might not be vacant)
*/
@NotNull
private static String getDefaultResourceName(@NotNull ThemeEditorContext context, final @NotNull String initialName) {
if (context.getCurrentTheme() == null || !context.getCurrentTheme().isReadOnly()) {
// If the currently selected theme is not read-only, then the expected
// behaviour of color picker would be to edit the existing resource.
return initialName;
}
final ResourceResolver resolver = context.getResourceResolver();
assert resolver != null;
final ResourceValue value = resolver.findResValue(SdkConstants.COLOR_RESOURCE_PREFIX + initialName, false);
// Value doesn't exist, safe to use initial guess
if (value == null) {
return initialName;
}
// Given value exist, need to add a suffix to initialName to make it unique
for (int i = 1; i <= 50; ++i) {
final String name = initialName + "_" + i;
if (resolver.findResValue(SdkConstants.COLOR_RESOURCE_PREFIX + name, false) == null) {
// Found a vacant name
return name;
}
}
// Made 50 iterations and still no luck finding a vacant name
// Just set a default name to empty string so user have to insert the name manually
return "";
}
}