aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java
diff options
context:
space:
mode:
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.java439
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);
+ }
+ }
+}