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      */
ChangeLayoutRefactoring(Map<String, String> arguments)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
ChangeLayoutRefactoring(List<Element> selectedElements, LayoutEditorDelegate delegate)109     ChangeLayoutRefactoring(List<Element> selectedElements, LayoutEditorDelegate delegate) {
110         super(selectedElements, delegate);
111     }
112 
ChangeLayoutRefactoring( IFile file, LayoutEditorDelegate delegate, ITextSelection selection, ITreeSelection treeSelection)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
checkInitialConditions(IProgressMonitor pm)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
createDescriptor()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
createArgumentMap()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
getName()167     public String getName() {
168         return "Change Layout";
169     }
170 
setType(String typeFqcn)171     void setType(String typeFqcn) {
172         mTypeFqcn = typeFqcn;
173     }
174 
setInitializedAttributes(String initializedAttributes)175     void setInitializedAttributes(String initializedAttributes) {
176         mInitializedAttributes = initializedAttributes;
177     }
178 
setFlatten(boolean flatten)179     void setFlatten(boolean flatten) {
180         mFlatten = flatten;
181     }
182 
183     @Override
initElements()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
computeChanges(IProgressMonitor monitor)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 */
addMissingWrapContentAttributes(MultiTextEdit rootEdit, Element layout, String oldType, String newType, Set<Element> skip)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 */
convertLinearToTable(MultiTextEdit rootEdit)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 */
convertLinearToRelative(MultiTextEdit rootEdit)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 */
removeOrientationAttribute(MultiTextEdit rootEdit, Element layout)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      */
convertRelativeToLinear(MultiTextEdit rootEdit)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      */
convertGeneric(MultiTextEdit rootEdit, String oldType, String newType, Element layout)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      */
removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout)511     private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout) {
512         removeUndefinedAttrs(rootEdit, layout, true /*removeLayoutAttrs*/);
513     }
514 
removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout, boolean removeLayoutAttrs)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 */
convertAnyToRelative(MultiTextEdit rootEdit, String oldType, String newType)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 */
convertAnyToGridLayout(MultiTextEdit rootEdit)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 {
Descriptor(String project, String description, String comment, Map<String, String> arguments)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
createRefactoring(Map<String, String> args)627         protected Refactoring createRefactoring(Map<String, String> args) {
628             return new ChangeLayoutRefactoring(args);
629         }
630     }
631 
getOldType()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
setRootView(CanvasViewInfo rootView)649     public void setRootView(CanvasViewInfo rootView) {
650         mRootView = rootView;
651     }
652 
653     @Override
createWizard()654     VisualRefactoringWizard createWizard() {
655         return new ChangeLayoutWizard(this, mDelegate);
656     }
657 }
658