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_PREFIX; 19 import static com.android.SdkConstants.ANDROID_URI; 20 import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; 21 import static com.android.SdkConstants.ATTR_ID; 22 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; 23 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; 24 import static com.android.SdkConstants.EXT_XML; 25 import static com.android.SdkConstants.VALUE_FILL_PARENT; 26 import static com.android.SdkConstants.VALUE_MATCH_PARENT; 27 import static com.android.SdkConstants.VALUE_WRAP_CONTENT; 28 29 import com.android.annotations.NonNull; 30 import com.android.annotations.VisibleForTesting; 31 import com.android.ide.common.xml.XmlFormatStyle; 32 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 33 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 34 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; 35 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 36 37 import org.eclipse.core.resources.IFile; 38 import org.eclipse.core.runtime.CoreException; 39 import org.eclipse.core.runtime.IProgressMonitor; 40 import org.eclipse.core.runtime.OperationCanceledException; 41 import org.eclipse.jface.text.ITextSelection; 42 import org.eclipse.jface.viewers.ITreeSelection; 43 import org.eclipse.ltk.core.refactoring.Change; 44 import org.eclipse.ltk.core.refactoring.Refactoring; 45 import org.eclipse.ltk.core.refactoring.RefactoringStatus; 46 import org.eclipse.ltk.core.refactoring.TextFileChange; 47 import org.eclipse.text.edits.DeleteEdit; 48 import org.eclipse.text.edits.InsertEdit; 49 import org.eclipse.text.edits.MultiTextEdit; 50 import org.eclipse.text.edits.TextEdit; 51 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 52 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 53 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 54 import org.w3c.dom.Attr; 55 import org.w3c.dom.Element; 56 57 import java.util.ArrayList; 58 import java.util.List; 59 import java.util.Map; 60 61 /** 62 * Inserts a new layout surrounding the current selection, migrates namespace 63 * attributes (if wrapping the root node), and optionally migrates layout 64 * attributes and updates references elsewhere. 65 */ 66 @SuppressWarnings("restriction") // XML model 67 public class WrapInRefactoring extends VisualRefactoring { 68 private static final String KEY_ID = "name"; //$NON-NLS-1$ 69 private static final String KEY_TYPE = "type"; //$NON-NLS-1$ 70 71 private String mId; 72 private String mTypeFqcn; 73 private String mInitializedAttributes; 74 75 /** 76 * This constructor is solely used by {@link Descriptor}, 77 * to replay a previous refactoring. 78 * @param arguments argument map created by #createArgumentMap. 79 */ 80 WrapInRefactoring(Map<String, String> arguments) { 81 super(arguments); 82 mId = arguments.get(KEY_ID); 83 mTypeFqcn = arguments.get(KEY_TYPE); 84 } 85 86 public WrapInRefactoring( 87 IFile file, 88 LayoutEditorDelegate delegate, 89 ITextSelection selection, 90 ITreeSelection treeSelection) { 91 super(file, delegate, selection, treeSelection); 92 } 93 94 @VisibleForTesting 95 WrapInRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) { 96 super(selectedElements, editor); 97 } 98 99 @Override 100 public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, 101 OperationCanceledException { 102 RefactoringStatus status = new RefactoringStatus(); 103 104 try { 105 pm.beginTask("Checking preconditions...", 6); 106 107 if (mSelectionStart == -1 || mSelectionEnd == -1) { 108 status.addFatalError("No selection to wrap"); 109 return status; 110 } 111 112 // Make sure the selection is contiguous 113 if (mTreeSelection != null) { 114 // TODO - don't do this if we based the selection on text. In this case, 115 // make sure we're -balanced-. 116 117 List<CanvasViewInfo> infos = getSelectedViewInfos(); 118 if (!validateNotEmpty(infos, status)) { 119 return status; 120 } 121 122 // Enforce that the selection is -contiguous- 123 if (!validateContiguous(infos, status)) { 124 return status; 125 } 126 } 127 128 // Ensures that we have a valid DOM model: 129 if (mElements.size() == 0) { 130 status.addFatalError("Nothing to wrap"); 131 return status; 132 } 133 134 pm.worked(1); 135 return status; 136 137 } finally { 138 pm.done(); 139 } 140 } 141 142 @Override 143 protected VisualRefactoringDescriptor createDescriptor() { 144 String comment = getName(); 145 return new Descriptor( 146 mProject.getName(), //project 147 comment, //description 148 comment, //comment 149 createArgumentMap()); 150 } 151 152 @Override 153 protected Map<String, String> createArgumentMap() { 154 Map<String, String> args = super.createArgumentMap(); 155 args.put(KEY_TYPE, mTypeFqcn); 156 args.put(KEY_ID, mId); 157 158 return args; 159 } 160 161 @Override 162 public String getName() { 163 return "Wrap in Container"; 164 } 165 166 void setId(String id) { 167 mId = id; 168 } 169 170 void setType(String typeFqcn) { 171 mTypeFqcn = typeFqcn; 172 } 173 174 void setInitializedAttributes(String initializedAttributes) { 175 mInitializedAttributes = initializedAttributes; 176 } 177 178 @Override 179 protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) { 180 // (1) Insert the new container in front of the beginning of the 181 // first wrapped view 182 // (2) If the container is the new root, transfer namespace declarations 183 // to it 184 // (3) Insert the closing tag of the new container at the end of the 185 // last wrapped view 186 // (4) Reindent the wrapped views 187 // (5) If the user requested it, update all layout references to the 188 // wrapped views with the new container? 189 // For that matter, does RelativeLayout even require it? Probably not, 190 // it can point inside the current layout... 191 192 // Add indent to all lines between mSelectionStart and mEnd 193 // TODO: Figure out the indentation amount? 194 // For now, use 4 spaces 195 String indentUnit = " "; //$NON-NLS-1$ 196 boolean separateAttributes = true; 197 IStructuredDocument document = mDelegate.getEditor().getStructuredDocument(); 198 String startIndent = AndroidXmlEditor.getIndentAtOffset(document, mSelectionStart); 199 200 String viewClass = getViewClass(mTypeFqcn); 201 String androidNsPrefix = getAndroidNamespacePrefix(); 202 203 204 IFile file = mDelegate.getEditor().getInputFile(); 205 List<Change> changes = new ArrayList<Change>(); 206 if (file == null) { 207 return changes; 208 } 209 TextFileChange change = new TextFileChange(file.getName(), file); 210 MultiTextEdit rootEdit = new MultiTextEdit(); 211 change.setTextType(EXT_XML); 212 213 String id = ensureNewId(mId); 214 215 // Update any layout references to the old id with the new id 216 if (id != null) { 217 String rootId = getRootId(); 218 IStructuredModel model = mDelegate.getEditor().getModelForRead(); 219 try { 220 IStructuredDocument doc = model.getStructuredDocument(); 221 if (doc != null) { 222 List<TextEdit> replaceIds = replaceIds(androidNsPrefix, 223 doc, mSelectionStart, mSelectionEnd, rootId, id); 224 for (TextEdit edit : replaceIds) { 225 rootEdit.addChild(edit); 226 } 227 } 228 } finally { 229 model.releaseFromRead(); 230 } 231 } 232 233 // Insert namespace elements? 234 StringBuilder namespace = null; 235 List<DeleteEdit> deletions = new ArrayList<DeleteEdit>(); 236 Element primary = getPrimaryElement(); 237 if (primary != null && getDomDocument().getDocumentElement() == primary) { 238 namespace = new StringBuilder(); 239 240 List<Attr> declarations = findNamespaceAttributes(primary); 241 for (Attr attribute : declarations) { 242 if (attribute instanceof IndexedRegion) { 243 // Delete the namespace declaration in the node which is no longer the root 244 IndexedRegion region = (IndexedRegion) attribute; 245 int startOffset = region.getStartOffset(); 246 int endOffset = region.getEndOffset(); 247 String text = getText(startOffset, endOffset); 248 DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset); 249 deletions.add(deletion); 250 rootEdit.addChild(deletion); 251 text = text.trim(); 252 253 // Insert the namespace declaration in the new root 254 if (separateAttributes) { 255 namespace.append('\n').append(startIndent).append(indentUnit); 256 } else { 257 namespace.append(' '); 258 } 259 namespace.append(text); 260 } 261 } 262 } 263 264 // Insert begin tag: <type ...> 265 StringBuilder sb = new StringBuilder(); 266 sb.append('<'); 267 sb.append(viewClass); 268 269 if (namespace != null) { 270 sb.append(namespace); 271 } 272 273 // Set the ID if any 274 if (id != null) { 275 if (separateAttributes) { 276 sb.append('\n').append(startIndent).append(indentUnit); 277 } else { 278 sb.append(' '); 279 } 280 sb.append(androidNsPrefix).append(':'); 281 sb.append(ATTR_ID).append('=').append('"').append(id).append('"'); 282 } 283 284 // If any of the elements are fill/match parent, use that instead 285 String width = VALUE_WRAP_CONTENT; 286 String height = VALUE_WRAP_CONTENT; 287 288 for (Element element : getElements()) { 289 String oldWidth = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); 290 String oldHeight = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); 291 292 if (VALUE_MATCH_PARENT.equals(oldWidth) || VALUE_FILL_PARENT.equals(oldWidth)) { 293 width = oldWidth; 294 } 295 if (VALUE_MATCH_PARENT.equals(oldHeight) || VALUE_FILL_PARENT.equals(oldHeight)) { 296 height = oldHeight; 297 } 298 } 299 300 // Add in width/height. 301 if (separateAttributes) { 302 sb.append('\n').append(startIndent).append(indentUnit); 303 } else { 304 sb.append(' '); 305 } 306 sb.append(androidNsPrefix).append(':'); 307 sb.append(ATTR_LAYOUT_WIDTH).append('=').append('"').append(width).append('"'); 308 309 if (separateAttributes) { 310 sb.append('\n').append(startIndent).append(indentUnit); 311 } else { 312 sb.append(' '); 313 } 314 sb.append(androidNsPrefix).append(':'); 315 sb.append(ATTR_LAYOUT_HEIGHT).append('=').append('"').append(height).append('"'); 316 317 if (mInitializedAttributes != null && mInitializedAttributes.length() > 0) { 318 for (String s : mInitializedAttributes.split(",")) { //$NON-NLS-1$ 319 sb.append(' '); 320 String[] nameValue = s.split("="); //$NON-NLS-1$ 321 String name = nameValue[0]; 322 String value = nameValue[1]; 323 if (name.startsWith(ANDROID_NS_NAME_PREFIX)) { 324 name = name.substring(ANDROID_NS_NAME_PREFIX.length()); 325 sb.append(androidNsPrefix).append(':'); 326 } 327 sb.append(name).append('=').append('"').append(value).append('"'); 328 } 329 } 330 331 // Transfer layout_ attributes (other than width and height) 332 if (primary != null) { 333 List<Attr> layoutAttributes = findLayoutAttributes(primary); 334 for (Attr attribute : layoutAttributes) { 335 String name = attribute.getLocalName(); 336 if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) 337 && ANDROID_URI.equals(attribute.getNamespaceURI())) { 338 // Already handled specially 339 continue; 340 } 341 342 if (attribute instanceof IndexedRegion) { 343 IndexedRegion region = (IndexedRegion) attribute; 344 int startOffset = region.getStartOffset(); 345 int endOffset = region.getEndOffset(); 346 String text = getText(startOffset, endOffset); 347 DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset); 348 rootEdit.addChild(deletion); 349 deletions.add(deletion); 350 351 if (separateAttributes) { 352 sb.append('\n').append(startIndent).append(indentUnit); 353 } else { 354 sb.append(' '); 355 } 356 sb.append(text.trim()); 357 } 358 } 359 } 360 361 // Finish open tag: 362 sb.append('>'); 363 sb.append('\n').append(startIndent).append(indentUnit); 364 365 InsertEdit beginEdit = new InsertEdit(mSelectionStart, sb.toString()); 366 rootEdit.addChild(beginEdit); 367 368 String nested = getText(mSelectionStart, mSelectionEnd); 369 int index = 0; 370 while (index != -1) { 371 index = nested.indexOf('\n', index); 372 if (index != -1) { 373 index++; 374 InsertEdit newline = new InsertEdit(mSelectionStart + index, indentUnit); 375 // Some of the deleted namespaces may have had newlines - be careful 376 // not to overlap edits 377 boolean covered = false; 378 for (DeleteEdit deletion : deletions) { 379 if (deletion.covers(newline)) { 380 covered = true; 381 break; 382 } 383 } 384 if (!covered) { 385 rootEdit.addChild(newline); 386 } 387 } 388 } 389 390 // Insert end tag: </type> 391 sb.setLength(0); 392 sb.append('\n').append(startIndent); 393 sb.append('<').append('/').append(viewClass).append('>'); 394 InsertEdit endEdit = new InsertEdit(mSelectionEnd, sb.toString()); 395 rootEdit.addChild(endEdit); 396 397 if (AdtPrefs.getPrefs().getFormatGuiXml()) { 398 MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT); 399 if (formatted != null) { 400 rootEdit = formatted; 401 } 402 } 403 404 change.setEdit(rootEdit); 405 changes.add(change); 406 return changes; 407 } 408 409 String getOldType() { 410 Element primary = getPrimaryElement(); 411 if (primary != null) { 412 String oldType = primary.getTagName(); 413 if (oldType.indexOf('.') == -1) { 414 oldType = ANDROID_WIDGET_PREFIX + oldType; 415 } 416 return oldType; 417 } 418 419 return null; 420 } 421 422 @Override 423 VisualRefactoringWizard createWizard() { 424 return new WrapInWizard(this, mDelegate); 425 } 426 427 public static class Descriptor extends VisualRefactoringDescriptor { 428 public Descriptor(String project, String description, String comment, 429 Map<String, String> arguments) { 430 super("com.android.ide.eclipse.adt.refactoring.wrapin", //$NON-NLS-1$ 431 project, description, comment, arguments); 432 } 433 434 @Override 435 protected Refactoring createRefactoring(Map<String, String> args) { 436 return new WrapInRefactoring(args); 437 } 438 } 439 } 440