| package org.jetbrains.android.refactoring; |
| |
| |
| import com.android.SdkConstants; |
| import com.android.ide.common.resources.configuration.FolderConfiguration; |
| import com.android.resources.ResourceFolderType; |
| import com.android.resources.ResourceType; |
| import com.android.tools.idea.rendering.IncludeReference; |
| import com.android.tools.idea.rendering.ResourceHelper; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.command.UndoConfirmationPolicy; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.*; |
| import com.intellij.psi.codeStyle.CodeStyleManager; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.psi.xml.XmlAttribute; |
| import com.intellij.psi.xml.XmlFile; |
| import com.intellij.psi.xml.XmlTag; |
| import com.intellij.util.containers.HashSet; |
| import com.intellij.util.xml.DomElement; |
| import com.intellij.util.xml.DomManager; |
| import org.jetbrains.android.actions.CreateResourceFileAction; |
| import org.jetbrains.android.dom.layout.Include; |
| import org.jetbrains.android.dom.layout.LayoutViewElement; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.util.AndroidCommonUtils; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.annotations.TestOnly; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * @author Eugene.Kudelevsky |
| */ |
| public class AndroidExtractAsIncludeAction extends AndroidBaseLayoutRefactoringAction { |
| @NonNls public static final String ACTION_ID = "AndroidExtractAsIncludeAction"; |
| |
| private final MyTestConfig myTestConfig; |
| |
| @SuppressWarnings("UnusedDeclaration") |
| public AndroidExtractAsIncludeAction() { |
| myTestConfig = null; |
| } |
| |
| @TestOnly |
| public AndroidExtractAsIncludeAction(@Nullable MyTestConfig testConfig) { |
| myTestConfig = testConfig; |
| |
| } |
| |
| @Override |
| protected void doRefactorForTags(@NotNull Project project, @NotNull XmlTag[] tags) { |
| if (tags.length == 0) { |
| return; |
| } |
| final PsiFile file = tags[0].getContainingFile(); |
| if (file == null) { |
| return; |
| } |
| XmlTag startTag = null; |
| XmlTag endTag = null; |
| int startOffset = Integer.MAX_VALUE; |
| int endOffset = -1; |
| |
| for (XmlTag tag : tags) { |
| final TextRange range = tag.getTextRange(); |
| |
| final int start = range.getStartOffset(); |
| if (start < startOffset) { |
| startOffset = start; |
| startTag = tag; |
| } |
| |
| final int end = range.getEndOffset(); |
| if (end > endOffset) { |
| endOffset = end; |
| endTag = tag; |
| } |
| } |
| assert startTag != null && endTag != null; |
| doRefactorForPsiRange(project, file, startTag, endTag); |
| } |
| |
| @Override |
| protected boolean isEnabledForTags(@NotNull XmlTag[] tags) { |
| if (tags.length == 0) { |
| return false; |
| } |
| final DomManager domManager = DomManager.getDomManager(tags[0].getProject()); |
| boolean containsViewElement = false; |
| |
| for (XmlTag tag : tags) { |
| final DomElement domElement = domManager.getDomElement(tag); |
| |
| if (!isSuitableDomElement(domElement)) { |
| return false; |
| } |
| if (domElement instanceof LayoutViewElement) { |
| containsViewElement = true; |
| } |
| } |
| if (!containsViewElement) { |
| return false; |
| } |
| final PsiElement parent = tags[0].getParent(); |
| |
| if (!(parent instanceof XmlTag) || parent.getContainingFile() == null) { |
| return false; |
| } |
| |
| for (int i = 1; i < tags.length; i++) { |
| if (tags[i].getParent() != parent) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| protected void doRefactorForPsiRange(@NotNull final Project project, @NotNull final PsiFile file, @NotNull final PsiElement from, |
| @NotNull final PsiElement to) { |
| final PsiDirectory dir = file.getContainingDirectory(); |
| if (dir == null) { |
| return; |
| } |
| final AndroidFacet facet = AndroidFacet.getInstance(from); |
| assert facet != null; |
| |
| final XmlTag parentTag = PsiTreeUtil.getParentOfType(from, XmlTag.class); |
| assert parentTag != null; |
| |
| final List<XmlTag> tagsInRange = collectAllTags(from, to); |
| assert tagsInRange.size() > 0 : "there is no tag inside the range"; |
| final String fileName = myTestConfig != null ? myTestConfig.myLayoutFileName : null; |
| final String dirName = dir.getName(); |
| final FolderConfiguration config = dirName.length() > 0 |
| ? FolderConfiguration.getConfig(dirName.split(SdkConstants.RES_QUALIFIER_SEP)) |
| : null; |
| final String title = "Extract Android Layout"; |
| |
| CommandProcessor.getInstance().executeCommand(project, new Runnable() { |
| @Override |
| public void run() { |
| final XmlFile newFile = |
| CreateResourceFileAction.createFileResource(facet, ResourceFolderType.LAYOUT, fileName, "temp_root", config, true, title); |
| |
| if (newFile != null) { |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| doRefactor(facet, file, newFile, from, to, parentTag, tagsInRange.size() > 1); |
| } |
| }); |
| } |
| } |
| }, title, null, UndoConfirmationPolicy.REQUEST_CONFIRMATION); |
| } |
| |
| private static void doRefactor(AndroidFacet facet, |
| PsiFile file, |
| XmlFile newFile, |
| PsiElement from, |
| PsiElement to, |
| XmlTag parentTag, |
| boolean wrapWithMerge) { |
| final Project project = facet.getModule().getProject(); |
| final String textToExtract = file.getText().substring(from.getTextRange().getStartOffset(), |
| to.getTextRange().getEndOffset()); |
| final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); |
| final Document document = documentManager.getDocument(newFile); |
| assert document != null; |
| document.setText("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + |
| (wrapWithMerge ? "<merge>\n" + textToExtract + "\n</merge>" : textToExtract)); |
| documentManager.commitDocument(document); |
| |
| final Set<String> unknownPrefixes = new HashSet<String>(); |
| |
| newFile.accept(new XmlRecursiveElementVisitor() { |
| @Override |
| public void visitXmlTag(XmlTag tag) { |
| super.visitXmlTag(tag); |
| final String prefix = tag.getNamespacePrefix(); |
| |
| if (!unknownPrefixes.contains(prefix) && tag.getNamespace().length() == 0) { |
| unknownPrefixes.add(prefix); |
| } |
| } |
| |
| @Override |
| public void visitXmlAttribute(XmlAttribute attribute) { |
| final String prefix = attribute.getNamespacePrefix(); |
| |
| if (!unknownPrefixes.contains(prefix) && attribute.getNamespace().length() == 0) { |
| unknownPrefixes.add(prefix); |
| } |
| } |
| }); |
| |
| final XmlTag rootTag = newFile.getRootTag(); |
| assert rootTag != null; |
| final XmlElementFactory elementFactory = XmlElementFactory.getInstance(project); |
| final XmlAttribute[] attributes = rootTag.getAttributes(); |
| final XmlAttribute firstAttribute = attributes.length > 0 ? attributes[0] : null; |
| |
| for (String prefix : unknownPrefixes) { |
| final String namespace = parentTag.getNamespaceByPrefix(prefix); |
| final String xmlNsAttrName = "xmlns:" + prefix; |
| |
| if (namespace.length() > 0 && rootTag.getAttribute(xmlNsAttrName) == null) { |
| final XmlAttribute xmlnsAttr = elementFactory.createXmlAttribute(xmlNsAttrName, namespace); |
| |
| if (firstAttribute != null) { |
| rootTag.addBefore(xmlnsAttr, firstAttribute); |
| } |
| else { |
| rootTag.add(xmlnsAttr); |
| } |
| } |
| } |
| |
| String includingLayout = SdkConstants.LAYOUT_RESOURCE_PREFIX + ResourceHelper.getResourceName(file); |
| IncludeReference.setIncludingLayout(project, newFile, includingLayout); |
| |
| final String resourceName = AndroidCommonUtils.getResourceName(ResourceType.LAYOUT.getName(), newFile.getName()); |
| final XmlTag includeTag = elementFactory.createTagFromText("<include layout=\"@layout/" + resourceName + "\"/>"); |
| parentTag.addAfter(includeTag, to); |
| parentTag.deleteChildRange(from, to); |
| |
| final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project); |
| codeStyleManager.reformat(newFile); |
| } |
| |
| @NotNull |
| private static List<XmlTag> collectAllTags(PsiElement from, PsiElement to) { |
| final List<XmlTag> result = new ArrayList<XmlTag>(); |
| PsiElement e = from; |
| |
| while (e != null) { |
| if (e instanceof XmlTag) { |
| result.add((XmlTag)e); |
| } |
| if (e == to) { |
| break; |
| } |
| e = e.getNextSibling(); |
| } |
| assert e != null : "invalid range"; |
| return result; |
| } |
| |
| @Override |
| protected boolean isEnabledForPsiRange(@NotNull PsiElement from, @Nullable PsiElement to) { |
| final DomManager domManager = DomManager.getDomManager(from.getProject()); |
| PsiElement e = from; |
| boolean containsViewElement = false; |
| |
| while (e != null) { |
| if (e instanceof XmlTag) { |
| final DomElement domElement = domManager.getDomElement((XmlTag)e); |
| |
| if (!isSuitableDomElement(domElement)) { |
| return false; |
| } |
| if (domElement instanceof LayoutViewElement) { |
| containsViewElement = true; |
| } |
| } |
| if (e == to) { |
| break; |
| } |
| e = e.getNextSibling(); |
| } |
| return containsViewElement; |
| } |
| |
| private static boolean isSuitableDomElement(DomElement element) { |
| return element instanceof LayoutViewElement || |
| element instanceof Include; |
| } |
| |
| static class MyTestConfig { |
| private final String myLayoutFileName; |
| |
| MyTestConfig(@NotNull String layoutFileName) { |
| myLayoutFileName = layoutFileName; |
| } |
| } |
| } |