Home | History | Annotate | Download | only in refactoring
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
     17 
     18 import static com.android.SdkConstants.ANDROID_URI;
     19 import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
     20 import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED;
     21 import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
     22 import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
     23 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
     24 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
     25 import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
     26 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
     27 import static com.android.SdkConstants.ATTR_ORIENTATION;
     28 import static com.android.SdkConstants.EXT_XML;
     29 import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
     30 import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
     31 import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
     32 import static com.android.SdkConstants.FQCN_RELATIVE_LAYOUT;
     33 import static com.android.SdkConstants.FQCN_TABLE_LAYOUT;
     34 import static com.android.SdkConstants.GESTURE_OVERLAY_VIEW;
     35 import static com.android.SdkConstants.LINEAR_LAYOUT;
     36 import static com.android.SdkConstants.TABLE_ROW;
     37 import static com.android.SdkConstants.VALUE_FALSE;
     38 import static com.android.SdkConstants.VALUE_VERTICAL;
     39 import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
     40 
     41 import com.android.SdkConstants;
     42 import com.android.annotations.NonNull;
     43 import com.android.annotations.VisibleForTesting;
     44 import com.android.ide.common.xml.XmlFormatStyle;
     45 import com.android.ide.eclipse.adt.AdtPlugin;
     46 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     47 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     48 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
     49 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
     50 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     51 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
     52 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
     53 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     54 
     55 import org.eclipse.core.resources.IFile;
     56 import org.eclipse.core.runtime.CoreException;
     57 import org.eclipse.core.runtime.IProgressMonitor;
     58 import org.eclipse.core.runtime.IStatus;
     59 import org.eclipse.core.runtime.OperationCanceledException;
     60 import org.eclipse.jface.text.ITextSelection;
     61 import org.eclipse.jface.viewers.ITreeSelection;
     62 import org.eclipse.ltk.core.refactoring.Change;
     63 import org.eclipse.ltk.core.refactoring.Refactoring;
     64 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
     65 import org.eclipse.ltk.core.refactoring.TextFileChange;
     66 import org.eclipse.text.edits.MalformedTreeException;
     67 import org.eclipse.text.edits.MultiTextEdit;
     68 import org.eclipse.text.edits.ReplaceEdit;
     69 import org.eclipse.text.edits.TextEdit;
     70 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     71 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     72 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     73 import org.w3c.dom.Attr;
     74 import org.w3c.dom.Element;
     75 import org.w3c.dom.NamedNodeMap;
     76 import org.w3c.dom.Node;
     77 import org.w3c.dom.NodeList;
     78 
     79 import java.util.ArrayList;
     80 import java.util.HashSet;
     81 import java.util.List;
     82 import java.util.Map;
     83 import java.util.Set;
     84 
     85 /**
     86  * Converts the selected layout into a layout of a different type.
     87  */
     88 @SuppressWarnings("restriction") // XML model
     89 public class ChangeLayoutRefactoring extends VisualRefactoring {
     90     private static final String KEY_TYPE = "type";       //$NON-NLS-1$
     91     private static final String KEY_FLATTEN = "flatten"; //$NON-NLS-1$
     92 
     93     private String mTypeFqcn;
     94     private String mInitializedAttributes;
     95     private boolean mFlatten;
     96 
     97     /**
     98      * This constructor is solely used by {@link Descriptor},
     99      * to replay a previous refactoring.
    100      * @param arguments argument map created by #createArgumentMap.
    101      */
    102     ChangeLayoutRefactoring(Map<String, String> arguments) {
    103         super(arguments);
    104         mTypeFqcn = arguments.get(KEY_TYPE);
    105         mFlatten = Boolean.parseBoolean(arguments.get(KEY_FLATTEN));
    106     }
    107 
    108     @VisibleForTesting
    109     ChangeLayoutRefactoring(List<Element> selectedElements, LayoutEditorDelegate delegate) {
    110         super(selectedElements, delegate);
    111     }
    112 
    113     public ChangeLayoutRefactoring(
    114             IFile file,
    115             LayoutEditorDelegate delegate,
    116             ITextSelection selection,
    117             ITreeSelection treeSelection) {
    118         super(file, delegate, selection, treeSelection);
    119     }
    120 
    121     @Override
    122     public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
    123             OperationCanceledException {
    124         RefactoringStatus status = new RefactoringStatus();
    125 
    126         try {
    127             pm.beginTask("Checking preconditions...", 2);
    128 
    129             if (mSelectionStart == -1 || mSelectionEnd == -1) {
    130                 status.addFatalError("No selection to convert");
    131                 return status;
    132             }
    133 
    134             if (mElements.size() != 1) {
    135                 status.addFatalError("Select precisely one layout to convert");
    136                 return status;
    137             }
    138 
    139             pm.worked(1);
    140             return status;
    141 
    142         } finally {
    143             pm.done();
    144         }
    145     }
    146 
    147     @Override
    148     protected VisualRefactoringDescriptor createDescriptor() {
    149         String comment = getName();
    150         return new Descriptor(
    151                 mProject.getName(), //project
    152                 comment, //description
    153                 comment, //comment
    154                 createArgumentMap());
    155     }
    156 
    157     @Override
    158     protected Map<String, String> createArgumentMap() {
    159         Map<String, String> args = super.createArgumentMap();
    160         args.put(KEY_TYPE, mTypeFqcn);
    161         args.put(KEY_FLATTEN, Boolean.toString(mFlatten));
    162 
    163         return args;
    164     }
    165 
    166     @Override
    167     public String getName() {
    168         return "Change Layout";
    169     }
    170 
    171     void setType(String typeFqcn) {
    172         mTypeFqcn = typeFqcn;
    173     }
    174 
    175     void setInitializedAttributes(String initializedAttributes) {
    176         mInitializedAttributes = initializedAttributes;
    177     }
    178 
    179     void setFlatten(boolean flatten) {
    180         mFlatten = flatten;
    181     }
    182 
    183     @Override
    184     protected List<Element> initElements() {
    185         List<Element> elements = super.initElements();
    186 
    187         // Don't convert a root GestureOverlayView; convert its child. This looks for
    188         // gesture overlays, and if found, it generates a new child list where the gesture
    189         // overlay children are replaced by their first element children
    190         for (Element element : elements) {
    191             String tagName = element.getTagName();
    192             if (tagName.equals(GESTURE_OVERLAY_VIEW)
    193                     || tagName.equals(FQCN_GESTURE_OVERLAY_VIEW)) {
    194                 List<Element> replacement = new ArrayList<Element>(elements.size());
    195                 for (Element e : elements) {
    196                     tagName = e.getTagName();
    197                     if (tagName.equals(GESTURE_OVERLAY_VIEW)
    198                             || tagName.equals(FQCN_GESTURE_OVERLAY_VIEW)) {
    199                         NodeList children = e.getChildNodes();
    200                         Element first = null;
    201                         for (int i = 0, n = children.getLength(); i < n; i++) {
    202                             Node node = children.item(i);
    203                             if (node.getNodeType() == Node.ELEMENT_NODE) {
    204                                 first = (Element) node;
    205                                 break;
    206                             }
    207                         }
    208                         if (first != null) {
    209                             e = first;
    210                         }
    211                     }
    212                     replacement.add(e);
    213                 }
    214                 return replacement;
    215             }
    216         }
    217 
    218         return elements;
    219     }
    220 
    221     @Override
    222     protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
    223         String name = getViewClass(mTypeFqcn);
    224 
    225         IFile file = mDelegate.getEditor().getInputFile();
    226         List<Change> changes = new ArrayList<Change>();
    227         if (file == null) {
    228             return changes;
    229         }
    230         TextFileChange change = new TextFileChange(file.getName(), file);
    231         MultiTextEdit rootEdit = new MultiTextEdit();
    232         change.setTextType(EXT_XML);
    233         changes.add(change);
    234 
    235         String text = getText(mSelectionStart, mSelectionEnd);
    236         Element layout = getPrimaryElement();
    237         String oldName = layout.getNodeName();
    238         int open = text.indexOf(oldName);
    239         int close = text.lastIndexOf(oldName);
    240 
    241         if (open != -1 && close != -1) {
    242             int oldLength = oldName.length();
    243             rootEdit.addChild(new ReplaceEdit(mSelectionStart + open, oldLength, name));
    244             if (close != open) { // Gracefully handle <FooLayout/>
    245                 rootEdit.addChild(new ReplaceEdit(mSelectionStart + close, oldLength, name));
    246             }
    247         }
    248 
    249         String oldId = getId(layout);
    250         String newId = ensureIdMatchesType(layout, mTypeFqcn, rootEdit);
    251         // Update any layout references to the old id with the new id
    252         if (oldId != null && newId != null) {
    253             IStructuredModel model = mDelegate.getEditor().getModelForRead();
    254             try {
    255                 IStructuredDocument doc = model.getStructuredDocument();
    256                 if (doc != null) {
    257                     List<TextEdit> replaceIds = replaceIds(getAndroidNamespacePrefix(), doc,
    258                             mSelectionStart,
    259                             mSelectionEnd, oldId, newId);
    260                     for (TextEdit edit : replaceIds) {
    261                         rootEdit.addChild(edit);
    262                     }
    263                 }
    264             } finally {
    265                 model.releaseFromRead();
    266             }
    267         }
    268 
    269         String oldType = getOldType();
    270         String newType = mTypeFqcn;
    271 
    272         if (newType.equals(FQCN_RELATIVE_LAYOUT)) {
    273             if (oldType.equals(FQCN_LINEAR_LAYOUT) && !mFlatten) {
    274                 // Hand-coded conversion specifically tailored for linear to relative, provided
    275                 // there is no hierarchy flattening
    276                 // TODO: use the RelativeLayoutConversionHelper for this; it does a better job
    277                 // analyzing gravities etc.
    278                 convertLinearToRelative(rootEdit);
    279                 removeUndefinedAttrs(rootEdit, layout);
    280                 addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, null);
    281             } else {
    282                 // Generic conversion to relative - can also flatten the hierarchy
    283                 convertAnyToRelative(rootEdit, oldType, newType);
    284                 // This already handles removing undefined layout attributes -- right?
    285                 //removeUndefinedLayoutAttrs(rootEdit, layout);
    286             }
    287         } else if (newType.equals(FQCN_GRID_LAYOUT)) {
    288             convertAnyToGridLayout(rootEdit);
    289             // Layout attributes on children have already been removed as part of conversion
    290             // during the flattening
    291             removeUndefinedAttrs(rootEdit, layout, false /*removeLayoutAttrs*/);
    292         } else if (oldType.equals(FQCN_RELATIVE_LAYOUT) && newType.equals(FQCN_LINEAR_LAYOUT)) {
    293             convertRelativeToLinear(rootEdit);
    294             removeUndefinedAttrs(rootEdit, layout);
    295             addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, null);
    296         } else if (oldType.equals(FQCN_LINEAR_LAYOUT) && newType.equals(FQCN_TABLE_LAYOUT)) {
    297             convertLinearToTable(rootEdit);
    298             removeUndefinedAttrs(rootEdit, layout);
    299             addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, null);
    300         } else {
    301             convertGeneric(rootEdit, oldType, newType, layout);
    302         }
    303 
    304         if (mInitializedAttributes != null && mInitializedAttributes.length() > 0) {
    305             String namespace = getAndroidNamespacePrefix();
    306             for (String s : mInitializedAttributes.split(",")) { //$NON-NLS-1$
    307                 String[] nameValue = s.split("="); //$NON-NLS-1$
    308                 String attribute = nameValue[0];
    309                 String value = nameValue[1];
    310                 String prefix = null;
    311                 String namespaceUri = null;
    312                 if (attribute.startsWith(SdkConstants.ANDROID_NS_NAME_PREFIX)) {
    313                     prefix = namespace;
    314                     namespaceUri = ANDROID_URI;
    315                     attribute = attribute.substring(SdkConstants.ANDROID_NS_NAME_PREFIX.length());
    316                 }
    317                 setAttribute(rootEdit, layout, namespaceUri,
    318                         prefix, attribute, value);
    319             }
    320         }
    321 
    322         if (AdtPrefs.getPrefs().getFormatGuiXml()) {
    323             MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
    324             if (formatted != null) {
    325                 rootEdit = formatted;
    326             }
    327         }
    328         change.setEdit(rootEdit);
    329 
    330         return changes;
    331     }
    332 
    333     /** Checks whether we need to add any missing attributes on the elements */
    334     private void addMissingWrapContentAttributes(MultiTextEdit rootEdit, Element layout,
    335             String oldType, String newType, Set<Element> skip) {
    336         if (oldType.equals(FQCN_GRID_LAYOUT) && !newType.equals(FQCN_GRID_LAYOUT)) {
    337             String namespace = getAndroidNamespacePrefix();
    338 
    339             for (Element child : DomUtilities.getChildren(layout)) {
    340                 if (skip != null && skip.contains(child)) {
    341                     continue;
    342                 }
    343 
    344                 if (!child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) {
    345                     setAttribute(rootEdit, child, ANDROID_URI,
    346                             namespace, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
    347                 }
    348                 if (!child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) {
    349                     setAttribute(rootEdit, child, ANDROID_URI,
    350                             namespace, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
    351                 }
    352             }
    353         }
    354     }
    355 
    356     /** Hand coded conversion from a LinearLayout to a TableLayout */
    357     private void convertLinearToTable(MultiTextEdit rootEdit) {
    358         // This is pretty easy; just switch the root tag (already done by the initial generic
    359         // conversion) and then convert all the children into <TableRow> elements.
    360         // Finally, get rid of the orientation attribute, if any.
    361         Element layout = getPrimaryElement();
    362         removeOrientationAttribute(rootEdit, layout);
    363 
    364         NodeList children = layout.getChildNodes();
    365         for (int i = 0, n = children.getLength(); i < n; i++) {
    366             Node node = children.item(i);
    367             if (node.getNodeType() == Node.ELEMENT_NODE) {
    368                 Element child = (Element) node;
    369                 if (node instanceof IndexedRegion) {
    370                     IndexedRegion region = (IndexedRegion) node;
    371                     int start = region.getStartOffset();
    372                     int end = region.getEndOffset();
    373                     String text = getText(start, end);
    374                     String oldName = child.getNodeName();
    375                     if (oldName.equals(LINEAR_LAYOUT)) {
    376                         removeOrientationAttribute(rootEdit, child);
    377                         int open = text.indexOf(oldName);
    378                         int close = text.lastIndexOf(oldName);
    379 
    380                         if (open != -1 && close != -1) {
    381                             int oldLength = oldName.length();
    382                             rootEdit.addChild(new ReplaceEdit(mSelectionStart + open, oldLength,
    383                                     TABLE_ROW));
    384                             if (close != open) { // Gracefully handle <FooLayout/>
    385                                 rootEdit.addChild(new ReplaceEdit(mSelectionStart + close,
    386                                         oldLength, TABLE_ROW));
    387                             }
    388                         }
    389                     } // else: WRAP in TableLayout!
    390                 }
    391             }
    392         }
    393     }
    394 
    395      /** Hand coded conversion from a LinearLayout to a RelativeLayout */
    396     private void convertLinearToRelative(MultiTextEdit rootEdit) {
    397         // This can be done accurately.
    398         Element layout = getPrimaryElement();
    399         // Horizontal is the default, so if no value is specified it is horizontal.
    400         boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
    401                 ATTR_ORIENTATION));
    402 
    403         String attributePrefix = getAndroidNamespacePrefix();
    404 
    405         // TODO: Consider gravity of each element
    406         // TODO: Consider weight of each element
    407         // Right now it simply makes a single attachment to keep the order.
    408 
    409         if (isVertical) {
    410             // Align each child to the bottom and left of its parent
    411             NodeList children = layout.getChildNodes();
    412             String prevId = null;
    413             for (int i = 0, n = children.getLength(); i < n; i++) {
    414                 Node node = children.item(i);
    415                 if (node.getNodeType() == Node.ELEMENT_NODE) {
    416                     Element child = (Element) node;
    417                     String id = ensureHasId(rootEdit, child, null);
    418                     if (prevId != null) {
    419                         setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
    420                                 ATTR_LAYOUT_BELOW, prevId);
    421                     }
    422                     prevId = id;
    423                 }
    424             }
    425         } else {
    426             // Align each child to the left
    427             NodeList children = layout.getChildNodes();
    428             boolean isBaselineAligned =
    429                 !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED));
    430 
    431             String prevId = null;
    432             for (int i = 0, n = children.getLength(); i < n; i++) {
    433                 Node node = children.item(i);
    434                 if (node.getNodeType() == Node.ELEMENT_NODE) {
    435                     Element child = (Element) node;
    436                     String id = ensureHasId(rootEdit, child, null);
    437                     if (prevId != null) {
    438                         setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
    439                                 ATTR_LAYOUT_TO_RIGHT_OF, prevId);
    440                         if (isBaselineAligned) {
    441                             setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
    442                                     ATTR_LAYOUT_ALIGN_BASELINE, prevId);
    443                         }
    444                     }
    445                     prevId = id;
    446                 }
    447             }
    448         }
    449     }
    450 
    451     /** Strips out the android:orientation attribute from the given linear layout element */
    452     private void removeOrientationAttribute(MultiTextEdit rootEdit, Element layout) {
    453         assert layout.getTagName().equals(LINEAR_LAYOUT);
    454         removeAttribute(rootEdit, layout, ANDROID_URI, ATTR_ORIENTATION);
    455     }
    456 
    457     /**
    458      * Hand coded conversion from a RelativeLayout to a LinearLayout
    459      *
    460      * @param rootEdit the root multi text edit to add edits to
    461      */
    462     private void convertRelativeToLinear(MultiTextEdit rootEdit) {
    463         // This is going to be lossy...
    464         // TODO: Attempt to "order" the items based on their visual positions
    465         // and insert them in that order in the LinearLayout.
    466         // TODO: Possibly use nesting if necessary, by spatial subdivision,
    467         // to accomplish roughly the same layout as the relative layout specifies.
    468     }
    469 
    470     /**
    471      * Hand coded -generic- conversion from one layout to another. This is not going to be
    472      * an accurate layout transformation; instead it simply migrates the layout attributes
    473      * that are supported, and adds defaults for any new required layout attributes. In
    474      * addition, it attempts to order the children visually based on where they fit in a
    475      * rendering. (Unsupported layout attributes will be removed by the caller at the
    476      * end.)
    477      * <ul>
    478      * <li>Try to handle nesting. Converting a *hierarchy* of layouts into a flatter
    479      * layout for powerful layouts that support it, like RelativeLayout.
    480      * <li>Try to do automatic "inference" about the layout. I can render it and look at
    481      * the ViewInfo positions and sizes. I can render it multiple times, at different
    482      * sizes, to infer "stretchiness" and "weight" properties of the children.
    483      * <li>Try to do indirect transformations. E.g. if I can go from A to B, and B to C,
    484      * then an attempt to go from A to C should perform conversions A to B and then B to
    485      * C.
    486      * </ul>
    487      *
    488      * @param rootEdit the root multi text edit to add edits to
    489      * @param oldType the fully qualified class name of the layout type we are converting
    490      *            from
    491      * @param newType the fully qualified class name of the layout type we are converting
    492      *            to
    493      * @param layout the layout to be converted
    494      */
    495     private void convertGeneric(MultiTextEdit rootEdit, String oldType, String newType,
    496             Element layout) {
    497         // TODO: Add hooks for 3rd party conversions getting registered through the
    498         // IViewRule interface.
    499 
    500         // For now we simply go with the default behavior, which is to just strip the
    501         // layout attributes that aren't supported.
    502         removeUndefinedAttrs(rootEdit, layout);
    503         addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, null);
    504     }
    505 
    506     /**
    507      * Removes all the unavailable attributes after a conversion, both on the
    508      * layout element itself as well as the layout attributes of any of the
    509      * children
    510      */
    511     private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout) {
    512         removeUndefinedAttrs(rootEdit, layout, true /*removeLayoutAttrs*/);
    513     }
    514 
    515     private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout,
    516             boolean removeLayoutAttrs) {
    517         ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn);
    518         if (descriptor == null) {
    519             return;
    520         }
    521 
    522         if (removeLayoutAttrs) {
    523             Set<String> defined = new HashSet<String>();
    524             AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes();
    525             for (AttributeDescriptor attribute : layoutAttributes) {
    526                 defined.add(attribute.getXmlLocalName());
    527             }
    528 
    529             NodeList children = layout.getChildNodes();
    530             for (int i = 0, n = children.getLength(); i < n; i++) {
    531                 Node node = children.item(i);
    532                 if (node.getNodeType() == Node.ELEMENT_NODE) {
    533                     Element child = (Element) node;
    534 
    535                     List<Attr> attributes = findLayoutAttributes(child);
    536                     for (Attr attribute : attributes) {
    537                         String name = attribute.getLocalName();
    538                         if (!defined.contains(name)) {
    539                             // Remove it
    540                             try {
    541                                 removeAttribute(rootEdit, child, attribute.getNamespaceURI(), name);
    542                             } catch (MalformedTreeException mte) {
    543                                 // Sometimes refactoring has modified attribute; not removing
    544                                 // it is non-fatal so just warn instead of letting refactoring
    545                                 // operation abort
    546                                 AdtPlugin.log(IStatus.WARNING,
    547                                         "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$
    548                                         "already modified during refactoring?", //$NON-NLS-1$
    549                                         attribute.getLocalName());
    550                             }
    551                         }
    552                     }
    553                 }
    554             }
    555         }
    556 
    557         // Also remove the unavailable attributes (not layout attributes) on the
    558         // converted element
    559         Set<String> defined = new HashSet<String>();
    560         AttributeDescriptor[] attributes = descriptor.getAttributes();
    561         for (AttributeDescriptor attribute : attributes) {
    562             defined.add(attribute.getXmlLocalName());
    563         }
    564 
    565         // Remove undefined attributes on the layout element itself
    566         NamedNodeMap attributeMap = layout.getAttributes();
    567         for (int i = 0, n = attributeMap.getLength(); i < n; i++) {
    568             Node attributeNode = attributeMap.item(i);
    569 
    570             String name = attributeNode.getLocalName();
    571             if (!name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
    572                     && ANDROID_URI.equals(attributeNode.getNamespaceURI())) {
    573                 if (!defined.contains(name)) {
    574                     // Remove it
    575                     removeAttribute(rootEdit, layout, ANDROID_URI, name);
    576                 }
    577             }
    578         }
    579     }
    580 
    581     /** Hand coded conversion from any layout to a RelativeLayout */
    582     private void convertAnyToRelative(MultiTextEdit rootEdit, String oldType, String newType) {
    583         // To perform a conversion from any other layout type, including nested conversion,
    584         Element layout = getPrimaryElement();
    585         CanvasViewInfo rootView = mRootView;
    586         if (rootView == null) {
    587             LayoutCanvas canvas = mDelegate.getGraphicalEditor().getCanvasControl();
    588             ViewHierarchy viewHierarchy = canvas.getViewHierarchy();
    589             rootView = viewHierarchy.getRoot();
    590         }
    591 
    592         RelativeLayoutConversionHelper helper =
    593             new RelativeLayoutConversionHelper(this, layout, mFlatten, rootEdit, rootView);
    594         helper.convertToRelative();
    595         List<Element> deletedElements = helper.getDeletedElements();
    596         Set<Element> deleted = null;
    597         if (deletedElements != null && deletedElements.size() > 0) {
    598             deleted = new HashSet<Element>(deletedElements);
    599         }
    600         addMissingWrapContentAttributes(rootEdit, layout, oldType, newType, deleted);
    601     }
    602 
    603     /** Hand coded conversion from any layout to a GridLayout */
    604     private void convertAnyToGridLayout(MultiTextEdit rootEdit) {
    605         // To perform a conversion from any other layout type, including nested conversion,
    606         Element layout = getPrimaryElement();
    607         CanvasViewInfo rootView = mRootView;
    608         if (rootView == null) {
    609             LayoutCanvas canvas = mDelegate.getGraphicalEditor().getCanvasControl();
    610             ViewHierarchy viewHierarchy = canvas.getViewHierarchy();
    611             rootView = viewHierarchy.getRoot();
    612         }
    613 
    614         GridLayoutConverter converter = new GridLayoutConverter(this, layout, mFlatten,
    615                 rootEdit, rootView);
    616         converter.convertToGridLayout();
    617     }
    618 
    619     public static class Descriptor extends VisualRefactoringDescriptor {
    620         public Descriptor(String project, String description, String comment,
    621                 Map<String, String> arguments) {
    622             super("com.android.ide.eclipse.adt.refactoring.convert", //$NON-NLS-1$
    623                     project, description, comment, arguments);
    624         }
    625 
    626         @Override
    627         protected Refactoring createRefactoring(Map<String, String> args) {
    628             return new ChangeLayoutRefactoring(args);
    629         }
    630     }
    631 
    632     String getOldType() {
    633         Element primary = getPrimaryElement();
    634         if (primary != null) {
    635             String oldType = primary.getTagName();
    636             if (oldType.indexOf('.') == -1) {
    637                 oldType = ANDROID_WIDGET_PREFIX + oldType;
    638             }
    639             return oldType;
    640         }
    641 
    642         return null;
    643     }
    644 
    645     @VisibleForTesting
    646     protected CanvasViewInfo mRootView;
    647 
    648     @VisibleForTesting
    649     public void setRootView(CanvasViewInfo rootView) {
    650         mRootView = rootView;
    651     }
    652 
    653     @Override
    654     VisualRefactoringWizard createWizard() {
    655         return new ChangeLayoutWizard(this, mDelegate);
    656     }
    657 }
    658