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.AndroidConstants.FD_RES_LAYOUT; 19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME; 20 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 25 import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; 26 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; 27 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; 28 import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; 29 import static com.android.ide.eclipse.adt.AdtConstants.EXT_XML; 30 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; 31 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS; 32 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_COLON; 33 import static com.android.resources.ResourceType.LAYOUT; 34 import static com.android.sdklib.SdkConstants.FD_RES; 35 36 import com.android.AndroidConstants; 37 import com.android.annotations.VisibleForTesting; 38 import com.android.ide.eclipse.adt.AdtConstants; 39 import com.android.ide.eclipse.adt.AdtPlugin; 40 import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; 41 import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; 42 import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; 43 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 44 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 45 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; 46 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 47 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 48 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 49 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; 50 import com.android.sdklib.SdkConstants; 51 52 import org.eclipse.core.resources.IContainer; 53 import org.eclipse.core.resources.IFile; 54 import org.eclipse.core.resources.IFolder; 55 import org.eclipse.core.resources.IProject; 56 import org.eclipse.core.resources.IResource; 57 import org.eclipse.core.runtime.CoreException; 58 import org.eclipse.core.runtime.IPath; 59 import org.eclipse.core.runtime.IProgressMonitor; 60 import org.eclipse.core.runtime.OperationCanceledException; 61 import org.eclipse.core.runtime.Path; 62 import org.eclipse.jface.dialogs.IInputValidator; 63 import org.eclipse.jface.text.ITextSelection; 64 import org.eclipse.jface.viewers.ITreeSelection; 65 import org.eclipse.ltk.core.refactoring.Change; 66 import org.eclipse.ltk.core.refactoring.NullChange; 67 import org.eclipse.ltk.core.refactoring.Refactoring; 68 import org.eclipse.ltk.core.refactoring.RefactoringStatus; 69 import org.eclipse.ltk.core.refactoring.TextFileChange; 70 import org.eclipse.swt.widgets.Display; 71 import org.eclipse.text.edits.InsertEdit; 72 import org.eclipse.text.edits.MultiTextEdit; 73 import org.eclipse.text.edits.ReplaceEdit; 74 import org.eclipse.text.edits.TextEdit; 75 import org.eclipse.ui.IWorkbenchPage; 76 import org.eclipse.wst.sse.core.StructuredModelManager; 77 import org.eclipse.wst.sse.core.internal.provisional.IModelManager; 78 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 79 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 80 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 81 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; 82 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; 83 import org.w3c.dom.Attr; 84 import org.w3c.dom.Document; 85 import org.w3c.dom.Element; 86 import org.w3c.dom.NamedNodeMap; 87 import org.w3c.dom.Node; 88 89 import java.io.IOException; 90 import java.util.ArrayList; 91 import java.util.List; 92 import java.util.Locale; 93 import java.util.Map; 94 95 /** 96 * Extracts the selection and writes it out as a separate layout file, then adds an 97 * include to that new layout file. Interactively asks the user for a new name for the 98 * layout. 99 */ 100 @SuppressWarnings("restriction") // XML model 101 public class ExtractIncludeRefactoring extends VisualRefactoring { 102 private static final String KEY_NAME = "name"; //$NON-NLS-1$ 103 private static final String KEY_OCCURRENCES = "all-occurrences"; //$NON-NLS-1$ 104 private String mLayoutName; 105 private boolean mReplaceOccurrences; 106 107 /** 108 * This constructor is solely used by {@link Descriptor}, 109 * to replay a previous refactoring. 110 * @param arguments argument map created by #createArgumentMap. 111 */ 112 ExtractIncludeRefactoring(Map<String, String> arguments) { 113 super(arguments); 114 mLayoutName = arguments.get(KEY_NAME); 115 mReplaceOccurrences = Boolean.parseBoolean(arguments.get(KEY_OCCURRENCES)); 116 } 117 118 public ExtractIncludeRefactoring( 119 IFile file, 120 LayoutEditorDelegate delegate, 121 ITextSelection selection, 122 ITreeSelection treeSelection) { 123 super(file, delegate, selection, treeSelection); 124 } 125 126 @VisibleForTesting 127 ExtractIncludeRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) { 128 super(selectedElements, editor); 129 } 130 131 @Override 132 public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, 133 OperationCanceledException { 134 RefactoringStatus status = new RefactoringStatus(); 135 136 try { 137 pm.beginTask("Checking preconditions...", 6); 138 139 if (mSelectionStart == -1 || mSelectionEnd == -1) { 140 status.addFatalError("No selection to extract"); 141 return status; 142 } 143 144 // Make sure the selection is contiguous 145 if (mTreeSelection != null) { 146 // TODO - don't do this if we based the selection on text. In this case, 147 // make sure we're -balanced-. 148 List<CanvasViewInfo> infos = getSelectedViewInfos(); 149 if (!validateNotEmpty(infos, status)) { 150 return status; 151 } 152 153 if (!validateNotRoot(infos, status)) { 154 return status; 155 } 156 157 // Disable if you've selected a single include tag 158 if (infos.size() == 1) { 159 UiViewElementNode uiNode = infos.get(0).getUiViewNode(); 160 if (uiNode != null) { 161 Node xmlNode = uiNode.getXmlNode(); 162 if (xmlNode.getLocalName().equals(LayoutDescriptors.VIEW_INCLUDE)) { 163 status.addWarning("No point in refactoring a single include tag"); 164 } 165 } 166 } 167 168 // Enforce that the selection is -contiguous- 169 if (!validateContiguous(infos, status)) { 170 return status; 171 } 172 } 173 174 // This also ensures that we have a valid DOM model: 175 if (mElements.size() == 0) { 176 status.addFatalError("Nothing to extract"); 177 return status; 178 } 179 180 pm.worked(1); 181 return status; 182 183 } finally { 184 pm.done(); 185 } 186 } 187 188 @Override 189 protected VisualRefactoringDescriptor createDescriptor() { 190 String comment = getName(); 191 return new Descriptor( 192 mProject.getName(), //project 193 comment, //description 194 comment, //comment 195 createArgumentMap()); 196 } 197 198 @Override 199 protected Map<String, String> createArgumentMap() { 200 Map<String, String> args = super.createArgumentMap(); 201 args.put(KEY_NAME, mLayoutName); 202 args.put(KEY_OCCURRENCES, Boolean.toString(mReplaceOccurrences)); 203 204 return args; 205 } 206 207 @Override 208 public String getName() { 209 return "Extract as Include"; 210 } 211 212 void setLayoutName(String layoutName) { 213 mLayoutName = layoutName; 214 } 215 216 void setReplaceOccurrences(boolean selection) { 217 mReplaceOccurrences = selection; 218 } 219 220 // ---- Actual implementation of Extract as Include modification computation ---- 221 222 @Override 223 protected List<Change> computeChanges(IProgressMonitor monitor) { 224 String extractedText = getExtractedText(); 225 226 String namespaceDeclarations = computeNamespaceDeclarations(); 227 228 // Insert namespace: 229 extractedText = insertNamespace(extractedText, namespaceDeclarations); 230 231 StringBuilder sb = new StringBuilder(); 232 sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$ 233 sb.append(extractedText); 234 sb.append('\n'); 235 236 List<Change> changes = new ArrayList<Change>(); 237 238 String newFileName = mLayoutName + DOT_XML; 239 IProject project = mDelegate.getEditor().getProject(); 240 IFile sourceFile = mDelegate.getEditor().getInputFile(); 241 if (sourceFile == null) { 242 return changes; 243 } 244 245 // Replace extracted elements by <include> tag 246 handleIncludingFile(changes, sourceFile, mSelectionStart, mSelectionEnd, 247 getDomDocument(), getPrimaryElement()); 248 249 // Also extract in other variations of the same file (landscape/portrait, etc) 250 boolean haveVariations = false; 251 if (mReplaceOccurrences) { 252 List<IFile> layouts = getOtherLayouts(sourceFile); 253 for (IFile file : layouts) { 254 IModelManager modelManager = StructuredModelManager.getModelManager(); 255 IStructuredModel model = null; 256 // We could enhance this with a SubMonitor to make the progress bar move as 257 // well. 258 monitor.subTask(String.format("Looking for duplicates in %1$s", 259 file.getProjectRelativePath())); 260 if (monitor.isCanceled()) { 261 throw new OperationCanceledException(); 262 } 263 264 try { 265 model = modelManager.getModelForRead(file); 266 if (model instanceof IDOMModel) { 267 IDOMModel domModel = (IDOMModel) model; 268 IDOMDocument otherDocument = domModel.getDocument(); 269 List<Element> otherElements = new ArrayList<Element>(); 270 Element otherPrimary = null; 271 272 for (Element element : getElements()) { 273 Element other = DomUtilities.findCorresponding(element, 274 otherDocument); 275 if (other != null) { 276 // See if the structure is similar to what we have in this 277 // document 278 if (DomUtilities.isEquivalent(element, other)) { 279 otherElements.add(other); 280 if (element == getPrimaryElement()) { 281 otherPrimary = other; 282 } 283 } 284 } 285 } 286 287 // Only perform extract in the other file if we find a match for 288 // ALL of elements being extracted, and if they too are contiguous 289 if (otherElements.size() == getElements().size() && 290 DomUtilities.isContiguous(otherElements)) { 291 // Find the range 292 int begin = Integer.MAX_VALUE; 293 int end = Integer.MIN_VALUE; 294 for (Element element : otherElements) { 295 // Yes!! Extract this one as well! 296 IndexedRegion region = getRegion(element); 297 end = Math.max(end, region.getEndOffset()); 298 begin = Math.min(begin, region.getStartOffset()); 299 } 300 handleIncludingFile(changes, file, begin, 301 end, otherDocument, otherPrimary); 302 haveVariations = true; 303 } 304 } 305 } catch (IOException e) { 306 AdtPlugin.log(e, null); 307 } catch (CoreException e) { 308 AdtPlugin.log(e, null); 309 } finally { 310 if (model != null) { 311 model.releaseFromRead(); 312 } 313 } 314 } 315 } 316 317 // Add change to create the new file 318 IContainer parent = sourceFile.getParent(); 319 if (haveVariations) { 320 // If we're extracting from multiple configuration folders, then we need to 321 // place the extracted include in the base layout folder (if not it goes next to 322 // the including file) 323 parent = mProject.getFolder(FD_RES).getFolder(FD_RES_LAYOUT); 324 } 325 IPath parentPath = parent.getProjectRelativePath(); 326 final IFile file = project.getFile(new Path(parentPath + WS_SEP + newFileName)); 327 TextFileChange addFile = new TextFileChange("Create new separate layout", file); 328 addFile.setTextType(AdtConstants.EXT_XML); 329 changes.add(addFile); 330 331 String newFile = sb.toString(); 332 if (AdtPrefs.getPrefs().getFormatGuiXml()) { 333 newFile = XmlPrettyPrinter.prettyPrint(newFile, 334 XmlFormatPreferences.create(), XmlFormatStyle.LAYOUT, null /*lineSeparator*/); 335 } 336 addFile.setEdit(new InsertEdit(0, newFile)); 337 338 Change finishHook = createFinishHook(file); 339 changes.add(finishHook); 340 341 return changes; 342 } 343 344 private void handleIncludingFile(List<Change> changes, 345 IFile sourceFile, int begin, int end, Document document, Element primary) { 346 TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile); 347 MultiTextEdit rootEdit = new MultiTextEdit(); 348 change.setTextType(EXT_XML); 349 changes.add(change); 350 351 String referenceId = getReferenceId(); 352 // Replace existing elements in the source file and insert <include> 353 String androidNsPrefix = getAndroidNamespacePrefix(document); 354 String include = computeIncludeString(primary, mLayoutName, androidNsPrefix, referenceId); 355 int length = end - begin; 356 ReplaceEdit replace = new ReplaceEdit(begin, length, include); 357 rootEdit.addChild(replace); 358 359 // Update any layout references to the old id with the new id 360 if (referenceId != null && primary != null) { 361 String rootId = getId(primary); 362 IStructuredModel model = null; 363 try { 364 model = StructuredModelManager.getModelManager().getModelForRead(sourceFile); 365 IStructuredDocument doc = model.getStructuredDocument(); 366 if (doc != null && rootId != null) { 367 List<TextEdit> replaceIds = replaceIds(androidNsPrefix, doc, begin, 368 end, rootId, referenceId); 369 for (TextEdit edit : replaceIds) { 370 rootEdit.addChild(edit); 371 } 372 373 if (AdtPrefs.getPrefs().getFormatGuiXml()) { 374 MultiTextEdit formatted = reformat(doc.get(), rootEdit, 375 XmlFormatStyle.LAYOUT); 376 if (formatted != null) { 377 rootEdit = formatted; 378 } 379 } 380 } 381 } catch (IOException e) { 382 AdtPlugin.log(e, null); 383 } catch (CoreException e) { 384 AdtPlugin.log(e, null); 385 } finally { 386 if (model != null) { 387 model.releaseFromRead(); 388 } 389 } 390 } 391 392 change.setEdit(rootEdit); 393 } 394 395 /** 396 * Returns a list of all the other layouts (in all configurations) in the project other 397 * than the given source layout where the refactoring was initiated. Never null. 398 */ 399 private List<IFile> getOtherLayouts(IFile sourceFile) { 400 List<IFile> layouts = new ArrayList<IFile>(100); 401 IPath sourcePath = sourceFile.getProjectRelativePath(); 402 IFolder resources = mProject.getFolder(SdkConstants.FD_RESOURCES); 403 try { 404 for (IResource folder : resources.members()) { 405 if (folder.getName().startsWith(AndroidConstants.FD_RES_LAYOUT) && 406 folder instanceof IFolder) { 407 IFolder layoutFolder = (IFolder) folder; 408 for (IResource file : layoutFolder.members()) { 409 if (file.getName().endsWith(EXT_XML) 410 && file instanceof IFile) { 411 if (!file.getProjectRelativePath().equals(sourcePath)) { 412 layouts.add((IFile) file); 413 } 414 } 415 } 416 } 417 } 418 } catch (CoreException e) { 419 AdtPlugin.log(e, null); 420 } 421 422 return layouts; 423 } 424 425 String getInitialName() { 426 String defaultName = ""; //$NON-NLS-1$ 427 Element primary = getPrimaryElement(); 428 if (primary != null) { 429 String id = primary.getAttributeNS(ANDROID_URI, ATTR_ID); 430 // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378 431 if (id != null && (id.startsWith(ID_PREFIX) || id.startsWith(NEW_ID_PREFIX))) { 432 // Use everything following the id/, and make it lowercase since that is 433 // the convention for layouts (and use Locale.US to ensure that "Image" becomes 434 // "image" etc) 435 defaultName = id.substring(id.indexOf('/') + 1).toLowerCase(Locale.US); 436 437 IInputValidator validator = ResourceNameValidator.create(true, mProject, LAYOUT); 438 439 if (validator.isValid(defaultName) != null) { // Already exists? 440 defaultName = ""; //$NON-NLS-1$ 441 } 442 } 443 } 444 445 return defaultName; 446 } 447 448 IFile getSourceFile() { 449 return mFile; 450 } 451 452 private Change createFinishHook(final IFile file) { 453 return new NullChange("Open extracted layout and refresh resources") { 454 @Override 455 public Change perform(IProgressMonitor pm) throws CoreException { 456 Display display = AdtPlugin.getDisplay(); 457 display.asyncExec(new Runnable() { 458 @Override 459 public void run() { 460 openFile(file); 461 mDelegate.getGraphicalEditor().refreshProjectResources(); 462 // Save file to trigger include finder scanning (as well as making 463 // the 464 // actual show-include feature work since it relies on reading 465 // files from 466 // disk, not a live buffer) 467 IWorkbenchPage page = mDelegate.getEditor().getEditorSite().getPage(); 468 page.saveEditor(mDelegate.getEditor(), false); 469 } 470 }); 471 472 // Not undoable: just return null instead of an undo-change. 473 return null; 474 } 475 }; 476 } 477 478 private String computeNamespaceDeclarations() { 479 String androidNsPrefix = null; 480 String namespaceDeclarations = null; 481 482 StringBuilder sb = new StringBuilder(); 483 List<Attr> attributeNodes = findNamespaceAttributes(); 484 for (Node attributeNode : attributeNodes) { 485 String prefix = attributeNode.getPrefix(); 486 if (XMLNS.equals(prefix)) { 487 sb.append(' '); 488 String name = attributeNode.getNodeName(); 489 sb.append(name); 490 sb.append('=').append('"'); 491 492 String value = attributeNode.getNodeValue(); 493 if (value.equals(ANDROID_URI)) { 494 androidNsPrefix = name; 495 if (androidNsPrefix.startsWith(XMLNS_COLON)) { 496 androidNsPrefix = androidNsPrefix.substring(XMLNS_COLON.length()); 497 } 498 } 499 sb.append(DomUtilities.toXmlAttributeValue(value)); 500 sb.append('"'); 501 } 502 } 503 namespaceDeclarations = sb.toString(); 504 505 if (androidNsPrefix == null) { 506 androidNsPrefix = ANDROID_NS_NAME; 507 } 508 509 if (namespaceDeclarations.length() == 0) { 510 sb.setLength(0); 511 sb.append(' '); 512 sb.append(XMLNS_COLON); 513 sb.append(androidNsPrefix); 514 sb.append('=').append('"'); 515 sb.append(ANDROID_URI); 516 sb.append('"'); 517 namespaceDeclarations = sb.toString(); 518 } 519 520 return namespaceDeclarations; 521 } 522 523 /** Returns the id to be used for the include tag itself (may be null) */ 524 private String getReferenceId() { 525 String rootId = getRootId(); 526 if (rootId != null) { 527 return rootId + "_ref"; 528 } 529 530 return null; 531 } 532 533 /** 534 * Compute the actual {@code <include>} string to be inserted in place of the old 535 * selection 536 */ 537 private static String computeIncludeString(Element primaryNode, String newName, 538 String androidNsPrefix, String referenceId) { 539 StringBuilder sb = new StringBuilder(); 540 sb.append("<include layout=\"@layout/"); //$NON-NLS-1$ 541 sb.append(newName); 542 sb.append('"'); 543 sb.append(' '); 544 545 // Create new id for the include itself 546 if (referenceId != null) { 547 sb.append(androidNsPrefix); 548 sb.append(':'); 549 sb.append(ATTR_ID); 550 sb.append('=').append('"'); 551 sb.append(referenceId); 552 sb.append('"').append(' '); 553 } 554 555 // Add id string, unless it's a <merge>, since we may need to adjust any layout 556 // references to apply to the <include> tag instead 557 558 // I should move all the layout_ attributes as well 559 // I also need to duplicate and modify the id and then replace 560 // everything else in the file with this new id... 561 562 // HACK: see issue 13494: We must duplicate the width/height attributes on the 563 // <include> statement for designtime rendering only 564 String width = null; 565 String height = null; 566 if (primaryNode == null) { 567 // Multiple selection - in that case we will be creating an outer <merge> 568 // so we need to set our own width/height on it 569 width = height = VALUE_WRAP_CONTENT; 570 } else { 571 if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) { 572 width = VALUE_WRAP_CONTENT; 573 } else { 574 width = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); 575 } 576 if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) { 577 height = VALUE_WRAP_CONTENT; 578 } else { 579 height = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); 580 } 581 } 582 if (width != null) { 583 sb.append(' '); 584 sb.append(androidNsPrefix); 585 sb.append(':'); 586 sb.append(ATTR_LAYOUT_WIDTH); 587 sb.append('=').append('"'); 588 sb.append(DomUtilities.toXmlAttributeValue(width)); 589 sb.append('"'); 590 } 591 if (height != null) { 592 sb.append(' '); 593 sb.append(androidNsPrefix); 594 sb.append(':'); 595 sb.append(ATTR_LAYOUT_HEIGHT); 596 sb.append('=').append('"'); 597 sb.append(DomUtilities.toXmlAttributeValue(height)); 598 sb.append('"'); 599 } 600 601 // Duplicate all the other layout attributes as well 602 if (primaryNode != null) { 603 NamedNodeMap attributes = primaryNode.getAttributes(); 604 for (int i = 0, n = attributes.getLength(); i < n; i++) { 605 Node attr = attributes.item(i); 606 String name = attr.getLocalName(); 607 if (name.startsWith(ATTR_LAYOUT_PREFIX) 608 && ANDROID_URI.equals(attr.getNamespaceURI())) { 609 if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) { 610 // Already handled 611 continue; 612 } 613 614 sb.append(' '); 615 sb.append(androidNsPrefix); 616 sb.append(':'); 617 sb.append(name); 618 sb.append('=').append('"'); 619 sb.append(DomUtilities.toXmlAttributeValue(attr.getNodeValue())); 620 sb.append('"'); 621 } 622 } 623 } 624 625 sb.append("/>"); 626 return sb.toString(); 627 } 628 629 /** Return the text in the document in the range start to end */ 630 private String getExtractedText() { 631 String xml = getText(mSelectionStart, mSelectionEnd); 632 Element primaryNode = getPrimaryElement(); 633 xml = stripTopLayoutAttributes(primaryNode, mSelectionStart, xml); 634 xml = dedent(xml); 635 636 // Wrap siblings in <merge>? 637 if (primaryNode == null) { 638 StringBuilder sb = new StringBuilder(); 639 sb.append("<merge>\n"); //$NON-NLS-1$ 640 // indent an extra level 641 for (String line : xml.split("\n")) { //$NON-NLS-1$ 642 sb.append(" "); //$NON-NLS-1$ 643 sb.append(line).append('\n'); 644 } 645 sb.append("</merge>\n"); //$NON-NLS-1$ 646 xml = sb.toString(); 647 } 648 649 return xml; 650 } 651 652 @Override 653 VisualRefactoringWizard createWizard() { 654 return new ExtractIncludeWizard(this, mDelegate); 655 } 656 657 public static class Descriptor extends VisualRefactoringDescriptor { 658 public Descriptor(String project, String description, String comment, 659 Map<String, String> arguments) { 660 super("com.android.ide.eclipse.adt.refactoring.extract.include", //$NON-NLS-1$ 661 project, description, comment, arguments); 662 } 663 664 @Override 665 protected Refactoring createRefactoring(Map<String, String> args) { 666 return new ExtractIncludeRefactoring(args); 667 } 668 } 669 } 670