OSDN Git Service

Extract as Include support for configuration variations
authorTor Norbye <tnorbye@google.com>
Mon, 7 Mar 2011 22:16:48 +0000 (14:16 -0800)
committerTor Norbye <tnorbye@google.com>
Sat, 12 Mar 2011 00:06:48 +0000 (16:06 -0800)
First, add support for the "Extract as Include" refactoring to update
all identical code fragments in configuration variations of the same
file.  For example, if you have a particular subtree of XML elements
in a file that you have then duplicated into say a landscape mode,
then applying the Extract as Include refactoring will locate the same
code fragment in both files, and replace *both* code fragments with an
include (and this is optional with a checkbox in the wizard
dialog). This only happens when the extracted code fragment is
"identical" in both files. The code fragments can vary in terms of XML
formatting and attribute order (and namespace prefix choice), but the
element hierarchy order, names, attributes defined and attribute
values must be identical.

Second, make fixes to the Change Layout and Change Widget Type
refactorings such that when the id of the converted element is
changed, then references to that id are updated as well.

Third, ensure that the refactorings are enabled even when there is no
text selection; in that case the refactoring will apply to the element
containing the caret.

Some test infrastructure improvements.

Change-Id: Idb4ba40f4217dba2b13881b3d06e269c80ba4b97

33 files changed:
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoring.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoringTest.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoringTest.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringTest.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoringTest.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract3.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract4.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract5.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-changeView2.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-extract3.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract4.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract5.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1-expected-extract4.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1-expected-extract5.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2-expected-extract4.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2-expected-extract5.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java

index 032fe28..753010f 100644 (file)
@@ -28,10 +28,18 @@ 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.w3c.dom.Attr;
+import org.w3c.dom.Document;
 import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @SuppressWarnings("restriction") // No replacement for restricted XML model yet
@@ -303,4 +311,239 @@ public class DomUtilities {
         return generated;
     }
 
