| package org.jetbrains.android.refactoring; |
| |
| import com.android.SdkConstants; |
| import com.android.resources.ResourceFolderType; |
| import com.android.resources.ResourceType; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.Result; |
| import com.intellij.openapi.command.UndoConfirmationPolicy; |
| import com.intellij.openapi.command.WriteCommandAction; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleUtilCore; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.xml.XmlAttribute; |
| import com.intellij.psi.xml.XmlTag; |
| import com.intellij.util.ArrayUtilRt; |
| import com.intellij.util.Processor; |
| import com.intellij.util.containers.HashSet; |
| import org.jetbrains.android.dom.layout.LayoutViewElement; |
| import org.jetbrains.android.dom.resources.ResourceElement; |
| import org.jetbrains.android.dom.resources.ResourceValue; |
| import org.jetbrains.android.dom.resources.Style; |
| import org.jetbrains.android.dom.resources.StyleItem; |
| import org.jetbrains.android.util.AndroidBundle; |
| import org.jetbrains.android.util.AndroidResourceUtil; |
| import org.jetbrains.android.util.AndroidUtils; |
| 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.Arrays; |
| import java.util.List; |
| import java.util.Set; |
| |
| import static com.android.SdkConstants.*; |
| |
| /** |
| * @author Eugene.Kudelevsky |
| */ |
| public class AndroidExtractStyleAction extends AndroidBaseLayoutRefactoringAction { |
| @NonNls public static final String ACTION_ID = "AndroidExtractStyleAction"; |
| |
| private static String[] NON_EXTRACTABLE_ATTRIBUTES = new String[]{ATTR_ID, ATTR_TEXT, ATTR_HINT, ATTR_SRC, ATTR_ON_CLICK}; |
| |
| private final MyTestConfig myTestConfig; |
| |
| public AndroidExtractStyleAction() { |
| myTestConfig = null; |
| } |
| |
| @TestOnly |
| public AndroidExtractStyleAction(@Nullable MyTestConfig testConfig) { |
| myTestConfig = testConfig; |
| } |
| |
| @Override |
| protected boolean isEnabledForTags(@NotNull XmlTag[] tags) { |
| return tags.length == 1 && doIsEnabled(tags[0]); |
| } |
| |
| public static boolean doIsEnabled(@NotNull XmlTag tag) { |
| return getLayoutViewElement(tag) != null && getExtractableAttributes(tag).size() > 0; |
| } |
| |
| @Nullable |
| public static String doExtractStyle(@NotNull Module module, |
| @NotNull final XmlTag viewTag, |
| final boolean addStyleAttributeToTag, |
| @Nullable MyTestConfig testConfig) { |
| final PsiFile file = viewTag.getContainingFile(); |
| if (file == null) { |
| return null; |
| } |
| final String dialogTitle = AndroidBundle.message("android.extract.style.title"); |
| final String fileName = AndroidResourceUtil.getDefaultResourceFileName(ResourceType.STYLE); |
| assert fileName != null; |
| final List<String> dirNames = Arrays.asList(ResourceFolderType.VALUES.getName()); |
| final List<XmlAttribute> extractableAttributes = getExtractableAttributes(viewTag); |
| final Project project = module.getProject(); |
| |
| if (extractableAttributes.size() == 0) { |
| AndroidUtils.reportError(project, "The tag doesn't contain any attributes that can be extracted", dialogTitle); |
| return null; |
| } |
| |
| final LayoutViewElement viewElement = getLayoutViewElement(viewTag); |
| assert viewElement != null; |
| final ResourceValue parentStyleValue = viewElement.getStyle().getValue(); |
| final String parentStyle; |
| boolean supportImplicitParent = false; |
| |
| if (parentStyleValue != null) { |
| parentStyle = parentStyleValue.getResourceName(); |
| if (ResourceType.STYLE != parentStyleValue.getType() || parentStyle == null || parentStyle.length() == 0) { |
| AndroidUtils.reportError(project, "Invalid parent style reference " + parentStyleValue.toString(), dialogTitle); |
| return null; |
| } |
| supportImplicitParent = parentStyleValue.getPackage() == null; |
| } |
| else { |
| parentStyle = null; |
| } |
| |
| final String styleName; |
| final List<XmlAttribute> styledAttributes; |
| final Module chosenModule; |
| final boolean searchStyleApplications; |
| |
| if (testConfig == null) { |
| final ExtractStyleDialog dialog = |
| new ExtractStyleDialog(module, fileName, supportImplicitParent ? parentStyle : null, dirNames, extractableAttributes); |
| dialog.setTitle(dialogTitle); |
| if (!dialog.showAndGet()) { |
| return null; |
| } |
| searchStyleApplications = dialog.isToSearchStyleApplications(); |
| chosenModule = dialog.getChosenModule(); |
| assert chosenModule != null; |
| |
| styledAttributes = dialog.getStyledAttributes(); |
| styleName = dialog.getStyleName(); |
| } |
| else { |
| testConfig.validate(extractableAttributes); |
| |
| chosenModule = testConfig.getModule(); |
| styleName = testConfig.getStyleName(); |
| final Set<String> attrsToExtract = new HashSet<String>(Arrays.asList(testConfig.getAttributesToExtract())); |
| styledAttributes = new ArrayList<XmlAttribute>(); |
| |
| for (XmlAttribute attribute : extractableAttributes) { |
| if (attrsToExtract.contains(attribute.getName())) { |
| styledAttributes.add(attribute); |
| } |
| } |
| searchStyleApplications = false; |
| } |
| final boolean[] success = {false}; |
| final Ref<Style> createdStyleRef = Ref.create(); |
| final boolean finalSupportImplicitParent = supportImplicitParent; |
| |
| new WriteCommandAction(project, "Extract Android Style '" + styleName + "'", file) { |
| @Override |
| protected void run(final Result result) throws Throwable { |
| final List<XmlAttribute> attributesToDelete = new ArrayList<XmlAttribute>(); |
| |
| if (!AndroidResourceUtil |
| .createValueResource(chosenModule, styleName, null, ResourceType.STYLE, fileName, dirNames, new Processor<ResourceElement>() { |
| @Override |
| public boolean process(ResourceElement element) { |
| assert element instanceof Style; |
| final Style style = (Style)element; |
| createdStyleRef.set(style); |
| |
| for (XmlAttribute attribute : styledAttributes) { |
| if (SdkConstants.NS_RESOURCES.equals(attribute.getNamespace())) { |
| final StyleItem item = style.addItem(); |
| item.getName().setStringValue("android:" + attribute.getLocalName()); |
| item.setStringValue(attribute.getValue()); |
| attributesToDelete.add(attribute); |
| } |
| } |
| |
| if (parentStyleValue != null && (!finalSupportImplicitParent || !styleName.startsWith(parentStyle + "."))) { |
| final String aPackage = parentStyleValue.getPackage(); |
| style.getParentStyle().setStringValue((aPackage != null ? aPackage + ":" : "") + parentStyle); |
| } |
| return true; |
| } |
| })) { |
| return; |
| } |
| |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| for (XmlAttribute attribute : attributesToDelete) { |
| attribute.delete(); |
| } |
| if (addStyleAttributeToTag) { |
| final LayoutViewElement viewElement = getLayoutViewElement(viewTag); |
| assert viewElement != null; |
| viewElement.getStyle().setStringValue("@style/" + styleName); |
| } |
| } |
| }); |
| success[0] = true; |
| } |
| |
| @Override |
| protected UndoConfirmationPolicy getUndoConfirmationPolicy() { |
| return UndoConfirmationPolicy.REQUEST_CONFIRMATION; |
| } |
| }.execute(); |
| |
| if (!success[0]) { |
| return null; |
| } |
| |
| final Style createdStyle = createdStyleRef.get(); |
| final XmlTag createdStyleTag = createdStyle != null ? createdStyle.getXmlTag() : null; |
| |
| if (createdStyleTag != null) { |
| final AndroidFindStyleApplicationsAction.MyStyleData createdStyleData = |
| AndroidFindStyleApplicationsAction.getStyleData(createdStyleTag); |
| |
| if (createdStyleData != null && searchStyleApplications) { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| AndroidFindStyleApplicationsAction.doRefactoringForTag( |
| createdStyleTag, createdStyleData, file, null); |
| } |
| }); |
| } |
| } |
| return styleName; |
| } |
| |
| @NotNull |
| static List<XmlAttribute> getExtractableAttributes(@NotNull XmlTag viewTag) { |
| final List<XmlAttribute> extractableAttributes = new ArrayList<XmlAttribute>(); |
| |
| for (XmlAttribute attribute : viewTag.getAttributes()) { |
| if (canBeExtracted(attribute)) { |
| extractableAttributes.add(attribute); |
| } |
| } |
| return extractableAttributes; |
| } |
| |
| private static boolean canBeExtracted(@NotNull XmlAttribute attribute) { |
| if (!(SdkConstants.NS_RESOURCES.equals(attribute.getNamespace()))) { |
| return false; |
| } |
| final String name = attribute.getLocalName(); |
| if (ArrayUtilRt.find(NON_EXTRACTABLE_ATTRIBUTES, name) >= 0) { |
| return false; |
| } |
| if (name.startsWith(ATTR_STYLE)) { |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| protected void doRefactorForTags(@NotNull Project project, @NotNull XmlTag[] tags) { |
| assert tags.length == 1; |
| |
| final XmlTag tag = tags[0]; |
| final Module module = ModuleUtilCore.findModuleForPsiElement(tag); |
| assert module != null; |
| doExtractStyle(module, tag, true, myTestConfig); |
| } |
| |
| static class MyTestConfig { |
| private final Module myModule; |
| private final String myStyleName; |
| private final String[] myAttributesToExtract; |
| |
| MyTestConfig(@NotNull Module module, |
| @NotNull String styleName, |
| @NotNull String[] attributesToExtract) { |
| myModule = module; |
| myStyleName = styleName; |
| myAttributesToExtract = attributesToExtract; |
| } |
| |
| @NotNull |
| public Module getModule() { |
| return myModule; |
| } |
| |
| @NotNull |
| public String getStyleName() { |
| return myStyleName; |
| } |
| |
| @NotNull |
| public String[] getAttributesToExtract() { |
| return myAttributesToExtract; |
| } |
| |
| public void validate(@NotNull List<XmlAttribute> extractableAttributes) { |
| } |
| } |
| } |