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