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 */
16package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
17
18import static com.android.SdkConstants.ANDROID_URI;
19import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
20import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED;
21import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
22import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
23import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
24import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
25import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
26import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
27import static com.android.SdkConstants.ATTR_ORIENTATION;
28import static com.android.SdkConstants.EXT_XML;
29import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
30import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
31import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
32import static com.android.SdkConstants.FQCN_RELATIVE_LAYOUT;
33import static com.android.SdkConstants.FQCN_TABLE_LAYOUT;
34import static com.android.SdkConstants.GESTURE_OVERLAY_VIEW;
35import static com.android.SdkConstants.LINEAR_LAYOUT;
36import static com.android.SdkConstants.TABLE_ROW;
37import static com.android.SdkConstants.VALUE_FALSE;
38import static com.android.SdkConstants.VALUE_VERTICAL;
39import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
40
41import com.android.SdkConstants;
42import com.android.annotations.NonNull;
43import com.android.annotations.VisibleForTesting;
44import com.android.ide.common.xml.XmlFormatStyle;
45import com.android.ide.eclipse.adt.AdtPlugin;
46import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
47import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
48import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
49import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
50import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
51import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
52import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
53import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
54
55import org.eclipse.core.resources.IFile;
56import org.eclipse.core.runtime.CoreException;
57import org.eclipse.core.runtime.IProgressMonitor;
58import org.eclipse.core.runtime.IStatus;
59import org.eclipse.core.runtime.OperationCanceledException;
60import org.eclipse.jface.text.ITextSelection;
61import org.eclipse.jface.viewers.ITreeSelection;
62import org.eclipse.ltk.core.refactoring.Change;
63import org.eclipse.ltk.core.refactoring.Refactoring;
64import org.eclipse.ltk.core.refactoring.RefactoringStatus;
65import org.eclipse.ltk.core.refactoring.TextFileChange;
66import org.eclipse.text.edits.MalformedTreeException;
67import org.eclipse.text.edits.MultiTextEdit;
68import org.eclipse.text.edits.ReplaceEdit;
69import org.eclipse.text.edits.TextEdit;
70import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
71import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
72import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
73import org.w3c.dom.Attr;
74import org.w3c.dom.Element;
75import org.w3c.dom.NamedNodeMap;
76import org.w3c.dom.Node;
77import org.w3c.dom.NodeList;
78
79import java.util.ArrayList;
80import java.util.HashSet;
81import java.util.List;
82import java.util.Map;
83import java.util.Set;
84
85/**
86 * Converts the selected layout into a layout of a different type.
87 */
88@SuppressWarnings("restriction") // XML model
89public 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