+    /**
+     * Returns the element children of the given element
+     *
+     * @param element the parent element
+     * @return a list of child elements, possibly empty but never null
+     */
+    public static List<Element> getChildren(Element element) {
+        // Convenience to avoid lots of ugly DOM access casting
+        NodeList children = element.getChildNodes();
+        // An iterator would have been more natural (to directly drive the child list
+        // iteration) but iterators can't be used in enhanced for loops...
+        List<Element> result = new ArrayList<Element>(children.getLength());
+        for (int i = 0, n = children.getLength(); i < n; i++) {
+            Node node = children.item(i);
+            if (node.getNodeType() == Node.ELEMENT_NODE) {
+                Element child = (Element) node;
+                result.add(child);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns true iff the given elements are contiguous siblings
+     *
+     * @param elements the elements to be tested
+     * @return true if the elements are contiguous siblings with no gaps
+     */
+    public static boolean isContiguous(List<Element> elements) {
+        if (elements.size() > 1) {
+            // All elements must be siblings (e.g. same parent)
+            Node parent = elements.get(0).getParentNode();
+            if (!(parent instanceof Element)) {
+                return false;
+            }
+            for (Element node : elements) {
+                if (parent != node.getParentNode()) {
+                    return false;
+                }
+            }
+
+            // Ensure that the siblings are contiguous; no gaps.
+            // If we've selected all the children of the parent then we don't need
+            // to look.
+            List<Element> siblings = DomUtilities.getChildren((Element) parent);
+            if (siblings.size() != elements.size()) {
+                Set<Element> nodeSet = new HashSet<Element>(elements);
+                boolean inRange = false;
+                int remaining = elements.size();
+                for (Element node : siblings) {
+                    boolean in = nodeSet.contains(node);
+                    if (in) {
+                        remaining--;
+                        if (remaining == 0) {
+                            break;
+                        }
+                        inRange = true;
+                    } else if (inRange) {
+                        return false;
+                    }
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Determines whether two element trees are equivalent. Two element trees are
+     * equivalent if they represent the same DOM structure (elements, attributes, and
+     * children in order). This is almost the same as simply checking whether the String
+     * representations of the two nodes are identical, but this allows for minor
+     * variations that are not semantically significant, such as variations in formatting
+     * or ordering of the element attribute declarations, and the text children are
+     * ignored (this is such that in for example layout where content is only used for
+     * indentation the indentation differences are ignored). Null trees are never equal.
+     *
+     * @param element1 the first element to compare
+     * @param element2 the second element to compare
+     * @return true if the two element hierarchies are logically equal
+     */
+    public static boolean isEquivalent(Element element1, Element element2) {
+        if (element1 == null || element2 == null) {
+            return false;
+        }
+
+        if (!element1.getTagName().equals(element2.getTagName())) {
+            return false;
+        }
+
+        // Check attribute map
+        NamedNodeMap attributes1 = element1.getAttributes();
+        NamedNodeMap attributes2 = element2.getAttributes();
+        if (attributes1.getLength() != attributes2.getLength()) {
+            return false;
+        }
+        if (attributes1.getLength() > 0) {
+            List<Attr> attributeNodes1 = new ArrayList<Attr>();
+            for (int i = 0, n = attributes1.getLength(); i < n; i++) {
+                attributeNodes1.add((Attr) attributes1.item(i));
+            }
+            List<Attr> attributeNodes2 = new ArrayList<Attr>();
+            for (int i = 0, n = attributes2.getLength(); i < n; i++) {
+                attributeNodes2.add((Attr) attributes2.item(i));
+            }
+            Collections.sort(attributeNodes1, ATTRIBUTE_COMPARATOR);
+            Collections.sort(attributeNodes2, ATTRIBUTE_COMPARATOR);
+            for (int i = 0; i < attributeNodes1.size(); i++) {
+                Attr attr1 = attributeNodes1.get(i);
+                Attr attr2 = attributeNodes2.get(i);
+                if (attr1.getLocalName() == null || attr2.getLocalName() == null) {
+                    if (!attr1.getName().equals(attr2.getName())) {
+                        return false;
+                    }
+                } else if (!attr1.getLocalName().equals(attr2.getLocalName())) {
+                    return false;
+                }
+                if (!attr1.getValue().equals(attr2.getValue())) {
+                    return false;
+                }
+                if (attr1.getNamespaceURI() == null) {
+                    if (attr2.getNamespaceURI() != null) {
+                        return false;
+                    }
+                } else if (attr2.getNamespaceURI() == null) {
+                    return false;
+                } else if (!attr1.getNamespaceURI().equals(attr2.getNamespaceURI())) {
+                    return false;
+                }
+            }
+        }
+
+        NodeList children1 = element1.getChildNodes();
+        NodeList children2 = element2.getChildNodes();
+        int nextIndex1 = 0;
+        int nextIndex2 = 0;
+        while (true) {
+            while (nextIndex1 < children1.getLength() &&
+                    children1.item(nextIndex1).getNodeType() != Node.ELEMENT_NODE) {
+                nextIndex1++;
+            }
+
+            while (nextIndex2 < children2.getLength() &&
+                    children2.item(nextIndex2).getNodeType() != Node.ELEMENT_NODE) {
+                nextIndex2++;
+            }
+
+            Element nextElement1 = (Element) (nextIndex1 < children1.getLength()
+                    ? children1.item(nextIndex1) : null);
+            Element nextElement2 = (Element) (nextIndex2 < children2.getLength()
+                    ? children2.item(nextIndex2) : null);
+            if (nextElement1 == null) {
+                if (nextElement2 == null) {
+                    return true;
+                } else {
+                    return false;
+                }
+            } else if (nextElement2 == null) {
+                return false;
+            } else if (!isEquivalent(nextElement1, nextElement2)) {
+                return false;
+            }
+            nextIndex1++;
+            nextIndex2++;
+        }
+    }
+
+    /**
+     * Finds the corresponding element in a document to a given element in another
+     * document. Note that this does <b>not</b> do any kind of equivalence check
+     * (see {@link #isEquivalent(Element, Element)}), and currently the search
+     * is only by id; there is no structural search.
+     *
+     * @param element the element to find an equivalent for
+     * @param document the document to search for an equivalent element in
+     * @return an equivalent element, or null
+     */
+    public static Element findCorresponding(Element element, Document document) {
+        // Make sure the method is called correctly -- the element is for a different
+        // document than the one we are searching
+        assert element.getOwnerDocument() != document;
+
+        // First search by id. This allows us to find the corresponding
+        String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+        if (id != null && id.length() > 0) {
+            if (id.startsWith(ID_PREFIX)) {
+                id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+            }
+
+            return findCorresponding(document.getDocumentElement(), id);
+        }
+
+        // TODO: Search by structure - look in the document and
+        // find a corresponding element in the same location in the structure,
+        // e.g. 4th child of root, 3rd child, 6th child, then pick node with tag "foo".
+
+        return null;
+    }
+
+    /** Helper method for {@link #findCorresponding(Element, Document)} */
+    private static Element findCorresponding(Element element, String targetId) {
+        String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+        if (id != null) { // Work around DOM bug
+            if (id.equals(targetId)) {
+                return element;
+            } else if (id.startsWith(ID_PREFIX)) {
+                id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+                if (id.equals(targetId)) {
+                    return element;
+                }
+            }
+        }
+
+        NodeList children = element.getChildNodes();
+        for (int i = 0, n = children.getLength(); i < n; i++) {
+            Node node = children.item(i);
+            if (node.getNodeType() == Node.ELEMENT_NODE) {
+                Element child = (Element) node;
+                Element match = findCorresponding(child, targetId);
+                if (match != null) {
+                    return match;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /** Can be used to sort attributes by name */
+    private static final Comparator<Attr> ATTRIBUTE_COMPARATOR = new Comparator<Attr>() {
+        public int compare(Attr a1, Attr a2) {
+            return a1.getName().compareTo(a2.getName());
+        }
+    };
 }
index b12cb62..4626de7 100644 (file)
@@ -53,7 +53,10 @@ import org.eclipse.ltk.core.refactoring.RefactoringStatus;
 import org.eclipse.ltk.core.refactoring.TextFileChange;
 import org.eclipse.text.edits.MultiTextEdit;
 import org.eclipse.text.edits.ReplaceEdit;
+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 org.w3c.dom.Node;
@@ -219,7 +222,25 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
             }
         }
 
-        ensureIdMatchesType(layout, mTypeFqcn, rootEdit);
+        String oldId = getId(layout);
+        String newId = ensureIdMatchesType(layout, mTypeFqcn, rootEdit);
+        // Update any layout references to the old id with the new id
+        if (oldId != null && newId != null) {
+            IStructuredModel model = mEditor.getModelForRead();
+            try {
+                IStructuredDocument doc = model.getStructuredDocument();
+                if (doc != null) {
+                    List<TextEdit> replaceIds = replaceIds(getAndroidNamespacePrefix(), doc,
+                            mSelectionStart,
+                            mSelectionEnd, oldId, newId);
+                    for (TextEdit edit : replaceIds) {
+                        rootEdit.addChild(edit);
+                    }
+                }
+            } finally {
+                model.releaseFromRead();
+            }
+        }
 
         String oldType = getOldType();
         String newType = mTypeFqcn;
index 6927568..04da01d 100644 (file)
@@ -21,9 +21,10 @@ import static com.android.ide.common.layout.LayoutConstants.RELATIVE_LAYOUT;
 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_INCLUDE;
 
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.util.Pair;
 
 import org.eclipse.core.resources.IProject;
-import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.SelectionAdapter;
 import org.eclipse.swt.events.SelectionEvent;
@@ -53,12 +54,12 @@ class ChangeLayoutWizard extends VisualRefactoringWizard {
     }
 
     /** Wizard page which inputs parameters for the {@link ChangeLayoutRefactoring} operation */
-    private static class InputPage extends UserInputWizardPage {
+    private static class InputPage extends VisualRefactoringInputPage {
         private final IProject mProject;
         private final String mOldType;
         private Combo mTypeCombo;
         private Button mFlatten;
-        private List<String> mClassNames;
+        private List<Pair<String, ViewElementDescriptor>> mClassNames;
 
         public InputPage(IProject project, String oldType) {
             super("ChangeLayoutInputPage");  //$NON-NLS-1$
@@ -91,6 +92,7 @@ class ChangeLayoutWizard extends VisualRefactoringWizard {
                 }
             };
             mTypeCombo.addSelectionListener(selectionListener);
+            mTypeCombo.addSelectionListener(mSelectionValidateListener);
 
             mFlatten = new Button(composite, SWT.CHECK);
             mFlatten.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
@@ -99,6 +101,7 @@ class ChangeLayoutWizard extends VisualRefactoringWizard {
             mFlatten.addSelectionListener(selectionListener);
             // Should flattening be selected by default?
             mFlatten.setSelection(true);
+            mFlatten.addSelectionListener(mSelectionValidateListener);
 
             // We don't exclude RelativeLayout even if the current layout is RelativeLayout,
             // in case you are trying to flatten the hierarchy for a hierarchy that has a
@@ -127,11 +130,12 @@ class ChangeLayoutWizard extends VisualRefactoringWizard {
             validatePage();
         }
 
-        private boolean validatePage() {
+        @Override
+        protected boolean validatePage() {
             boolean ok = true;
 
             int selectionIndex = mTypeCombo.getSelectionIndex();
-            String type = selectionIndex != -1 ? mClassNames.get(selectionIndex) : null;
+            String type = selectionIndex != -1 ? mClassNames.get(selectionIndex).getFirst() : null;
             if (type == null) {
                 setErrorMessage("Select a layout type");
                 ok = false; // The user has chosen a separator
index 181f413..045062a 100644 (file)
@@ -39,7 +39,10 @@ import org.eclipse.ltk.core.refactoring.RefactoringStatus;
 import org.eclipse.ltk.core.refactoring.TextFileChange;
 import org.eclipse.text.edits.MultiTextEdit;
 import org.eclipse.text.edits.ReplaceEdit;
+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 org.w3c.dom.NamedNodeMap;
@@ -172,7 +175,28 @@ public class ChangeViewRefactoring extends VisualRefactoring {
             }
 
             // Change tag type
-            ensureIdMatchesType(element, mTypeFqcn, rootEdit);
+            String oldId = getId(element);
+            String newId = ensureIdMatchesType(element, mTypeFqcn, rootEdit);
+            // Update any layout references to the old id with the new id
+            if (oldId != null && newId != null) {
+                IStructuredModel model = mEditor.getModelForRead();
+                try {
+                    IStructuredDocument doc = model.getStructuredDocument();
+                    if (doc != null) {
+                        IndexedRegion range = getRegion(element);
+                        int skipStart = range.getStartOffset();
+                        int skipEnd = range.getEndOffset();
+                        List<TextEdit> replaceIds = replaceIds(getAndroidNamespacePrefix(), doc,
+                                skipStart, skipEnd,
+                                oldId, newId);
+                        for (TextEdit edit : replaceIds) {
+                            rootEdit.addChild(edit);
+                        }
+                    }
+                } finally {
+                    model.releaseFromRead();
+                }
+            }
 
             // Strip out attributes that no longer make sense
             removeUndefinedAttrs(rootEdit, element);
index a1e2435..96d8408 100644 (file)
@@ -26,10 +26,7 @@ import com.android.sdklib.IAndroidTarget;
 import com.android.util.Pair;
 
 import org.eclipse.core.resources.IProject;
-import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
 import org.eclipse.swt.SWT;
-import org.eclipse.swt.events.SelectionAdapter;
-import org.eclipse.swt.events.SelectionEvent;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Combo;
@@ -66,7 +63,7 @@ class ChangeViewWizard extends VisualRefactoringWizard {
     }
 
     /** Wizard page which inputs parameters for the {@link ChangeViewRefactoring} operation */
-    private static class InputPage extends UserInputWizardPage {
+    private static class InputPage extends VisualRefactoringInputPage {
         private final IProject mProject;
         private Combo mTypeCombo;
         private final String mOldType;
@@ -88,13 +85,7 @@ class ChangeViewWizard extends VisualRefactoringWizard {
 
             mTypeCombo = new Combo(composite, SWT.READ_ONLY);
             mTypeCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
-            SelectionAdapter selectionListener = new SelectionAdapter() {
-                @Override
-                public void widgetSelected(SelectionEvent e) {
-                    validatePage();
-                }
-            };
-            mTypeCombo.addSelectionListener(selectionListener);
+            mTypeCombo.addSelectionListener(mSelectionValidateListener);
 
             mClassNames = getWidgetTypes(mOldType, mTypeCombo);
             mTypeCombo.select(0);
@@ -177,7 +168,8 @@ class ChangeViewWizard extends VisualRefactoringWizard {
             return classNames;
         }
 
-        private boolean validatePage() {
+        @Override
+        protected boolean validatePage() {
             boolean ok = true;
             int selectionIndex = mTypeCombo.getSelectionIndex();
             String type = selectionIndex != -1 ? mClassNames.get(selectionIndex) : null;
index d5921bf..1909a18 100644 (file)
@@ -15,6 +15,7 @@
  */
 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
 
+import static com.android.AndroidConstants.FD_RES_LAYOUT;
 import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME;
 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
@@ -30,6 +31,7 @@ import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_COLON;
 import static com.android.resources.ResourceType.LAYOUT;
+import static com.android.sdklib.SdkConstants.FD_RES;
 
 import com.android.annotations.VisibleForTesting;
 import com.android.ide.eclipse.adt.AdtConstants;
@@ -40,11 +42,11 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
-import com.android.util.Pair;
 
 import org.eclipse.core.resources.IContainer;
 import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.IProgressMonitor;
@@ -64,13 +66,20 @@ import org.eclipse.text.edits.MultiTextEdit;
 import org.eclipse.text.edits.ReplaceEdit;
 import org.eclipse.text.edits.TextEdit;
 import org.eclipse.ui.IWorkbenchPage;
+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.Document;
 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.List;
 import java.util.Map;
@@ -84,10 +93,8 @@ import java.util.Map;
 public class ExtractIncludeRefactoring extends VisualRefactoring {
     private static final String KEY_NAME = "name";                      //$NON-NLS-1$
     private static final String KEY_OCCURRENCES = "all-occurrences";    //$NON-NLS-1$
-    private static final String KEY_UPDATE_REFS = "update-refs";        //$NON-NLS-1$
     private String mLayoutName;
     private boolean mReplaceOccurrences;
-    private boolean mUpdateReferences;
 
     /**
      * This constructor is solely used by {@link Descriptor},
@@ -97,7 +104,6 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
     ExtractIncludeRefactoring(Map<String, String> arguments) {
         super(arguments);
         mLayoutName = arguments.get(KEY_NAME);
-        mUpdateReferences = Boolean.parseBoolean(arguments.get(KEY_UPDATE_REFS));
         mReplaceOccurrences  = Boolean.parseBoolean(arguments.get(KEY_OCCURRENCES));
     }
 
@@ -182,7 +188,6 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
     protected Map<String, String> createArgumentMap() {
         Map<String, String> args = super.createArgumentMap();
         args.put(KEY_NAME, mLayoutName);
-        args.put(KEY_UPDATE_REFS, Boolean.toString(mUpdateReferences));
         args.put(KEY_OCCURRENCES, Boolean.toString(mReplaceOccurrences));
 
         return args;
@@ -197,10 +202,6 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
         mLayoutName = layoutName;
     }
 
-    void setUpdateReferences(boolean selection) {
-        mUpdateReferences = selection;
-    }
-
     void setReplaceOccurrences(boolean selection) {
         mReplaceOccurrences = selection;
     }
@@ -211,9 +212,7 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
     protected List<Change> computeChanges() {
         String extractedText = getExtractedText();
 
-        Pair<String, String> namespace = computeNamespaces();
-        String androidNsPrefix = namespace.getFirst();
-        String namespaceDeclarations = namespace.getSecond();
+        String namespaceDeclarations = computeNamespaceDeclarations();
 
         // Insert namespace:
         extractedText = insertNamespace(extractedText, namespaceDeclarations);
@@ -229,6 +228,94 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
         IProject project = mEditor.getProject();
         IFile sourceFile = mEditor.getInputFile();
 
+        // Replace extracted elements by <include> tag
+        handleIncludingFile(changes, sourceFile, mSelectionStart, mSelectionEnd,
+                getDomDocument(), getPrimaryElement());
+
+        // Also extract in other variations of the same file (landscape/portrait, etc)
+        boolean haveVariations = false;
+        if (mReplaceOccurrences) {
+            //String id = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
+            List<IFile> variations = getConfigurationVariations(sourceFile);
+            for (IFile variation : variations) {
+                IModelManager modelManager = StructuredModelManager.getModelManager();
+                IStructuredModel model = null;
+                try {
+                    model = modelManager.getModelForRead(variation);
+                    if (model instanceof IDOMModel) {
+                        IDOMModel domModel = (IDOMModel) model;
+                        IDOMDocument otherDocument = domModel.getDocument();
+                        List<Element> otherElements = new ArrayList<Element>();
+                        Element otherPrimary = null;
+
+                        for (Element element : getElements()) {
+                            Element other = DomUtilities.findCorresponding(element,
+                                    otherDocument);
+                            if (other != null) {
+                                // See if the structure is similar to what we have in this
+                                // document
+                                if (DomUtilities.isEquivalent(element, other)) {
+                                    otherElements.add(other);
+                                    if (element == getPrimaryElement()) {
+                                        otherPrimary = other;
+                                    }
+                                }
+                            }
+                        }
+
+                        // Only perform extract in the other file if we find a match for
+                        // ALL of elements being extracted, and if they too are contiguous
+                        if (otherElements.size() == getElements().size() &&
+                                DomUtilities.isContiguous(otherElements)) {
+                            // Find the range
+                            int begin = Integer.MAX_VALUE;
+                            int end = Integer.MIN_VALUE;
+                            for (Element element : otherElements) {
+                                // Yes!! Extract this one as well!
+                                IndexedRegion region = getRegion(element);
+                                end = Math.max(end, region.getEndOffset());
+                                begin = Math.min(begin, region.getStartOffset());
+                            }
+                            handleIncludingFile(changes, variation, begin,
+                                    end, otherDocument, otherPrimary);
+                            haveVariations = true;
+                        }
+                    }
+                } catch (IOException e) {
+                    AdtPlugin.log(e, null);
+                } catch (CoreException e) {
+                    AdtPlugin.log(e, null);
+                } finally {
+                    if (model != null) {
+                        model.releaseFromRead();
+                    }
+                }
+            }
+        }
+
+        // Add change to create the new file
+        IContainer parent = sourceFile.getParent();
+        if (haveVariations) {
+            // If we're extracting from multiple configuration folders, then we need to
+            // place the extracted include in the base layout folder (if not it goes next to
+            // the including file)
+            parent = mProject.getFolder(FD_RES).getFolder(FD_RES_LAYOUT);
+        }
+        IPath parentPath = parent.getProjectRelativePath();
+        final IFile file = project.getFile(new Path(parentPath + WS_SEP + newFileName));
+        TextFileChange addFile = new TextFileChange("Create new separate layout", file);
+        addFile.setTextType(AdtConstants.EXT_XML);
+        changes.add(addFile);
+        addFile.setEdit(new InsertEdit(0, sb.toString()));
+
+        Change finishHook = createFinishHook(file);
+        changes.add(finishHook);
+
+        return changes;
+    }
+
+    private void handleIncludingFile(List<Change> changes,
+            IFile sourceFile, int begin, int end, Document document, Element primary) {
         TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile);
         MultiTextEdit rootEdit = new MultiTextEdit();
         change.setEdit(rootEdit);
@@ -237,42 +324,75 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
 
         String referenceId = getReferenceId();
         // Replace existing elements in the source file and insert <include>
-        String include = computeIncludeString(mLayoutName, androidNsPrefix, referenceId);
-        int length = mSelectionEnd - mSelectionStart;
-        ReplaceEdit replace = new ReplaceEdit(mSelectionStart, length, include);
+        String androidNsPrefix = getAndroidNamespacePrefix(document);
+        String include = computeIncludeString(primary, mLayoutName, androidNsPrefix, referenceId);
+        int length = end - begin;
+        ReplaceEdit replace = new ReplaceEdit(begin, length, include);
         rootEdit.addChild(replace);
 
         // Update any layout references to the old id with the new id
-        if (mUpdateReferences && referenceId != null) {
-            String rootId = getRootId();
-            IStructuredModel model = mEditor.getModelForRead();
+        if (referenceId != null && primary != null) {
+            String rootId = getId(primary);
+            IStructuredModel model = null;
             try {
+                model = StructuredModelManager.getModelManager().getModelForRead(sourceFile);
                 IStructuredDocument doc = model.getStructuredDocument();
-                if (doc != null) {
-                    List<TextEdit> replaceIds = replaceIds(doc, mSelectionStart,
-                            mSelectionEnd, rootId, referenceId);
+                if (doc != null && rootId != null) {
+                    List<TextEdit> replaceIds = replaceIds(androidNsPrefix, doc, begin,
+                            end, rootId, referenceId);
                     for (TextEdit edit : replaceIds) {
                         rootEdit.addChild(edit);
                     }
                 }
+            } catch (IOException e) {
+                AdtPlugin.log(e, null);
+            } catch (CoreException e) {
+                AdtPlugin.log(e, null);
             } finally {
-                model.releaseFromRead();
+                if (model != null) {
+                    model.releaseFromRead();
+                }
             }
         }
+    }
 
-        // Add change to create the new file
-        IContainer parent = sourceFile.getParent();
-        IPath parentPath = parent.getProjectRelativePath();
-        final IFile file = project.getFile(new Path(parentPath + WS_SEP + newFileName));
-        TextFileChange addFile = new TextFileChange("Create new separate layout", file);
-        addFile.setTextType(AdtConstants.EXT_XML);
-        changes.add(addFile);
-        addFile.setEdit(new InsertEdit(0, sb.toString()));
+    /**
+     * This method returns all the configuration variations of the given layout. For
+     * example, if you have both layout/foo.xml and layout-land/foo.xml and
+     * layout-xlarge/foo.xml, then calling this method on any of the three files will
+     * return the other two.
+     *
+     * @param file the file to find configuration variations of
+     * @return the other layout variations of the file
+     */
+    public static List<IFile> getConfigurationVariations(IFile file) {
+        List<IFile> variations = new ArrayList<IFile>();
 
-        Change finishHook = createFinishHook(file);
-        changes.add(finishHook);
+        // This currently just searches the layout folders in the project for an exact
+        // resource name match. This could later use ProjectResources instead, but
+        // currently that's not done since it doesn't work from the tests.
 
-        return changes;
+        String name = file.getName();
+        IContainer resFolder = file.getParent().getParent();
+        try {
+            for (IResource member : resFolder.members()) {
+                if (member.getName().startsWith(FD_RES_LAYOUT)) {
+                    if (member instanceof IContainer) {
+                        IContainer container = (IContainer) member;
+                        IResource alternative = container.findMember(name);
+                        IPath relPath = file.getProjectRelativePath();
+                        if (alternative instanceof IFile
+                                && !alternative.getProjectRelativePath().equals(relPath)) {
+                            variations.add((IFile) alternative);
+                        }
+                    }
+                }
+            }
+        } catch (CoreException e) {
+            AdtPlugin.log(e, null);
+        }
+
+        return variations;
     }
 
     String getInitialName() {
@@ -297,6 +417,10 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
         return defaultName;
     }
 
+    IFile getSourceFile() {
+        return mFile;
+    }
+
     private Change createFinishHook(final IFile file) {
         return new NullChange("Open extracted layout and refresh resources") {
             @Override
@@ -322,7 +446,7 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
         };
     }
 
-    private Pair<String, String> computeNamespaces() {
+    private String computeNamespaceDeclarations() {
         String androidNsPrefix = null;
         String namespaceDeclarations = null;
 
@@ -364,7 +488,7 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
             namespaceDeclarations = sb.toString();
         }
 
-        return Pair.of(androidNsPrefix, namespaceDeclarations);
+        return namespaceDeclarations;
     }
 
     /** Returns the id to be used for the include tag itself (may be null) */
@@ -381,8 +505,8 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
      * Compute the actual {@code <include>} string to be inserted in place of the old
      * selection
      */
-    private String computeIncludeString(String newName, String androidNsPrefix,
-            String referenceId) {
+    private static String computeIncludeString(Element primaryNode, String newName,
+            String androidNsPrefix, String referenceId) {
         StringBuilder sb = new StringBuilder();
         sb.append("<include layout=\"@layout/"); //$NON-NLS-1$
         sb.append(newName);
@@ -408,7 +532,6 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
 
         // HACK: see issue 13494: We must duplicate the width/height attributes on the
         // <include> statement for designtime rendering only
-        Element primaryNode = getPrimaryElement();
         String width = null;
         String height = null;
         if (primaryNode == null) {
index 403c753..9b111ab 100644 (file)
 
 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
 
-import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
 import com.android.resources.ResourceType;
 
+import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IProject;
-import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
 import org.eclipse.swt.SWT;
-import org.eclipse.swt.events.ModifyEvent;
-import org.eclipse.swt.events.ModifyListener;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Button;
@@ -32,6 +30,8 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Text;
 
+import java.util.List;
+
 class ExtractIncludeWizard extends VisualRefactoringWizard {
     public ExtractIncludeWizard(ExtractIncludeRefactoring ref, LayoutEditor editor) {
         super(ref, editor);
@@ -42,20 +42,22 @@ class ExtractIncludeWizard extends VisualRefactoringWizard {
     protected void addUserInputPages() {
         ExtractIncludeRefactoring ref = (ExtractIncludeRefactoring) getRefactoring();
         String initialName = ref.getInitialName();
-        addPage(new InputPage(mEditor.getProject(), initialName));
+        IFile sourceFile = ref.getSourceFile();
+        addPage(new InputPage(mEditor.getProject(), sourceFile, initialName));
     }
 
     /** Wizard page which inputs parameters for the {@link ExtractIncludeRefactoring} operation */
-    private static class InputPage extends UserInputWizardPage {
+    private static class InputPage extends VisualRefactoringInputPage {
         private final IProject mProject;
+        private final IFile mSourceFile;
         private final String mSuggestedName;
         private Text mNameText;
-        private Button mUpdateReferences;
         private Button mReplaceAllOccurrences;
 
-        public InputPage(IProject project, String suggestedName) {
-            super("ExtractIncludeInputPage");  //$NON-NLS-1$
+        public InputPage(IProject project, IFile sourceFile, String suggestedName) {
+            super("ExtractIncludeInputPage");
             mProject = project;
+            mSourceFile = sourceFile;
             mSuggestedName = suggestedName;
         }
 
@@ -69,23 +71,19 @@ class ExtractIncludeWizard extends VisualRefactoringWizard {
 
             mNameText = new Text(composite, SWT.BORDER);
             mNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
-            mNameText.addModifyListener(new ModifyListener() {
-                public void modifyText(ModifyEvent e) {
-                    validatePage();
-                }
-            });
-
-            mUpdateReferences = new Button(composite, SWT.CHECK);
-            mUpdateReferences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
-                    false, false, 2, 1));
-            mUpdateReferences.setSelection(true);
-            mUpdateReferences.setText("Update layout references");
+            mNameText.addModifyListener(mModifyValidateListener);
 
             mReplaceAllOccurrences = new Button(composite, SWT.CHECK);
             mReplaceAllOccurrences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
                     false, false, 2, 1));
-            mReplaceAllOccurrences.setText("Replace all occurrences with include to new layout");
-            mReplaceAllOccurrences.setEnabled(false);
+            mReplaceAllOccurrences.setText(
+                    "Replace occurrences in all layouts with include to new layout");
+            List<IFile> variations =
+                ExtractIncludeRefactoring.getConfigurationVariations(mSourceFile);
+            boolean enabled = variations.size() > 0;
+            mReplaceAllOccurrences.setEnabled(enabled);
+            mReplaceAllOccurrences.setSelection(enabled);
+            mReplaceAllOccurrences.addSelectionListener(mSelectionValidateListener);
 
             // Initialize UI:
             if (mSuggestedName != null) {
@@ -96,7 +94,8 @@ class ExtractIncludeWizard extends VisualRefactoringWizard {
             validatePage();
         }
 
-        private boolean validatePage() {
+        @Override
+        protected boolean validatePage() {
             boolean ok = true;
 
             String text = mNameText.getText().trim();
@@ -122,7 +121,6 @@ class ExtractIncludeWizard extends VisualRefactoringWizard {
                     (ExtractIncludeRefactoring) getRefactoring();
                 refactoring.setLayoutName(text);
                 refactoring.setReplaceOccurrences(mReplaceAllOccurrences.getSelection());
-                refactoring.setUpdateReferences(mUpdateReferences.getSelection());
             }
 
             setPageComplete(ok);
index 8834210..6e85412 100644 (file)
@@ -65,6 +65,7 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
 import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
 import com.android.util.Pair;
 
 import org.eclipse.core.runtime.IStatus;
@@ -370,29 +371,6 @@ class RelativeLayoutConversionHelper {
     }
 
     /**
-     * Returns the element children of the given element
-     *
-     * @param element the parent element
-     * @return a list of child elements, possibly empty but never null
-     */
-    public static List<Element> getChildren(Element element) {
-        // Convenience to avoid lots of ugly DOM access casting
-        NodeList children = element.getChildNodes();
-        // An iterator would have been more natural (to directly drive the child list
-        // iteration) but iterators can't be used in enhanced for loops...
-        List<Element> result = new ArrayList<Element>(children.getLength());
-        for (int i = 0, n = children.getLength(); i < n; i++) {
-            Node node = children.item(i);
-            if (node.getNodeType() == Node.ELEMENT_NODE) {
-                Element child = (Element) node;
-                result.add(child);
-            }
-        }
-
-        return result;
-    }
-
-    /**
      * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it
      * does not define a weight
      */
@@ -417,7 +395,7 @@ class RelativeLayoutConversionHelper {
      */
     private float getWeightSum(Element linearLayout) {
         float sum = 0;
-        for (Element child : getChildren(linearLayout)) {
+        for (Element child : DomUtilities.getChildren(linearLayout)) {
             sum += getWeight(child);
         }
 
@@ -439,7 +417,7 @@ class RelativeLayoutConversionHelper {
             // Baseline alignment. Find the tallest child and set it as the baseline reference.
             int tallestHeight = 0;
             View tallest = null;
-            for (Element child : getChildren(layout)) {
+            for (Element child : DomUtilities.getChildren(layout)) {
                 View view = edgeList.getView(child);
                 if (view != null && view.getHeight() > tallestHeight) {
                     tallestHeight = view.getHeight();
@@ -454,7 +432,7 @@ class RelativeLayoutConversionHelper {
         float weightSum = getWeightSum(layout);
         float cumulativeWeight = 0;
 
-        List<Element> children = getChildren(layout);
+        List<Element> children = DomUtilities.getChildren(layout);
         String prevId = null;
         boolean isFirstChild = true;
         boolean linkBackwards = true;
index 3cce3b3..65ec7fd 100644 (file)
@@ -32,7 +32,6 @@ import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
-import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
@@ -210,119 +209,6 @@ public abstract class VisualRefactoring extends Refactoring {
         mElements = initElements();
     }
 
-    @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;
-            }
-
-            // 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 = new ArrayList<CanvasViewInfo>();
-                for (TreePath path : mTreeSelection.getPaths()) {
-                    Object lastSegment = path.getLastSegment();
-                    if (lastSegment instanceof CanvasViewInfo) {
-                        infos.add((CanvasViewInfo) lastSegment);
-                    }
-                }
-
-                if (infos.size() == 0) {
-                    status.addFatalError("No selection to extract");
-                    return status;
-                }
-
-                // Can't extract the root -- wouldn't that be pointless? (or maybe not
-                // always)
-                for (CanvasViewInfo info : infos) {
-                    if (info.isRoot()) {
-                        status.addFatalError("Cannot refactor the root");
-                        return status;
-                    }
-                }
-
-                // Disable if you've selected a single include tag
-                if (infos.size() == 1) {
-                    UiViewElementNode uiNode = infos.get(0).getUiViewNode();
-                    if (uiNode != null) {
-                        Node xmlNode = uiNode.getXmlNode();
-                        if (xmlNode.getLocalName().equals(LayoutDescriptors.VIEW_INCLUDE)) {
-                            status.addWarning("No point in refactoring a single include tag");
-                        }
-                    }
-                }
-
-                // Enforce that the selection is -contiguous-
-                if (infos.size() > 1) {
-                    // All elements must be siblings (e.g. same parent)
-                    List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(infos
-                            .size());
-                    for (CanvasViewInfo info : infos) {
-                        UiViewElementNode node = info.getUiViewNode();
-                        if (node != null) {
-                            nodes.add(node);
-                        }
-                    }
-                    if (nodes.size() == 0) {
-                        status.addFatalError("No selected views");
-                        return status;
-                    }
-
-                    UiElementNode parent = nodes.get(0).getUiParent();
-                    for (UiViewElementNode node : nodes) {
-                        if (parent != node.getUiParent()) {
-                            status.addFatalError("The selected elements must be adjacent");
-                            return status;
-                        }
-                    }
-                    // Ensure that the siblings are contiguous; no gaps.
-                    // If we've selected all the children of the parent then we don't need
-                    // to look.
-                    List<UiElementNode> siblings = parent.getUiChildren();
-                    if (siblings.size() != nodes.size()) {
-                        Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes);
-                        boolean inRange = false;
-                        int remaining = nodes.size();
-                        for (UiElementNode node : siblings) {
-                            boolean in = nodeSet.contains(node);
-                            if (in) {
-                                remaining--;
-                                if (remaining == 0) {
-                                    break;
-                                }
-                                inRange = true;
-                            } else if (inRange) {
-                                status.addFatalError("The selected elements must be adjacent");
-                                return status;
-                            }
-                        }
-                    }
-                }
-            }
-
-            // 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();
-        }
-    }
-
     protected abstract List<Change> computeChanges();
 
     @Override
@@ -429,7 +315,8 @@ public abstract class VisualRefactoring extends Refactoring {
 
 
     /** Produce a list of edits to replace references to the given id with the given new id */
-    protected List<TextEdit> replaceIds(IStructuredDocument doc, int skipStart, int skipEnd,
+    protected static List<TextEdit> replaceIds(String androidNamePrefix,
+            IStructuredDocument doc, int skipStart, int skipEnd,
             String rootId, String referenceId) {
         if (rootId == null) {
             return Collections.emptyList();
@@ -448,7 +335,7 @@ public abstract class VisualRefactoring extends Refactoring {
             return Collections.emptyList();
         }
 
-        String namePrefix = getAndroidNamespacePrefix() + ':' + ATTR_LAYOUT_PREFIX;
+        String namePrefix = androidNamePrefix + ':' + ATTR_LAYOUT_PREFIX;
         List<TextEdit> edits = new ArrayList<TextEdit>();
 
         IStructuredDocumentRegion region = doc.getFirstStructuredDocumentRegion();
@@ -528,8 +415,37 @@ public abstract class VisualRefactoring extends Refactoring {
         return mAndroidNamespacePrefix;
     }
 
+    protected static String getAndroidNamespacePrefix(Document document) {
+        String nsPrefix = null;
+        List<Attr> attributeNodes = findNamespaceAttributes(document);
+        for (Node attributeNode : attributeNodes) {
+            String prefix = attributeNode.getPrefix();
+            if (XMLNS.equals(prefix)) {
+                String name = attributeNode.getNodeName();
+                String value = attributeNode.getNodeValue();
+                if (value.equals(ANDROID_URI)) {
+                    nsPrefix = name;
+                    if (nsPrefix.startsWith(XMLNS_COLON)) {
+                        nsPrefix =
+                            nsPrefix.substring(XMLNS_COLON.length());
+                    }
+                }
+            }
+        }
+
+        if (nsPrefix == null) {
+            nsPrefix = ANDROID_NS_NAME;
+        }
+
+        return nsPrefix;
+    }
+
     protected List<Attr> findNamespaceAttributes() {
         Document document = getDomDocument();
+        return findNamespaceAttributes(document);
+    }
+
+    protected static List<Attr> findNamespaceAttributes(Document document) {
         if (document != null) {
             Element root = document.getDocumentElement();
             return findNamespaceAttributes(root);
@@ -538,7 +454,7 @@ public abstract class VisualRefactoring extends Refactoring {
         return Collections.emptyList();
     }
 
-    protected List<Attr> findNamespaceAttributes(Node root) {
+    protected static List<Attr> findNamespaceAttributes(Node root) {
         List<Attr> result = new ArrayList<Attr>();
         NamedNodeMap attributes = root.getAttributes();
         for (int i = 0, n = attributes.getLength(); i < n; i++) {
@@ -894,7 +810,11 @@ public abstract class VisualRefactoring extends Refactoring {
         return true;
     }
 
-    protected void ensureIdMatchesType(Element element, String newType, MultiTextEdit rootEdit) {
+    /**
+     * Updates the given element with a new name if the current id reflects the old
+     * element type. If the name was changed, it will return the new name.
+     */
+    protected String ensureIdMatchesType(Element element, String newType, MultiTextEdit rootEdit) {
         String oldType = element.getTagName();
         if (oldType.indexOf('.') == -1) {
             oldType = ANDROID_WIDGET_PREFIX + oldType;
@@ -903,8 +823,10 @@ public abstract class VisualRefactoring extends Refactoring {
         String id = getId(element);
         if (id == null || id.toLowerCase().contains(oldTypeBase.toLowerCase())) {
             String newTypeBase = newType.substring(newType.lastIndexOf('.') + 1);
-            ensureHasId(rootEdit, element, newTypeBase);
+            return ensureHasId(rootEdit, element, newTypeBase);
         }
+
+        return null;
     }
 
     protected static IndexedRegion getRegion(Node node) {
index 627f1b1..4818132 100644 (file)
@@ -15,8 +15,8 @@
  */
 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
 
-import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
 
@@ -56,7 +56,7 @@ abstract class VisualRefactoringAction implements IWorkbenchWindowActionDelegate
     /**
      * Examine the selection to determine if the action should be enabled or not.
      * <p/>
-     * Keep a link to the relevant selection structure (i.e. a part of the Java AST).
+     * Keep a link to the relevant selection structure
      */
     public void selectionChanged(IAction action, ISelection selection) {
         // Look for selections in XML and in the layout UI editor
@@ -76,10 +76,8 @@ abstract class VisualRefactoringAction implements IWorkbenchWindowActionDelegate
 
         if (selection instanceof ITextSelection) {
             mTextSelection = (ITextSelection) selection;
-            if (mTextSelection.getLength() > 0) {
-                editor = getActiveEditor();
-                mFile = getSelectedFile(editor);
-            }
+            editor = getActiveEditor();
+            mFile = getSelectedFile(editor);
         } else if (selection instanceof ITreeSelection) {
              Object firstElement = ((ITreeSelection)selection).getFirstElement();
              if (firstElement instanceof CanvasViewInfo) {
index dceba6e..2b6f25e 100644 (file)
@@ -19,6 +19,11 @@ import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
 
 import org.eclipse.ltk.core.refactoring.Refactoring;
 import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
 
 public abstract class VisualRefactoringWizard extends RefactoringWizard {
     protected final LayoutEditor mEditor;
@@ -38,4 +43,33 @@ public abstract class VisualRefactoringWizard extends RefactoringWizard {
             mEditor.refreshXmlModel();
         }
     }
+
+    protected abstract static class VisualRefactoringInputPage extends UserInputWizardPage {
+        public VisualRefactoringInputPage(String name) {
+            super(name);
+        }
+
+        /**
+         * Listener which can be attached on any widget in the wizard page to force
+         * modifications of the associated widget to validate the page again
+         */
+        protected ModifyListener mModifyValidateListener = new ModifyListener() {
+            public void modifyText(ModifyEvent e) {
+                validatePage();
+            }
+        };
+
+        /**
+         * Listener which can be attached on any widget in the wizard page to force
+         * selection changes of the associated widget to validate the page again
+         */
+        protected SelectionAdapter mSelectionValidateListener = new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                validatePage();
+            }
+        };
+
+        protected abstract boolean validatePage();
+    }
 }
index 3c95d94..708304a 100644 (file)
@@ -15,6 +15,7 @@
  */
 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
 
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME_PREFIX;
 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
 import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
@@ -63,11 +64,10 @@ import java.util.Map;
 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 static final String KEY_UPDATE_REFS = "update-refs";           //$NON-NLS-1$
 
     private String mId;
     private String mTypeFqcn;
-    private boolean mUpdateReferences;
+    private String mInitializedAttributes;
 
     /**
      * This constructor is solely used by {@link Descriptor},
@@ -78,7 +78,6 @@ public class WrapInRefactoring extends VisualRefactoring {
         super(arguments);
         mId = arguments.get(KEY_ID);
         mTypeFqcn = arguments.get(KEY_TYPE);
-        mUpdateReferences = Boolean.parseBoolean(arguments.get(KEY_UPDATE_REFS));
     }
 
     public WrapInRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
@@ -149,7 +148,6 @@ public class WrapInRefactoring extends VisualRefactoring {
         Map<String, String> args = super.createArgumentMap();
         args.put(KEY_TYPE, mTypeFqcn);
         args.put(KEY_ID, mId);
-        args.put(KEY_UPDATE_REFS, Boolean.toString(mUpdateReferences));
 
         return args;
     }
@@ -167,8 +165,8 @@ public class WrapInRefactoring extends VisualRefactoring {
         mTypeFqcn = typeFqcn;
     }
 
-    void setUpdateReferences(boolean selection) {
-        mUpdateReferences = selection;
+    void setInitializedAttributes(String initializedAttributes) {
+        mInitializedAttributes = initializedAttributes;
     }
 
     @Override
@@ -194,6 +192,8 @@ public class WrapInRefactoring extends VisualRefactoring {
         String startIndent = AndroidXmlEditor.getIndentAtOffset(document, mSelectionStart);
 
         String viewClass = getViewClass(mTypeFqcn);
+        String androidNsPrefix = getAndroidNamespacePrefix();
+
 
         IFile file = mEditor.getInputFile();
         List<Change> changes = new ArrayList<Change>();
@@ -205,14 +205,14 @@ public class WrapInRefactoring extends VisualRefactoring {
         String id = ensureNewId(mId);
 
         // Update any layout references to the old id with the new id
-        if (mUpdateReferences && id != null) {
+        if (id != null) {
             String rootId = getRootId();
             IStructuredModel model = mEditor.getModelForRead();
             try {
                 IStructuredDocument doc = model.getStructuredDocument();
                 if (doc != null) {
-                    List<TextEdit> replaceIds = replaceIds(doc, mSelectionStart,
-                            mSelectionEnd, rootId, id);
+                    List<TextEdit> replaceIds = replaceIds(androidNsPrefix,
+                            doc, mSelectionStart, mSelectionEnd, rootId, id);
                     for (TextEdit edit : replaceIds) {
                         rootEdit.addChild(edit);
                     }
@@ -262,8 +262,6 @@ public class WrapInRefactoring extends VisualRefactoring {
             sb.append(namespace);
         }
 
-        String androidNsPrefix = getAndroidNamespacePrefix();
-
         // Set the ID if any
         if (id != null) {
             if (separateAttributes) {
@@ -308,8 +306,22 @@ public class WrapInRefactoring extends VisualRefactoring {
         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 (mUpdateReferences) {
+        if (primary != null) {
             List<Attr> layoutAttributes = findLayoutAttributes(primary);
             for (Attr attribute : layoutAttributes) {
                 String name = attribute.getLocalName();
index 033a657..d9e746c 100644 (file)
@@ -29,6 +29,7 @@ import static com.android.sdklib.SdkConstants.FN_FRAMEWORK_LIBRARY;
 import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
@@ -54,15 +55,9 @@ import org.eclipse.jdt.core.search.SearchPattern;
 import org.eclipse.jdt.core.search.SearchRequestor;
 import org.eclipse.jdt.internal.core.ResolvedBinaryType;
 import org.eclipse.jdt.internal.core.ResolvedSourceType;
-import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
 import org.eclipse.swt.SWT;
-import org.eclipse.swt.events.ModifyEvent;
-import org.eclipse.swt.events.ModifyListener;
-import org.eclipse.swt.events.SelectionAdapter;
-import org.eclipse.swt.events.SelectionEvent;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
-import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Combo;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Label;
@@ -91,13 +86,12 @@ class WrapInWizard extends VisualRefactoringWizard {
     }
 
     /** Wizard page which inputs parameters for the {@link WrapInRefactoring} operation */
-    private static class InputPage extends UserInputWizardPage {
+    private static class InputPage extends VisualRefactoringInputPage {
         private final IProject mProject;
         private final String mOldType;
         private Text mIdText;
         private Combo mTypeCombo;
-        private Button mUpdateReferences;
-        private List<String> mClassNames;
+        private List<Pair<String, ViewElementDescriptor>> mClassNames;
 
         public InputPage(IProject project, String oldType) {
             super("WrapInInputPage");  //$NON-NLS-1$
@@ -115,13 +109,7 @@ class WrapInWizard extends VisualRefactoringWizard {
 
             mTypeCombo = new Combo(composite, SWT.READ_ONLY);
             mTypeCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
-            SelectionAdapter selectionListener = new SelectionAdapter() {
-                @Override
-                public void widgetSelected(SelectionEvent e) {
-                    validatePage();
-                }
-            };
-            mTypeCombo.addSelectionListener(selectionListener);
+            mTypeCombo.addSelectionListener(mSelectionValidateListener);
 
             Label idLabel = new Label(composite, SWT.NONE);
             idLabel.setText("New Layout Id:");
@@ -129,18 +117,7 @@ class WrapInWizard extends VisualRefactoringWizard {
 
             mIdText = new Text(composite, SWT.BORDER);
             mIdText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
-            mIdText.addModifyListener(new ModifyListener() {
-                public void modifyText(ModifyEvent e) {
-                    validatePage();
-                }
-            });
-
-            mUpdateReferences = new Button(composite, SWT.CHECK);
-            mUpdateReferences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
-                    false, false, 2, 1));
-            mUpdateReferences.setSelection(true);
-            mUpdateReferences.setText("Update layout references");
-            mUpdateReferences.addSelectionListener(selectionListener);
+            mIdText.addModifyListener(mModifyValidateListener);
 
             Set<String> exclude = Collections.singleton(VIEW_INCLUDE);
             mClassNames = addLayouts(mProject, mOldType, mTypeCombo, exclude, true);
@@ -152,18 +129,15 @@ class WrapInWizard extends VisualRefactoringWizard {
             mTypeCombo.setFocus();
         }
 
-        private boolean validatePage() {
+        @Override
+        protected boolean validatePage() {
             boolean ok = true;
 
             String id = mIdText.getText().trim();
 
             if (id.length() == 0) {
-                // It's okay to not define a title...
-                // ...unless you want to update references
-                if (mUpdateReferences.getSelection()) {
-                    setErrorMessage("ID required when updating layout references");
-                    ok = false;
-                }
+                setErrorMessage("ID required");
+                ok = false;
             } else {
                 // ...but if you do, it has to be valid!
                 ResourceNameValidator validator = ResourceNameValidator.create(false, mProject,
@@ -176,7 +150,7 @@ class WrapInWizard extends VisualRefactoringWizard {
             }
 
             int selectionIndex = mTypeCombo.getSelectionIndex();
-            String type = selectionIndex != -1 ? mClassNames.get(selectionIndex) : null;
+            String type = selectionIndex != -1 ? mClassNames.get(selectionIndex).getFirst() : null;
             if (type == null) {
                 setErrorMessage("Select a container type");
                 ok = false; // The user has chosen a separator
@@ -190,7 +164,16 @@ class WrapInWizard extends VisualRefactoringWizard {
                     (WrapInRefactoring) getRefactoring();
                 refactoring.setId(id);
                 refactoring.setType(type);
-                refactoring.setUpdateReferences(mUpdateReferences.getSelection());
+
+                ViewElementDescriptor descriptor = mClassNames.get(selectionIndex).getSecond();
+                if (descriptor instanceof PaletteMetadataDescriptor) {
+                    PaletteMetadataDescriptor paletteDescriptor =
+                        (PaletteMetadataDescriptor) descriptor;
+                    String initializedAttributes = paletteDescriptor.getInitializedAttributes();
+                    refactoring.setInitializedAttributes(initializedAttributes);
+                } else {
+                    refactoring.setInitializedAttributes(null);
+                }
             }
 
             setPageComplete(ok);
@@ -198,17 +181,19 @@ class WrapInWizard extends VisualRefactoringWizard {
         }
     }
 
-    static List<String> addLayouts(IProject project, String oldType, Combo combo,
+    static List<Pair<String, ViewElementDescriptor>> addLayouts(IProject project,
+            String oldType, Combo combo,
             Set<String> exclude, boolean addGestureOverlay) {
-        List<String> classNames = new ArrayList<String>();
+        List<Pair<String, ViewElementDescriptor>> classNames =
+            new ArrayList<Pair<String, ViewElementDescriptor>>();
 
-        if (oldType.equals(FQCN_RADIO_BUTTON)) {
+        if (oldType != null && oldType.equals(FQCN_RADIO_BUTTON)) {
             combo.add(RADIO_GROUP);
             // NOT a fully qualified name since android widgets do not include the package
-            classNames.add(RADIO_GROUP);
+            classNames.add(Pair.of(RADIO_GROUP, (ViewElementDescriptor) null));
 
             combo.add(SEPARATOR_LABEL);
-            classNames.add(null);
+            classNames.add(Pair.<String,ViewElementDescriptor>of(null, null));
         }
 
         Pair<List<String>,List<String>> result = findViews(project, true);
@@ -217,10 +202,10 @@ class WrapInWizard extends VisualRefactoringWizard {
         if (customViews.size() > 0) {
             for (String view : customViews) {
                 combo.add(view);
-                classNames.add(view);
+                classNames.add(Pair.of(view, (ViewElementDescriptor) null));
             }
             combo.add(SEPARATOR_LABEL);
-            classNames.add(null);
+            classNames.add(Pair.<String,ViewElementDescriptor>of(null, null));
         }
 
         // Populate type combo
@@ -251,7 +236,7 @@ class WrapInWizard extends VisualRefactoringWizard {
                             String className = d.getFullClassName();
                             if (exclude == null || !exclude.contains(className)) {
                                 combo.add(d.getUiName());
-                                classNames.add(className);
+                                classNames.add(Pair.of(className, d));
                             }
                         }
 
@@ -262,7 +247,7 @@ class WrapInWizard extends VisualRefactoringWizard {
                         if (thirdPartyViews.size() > 0) {
                             for (String view : thirdPartyViews) {
                                 combo.add(view);
-                                classNames.add(view);
+                                classNames.add(Pair.of(view, (ViewElementDescriptor) null));
                             }
                             combo.add(SEPARATOR_LABEL);
                             classNames.add(null);
@@ -270,10 +255,11 @@ class WrapInWizard extends VisualRefactoringWizard {
 
                         if (addGestureOverlay) {
                             combo.add(GESTURE_OVERLAY_VIEW);
-                            classNames.add(FQCN_GESTURE_OVERLAY_VIEW);
+                            classNames.add(Pair.<String, ViewElementDescriptor> of(
+                                    FQCN_GESTURE_OVERLAY_VIEW, null));
 
                             combo.add(SEPARATOR_LABEL);
-                            classNames.add(null);
+                            classNames.add(Pair.<String,ViewElementDescriptor>of(null, null));
                         }
                     }
 
@@ -286,14 +272,14 @@ class WrapInWizard extends VisualRefactoringWizard {
                         String className = d.getFullClassName();
                         if (exclude == null || !exclude.equals(className)) {
                             combo.add(d.getUiName());
-                            classNames.add(className);
+                            classNames.add(Pair.of(className, d));
                         }
                     }
                 }
             }
         } else {
             combo.add("SDK not initialized");
-            classNames.add(null);
+            classNames.add(Pair.<String,ViewElementDescriptor>of(null, null));
         }
 
         return classNames;
index a8e1f26..b18afa1 100644 (file)
@@ -15,9 +15,6 @@
  */
 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
 
-import static com.android.AndroidConstants.FD_RES_LAYOUT;
-import static com.android.sdklib.SdkConstants.FD_RES;
-
 import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
@@ -81,19 +78,31 @@ public class AdtProjectTest extends SdkTestCase {
         return sProject;
     }
 
-    protected IFile getTestFile(IProject project, String name) throws Exception {
-        IFolder resFolder = project.getFolder(FD_RES);
+    protected IFile getTestDataFile(IProject project, String name) throws Exception {
+        return getTestDataFile(project, name, name);
+    }
+
+    protected IFile getTestDataFile(IProject project, String sourceName,
+            String destPath) throws Exception {
+        String[] split = destPath.split("/"); //$NON-NLS-1$
+        assertTrue(split.length > 1);
+        IFolder folder = project.getFolder(split[0]);
         NullProgressMonitor monitor = new NullProgressMonitor();
-        if (!resFolder.exists()) {
-            resFolder.create(true /* force */, true /* local */, monitor);
+        if (!folder.exists()) {
+            folder.create(true /* force */, true /* local */, monitor);
         }
-        IFolder layoutfolder = resFolder.getFolder(FD_RES_LAYOUT);
-        if (!layoutfolder.exists()) {
-            layoutfolder.create(true /* force */, true /* local */, monitor);
+        for (int i = 1, n = split.length; i < n -1; i++) {
+            IFolder subFolder = folder.getFolder(split[i]);
+            if (!subFolder.exists()) {
+                subFolder.create(true /* force */, true /* local */, monitor);
+            }
+            folder = subFolder;
         }
-        IFile file = layoutfolder.getFile(name);
+
+        String name = split[split.length - 1];
+        IFile file = folder.getFile(name);
         if (!file.exists()) {
-            String xml = readTestFile(name, true);
+            String xml = readTestFile(sourceName, true);
             InputStream bstream = new ByteArrayInputStream(xml.getBytes("UTF-8")); //$NON-NLS-1$
             file.create(bstream, false /* force */, monitor);
         }
index 956fc5d..227e354 100644 (file)
@@ -65,7 +65,7 @@ public class ChangeLayoutRefactoringTest extends RefactoringTest {
     }
 
     private void checkRefactoring(String basename, boolean flatten) throws Exception {
-        IFile file = getTestFile(sProject, basename);
+        IFile file = getLayoutFile(sProject, basename);
         TestContext info = setupTestContext(file, basename);
         TestLayoutEditor layoutEditor = info.mLayoutEditor;
         CanvasViewInfo rootView = info.mRootView;
index 6cc92d5..610b48f 100644 (file)
@@ -26,20 +26,31 @@ import java.util.List;
 public class ChangeViewRefactoringTest extends RefactoringTest {
 
     public void testChangeView1() throws Exception {
-        checkRefactoring("sample1a.xml", "@+id/button1", "@+id/button6");
+        checkRefactoring("sample1a.xml", FQCN_RADIO_BUTTON, "@+id/button1", "@+id/button6");
     }
 
-    private void checkRefactoring(String basename, String... ids) throws Exception {
+    public void testChangeView2() throws Exception {
+        // Tests (1) updating references to the renamed id of the changed widgets
+        // (e.g. button3 is renamed to imageButton1 and layout references to button3
+        // must be updated), and (2) removal of attributes not available in the new type
+        // (the text property is removed since it is not available on the new widget
+        // type ImageButton)
+        checkRefactoring("sample2.xml", "android.widget.ImageButton",
+                "@+id/button3", "@+id/button5");
+    }
+
+    private void checkRefactoring(String basename, String newType,
+            String... ids) throws Exception {
         assertTrue(ids.length > 0);
 
-        IFile file = getTestFile(sProject, basename);
+        IFile file = getLayoutFile(sProject, basename);
         TestContext info = setupTestContext(file, basename);
         TestLayoutEditor layoutEditor = info.mLayoutEditor;
         List<Element> selectedElements = getElements(info.mElement, ids);
 
         ChangeViewRefactoring refactoring = new ChangeViewRefactoring(selectedElements,
                 layoutEditor);
-        refactoring.setType(FQCN_RADIO_BUTTON);
+        refactoring.setType(newType);
 
         List<Change> changes = refactoring.computeChanges();
         checkEdits(basename, changes);
index 0e96b34..203a0bb 100644 (file)
@@ -18,30 +18,73 @@ package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
 import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML;
 
 import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IPath;
 import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
 import org.w3c.dom.Element;
 
-import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class ExtractIncludeRefactoringTest extends RefactoringTest {
 
     public void testExtract1() throws Exception {
         // Basic: Extract a single button
-        checkRefactoring("sample3.xml", "newlayout1", true, true, "@+id/button2");
+        checkRefactoring("sample3.xml", "newlayout1", false, null, 2, "@+id/button2");
     }
 
     public void testExtract2() throws Exception {
         // Extract a couple of elements
-        checkRefactoring("sample3.xml", "newlayout2", true, true,
+        checkRefactoring("sample3.xml", "newlayout2",  false, null, 2,
                 "@+id/button2", "@+id/android_logo");
     }
 
+    public void testExtract3() throws Exception {
+        // Test to make sure layout attributes are updated
+        checkRefactoring("sample2.xml", "newlayout3", false, null, 2,
+                "@+id/button3");
+    }
+
+    public void testExtract4() throws Exception {
+        // Tests extracting from -multiple- files (as well as with custom android namespace
+        // prefix)
+
+        // Make sure the variation-files exist
+        Map<IPath, String> extraFiles = new HashMap<IPath, String>();
+        extraFiles.put(getTestDataFile(getProject(), "sample3-variation1.xml",
+                "res/layout-land/sample3.xml").getProjectRelativePath(),
+                "sample3-variation1.xml");
+        extraFiles.put(getTestDataFile(getProject(), "sample3-variation2.xml",
+                "res/layout-xlarge-land/sample3.xml").getProjectRelativePath(),
+                "sample3-variation2.xml");
+
+        checkRefactoring("sample3.xml", "newlayout3", true, extraFiles, 4,
+                "@+id/android_logo");
+    }
+
+    public void testExtract5() throws Exception {
+        // Tests extracting from multiple files with -contiguous regions-.
+
+        // Make sure the variation-files exist
+        Map<IPath, String> extraFiles = new HashMap<IPath, String>();
+        extraFiles.put(getTestDataFile(getProject(), "sample3-variation1.xml",
+                "res/layout-land/sample3.xml").getProjectRelativePath(),
+                "sample3-variation1.xml");
+        extraFiles.put(getTestDataFile(getProject(), "sample3-variation2.xml",
+                "res/layout-xlarge-land/sample3.xml").getProjectRelativePath(),
+                "sample3-variation2.xml");
+
+        checkRefactoring("sample3.xml", "newlayout3", true, extraFiles, 4,
+                "@+id/android_logo", "@+id/button1");
+    }
+
     private void checkRefactoring(String basename, String layoutName,
-            boolean updateRefs, boolean replaceOccurrences, String... ids) throws Exception {
+            boolean replaceOccurrences, Map<IPath,String> extraFiles,
+            int expectedModifiedFileCount, String... ids) throws Exception {
         assertTrue(ids.length > 0);
 
-        IFile file = getTestFile(sProject, basename);
+        IFile file = getLayoutFile(sProject, basename);
         TestContext info = setupTestContext(file, basename);
         TestLayoutEditor layoutEditor = info.mLayoutEditor;
         List<Element> selectedElements = getElements(info.mElement, ids);
@@ -49,16 +92,28 @@ public class ExtractIncludeRefactoringTest extends RefactoringTest {
         ExtractIncludeRefactoring refactoring = new ExtractIncludeRefactoring(selectedElements,
                 layoutEditor);
         refactoring.setLayoutName(layoutName);
-        refactoring.setUpdateReferences(updateRefs);
         refactoring.setReplaceOccurrences(replaceOccurrences);
         List<Change> changes = refactoring.computeChanges();
 
-        assertEquals(3, changes.size());
+        assertTrue(changes.size() >= 3);
+
+        Map<IPath,String> fileToGolden = new HashMap<IPath,String>();
+        IPath sourcePath = file.getProjectRelativePath();
+        fileToGolden.put(sourcePath, basename);
+        IPath newPath = sourcePath.removeLastSegments(1).append(layoutName + DOT_XML);
+        fileToGolden.put(newPath, layoutName + DOT_XML);
+        if (extraFiles != null) {
+            fileToGolden.putAll(extraFiles);
+        }
 
-        // The first change is to update the current file:
-        checkEdits(basename, Collections.singletonList(changes.get(0)));
+        checkEdits(changes, fileToGolden);
 
-        // The second change is to create the new extracted file
-        checkEdits(layoutName + DOT_XML, Collections.singletonList(changes.get(1)));
+        int modifiedFileCount = 0;
+        for (Change change : changes) {
+            if (change instanceof TextFileChange) {
+                modifiedFileCount++;
+            }
+        }
+        assertEquals(expectedModifiedFileCount, modifiedFileCount);
     }
 }
index 0f7b66a..372026c 100644 (file)
  */
 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
 
+import static com.android.AndroidConstants.FD_RES_LAYOUT;
 import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
 import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML;
+import static com.android.sdklib.SdkConstants.FD_RES;
 
 import com.android.ide.common.rendering.api.ViewInfo;
 import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
 import com.android.sdklib.SdkConstants;
 
 import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IPath;
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.Document;
 import org.eclipse.jface.text.IDocument;
@@ -47,17 +52,23 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 @SuppressWarnings("restriction")
 public class RefactoringTest extends AdtProjectTest {
+
+    protected IFile getLayoutFile(IProject project, String name) throws Exception {
+        return getTestDataFile(project, name, FD_RES + "/" + FD_RES_LAYOUT + "/" + name);
+    }
+
     protected static Element findElementById(Element root, String id) {
         if (id.equals(VisualRefactoring.getId(root))) {
             return root;
         }
 
-        for (Element child : RelativeLayoutConversionHelper.getChildren(root)) {
+        for (Element child : DomUtilities.getChildren(root)) {
             Element result = findElementById(child, id);
             if (result != null) {
                 return result;
@@ -106,6 +117,42 @@ public class RefactoringTest extends AdtProjectTest {
         assertEqualsGolden(basename, actual);
     }
 
+    protected void checkEdits(List<Change> changes,
+            Map<IPath, String> fileToGoldenName) throws BadLocationException,
+            IOException {
+        for (Change change : changes) {
+            if (change instanceof TextFileChange) {
+                TextFileChange tf = (TextFileChange) change;
+                IFile file = tf.getFile();
+                assertNotNull(file);
+                IPath path = file.getProjectRelativePath();
+                String goldenName = fileToGoldenName.get(path);
+                assertNotNull(goldenName);
+
+                String xml = readTestFile(goldenName, false);
+                if (xml == null) { // New file
+                    xml = ""; //$NON-NLS-1$
+                }
+                IDocument document = new Document();
+                document.set(xml);
+
+                TextEdit edit = tf.getEdit();
+                if (edit instanceof MultiTextEdit) {
+                    MultiTextEdit edits = (MultiTextEdit) edit;
+                    edits.apply(document);
+                } else {
+                    edit.apply(document);
+                }
+
+                String actual = document.get();
+                assertEqualsGolden(goldenName, actual);
+            } else {
+                System.out.println("Ignoring non-textfilechange in refactoring result");
+                assertNull(change.getAffectedObjects());
+            }
+        }
+    }
+
     protected void assertEqualsGolden(String basename, String actual) {
         String testName = getName();
         if (testName.startsWith("test")) {
@@ -145,7 +192,7 @@ public class RefactoringTest extends AdtProjectTest {
     }
 
     protected UiViewElementNode createModel(UiViewElementNode parent, Element element) {
-        List<Element> children = RelativeLayoutConversionHelper.getChildren(element);
+        List<Element> children = DomUtilities.getChildren(element);
         String fqcn = ANDROID_WIDGET_PREFIX + element.getTagName();
         boolean hasChildren = children.size() > 0;
         UiViewElementNode node = createNode(parent, fqcn, hasChildren);
index f8238e1..7c43443 100644 (file)
@@ -44,7 +44,7 @@ public class WrapInRefactoringTest extends RefactoringTest {
     private void checkRefactoring(String basename, String fqcn, String... ids) throws Exception {
         assertTrue(ids.length > 0);
 
-        IFile file = getTestFile(sProject, basename);
+        IFile file = getLayoutFile(sProject, basename);
         TestContext info = setupTestContext(file, basename);
         TestLayoutEditor layoutEditor = info.mLayoutEditor;
         List<Element> selectedElements = getElements(info.mElement, ids);
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract3.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract3.xml
new file mode 100644 (file)
index 0000000..6ba61c8
--- /dev/null
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Button xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:id="@+id/button3" android:layout_height="wrap_content" android:text="Button" ></Button>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract4.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract4.xml
new file mode 100644 (file)
index 0000000..ee8568e
--- /dev/null
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" />
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract5.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout3-expected-extract5.xml
new file mode 100644 (file)
index 0000000..be57066
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+    <ImageView android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" android:layout_weight="1.0" />
+</merge>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-changeView2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-changeView2.xml
new file mode 100644 (file)
index 0000000..5a55498
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+    <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+    <RelativeLayout android:layout_height="match_parent" android:id="@+id/relativeLayout1" android:layout_width="match_parent">
+        <Button android:layout_width="wrap_content" android:layout_alignParentLeft="true" android:id="@+id/button2" android:layout_height="wrap_content" android:text="Button"></Button>
+        <ImageButton android:layout_width="wrap_content" android:id="@+id/ImageButton1" android:layout_below="@+id/button2" android:layout_height="wrap_content" android:layout_toRightOf="@+id/button2"></ImageButton>
+        <Button android:layout_width="wrap_content" android:id="@+id/button4" android:layout_below="@+id/ImageButton1" android:layout_height="wrap_content" android:text="Button" android:layout_toRightOf="@+id/ImageButton1"></Button>
+        <CheckBox android:layout_width="wrap_content" android:layout_below="@+id/button4" android:id="@+id/checkBox1" android:layout_height="wrap_content" android:text="CheckBox" android:layout_toLeftOf="@+id/button4"></CheckBox>
+        <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/ImageButton2" android:layout_alignParentRight="true"></ImageButton>
+    </RelativeLayout>
+</LinearLayout>
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-extract3.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-extract3.xml
new file mode 100644 (file)
index 0000000..e4ff731
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+    <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+    <RelativeLayout android:layout_height="match_parent" android:id="@+id/relativeLayout1" android:layout_width="match_parent">
+        <Button android:layout_width="wrap_content" android:layout_alignParentLeft="true" android:id="@+id/button2" android:layout_height="wrap_content" android:text="Button"></Button>
+        <include layout="@layout/newlayout3" android:id="@+id/button3_ref"  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/button2" android:layout_toRightOf="@+id/button2"/>
+        <Button android:layout_width="wrap_content" android:id="@+id/button4" android:layout_below="@+id/button3_ref" android:layout_height="wrap_content" android:text="Button" android:layout_toRightOf="@+id/button3_ref"></Button>
+        <CheckBox android:layout_width="wrap_content" android:layout_below="@+id/button4" android:id="@+id/checkBox1" android:layout_height="wrap_content" android:text="CheckBox" android:layout_toLeftOf="@+id/button4"></CheckBox>
+        <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" android:id="@+id/button5" android:layout_alignParentRight="true"></Button>
+    </RelativeLayout>
+</LinearLayout>
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract4.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract4.xml
new file mode 100644 (file)
index 0000000..445f88a
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+    <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+    <include layout="@layout/newlayout3" android:id="@+id/android_logo_ref"  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1.0"/>
+    <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract5.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract5.xml
new file mode 100644 (file)
index 0000000..8df41ca
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+    <include layout="@layout/newlayout3"  android:layout_width="wrap_content" android:layout_height="wrap_content"/>
+    <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1-expected-extract4.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1-expected-extract4.xml
new file mode 100644 (file)
index 0000000..d616269
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+    <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+    <LinearLayout android:id="@+id/newlinear2" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+        <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+        <include layout="@layout/newlayout3" android:id="@+id/android_logo_ref"  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1.0"/>
+    </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1-expected-extract5.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1-expected-extract5.xml
new file mode 100644 (file)
index 0000000..b37e7be
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+    <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+    <LinearLayout android:id="@+id/newlinear2" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+        <include layout="@layout/newlayout3"  android:layout_width="wrap_content" android:layout_height="wrap_content"/>
+    </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation1.xml
new file mode 100644 (file)
index 0000000..9cf3c43
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+    <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+    <LinearLayout android:id="@+id/newlinear2" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+        <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+        <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" 
+         android:focusable="false" android:clickable="false" android:layout_weight="1.0" android:id="@+id/android_logo" />
+    </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2-expected-extract4.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2-expected-extract4.xml
new file mode 100644 (file)
index 0000000..40676ab
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:customprefix="http://schemas.android.com/apk/res/android" customprefix:id="@+id/newlinear" customprefix:orientation="vertical" customprefix:layout_width="match_parent" customprefix:layout_height="match_parent">
+    <include layout="@layout/newlayout3" customprefix:id="@+id/android_logo_ref"  customprefix:layout_width="wrap_content" customprefix:layout_height="wrap_content" customprefix:layout_weight="1.0"/>
+    <Button customprefix:text="Button" customprefix:id="@+id/button1" customprefix:layout_width="wrap_content" customprefix:layout_height="wrap_content"></Button>
+    <Button customprefix:text="Button" customprefix:id="@+id/button2" customprefix:layout_width="wrap_content" customprefix:layout_height="wrap_content"></Button>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2-expected-extract5.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2-expected-extract5.xml
new file mode 100644 (file)
index 0000000..c51dc37
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:customprefix="http://schemas.android.com/apk/res/android" customprefix:id="@+id/newlinear" customprefix:orientation="vertical" customprefix:layout_width="match_parent" customprefix:layout_height="match_parent">
+    <include layout="@layout/newlayout3"  customprefix:layout_width="wrap_content" customprefix:layout_height="wrap_content"/>
+    <Button customprefix:text="Button" customprefix:id="@+id/button2" customprefix:layout_width="wrap_content" customprefix:layout_height="wrap_content"></Button>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-variation2.xml
new file mode 100644 (file)
index 0000000..48d4790
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:customprefix="http://schemas.android.com/apk/res/android" customprefix:id="@+id/newlinear" customprefix:orientation="vertical" customprefix:layout_width="match_parent" customprefix:layout_height="match_parent">
+    <ImageView customprefix:id="@+id/android_logo" customprefix:layout_width="wrap_content" 
+         customprefix:layout_height="wrap_content" customprefix:src="@drawable/android_button" customprefix:focusable="false" customprefix:clickable="false" customprefix:layout_weight="1.0" />
+    <Button customprefix:text="Button" customprefix:id="@+id/button1" customprefix:layout_width="wrap_content" customprefix:layout_height="wrap_content"></Button>
+    <Button customprefix:text="Button" customprefix:id="@+id/button2" customprefix:layout_width="wrap_content" customprefix:layout_height="wrap_content"></Button>
+</LinearLayout>
+
index 7504bef..049e1cc 100644 (file)
  */
 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
 
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
 import junit.framework.TestCase;
 
 public class DomUtilitiesTest extends TestCase {
@@ -29,4 +37,83 @@ public class DomUtilitiesTest extends TestCase {
         assertEquals("foo&quot;b&apos;&apos;ar",
                 DomUtilities.toXmlAttributeValue("foo\"b''ar"));
     }
+
+    public void testIsEquivalent() throws Exception {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        factory.setValidating(false);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        Document document1 = builder.newDocument();
+        Document document2 = builder.newDocument();
+        document1.appendChild(document1.createElement("root"));
+        document2.appendChild(document2.createElement("root"));
+
+        assertFalse(DomUtilities.isEquivalent(null, null));
+        Element root1 = document1.getDocumentElement();
+        assertFalse(DomUtilities.isEquivalent(null, root1));
+        Element root2 = document2.getDocumentElement();
+        assertFalse(DomUtilities.isEquivalent(root2, null));
+        assertTrue(DomUtilities.isEquivalent(root1, root2));
+
+        root1.appendChild(document1.createTextNode("    "));
+        // Differences in text are NOT significant!
+        assertTrue(DomUtilities.isEquivalent(root1, root2));
+        root2.appendChild(document2.createTextNode("    "));
+        assertTrue(DomUtilities.isEquivalent(root1, root2));
+
+        Element foo1 = document1.createElement("foo");
+        Element foo2 = document2.createElement("foo");
+        root1.appendChild(foo1);
+        assertFalse(DomUtilities.isEquivalent(root1, root2));
+        root2.appendChild(foo2);
+        assertTrue(DomUtilities.isEquivalent(root1, root2));
+
+        root1.appendChild(document1.createElement("bar"));
+        assertFalse(DomUtilities.isEquivalent(root1, root2));
+        root2.appendChild(document2.createElement("bar"));
+        assertTrue(DomUtilities.isEquivalent(root1, root2));
+
+        // Add attributes in opposite order
+        foo1.setAttribute("attribute1", "value1");
+        foo1.setAttribute("attribute2", "value2");
+        assertFalse(DomUtilities.isEquivalent(root1, root2));
+        foo2.setAttribute("attribute2", "value2");
+        foo2.setAttribute("attribute1", "valueWrong");
+        assertFalse(DomUtilities.isEquivalent(root1, root2));
+        foo2.setAttribute("attribute1", "value1");
+        assertTrue(DomUtilities.isEquivalent(root1, root2));
+
+        // TODO - test different tag names
+        // TODO - test different name spaces!
+    }
+
+    public void testIsContiguous() throws Exception {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        factory.setValidating(false);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        Document document = builder.newDocument();
+        document.appendChild(document.createElement("root"));
+        Element root = document.getDocumentElement();
+        root.appendChild(document.createTextNode("    "));
+        Element foo = document.createElement("foo");
+        root.appendChild(foo);
+        root.appendChild(document.createTextNode("    "));
+        Element bar = document.createElement("bar");
+        root.appendChild(bar);
+        Element baz = document.createElement("baz");
+        root.appendChild(baz);
+
+        assertTrue(DomUtilities.isContiguous(Arrays.asList(foo)));
+        assertTrue(DomUtilities.isContiguous(Arrays.asList(foo, bar)));
+        assertTrue(DomUtilities.isContiguous(Arrays.asList(foo, bar, baz)));
+        assertTrue(DomUtilities.isContiguous(Arrays.asList(foo, bar, baz)));
+        assertTrue(DomUtilities.isContiguous(Arrays.asList(bar, baz, foo)));
+        assertTrue(DomUtilities.isContiguous(Arrays.asList(baz, bar, foo)));
+        assertTrue(DomUtilities.isContiguous(Arrays.asList(baz, foo, bar)));
+
+        assertFalse(DomUtilities.isContiguous(Arrays.asList(foo, baz)));
+        assertFalse(DomUtilities.isContiguous(Arrays.asList(root, baz)));
+    }
+
 }