blob: 886161780f6744cfac08f47f3295256b3ce5f23f [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.actions;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.configurations.ConfigurationManager;
import com.android.tools.idea.configurations.RenderContext;
import com.android.tools.idea.rendering.PsiResourceItem;
import com.android.tools.idea.rendering.ResourceHelper;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import com.intellij.codeInsight.intention.AbstractIntentionAction;
import com.intellij.codeInsight.navigation.NavigationUtil;
import com.intellij.ide.actions.ElementCreator;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.InputValidator;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.io.StreamUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.android.actions.CreateResourceDirectoryDialog;
import org.jetbrains.android.dom.resources.ResourceElement;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.inspections.lint.AndroidLintQuickFix;
import org.jetbrains.android.inspections.lint.AndroidQuickfixContexts;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static com.android.SdkConstants.ATTR_NAME;
/**
* Action in XML files which lets you override a resource (by creating a new resource in a different resource folder
* <p>
* <ul>
* <li>Display validation errors when the folder name is invalid</li>
* <li>Offer to override the resource in a specific variant?</li>
* <li>Offer specific suggestions for folder configurations based on resource type. For example, for a string value
* it's probably a locale; for a style it's probably an API version, for a layout it's probably
* a screen size or an orientation, and so on.</li>
* </ul>
*/
public class OverrideResourceAction extends AbstractIntentionAction {
private static String getActionName(@Nullable String folder) {
return "Override Resource in " + (folder != null ? folder : "Other Configuration...");
}
@NotNull
@Override
public String getText() {
return getActionName(null);
}
@Override
public boolean startInWriteAction() {
return true;
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
if (file instanceof XmlFile && file.isValid() && AndroidFacet.getInstance(file) != null) {
ResourceFolderType folderType = ResourceHelper.getFolderType(file);
if (folderType == null) {
return false;
} else if (folderType != ResourceFolderType.VALUES) {
return true;
} else {
return isAvailable(getValueTag(editor, file), file);
}
}
return false;
}
public boolean isAvailable(@Nullable XmlTag tag, PsiFile file) {
if (file instanceof XmlFile && file.isValid() && AndroidFacet.getInstance(file) != null) {
ResourceFolderType folderType = ResourceHelper.getFolderType(file);
if (folderType == null) {
return false;
} else if (folderType != ResourceFolderType.VALUES) {
return true;
} else {
// In value files, you can invoke this action if the caret is on or inside an element (other than the
// root <resources> tag). Only accept the element if it has a known type with a known name.
if (tag != null && tag.getAttributeValue(ATTR_NAME) != null) {
return AndroidResourceUtil.getResourceForResourceTag(tag) != null;
}
}
}
return false;
}
@Override
public void invoke(@NotNull final Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
AndroidFacet facet = AndroidFacet.getInstance(file);
ResourceFolderType folderType = ResourceHelper.getFolderType(file);
if (facet == null || folderType == null) {
// shouldn't happen; we checked in isAvailable
return;
}
if (folderType != ResourceFolderType.VALUES) {
forkResourceFile((XmlFile)file, null, true);
} else if (editor != null) {
forkResourceValue(project, editor, file, facet, null, true);
}
}
private static void forkResourceValue(@NotNull Project project,
@NotNull Editor editor,
@NotNull PsiFile file,
@NotNull AndroidFacet facet,
@Nullable PsiDirectory dir,
boolean open) {
XmlTag tag = getValueTag(editor, file);
if (tag == null) {
return; // shouldn't happen; we checked in isAvailable
}
forkResourceValue(project, tag, file, facet, dir, open);
}
@Nullable
private static PsiDirectory findRes(@NotNull PsiFile file) {
PsiDirectory resourceFolder = file.getParent();
return resourceFolder == null ? null : resourceFolder.getParent();
}
/**
* Create a variation (copy) of a given resource.
* @param project Current project
* @param tag Resource to be copied
* @param file File containing the resource
* @param facet Facet to contain the new resource
* @param dir Directory to contain the new resource, or null to ask the user
* @param open if true, open the file containing the new resource
*/
public static void forkResourceValue(@NotNull Project project,
@NotNull XmlTag tag,
@NotNull PsiFile file,
@NotNull AndroidFacet facet,
@Nullable PsiDirectory dir,
boolean open) {
PsiDirectory resFolder = findRes(file);
if (resFolder == null) {
return; // shouldn't happen; we checked in isAvailable
}
String name = tag.getAttributeValue(ATTR_NAME);
ResourceType type = AndroidResourceUtil.getResourceForResourceTag(tag);
if (name == null || type == null) {
return; // shouldn't happen; we checked in isAvailable
}
if (dir == null) {
dir = selectFolderDir(project, resFolder.getVirtualFile(), ResourceFolderType.VALUES);
}
if (dir != null) {
String value = PsiResourceItem.getTextContent(tag).trim();
createValueResource(file, facet, dir, name, value, type, tag.getText(), open);
}
}
@Nullable
private static XmlTag getEditorTag(Editor editor, PsiFile file) {
final int offset = editor.getCaretModel().getOffset();
final PsiElement psiElement = file.findElementAt(offset);
if (psiElement != null) {
return PsiTreeUtil.getParentOfType(psiElement, XmlTag.class, false);
}
return null;
}
@Nullable
private static XmlTag getValueTag(Editor editor, PsiFile file) {
return getValueTag(getEditorTag(editor, file));
}
@Nullable
public static XmlTag getValueTag(@Nullable XmlTag tag) {
XmlTag current = null;
if (tag != null) {
current = tag;
XmlTag parent = current.getParentTag();
while (parent != null) {
XmlTag parentParent = parent.getParentTag();
if (parentParent == null) {
break;
}
current = parent;
parent = parentParent;
}
}
return current;
}
private static void createValueResource(@NotNull PsiFile file,
@NotNull AndroidFacet facet,
@NotNull PsiDirectory dir,
@NotNull final String resName,
@NotNull final String value,
@NotNull final ResourceType type,
@NotNull final String oldTagText,
boolean open) {
final String filename = file.getName();
final List<String> dirNames = Collections.singletonList(dir.getName());
final Module module = facet.getModule();
final AtomicReference<PsiElement> openAfter = new AtomicReference<PsiElement>();
final WriteCommandAction<Void> action = new WriteCommandAction<Void>(facet.getModule().getProject(),
"Override Resource " + resName, file) {
@Override
protected void run(@NotNull Result<Void> result) {
List<ResourceElement> elements = Lists.newArrayListWithExpectedSize(1);
// AndroidResourceUtil.createValueResource will create a new resource value in the given resource
// folder (and record the corresponding tags added in the elements list passed into it).
// However, it only creates a new element and sets the name attribute on it; it does not
// transfer attributes, child content etc. Therefore, we use this utility method first to
// create the corresponding tag, and then *afterwards* we will replace the tag with a text copy
// from the resource tag we are overriding. We do this all under a single write lock such
// that it becomes a single atomic operation.
AndroidResourceUtil.createValueResource(module, resName, type, filename, dirNames, value, elements);
if (elements.size() == 1) {
final XmlTag tag = elements.get(0).getXmlTag();
if (tag != null && tag.isValid()) {
try {
XmlTag tagFromText = XmlElementFactory.getInstance(tag.getProject()).createTagFromText(oldTagText);
PsiElement replaced = tag.replace(tagFromText);
openAfter.set(replaced);
} catch (IncorrectOperationException e) {
// The user tried to override an invalid XML fragment: don't attempt to do a replacement in that case
openAfter.set(tag);
}
}
}
}
};
action.execute();
PsiElement tag = openAfter.get();
if (open && tag != null) {
NavigationUtil.openFileWithPsiElement(tag, true, true);
}
}
/**
* Create a variation (copy) of a given layout file
*
* @param context the render context for the layout file to fork
* @param newFolder the resource folder to create, or null to ask the user
* @param open if true, open the file after creating it
*/
public static void forkResourceFile(@NotNull RenderContext context, @Nullable String newFolder, boolean open) {
final VirtualFile file = context.getVirtualFile();
if (file == null) {
assert false;
return; // Should not happen
}
Module module = context.getModule();
if (module == null) {
assert false;
return; // Should not happen
}
XmlFile xmlFile = context.getXmlFile();
Configuration configuration = context.getConfiguration();
forkResourceFile(module, ResourceFolderType.LAYOUT, file, xmlFile, newFolder, configuration, open);
}
/**
* Create a variation (copy) of a given resource file (of a given type).
*
* @param xmlFile the XML resource file to fork
* @param myNewFolder the resource folder to create, or null to ask the user
* @param open if true, open the file after creating it
*/
public static void forkResourceFile(@NotNull final XmlFile xmlFile, @Nullable String myNewFolder, boolean open) {
VirtualFile file = xmlFile.getVirtualFile();
if (file == null) {
return;
}
Module module = AndroidPsiUtils.getModuleSafely(xmlFile);
if (module == null) {
return;
}
ResourceFolderType folderType = ResourceHelper.getFolderType(xmlFile);
if (folderType == null || folderType == ResourceFolderType.VALUES) {
return;
}
Configuration configuration = null;
if (folderType == ResourceFolderType.LAYOUT) {
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet != null) {
configuration = facet.getConfigurationManager().getConfiguration(file);
}
}
// Suppress: IntelliJ claims folderType can be null here, but it can't (and inserting assert folderType != null is correctly
// identified as redundant)
//noinspection ConstantConditions
forkResourceFile(module, folderType, file, xmlFile, myNewFolder, configuration, open);
}
private static void forkResourceFile(@NotNull Module module,
@NotNull final ResourceFolderType folderType,
@NotNull final VirtualFile file,
@Nullable final XmlFile xmlFile,
@Nullable String myNewFolder,
@Nullable Configuration configuration,
boolean open) {
final Project project = module.getProject();
final FolderConfiguration folderConfig;
if (myNewFolder == null) {
// Open a file chooser to select the configuration to be created
VirtualFile parentFolder = file.getParent();
assert parentFolder != null;
VirtualFile res = parentFolder.getParent();
folderConfig = selectFolderConfig(project, res, folderType);
}
else {
folderConfig = FolderConfiguration.getConfigForFolder(myNewFolder);
}
if (folderConfig == null) {
return;
}
final Computable<Pair<String, VirtualFile>> computable = new Computable<Pair<String, VirtualFile>>() {
@Override
public Pair<String, VirtualFile> compute() {
String folderName = folderConfig.getFolderName(folderType);
try {
VirtualFile parentFolder = file.getParent();
assert parentFolder != null;
VirtualFile res = parentFolder.getParent();
VirtualFile newParentFolder = res.findChild(folderName);
if (newParentFolder == null) {
newParentFolder = res.createChildDirectory(this, folderName);
if (newParentFolder == null) {
String message = String.format("Could not create folder %1$s in %2$s", folderName, res.getPath());
return Pair.of(message, null);
}
}
final VirtualFile existing = newParentFolder.findChild(file.getName());
if (existing != null && existing.exists()) {
String message = String.format("File 'res/%1$s/%2$s' already exists!", folderName, file.getName());
return Pair.of(message, null);
}
// Attempt to get the document from the PSI file rather than the file on disk: get edited contents too
String text;
if (xmlFile != null) {
text = xmlFile.getText();
}
else {
text = StreamUtil.readText(file.getInputStream(), "UTF-8");
}
VirtualFile newFile = newParentFolder.createChildData(this, file.getName());
VfsUtil.saveText(newFile, text);
return Pair.of(null, newFile);
}
catch (IOException e2) {
String message = String.format("Failed to create File 'res/%1$s/%2$s' : %3$s", folderName, file.getName(), e2.getMessage());
return Pair.of(message, null);
}
}
};
WriteCommandAction<Pair<String, VirtualFile>> action = new WriteCommandAction<Pair<String, VirtualFile>>(project, "Add Resource") {
@Override
protected void run(@NotNull Result<Pair<String, VirtualFile>> result) throws Throwable {
result.setResult(computable.compute());
}
};
Pair<String, VirtualFile> result = action.execute().getResultObject();
String error = result.getFirst();
VirtualFile newFile = result.getSecond();
if (error != null) {
Messages.showErrorDialog(project, error, "Create Resource");
}
else {
// First create a compatible configuration based on the current configuration
if (configuration != null) {
ConfigurationManager configurationManager = configuration.getConfigurationManager();
configurationManager.createSimilar(newFile, file);
}
if (open) {
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, newFile, -1);
FileEditorManager.getInstance(project).openEditor(descriptor, true);
}
}
}
/** Allow unit tests to pick a folder instead of going through the interactive dialogs */
@VisibleForTesting
@Nullable
public static String ourTargetFolderName;
@Nullable
public static PsiDirectory selectFolderDir(final Project project, VirtualFile res, ResourceFolderType folderType) {
final PsiDirectory directory = PsiManager.getInstance(project).findDirectory(res);
if (directory == null) {
return null;
}
if (ApplicationManager.getApplication().isUnitTestMode() && ourTargetFolderName != null) {
PsiDirectory subDirectory = directory.findSubdirectory(ourTargetFolderName);
if (subDirectory != null) {
return subDirectory;
}
return directory.createSubdirectory(ourTargetFolderName);
}
final CreateResourceDirectoryDialog dialog = new CreateResourceDirectoryDialog(project, folderType, directory, null) {
@Override
protected InputValidator createValidator() {
return new ResourceDirectorySelector(project, directory);
}
};
dialog.setTitle("Select Resource Directory");
dialog.show();
final InputValidator validator = dialog.getValidator();
if (validator != null) {
PsiElement[] createdElements = ((ResourceDirectorySelector)validator).getCreatedElements();
if (createdElements != null && createdElements.length > 0) {
return (PsiDirectory)createdElements[0];
}
}
return null;
}
@Nullable
public static FolderConfiguration selectFolderConfig(final Project project, VirtualFile res, ResourceFolderType folderType) {
PsiDirectory dir = selectFolderDir(project, res, folderType);
if (dir != null) {
return FolderConfiguration.getConfigForFolder(dir.getName());
}
return null;
}
/**
* Selects (and optionally creates) a resource directory
*/
private static class ResourceDirectorySelector extends ElementCreator implements InputValidator {
private final PsiDirectory myDirectory;
private PsiElement[] myCreatedElements = PsiElement.EMPTY_ARRAY;
public ResourceDirectorySelector(final Project project, final PsiDirectory directory) {
super(project, "Select Resource Directory");
myDirectory = directory;
}
@Override
public boolean checkInput(final String inputString) {
return ResourceFolderType.getFolderType(inputString) != null && FolderConfiguration.getConfigForFolder(inputString) != null;
}
@Override
public PsiElement[] create(String newName) throws Exception {
PsiDirectory subdirectory = myDirectory.findSubdirectory(newName);
if (subdirectory == null) {
subdirectory = myDirectory.createSubdirectory(newName);
}
return new PsiElement[] { subdirectory };
}
@Override
public String getActionName(String newName) {
return "Select Resource Directory";
}
@Override
public boolean canClose(final String inputString) {
// Already exists: ok
PsiDirectory subdirectory = myDirectory.findSubdirectory(inputString);
if (subdirectory != null) {
myCreatedElements = new PsiDirectory[]{subdirectory};
return true;
}
myCreatedElements = tryCreate(inputString);
return myCreatedElements.length > 0;
}
public final PsiElement[] getCreatedElements() {
return myCreatedElements;
}
}
/** Create a lint quickfix which overrides the resource at the given {@link com.intellij.psi.PsiElement} */
public static AndroidLintQuickFix createFix(@Nullable String folder) {
return new OverrideElementFix(folder);
}
private static class OverrideElementFix implements AndroidLintQuickFix {
private final String myFolder;
private OverrideElementFix(@Nullable String folder) {
myFolder = folder;
}
@Override
public void apply(@NotNull PsiElement startElement, @NotNull PsiElement endElement, @NotNull AndroidQuickfixContexts.Context context) {
PsiFile file = startElement.getContainingFile();
if (file instanceof XmlFile) {
ResourceFolderType folderType = ResourceHelper.getFolderType(file);
if (folderType != null) {
if (folderType != ResourceFolderType.VALUES) {
forkResourceFile((XmlFile)file, myFolder, true);
} else {
XmlTag tag = getValueTag(PsiTreeUtil.getParentOfType(startElement, XmlTag.class, false));
if (tag != null) {
AndroidFacet facet = AndroidFacet.getInstance(startElement);
if (facet != null) {
PsiDirectory dir = null;
if (myFolder != null) {
PsiDirectory resFolder = findRes(file);
if (resFolder != null) {
dir = resFolder.findSubdirectory(myFolder);
if (dir == null) {
dir = resFolder.createSubdirectory(myFolder);
}
}
}
forkResourceValue(startElement.getProject(), tag, file, facet, dir, true);
}
}
}
}
}
}
@Override
public boolean isApplicable(@NotNull PsiElement startElement,
@NotNull PsiElement endElement,
@NotNull AndroidQuickfixContexts.ContextType contextType) {
return true;
}
@NotNull
@Override
public String getName() {
return getActionName(myFolder);
}
}
}