| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php |
| * |
| * 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.ide.eclipse.adt.internal.editors.layout.refactoring; |
| |
| import static com.android.SdkConstants.ANDROID_NS_NAME; |
| import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ATTR_HINT; |
| import static com.android.SdkConstants.ATTR_ID; |
| import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN; |
| import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; |
| import static com.android.SdkConstants.ATTR_NAME; |
| import static com.android.SdkConstants.ATTR_ON_CLICK; |
| import static com.android.SdkConstants.ATTR_PARENT; |
| import static com.android.SdkConstants.ATTR_SRC; |
| import static com.android.SdkConstants.ATTR_STYLE; |
| import static com.android.SdkConstants.ATTR_TEXT; |
| import static com.android.SdkConstants.EXT_XML; |
| import static com.android.SdkConstants.FD_RESOURCES; |
| import static com.android.SdkConstants.FD_RES_VALUES; |
| import static com.android.SdkConstants.PREFIX_ANDROID; |
| import static com.android.SdkConstants.PREFIX_RESOURCE_REF; |
| import static com.android.SdkConstants.REFERENCE_STYLE; |
| import static com.android.SdkConstants.TAG_ITEM; |
| import static com.android.SdkConstants.TAG_RESOURCES; |
| import static com.android.SdkConstants.XMLNS_PREFIX; |
| import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.ide.common.rendering.api.ResourceValue; |
| import com.android.ide.common.resources.ResourceResolver; |
| import com.android.ide.common.xml.XmlFormatStyle; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; |
| import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; |
| import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; |
| import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard; |
| import com.android.utils.Pair; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.jface.text.ITextSelection; |
| import org.eclipse.jface.viewers.ITreeSelection; |
| import org.eclipse.ltk.core.refactoring.Change; |
| import org.eclipse.ltk.core.refactoring.Refactoring; |
| import org.eclipse.ltk.core.refactoring.RefactoringStatus; |
| import org.eclipse.ltk.core.refactoring.TextFileChange; |
| import org.eclipse.text.edits.InsertEdit; |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.wst.sse.core.StructuredModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| |
| /** |
| * Extracts the selection and writes it out as a separate layout file, then adds an |
| * include to that new layout file. Interactively asks the user for a new name for the |
| * layout. |
| * <p> |
| * Remaining work to do / Possible enhancements: |
| * <ul> |
| * <li>Optionally look in other files in the project and attempt to set style attributes |
| * in other cases where the style attributes match? |
| * <li>If the elements we are extracting from already contain a style attribute, set that |
| * style as the parent style of the current style? |
| * <li>Add a parent-style picker to the wizard (initialized with the above if applicable) |
| * <li>Pick up indentation settings from the XML module |
| * <li>Integrate with themes somehow -- make an option to have the extracted style go into |
| * the theme instead |
| * </ul> |
| */ |
| @SuppressWarnings("restriction") // XML model |
| public class ExtractStyleRefactoring extends VisualRefactoring { |
| private static final String KEY_NAME = "name"; //$NON-NLS-1$ |
| private static final String KEY_REMOVE_EXTRACTED = "removeextracted"; //$NON-NLS-1$ |
| private static final String KEY_REMOVE_ALL = "removeall"; //$NON-NLS-1$ |
| private static final String KEY_APPLY_STYLE = "applystyle"; //$NON-NLS-1$ |
| private static final String KEY_PARENT = "parent"; //$NON-NLS-1$ |
| private String mStyleName; |
| /** The name of the file in res/values/ that the style will be added to. Normally |
| * res/values/styles.xml - but unit tests pick other names */ |
| private String mStyleFileName = "styles.xml"; |
| /** Set a style reference on the extracted elements? */ |
| private boolean mApplyStyle; |
| /** Remove the attributes that were extracted? */ |
| private boolean mRemoveExtracted; |
| /** List of attributes chosen by the user to be extracted */ |
| private List<Attr> mChosenAttributes = new ArrayList<Attr>(); |
| /** Remove all attributes that match the extracted attributes names, regardless of value */ |
| private boolean mRemoveAll; |
| /** The parent style to extend */ |
| private String mParent; |
| /** The full list of available attributes in the refactoring */ |
| private Map<String, List<Attr>> mAvailableAttributes; |
| |
| /** |
| * This constructor is solely used by {@link Descriptor}, |
| * to replay a previous refactoring. |
| * @param arguments argument map created by #createArgumentMap. |
| */ |
| ExtractStyleRefactoring(Map<String, String> arguments) { |
| super(arguments); |
| mStyleName = arguments.get(KEY_NAME); |
| mRemoveExtracted = Boolean.parseBoolean(arguments.get(KEY_REMOVE_EXTRACTED)); |
| mRemoveAll = Boolean.parseBoolean(arguments.get(KEY_REMOVE_ALL)); |
| mApplyStyle = Boolean.parseBoolean(arguments.get(KEY_APPLY_STYLE)); |
| mParent = arguments.get(KEY_PARENT); |
| if (mParent != null && mParent.length() == 0) { |
| mParent = null; |
| } |
| } |
| |
| public ExtractStyleRefactoring( |
| IFile file, |
| LayoutEditorDelegate delegate, |
| ITextSelection selection, |
| ITreeSelection treeSelection) { |
| super(file, delegate, selection, treeSelection); |
| } |
| |
| @VisibleForTesting |
| ExtractStyleRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) { |
| super(selectedElements, editor); |
| } |
| |
| @Override |
| public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, |
| OperationCanceledException { |
| RefactoringStatus status = new RefactoringStatus(); |
| |
| try { |
| pm.beginTask("Checking preconditions...", 6); |
| |
| if (mSelectionStart == -1 || mSelectionEnd == -1) { |
| status.addFatalError("No selection to extract"); |
| return status; |
| } |
| |
| // This also ensures that we have a valid DOM model: |
| if (mElements.size() == 0) { |
| status.addFatalError("Nothing to extract"); |
| return status; |
| } |
| |
| pm.worked(1); |
| return status; |
| |
| } finally { |
| pm.done(); |
| } |
| } |
| |
| @Override |
| protected VisualRefactoringDescriptor createDescriptor() { |
| String comment = getName(); |
| return new Descriptor( |
| mProject.getName(), //project |
| comment, //description |
| comment, //comment |
| createArgumentMap()); |
| } |
| |
| @Override |
| protected Map<String, String> createArgumentMap() { |
| Map<String, String> args = super.createArgumentMap(); |
| args.put(KEY_NAME, mStyleName); |
| args.put(KEY_REMOVE_EXTRACTED, Boolean.toString(mRemoveExtracted)); |
| args.put(KEY_REMOVE_ALL, Boolean.toString(mRemoveAll)); |
| args.put(KEY_APPLY_STYLE, Boolean.toString(mApplyStyle)); |
| args.put(KEY_PARENT, mParent != null ? mParent : ""); |
| |
| return args; |
| } |
| |
| @Override |
| public String getName() { |
| return "Extract Style"; |
| } |
| |
| void setStyleName(String styleName) { |
| mStyleName = styleName; |
| } |
| |
| void setStyleFileName(String styleFileName) { |
| mStyleFileName = styleFileName; |
| } |
| |
| void setChosenAttributes(List<Attr> attributes) { |
| mChosenAttributes = attributes; |
| } |
| |
| void setRemoveExtracted(boolean removeExtracted) { |
| mRemoveExtracted = removeExtracted; |
| } |
| |
| void setApplyStyle(boolean applyStyle) { |
| mApplyStyle = applyStyle; |
| } |
| |
| void setRemoveAll(boolean removeAll) { |
| mRemoveAll = removeAll; |
| } |
| |
| void setParent(String parent) { |
| mParent = parent; |
| } |
| |
| // ---- Actual implementation of Extract Style modification computation ---- |
| |
| /** |
| * Returns two items: a map from attribute name to a list of attribute nodes of that |
| * name, and a subset of these attributes that fall within the text selection |
| * (used to drive initial selection in the wizard) |
| */ |
| Pair<Map<String, List<Attr>>, Set<Attr>> getAvailableAttributes() { |
| mAvailableAttributes = new TreeMap<String, List<Attr>>(); |
| Set<Attr> withinSelection = new HashSet<Attr>(); |
| for (Element element : getElements()) { |
| IndexedRegion elementRegion = getRegion(element); |
| boolean allIncluded = |
| (mOriginalSelectionStart <= elementRegion.getStartOffset() && |
| mOriginalSelectionEnd >= elementRegion.getEndOffset()); |
| |
| NamedNodeMap attributeMap = element.getAttributes(); |
| for (int i = 0, n = attributeMap.getLength(); i < n; i++) { |
| Attr attribute = (Attr) attributeMap.item(i); |
| |
| String name = attribute.getLocalName(); |
| if (!isStylableAttribute(name)) { |
| // Don't offer to extract attributes that don't make sense in |
| // styles (like "id" or "style"), or attributes that the user |
| // probably does not want to define in styles (like layout |
| // attributes such as layout_width, or the label of a button etc). |
| // This makes the options offered listed in the wizard simpler. |
| // In special cases where the user *does* want to set one of these |
| // attributes, they can always do it manually so optimize for |
| // the common case here. |
| continue; |
| } |
| |
| // Skip attributes that are in a namespace other than the Android one |
| String namespace = attribute.getNamespaceURI(); |
| if (namespace != null && !ANDROID_URI.equals(namespace)) { |
| continue; |
| } |
| |
| if (!allIncluded) { |
| IndexedRegion region = getRegion(attribute); |
| boolean attributeIncluded = mOriginalSelectionStart < region.getEndOffset() && |
| mOriginalSelectionEnd >= region.getStartOffset(); |
| if (attributeIncluded) { |
| withinSelection.add(attribute); |
| } |
| } else { |
| withinSelection.add(attribute); |
| } |
| |
| List<Attr> list = mAvailableAttributes.get(name); |
| if (list == null) { |
| list = new ArrayList<Attr>(); |
| mAvailableAttributes.put(name, list); |
| } |
| list.add(attribute); |
| } |
| } |
| |
| return Pair.of(mAvailableAttributes, withinSelection); |
| } |
| |
| /** |
| * Returns whether the given local attribute name is one the style wizard |
| * should present as a selectable attribute to be extracted. |
| * |
| * @param name the attribute name, not including a namespace prefix |
| * @return true if the name is one that the user can extract |
| */ |
| public static boolean isStylableAttribute(String name) { |
| return !(name == null |
| || name.equals(ATTR_ID) |
| || name.startsWith(ATTR_STYLE) |
| || (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) && |
| !name.startsWith(ATTR_LAYOUT_MARGIN)) |
| || name.equals(ATTR_TEXT) |
| || name.equals(ATTR_HINT) |
| || name.equals(ATTR_SRC) |
| || name.equals(ATTR_ON_CLICK)); |
| } |
| |
| IFile getStyleFile(IProject project) { |
| return project.getFile(new Path(FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP |
| + mStyleFileName)); |
| } |
| |
| @Override |
| protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) { |
| List<Change> changes = new ArrayList<Change>(); |
| if (mChosenAttributes.size() == 0) { |
| return changes; |
| } |
| |
| IFile file = getStyleFile(mDelegate.getEditor().getProject()); |
| boolean createFile = !file.exists(); |
| int insertAtIndex; |
| String initialIndent = null; |
| if (!createFile) { |
| Pair<Integer, String> context = computeInsertContext(file); |
| insertAtIndex = context.getFirst(); |
| initialIndent = context.getSecond(); |
| } else { |
| insertAtIndex = 0; |
| } |
| |
| TextFileChange addFile = new TextFileChange("Create new separate style declaration", file); |
| addFile.setTextType(EXT_XML); |
| changes.add(addFile); |
| String styleString = computeStyleDeclaration(createFile, initialIndent); |
| addFile.setEdit(new InsertEdit(insertAtIndex, styleString)); |
| |
| // Remove extracted attributes? |
| MultiTextEdit rootEdit = new MultiTextEdit(); |
| if (mRemoveExtracted || mRemoveAll) { |
| for (Attr attribute : mChosenAttributes) { |
| List<Attr> list = mAvailableAttributes.get(attribute.getLocalName()); |
| for (Attr attr : list) { |
| if (mRemoveAll || attr.getValue().equals(attribute.getValue())) { |
| removeAttribute(rootEdit, attr); |
| } |
| } |
| } |
| } |
| |
| // Set the style attribute? |
| if (mApplyStyle) { |
| for (Element element : getElements()) { |
| String value = PREFIX_RESOURCE_REF + REFERENCE_STYLE + mStyleName; |
| setAttribute(rootEdit, element, null, null, ATTR_STYLE, value); |
| } |
| } |
| |
| if (rootEdit.hasChildren()) { |
| IFile sourceFile = mDelegate.getEditor().getInputFile(); |
| if (sourceFile == null) { |
| return changes; |
| } |
| TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile); |
| change.setTextType(EXT_XML); |
| changes.add(change); |
| |
| if (AdtPrefs.getPrefs().getFormatGuiXml()) { |
| MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT); |
| if (formatted != null) { |
| rootEdit = formatted; |
| } |
| } |
| |
| change.setEdit(rootEdit); |
| } |
| |
| return changes; |
| } |
| |
| private String computeStyleDeclaration(boolean createFile, String initialIndent) { |
| StringBuilder sb = new StringBuilder(); |
| if (createFile) { |
| sb.append(NewXmlFileWizard.XML_HEADER_LINE); |
| sb.append('<').append(TAG_RESOURCES).append(' '); |
| sb.append(XMLNS_PREFIX).append(ANDROID_NS_NAME).append('=').append('"'); |
| sb.append(ANDROID_URI); |
| sb.append('"').append('>').append('\n'); |
| } |
| |
| // Indent. Use the existing indent found for previous <style> elements in |
| // the resource file - but if that indent was 0 (e.g. <style> elements are |
| // at the left margin) only use it to indent the style elements and use a real |
| // nonzero indent for its children. |
| String indent = " "; //$NON-NLS-1$ |
| if (initialIndent == null) { |
| initialIndent = indent; |
| } else if (initialIndent.length() > 0) { |
| indent = initialIndent; |
| } |
| sb.append(initialIndent); |
| String styleTag = "style"; //$NON-NLS-1$ // TODO - use constant in parallel changeset |
| sb.append('<').append(styleTag).append(' ').append(ATTR_NAME).append('=').append('"'); |
| sb.append(mStyleName); |
| sb.append('"'); |
| if (mParent != null) { |
| sb.append(' ').append(ATTR_PARENT).append('=').append('"'); |
| sb.append(mParent); |
| sb.append('"'); |
| } |
| sb.append('>').append('\n'); |
| |
| for (Attr attribute : mChosenAttributes) { |
| sb.append(initialIndent).append(indent); |
| sb.append('<').append(TAG_ITEM).append(' ').append(ATTR_NAME).append('=').append('"'); |
| // We've already enforced that regardless of prefix, only attributes with |
| // an Android namespace can be in the set of chosen attributes. Rewrite the |
| // prefix to android here. |
| if (attribute.getPrefix() != null) { |
| sb.append(ANDROID_NS_NAME_PREFIX); |
| } |
| sb.append(attribute.getLocalName()); |
| sb.append('"').append('>'); |
| sb.append(attribute.getValue()); |
| sb.append('<').append('/').append(TAG_ITEM).append('>').append('\n'); |
| } |
| sb.append(initialIndent).append('<').append('/').append(styleTag).append('>').append('\n'); |
| |
| if (createFile) { |
| sb.append('<').append('/').append(TAG_RESOURCES).append('>').append('\n'); |
| } |
| String styleString = sb.toString(); |
| return styleString; |
| } |
| |
| /** Computes the location in the file to insert the new style element at, as well as |
| * the exact indent string to use to indent the {@code <style>} element. |
| * @param file the styles.xml file to insert into |
| * @return a pair of an insert offset and an indent string |
| */ |
| private Pair<Integer, String> computeInsertContext(final IFile file) { |
| int insertAtIndex = -1; |
| // Find the insert of the final </resources> item where we will insert |
| // the new style elements. |
| String indent = null; |
| IModelManager modelManager = StructuredModelManager.getModelManager(); |
| IStructuredModel model = null; |
| try { |
| model = modelManager.getModelForRead(file); |
| if (model instanceof IDOMModel) { |
| IDOMModel domModel = (IDOMModel) model; |
| IDOMDocument otherDocument = domModel.getDocument(); |
| Element root = otherDocument.getDocumentElement(); |
| Node lastChild = root.getLastChild(); |
| if (lastChild != null) { |
| if (lastChild instanceof IndexedRegion) { |
| IndexedRegion region = (IndexedRegion) lastChild; |
| insertAtIndex = region.getStartOffset() + region.getLength(); |
| } |
| |
| // Compute indent |
| while (lastChild != null) { |
| if (lastChild.getNodeType() == Node.ELEMENT_NODE) { |
| IStructuredDocument document = model.getStructuredDocument(); |
| indent = AndroidXmlEditor.getIndent(document, lastChild); |
| break; |
| } |
| lastChild = lastChild.getPreviousSibling(); |
| } |
| } |
| } |
| } catch (IOException e) { |
| AdtPlugin.log(e, null); |
| } catch (CoreException e) { |
| AdtPlugin.log(e, null); |
| } finally { |
| if (model != null) { |
| model.releaseFromRead(); |
| } |
| } |
| |
| if (insertAtIndex == -1) { |
| String contents = AdtPlugin.readFile(file); |
| insertAtIndex = contents.indexOf("</" + TAG_RESOURCES + ">"); //$NON-NLS-1$ |
| if (insertAtIndex == -1) { |
| insertAtIndex = contents.length(); |
| } |
| } |
| |
| return Pair.of(insertAtIndex, indent); |
| } |
| |
| @Override |
| VisualRefactoringWizard createWizard() { |
| return new ExtractStyleWizard(this, mDelegate); |
| } |
| |
| public static class Descriptor extends VisualRefactoringDescriptor { |
| public Descriptor(String project, String description, String comment, |
| Map<String, String> arguments) { |
| super("com.android.ide.eclipse.adt.refactoring.extract.style", //$NON-NLS-1$ |
| project, description, comment, arguments); |
| } |
| |
| @Override |
| protected Refactoring createRefactoring(Map<String, String> args) { |
| return new ExtractStyleRefactoring(args); |
| } |
| } |
| |
| /** |
| * Determines the parent style to be used for this refactoring |
| * |
| * @return the parent style to be used for this refactoring |
| */ |
| public String getParentStyle() { |
| Set<String> styles = new HashSet<String>(); |
| for (Element element : getElements()) { |
| // Includes "" for elements not setting the style |
| styles.add(element.getAttribute(ATTR_STYLE)); |
| } |
| |
| if (styles.size() > 1) { |
| // The elements differ in what style attributes they are set to |
| return null; |
| } |
| |
| String style = styles.iterator().next(); |
| if (style != null && style.length() > 0) { |
| return style; |
| } |
| |
| // None of the elements set the style -- see if they have the same widget types |
| // and if so offer to extend the theme style for that widget type |
| |
| Set<String> types = new HashSet<String>(); |
| for (Element element : getElements()) { |
| types.add(element.getTagName()); |
| } |
| |
| if (types.size() == 1) { |
| String view = DescriptorsUtils.getBasename(types.iterator().next()); |
| |
| ResourceResolver resolver = mDelegate.getGraphicalEditor().getResourceResolver(); |
| // Look up the theme item name, which for a Button would be "buttonStyle", and so on. |
| String n = Character.toLowerCase(view.charAt(0)) + view.substring(1) |
| + "Style"; //$NON-NLS-1$ |
| ResourceValue value = resolver.findItemInTheme(n); |
| if (value != null) { |
| ResourceValue resolvedValue = resolver.resolveResValue(value); |
| String name = resolvedValue.getName(); |
| if (name != null) { |
| if (resolvedValue.isFramework()) { |
| return PREFIX_ANDROID + name; |
| } else { |
| return name; |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| } |