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_NS_NAME; 19 import static com.android.SdkConstants.ANDROID_URI; 20 import static com.android.SdkConstants.ATTR_ID; 21 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; 22 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; 23 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; 24 import static com.android.SdkConstants.DOT_XML; 25 import static com.android.SdkConstants.EXT_XML; 26 import static com.android.SdkConstants.FD_RES; 27 import static com.android.SdkConstants.FD_RESOURCES; 28 import static com.android.SdkConstants.FD_RES_LAYOUT; 29 import static com.android.SdkConstants.ID_PREFIX; 30 import static com.android.SdkConstants.NEW_ID_PREFIX; 31 import static com.android.SdkConstants.VALUE_WRAP_CONTENT; 32 import static com.android.SdkConstants.VIEW_INCLUDE; 33 import static com.android.SdkConstants.XMLNS; 34 import static com.android.SdkConstants.XMLNS_PREFIX; 35 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; 36 import static com.android.resources.ResourceType.LAYOUT; 37 38 import com.android.annotations.NonNull; 39 import com.android.annotations.VisibleForTesting; 40 import com.android.ide.common.xml.XmlFormatStyle; 41 import com.android.ide.eclipse.adt.AdtPlugin; 42 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; 43 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; 44 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 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.utils.XmlUtils; 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(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 @NonNull 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(EXT_XML); 329 changes.add(addFile); 330 331 String newFile = sb.toString(); 332 if (AdtPrefs.getPrefs().getFormatGuiXml()) { 333 newFile = EclipseXmlPrettyPrinter.prettyPrint(newFile, 334 EclipseXmlFormatPreferences.create(), XmlFormatStyle.LAYOUT, 335 null /*lineSeparator*/); 336 } 337 addFile.setEdit(new InsertEdit(0, newFile)); 338 339 Change finishHook = createFinishHook(file); 340 changes.add(finishHook); 341 342 return changes; 343 } 344 345 private void handleIncludingFile(List<Change> changes, 346 IFile sourceFile, int begin, int end, Document document, Element primary) { 347 TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile); 348 MultiTextEdit rootEdit = new MultiTextEdit(); 349 change.setTextType(EXT_XML); 350 changes.add(change); 351 352 String referenceId = getReferenceId(); 353 // Replace existing elements in the source file and insert <include> 354 String androidNsPrefix = getAndroidNamespacePrefix(document); 355 String include = computeIncludeString(primary, mLayoutName, androidNsPrefix, referenceId); 356 int length = end - begin; 357 ReplaceEdit replace = new ReplaceEdit(begin, length, include); 358 rootEdit.addChild(replace); 359 360 // Update any layout references to the old id with the new id 361 if (referenceId != null && primary != null) { 362 String rootId = getId(primary); 363 IStructuredModel model = null; 364 try { 365 model = StructuredModelManager.getModelManager().getModelForRead(sourceFile); 366 IStructuredDocument doc = model.getStructuredDocument(); 367 if (doc != null && rootId != null) { 368 List<TextEdit> replaceIds = replaceIds(androidNsPrefix, doc, begin, 369 end, rootId, referenceId); 370 for (TextEdit edit : replaceIds) { 371 rootEdit.addChild(edit); 372 } 373 374 if (AdtPrefs.getPrefs().getFormatGuiXml()) { 375 MultiTextEdit formatted = reformat(doc.get(), rootEdit, 376 XmlFormatStyle.LAYOUT); 377 if (formatted != null) { 378 rootEdit = formatted; 379 } 380 } 381 } 382 } catch (IOException e) { 383 AdtPlugin.log(e, null); 384 } catch (CoreException e) { 385 AdtPlugin.log(e, null); 386 } finally { 387 if (model != null) { 388 model.releaseFromRead(); 389 } 390 } 391 } 392 393 change.setEdit(rootEdit); 394 } 395 396 /** 397 * Returns a list of all the other layouts (in all configurations) in the project other 398 * than the given source layout where the refactoring was initiated. Never null. 399 */ 400 private List<IFile> getOtherLayouts(IFile sourceFile) { 401 List<IFile> layouts = new ArrayList<IFile>(100); 402 IPath sourcePath = sourceFile.getProjectRelativePath(); 403 IFolder resources = mProject.getFolder(FD_RESOURCES); 404 try { 405 for (IResource folder : resources.members()) { 406 if (folder.getName().startsWith(FD_RES_LAYOUT) && 407 folder instanceof IFolder) { 408 IFolder layoutFolder = (IFolder) folder; 409 for (IResource file : layoutFolder.members()) { 410 if (file.getName().endsWith(EXT_XML) 411 && file instanceof IFile) { 412 if (!file.getProjectRelativePath().equals(sourcePath)) { 413 layouts.add((IFile) file); 414 } 415 } 416 } 417 } 418 } 419 } catch (CoreException e) { 420 AdtPlugin.log(e, null); 421 } 422 423 return layouts; 424 } 425 426 String getInitialName() { 427 String defaultName = ""; //$NON-NLS-1$ 428 Element primary = getPrimaryElement(); 429 if (primary != null) { 430 String id = primary.getAttributeNS(ANDROID_URI, ATTR_ID); 431 // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378 432 if (id != null && (id.startsWith(ID_PREFIX) || id.startsWith(NEW_ID_PREFIX))) { 433 // Use everything following the id/, and make it lowercase since that is 434 // the convention for layouts (and use Locale.US to ensure that "Image" becomes 435 // "image" etc) 436 defaultName = id.substring(id.indexOf('/') + 1).toLowerCase(Locale.US); 437 438 IInputValidator validator = ResourceNameValidator.create(true, mProject, LAYOUT); 439 440 if (validator.isValid(defaultName) != null) { // Already exists? 441 defaultName = ""; //$NON-NLS-1$ 442 } 443 } 444 } 445 446 return defaultName; 447 } 448 449 IFile getSourceFile() { 450 return mFile; 451 } 452 453 private Change createFinishHook(final IFile file) { 454 return new NullChange("Open extracted layout and refresh resources") { 455 @Override 456 public Change perform(IProgressMonitor pm) throws CoreException { 457 Display display = AdtPlugin.getDisplay(); 458 display.asyncExec(new Runnable() { 459 @Override 460 public void run() { 461 openFile(file); 462 mDelegate.getGraphicalEditor().refreshProjectResources(); 463 // Save file to trigger include finder scanning (as well as making 464 // the 465 // actual show-include feature work since it relies on reading 466 // files from 467 // disk, not a live buffer) 468 IWorkbenchPage page = mDelegate.getEditor().getEditorSite().getPage(); 469 page.saveEditor(mDelegate.getEditor(), false); 470 } 471 }); 472 473 // Not undoable: just return null instead of an undo-change. 474 return null; 475 } 476 }; 477 } 478 479 private String computeNamespaceDeclarations() { 480 String androidNsPrefix = null; 481 String namespaceDeclarations = null; 482 483 StringBuilder sb = new StringBuilder(); 484 List<Attr> attributeNodes = findNamespaceAttributes(); 485 for (Node attributeNode : attributeNodes) { 486 String prefix = attributeNode.getPrefix(); 487 if (XMLNS.equals(prefix)) { 488 sb.append(' '); 489 String name = attributeNode.getNodeName(); 490 sb.append(name); 491 sb.append('=').append('"'); 492 493 String value = attributeNode.getNodeValue(); 494 if (value.equals(ANDROID_URI)) { 495 androidNsPrefix = name; 496 if (androidNsPrefix.startsWith(XMLNS_PREFIX)) { 497 androidNsPrefix = androidNsPrefix.substring(XMLNS_PREFIX.length()); 498 } 499 } 500 sb.append(XmlUtils.toXmlAttributeValue(value)); 501 sb.append('"'); 502 } 503 } 504 namespaceDeclarations = sb.toString(); 505 506 if (androidNsPrefix == null) { 507 androidNsPrefix = ANDROID_NS_NAME; 508 } 509 510 if (namespaceDeclarations.length() == 0) { 511 sb.setLength(0); 512 sb.append(' '); 513 sb.append(XMLNS_PREFIX); 514 sb.append(androidNsPrefix); 515 sb.append('=').append('"'); 516 sb.append(ANDROID_URI); 517 sb.append('"'); 518 namespaceDeclarations = sb.toString(); 519 } 520 521 return namespaceDeclarations; 522 } 523 524 /** Returns the id to be used for the include tag itself (may be null) */ 525 private String getReferenceId() { 526 String rootId = getRootId(); 527 if (rootId != null) { 528 return rootId + "_ref"; 529 } 530 531 return null; 532 } 533 534 /** 535 * Compute the actual {@code <include>} string to be inserted in place of the old 536 * selection 537 */ 538 private static String computeIncludeString(Element primaryNode, String newName, 539 String androidNsPrefix, String referenceId) { 540 StringBuilder sb = new StringBuilder(); 541 sb.append("<include layout=\"@layout/"); //$NON-NLS-1$ 542 sb.append(newName); 543 sb.append('"'); 544 sb.append(' '); 545 546 // Create new id for the include itself 547 if (referenceId != null) { 548 sb.append(androidNsPrefix); 549 sb.append(':'); 550 sb.append(ATTR_ID); 551 sb.append('=').append('"'); 552 sb.append(referenceId); 553 sb.append('"').append(' '); 554 } 555 556 // Add id string, unless it's a <merge>, since we may need to adjust any layout 557 // references to apply to the <include> tag instead 558 559 // I should move all the layout_ attributes as well 560 // I also need to duplicate and modify the id and then replace 561 // everything else in the file with this new id... 562 563 // HACK: see issue 13494: We must duplicate the width/height attributes on the 564 // <include> statement for designtime rendering only 565 String width = null; 566 String height = null; 567 if (primaryNode == null) { 568 // Multiple selection - in that case we will be creating an outer <merge> 569 // so we need to set our own width/height on it 570 width = height = VALUE_WRAP_CONTENT; 571 } else { 572 if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) { 573 width = VALUE_WRAP_CONTENT; 574 } else { 575 width = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); 576 } 577 if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) { 578 height = VALUE_WRAP_CONTENT; 579 } else { 580 height = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); 581 } 582 } 583 if (width != null) { 584 sb.append(' '); 585 sb.append(androidNsPrefix); 586 sb.append(':'); 587 sb.append(ATTR_LAYOUT_WIDTH); 588 sb.append('=').append('"'); 589 sb.append(XmlUtils.toXmlAttributeValue(width)); 590 sb.append('"'); 591 } 592 if (height != null) { 593 sb.append(' '); 594 sb.append(androidNsPrefix); 595 sb.append(':'); 596 sb.append(ATTR_LAYOUT_HEIGHT); 597 sb.append('=').append('"'); 598 sb.append(XmlUtils.toXmlAttributeValue(height)); 599 sb.append('"'); 600 } 601 602 // Duplicate all the other layout attributes as well 603 if (primaryNode != null) { 604 NamedNodeMap attributes = primaryNode.getAttributes(); 605 for (int i = 0, n = attributes.getLength(); i < n; i++) { 606 Node attr = attributes.item(i); 607 String name = attr.getLocalName(); 608 if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) 609 && ANDROID_URI.equals(attr.getNamespaceURI())) { 610 if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) { 611 // Already handled 612 continue; 613 } 614 615 sb.append(' '); 616 sb.append(androidNsPrefix); 617 sb.append(':'); 618 sb.append(name); 619 sb.append('=').append('"'); 620 sb.append(XmlUtils.toXmlAttributeValue(attr.getNodeValue())); 621 sb.append('"'); 622 } 623 } 624 } 625 626 sb.append("/>"); 627 return sb.toString(); 628 } 629 630 /** Return the text in the document in the range start to end */ 631 private String getExtractedText() { 632 String xml = getText(mSelectionStart, mSelectionEnd); 633 Element primaryNode = getPrimaryElement(); 634 xml = stripTopLayoutAttributes(primaryNode, mSelectionStart, xml); 635 xml = dedent(xml); 636 637 // Wrap siblings in <merge>? 638 if (primaryNode == null) { 639 StringBuilder sb = new StringBuilder(); 640 sb.append("<merge>\n"); //$NON-NLS-1$ 641 // indent an extra level 642 for (String line : xml.split("\n")) { //$NON-NLS-1$ 643 sb.append(" "); //$NON-NLS-1$ 644 sb.append(line).append('\n'); 645 } 646 sb.append("</merge>\n"); //$NON-NLS-1$ 647 xml = sb.toString(); 648 } 649 650 return xml; 651 } 652 653 @Override 654 VisualRefactoringWizard createWizard() { 655 return new ExtractIncludeWizard(this, mDelegate); 656 } 657 658 public static class Descriptor extends VisualRefactoringDescriptor { 659 public Descriptor(String project, String description, String comment, 660 Map<String, String> arguments) { 661 super("com.android.ide.eclipse.adt.refactoring.extract.include", //$NON-NLS-1$ 662 project, description, comment, arguments); 663 } 664 665 @Override 666 protected Refactoring createRefactoring(Map<String, String> args) { 667 return new ExtractIncludeRefactoring(args); 668 } 669 } 670 } 671