diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java | 439 |
1 files changed, 439 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java new file mode 100644 index 000000000..07b00b8da --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java @@ -0,0 +1,439 @@ +/* + * 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_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.VALUE_FILL_PARENT; +import static com.android.SdkConstants.VALUE_MATCH_PARENT; +import static com.android.SdkConstants.VALUE_WRAP_CONTENT; + +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +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.DeleteEdit; +import org.eclipse.text.edits.InsertEdit; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.TextEdit; +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.w3c.dom.Attr; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Inserts a new layout surrounding the current selection, migrates namespace + * attributes (if wrapping the root node), and optionally migrates layout + * attributes and updates references elsewhere. + */ +@SuppressWarnings("restriction") // XML model +public class WrapInRefactoring extends VisualRefactoring { + private static final String KEY_ID = "name"; //$NON-NLS-1$ + private static final String KEY_TYPE = "type"; //$NON-NLS-1$ + + private String mId; + private String mTypeFqcn; + private String mInitializedAttributes; + + /** + * This constructor is solely used by {@link Descriptor}, + * to replay a previous refactoring. + * @param arguments argument map created by #createArgumentMap. + */ + WrapInRefactoring(Map<String, String> arguments) { + super(arguments); + mId = arguments.get(KEY_ID); + mTypeFqcn = arguments.get(KEY_TYPE); + } + + public WrapInRefactoring( + IFile file, + LayoutEditorDelegate delegate, + ITextSelection selection, + ITreeSelection treeSelection) { + super(file, delegate, selection, treeSelection); + } + + @VisibleForTesting + WrapInRefactoring(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 wrap"); + return status; + } + + // Make sure the selection is contiguous + if (mTreeSelection != null) { + // TODO - don't do this if we based the selection on text. In this case, + // make sure we're -balanced-. + + List<CanvasViewInfo> infos = getSelectedViewInfos(); + if (!validateNotEmpty(infos, status)) { + return status; + } + + // Enforce that the selection is -contiguous- + if (!validateContiguous(infos, status)) { + return status; + } + } + + // Ensures that we have a valid DOM model: + if (mElements.size() == 0) { + status.addFatalError("Nothing to wrap"); + 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_TYPE, mTypeFqcn); + args.put(KEY_ID, mId); + + return args; + } + + @Override + public String getName() { + return "Wrap in Container"; + } + + void setId(String id) { + mId = id; + } + + void setType(String typeFqcn) { + mTypeFqcn = typeFqcn; + } + + void setInitializedAttributes(String initializedAttributes) { + mInitializedAttributes = initializedAttributes; + } + + @Override + protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) { + // (1) Insert the new container in front of the beginning of the + // first wrapped view + // (2) If the container is the new root, transfer namespace declarations + // to it + // (3) Insert the closing tag of the new container at the end of the + // last wrapped view + // (4) Reindent the wrapped views + // (5) If the user requested it, update all layout references to the + // wrapped views with the new container? + // For that matter, does RelativeLayout even require it? Probably not, + // it can point inside the current layout... + + // Add indent to all lines between mSelectionStart and mEnd + // TODO: Figure out the indentation amount? + // For now, use 4 spaces + String indentUnit = " "; //$NON-NLS-1$ + boolean separateAttributes = true; + IStructuredDocument document = mDelegate.getEditor().getStructuredDocument(); + String startIndent = AndroidXmlEditor.getIndentAtOffset(document, mSelectionStart); + + String viewClass = getViewClass(mTypeFqcn); + String androidNsPrefix = getAndroidNamespacePrefix(); + + + IFile file = mDelegate.getEditor().getInputFile(); + List<Change> changes = new ArrayList<Change>(); + if (file == null) { + return changes; + } + TextFileChange change = new TextFileChange(file.getName(), file); + MultiTextEdit rootEdit = new MultiTextEdit(); + change.setTextType(EXT_XML); + + String id = ensureNewId(mId); + + // Update any layout references to the old id with the new id + if (id != null) { + String rootId = getRootId(); + IStructuredModel model = mDelegate.getEditor().getModelForRead(); + try { + IStructuredDocument doc = model.getStructuredDocument(); + if (doc != null) { + List<TextEdit> replaceIds = replaceIds(androidNsPrefix, + doc, mSelectionStart, mSelectionEnd, rootId, id); + for (TextEdit edit : replaceIds) { + rootEdit.addChild(edit); + } + } + } finally { + model.releaseFromRead(); + } + } + + // Insert namespace elements? + StringBuilder namespace = null; + List<DeleteEdit> deletions = new ArrayList<DeleteEdit>(); + Element primary = getPrimaryElement(); + if (primary != null && getDomDocument().getDocumentElement() == primary) { + namespace = new StringBuilder(); + + List<Attr> declarations = findNamespaceAttributes(primary); + for (Attr attribute : declarations) { + if (attribute instanceof IndexedRegion) { + // Delete the namespace declaration in the node which is no longer the root + IndexedRegion region = (IndexedRegion) attribute; + int startOffset = region.getStartOffset(); + int endOffset = region.getEndOffset(); + String text = getText(startOffset, endOffset); + DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset); + deletions.add(deletion); + rootEdit.addChild(deletion); + text = text.trim(); + + // Insert the namespace declaration in the new root + if (separateAttributes) { + namespace.append('\n').append(startIndent).append(indentUnit); + } else { + namespace.append(' '); + } + namespace.append(text); + } + } + } + + // Insert begin tag: <type ...> + StringBuilder sb = new StringBuilder(); + sb.append('<'); + sb.append(viewClass); + + if (namespace != null) { + sb.append(namespace); + } + + // Set the ID if any + if (id != null) { + if (separateAttributes) { + sb.append('\n').append(startIndent).append(indentUnit); + } else { + sb.append(' '); + } + sb.append(androidNsPrefix).append(':'); + sb.append(ATTR_ID).append('=').append('"').append(id).append('"'); + } + + // If any of the elements are fill/match parent, use that instead + String width = VALUE_WRAP_CONTENT; + String height = VALUE_WRAP_CONTENT; + + for (Element element : getElements()) { + String oldWidth = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); + String oldHeight = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); + + if (VALUE_MATCH_PARENT.equals(oldWidth) || VALUE_FILL_PARENT.equals(oldWidth)) { + width = oldWidth; + } + if (VALUE_MATCH_PARENT.equals(oldHeight) || VALUE_FILL_PARENT.equals(oldHeight)) { + height = oldHeight; + } + } + + // Add in width/height. + if (separateAttributes) { + sb.append('\n').append(startIndent).append(indentUnit); + } else { + sb.append(' '); + } + sb.append(androidNsPrefix).append(':'); + sb.append(ATTR_LAYOUT_WIDTH).append('=').append('"').append(width).append('"'); + + if (separateAttributes) { + sb.append('\n').append(startIndent).append(indentUnit); + } else { + sb.append(' '); + } + sb.append(androidNsPrefix).append(':'); + sb.append(ATTR_LAYOUT_HEIGHT).append('=').append('"').append(height).append('"'); + + if (mInitializedAttributes != null && mInitializedAttributes.length() > 0) { + for (String s : mInitializedAttributes.split(",")) { //$NON-NLS-1$ + sb.append(' '); + String[] nameValue = s.split("="); //$NON-NLS-1$ + String name = nameValue[0]; + String value = nameValue[1]; + if (name.startsWith(ANDROID_NS_NAME_PREFIX)) { + name = name.substring(ANDROID_NS_NAME_PREFIX.length()); + sb.append(androidNsPrefix).append(':'); + } + sb.append(name).append('=').append('"').append(value).append('"'); + } + } + + // Transfer layout_ attributes (other than width and height) + if (primary != null) { + List<Attr> layoutAttributes = findLayoutAttributes(primary); + for (Attr attribute : layoutAttributes) { + String name = attribute.getLocalName(); + if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) + && ANDROID_URI.equals(attribute.getNamespaceURI())) { + // Already handled specially + continue; + } + + if (attribute instanceof IndexedRegion) { + IndexedRegion region = (IndexedRegion) attribute; + int startOffset = region.getStartOffset(); + int endOffset = region.getEndOffset(); + String text = getText(startOffset, endOffset); + DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset); + rootEdit.addChild(deletion); + deletions.add(deletion); + + if (separateAttributes) { + sb.append('\n').append(startIndent).append(indentUnit); + } else { + sb.append(' '); + } + sb.append(text.trim()); + } + } + } + + // Finish open tag: + sb.append('>'); + sb.append('\n').append(startIndent).append(indentUnit); + + InsertEdit beginEdit = new InsertEdit(mSelectionStart, sb.toString()); + rootEdit.addChild(beginEdit); + + String nested = getText(mSelectionStart, mSelectionEnd); + int index = 0; + while (index != -1) { + index = nested.indexOf('\n', index); + if (index != -1) { + index++; + InsertEdit newline = new InsertEdit(mSelectionStart + index, indentUnit); + // Some of the deleted namespaces may have had newlines - be careful + // not to overlap edits + boolean covered = false; + for (DeleteEdit deletion : deletions) { + if (deletion.covers(newline)) { + covered = true; + break; + } + } + if (!covered) { + rootEdit.addChild(newline); + } + } + } + + // Insert end tag: </type> + sb.setLength(0); + sb.append('\n').append(startIndent); + sb.append('<').append('/').append(viewClass).append('>'); + InsertEdit endEdit = new InsertEdit(mSelectionEnd, sb.toString()); + rootEdit.addChild(endEdit); + + if (AdtPrefs.getPrefs().getFormatGuiXml()) { + MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT); + if (formatted != null) { + rootEdit = formatted; + } + } + + change.setEdit(rootEdit); + changes.add(change); + return changes; + } + + String getOldType() { + Element primary = getPrimaryElement(); + if (primary != null) { + String oldType = primary.getTagName(); + if (oldType.indexOf('.') == -1) { + oldType = ANDROID_WIDGET_PREFIX + oldType; + } + return oldType; + } + + return null; + } + + @Override + VisualRefactoringWizard createWizard() { + return new WrapInWizard(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.wrapin", //$NON-NLS-1$ + project, description, comment, arguments); + } + + @Override + protected Refactoring createRefactoring(Map<String, String> args) { + return new WrapInRefactoring(args); + } + } +} |