diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java | 579 |
1 files changed, 579 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java new file mode 100644 index 000000000..9b1770d82 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java @@ -0,0 +1,579 @@ +/* + * 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; + } +} |