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_NS_NAME_PREFIX; 20 import static com.android.SdkConstants.ANDROID_URI; 21 import static com.android.SdkConstants.ATTR_HINT; 22 import static com.android.SdkConstants.ATTR_ID; 23 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN; 24 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; 25 import static com.android.SdkConstants.ATTR_NAME; 26 import static com.android.SdkConstants.ATTR_ON_CLICK; 27 import static com.android.SdkConstants.ATTR_PARENT; 28 import static com.android.SdkConstants.ATTR_SRC; 29 import static com.android.SdkConstants.ATTR_STYLE; 30 import static com.android.SdkConstants.ATTR_TEXT; 31 import static com.android.SdkConstants.EXT_XML; 32 import static com.android.SdkConstants.FD_RESOURCES; 33 import static com.android.SdkConstants.FD_RES_VALUES; 34 import static com.android.SdkConstants.PREFIX_ANDROID; 35 import static com.android.SdkConstants.PREFIX_RESOURCE_REF; 36 import static com.android.SdkConstants.REFERENCE_STYLE; 37 import static com.android.SdkConstants.TAG_ITEM; 38 import static com.android.SdkConstants.TAG_RESOURCES; 39 import static com.android.SdkConstants.XMLNS_PREFIX; 40 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; 41 42 import com.android.annotations.NonNull; 43 import com.android.annotations.VisibleForTesting; 44 import com.android.ide.common.rendering.api.ResourceValue; 45 import com.android.ide.common.resources.ResourceResolver; 46 import com.android.ide.common.xml.XmlFormatStyle; 47 import com.android.ide.eclipse.adt.AdtPlugin; 48 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 49 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 50 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 51 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 52 import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard; 53 import com.android.utils.Pair; 54 55 import org.eclipse.core.resources.IFile; 56 import org.eclipse.core.resources.IProject; 57 import org.eclipse.core.runtime.CoreException; 58 import org.eclipse.core.runtime.IProgressMonitor; 59 import org.eclipse.core.runtime.OperationCanceledException; 60 import org.eclipse.core.runtime.Path; 61 import org.eclipse.jface.text.ITextSelection; 62 import org.eclipse.jface.viewers.ITreeSelection; 63 import org.eclipse.ltk.core.refactoring.Change; 64 import org.eclipse.ltk.core.refactoring.Refactoring; 65 import org.eclipse.ltk.core.refactoring.RefactoringStatus; 66 import org.eclipse.ltk.core.refactoring.TextFileChange; 67 import org.eclipse.text.edits.InsertEdit; 68 import org.eclipse.text.edits.MultiTextEdit; 69 import org.eclipse.wst.sse.core.StructuredModelManager; 70 import org.eclipse.wst.sse.core.internal.provisional.IModelManager; 71 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 72 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 73 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 74 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; 75 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; 76 import org.w3c.dom.Attr; 77 import org.w3c.dom.Element; 78 import org.w3c.dom.NamedNodeMap; 79 import org.w3c.dom.Node; 80 81 import java.io.IOException; 82 import java.util.ArrayList; 83 import java.util.HashSet; 84 import java.util.List; 85 import java.util.Map; 86 import java.util.Set; 87 import java.util.TreeMap; 88 89 /** 90 * Extracts the selection and writes it out as a separate layout file, then adds an 91 * include to that new layout file. Interactively asks the user for a new name for the 92 * layout. 93 * <p> 94 * Remaining work to do / Possible enhancements: 95 * <ul> 96 * <li>Optionally look in other files in the project and attempt to set style attributes 97 * in other cases where the style attributes match? 98 * <li>If the elements we are extracting from already contain a style attribute, set that 99 * style as the parent style of the current style? 100 * <li>Add a parent-style picker to the wizard (initialized with the above if applicable) 101 * <li>Pick up indentation settings from the XML module 102 * <li>Integrate with themes somehow -- make an option to have the extracted style go into 103 * the theme instead 104 * </ul> 105 */ 106 @SuppressWarnings("restriction") // XML model 107 public class ExtractStyleRefactoring extends VisualRefactoring { 108 private static final String KEY_NAME = "name"; //$NON-NLS-1$ 109 private static final String KEY_REMOVE_EXTRACTED = "removeextracted"; //$NON-NLS-1$ 110 private static final String KEY_REMOVE_ALL = "removeall"; //$NON-NLS-1$ 111 private static final String KEY_APPLY_STYLE = "applystyle"; //$NON-NLS-1$ 112 private static final String KEY_PARENT = "parent"; //$NON-NLS-1$ 113 private String mStyleName; 114 /** The name of the file in res/values/ that the style will be added to. Normally 115 * res/values/styles.xml - but unit tests pick other names */ 116 private String mStyleFileName = "styles.xml"; 117 /** Set a style reference on the extracted elements? */ 118 private boolean mApplyStyle; 119 /** Remove the attributes that were extracted? */ 120 private boolean mRemoveExtracted; 121 /** List of attributes chosen by the user to be extracted */ 122 private List<Attr> mChosenAttributes = new ArrayList<Attr>(); 123 /** Remove all attributes that match the extracted attributes names, regardless of value */ 124 private boolean mRemoveAll; 125 /** The parent style to extend */ 126 private String mParent; 127 /** The full list of available attributes in the refactoring */ 128 private Map<String, List<Attr>> mAvailableAttributes; 129 130 /** 131 * This constructor is solely used by {@link Descriptor}, 132 * to replay a previous refactoring. 133 * @param arguments argument map created by #createArgumentMap. 134 */ ExtractStyleRefactoring(Map<String, String> arguments)135 ExtractStyleRefactoring(Map<String, String> arguments) { 136 super(arguments); 137 mStyleName = arguments.get(KEY_NAME); 138 mRemoveExtracted = Boolean.parseBoolean(arguments.get(KEY_REMOVE_EXTRACTED)); 139 mRemoveAll = Boolean.parseBoolean(arguments.get(KEY_REMOVE_ALL)); 140 mApplyStyle = Boolean.parseBoolean(arguments.get(KEY_APPLY_STYLE)); 141 mParent = arguments.get(KEY_PARENT); 142 if (mParent != null && mParent.length() == 0) { 143 mParent = null; 144 } 145 } 146 ExtractStyleRefactoring( IFile file, LayoutEditorDelegate delegate, ITextSelection selection, ITreeSelection treeSelection)147 public ExtractStyleRefactoring( 148 IFile file, 149 LayoutEditorDelegate delegate, 150 ITextSelection selection, 151 ITreeSelection treeSelection) { 152 super(file, delegate, selection, treeSelection); 153 } 154 155 @VisibleForTesting ExtractStyleRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor)156 ExtractStyleRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) { 157 super(selectedElements, editor); 158 } 159 160 @Override checkInitialConditions(IProgressMonitor pm)161 public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, 162 OperationCanceledException { 163 RefactoringStatus status = new RefactoringStatus(); 164 165 try { 166 pm.beginTask("Checking preconditions...", 6); 167 168 if (mSelectionStart == -1 || mSelectionEnd == -1) { 169 status.addFatalError("No selection to extract"); 170 return status; 171 } 172 173 // This also ensures that we have a valid DOM model: 174 if (mElements.size() == 0) { 175 status.addFatalError("Nothing to extract"); 176 return status; 177 } 178 179 pm.worked(1); 180 return status; 181 182 } finally { 183 pm.done(); 184 } 185 } 186 187 @Override createDescriptor()188 protected VisualRefactoringDescriptor createDescriptor() { 189 String comment = getName(); 190 return new Descriptor( 191 mProject.getName(), //project 192 comment, //description 193 comment, //comment 194 createArgumentMap()); 195 } 196 197 @Override createArgumentMap()198 protected Map<String, String> createArgumentMap() { 199 Map<String, String> args = super.createArgumentMap(); 200 args.put(KEY_NAME, mStyleName); 201 args.put(KEY_REMOVE_EXTRACTED, Boolean.toString(mRemoveExtracted)); 202 args.put(KEY_REMOVE_ALL, Boolean.toString(mRemoveAll)); 203 args.put(KEY_APPLY_STYLE, Boolean.toString(mApplyStyle)); 204 args.put(KEY_PARENT, mParent != null ? mParent : ""); 205 206 return args; 207 } 208 209 @Override getName()210 public String getName() { 211 return "Extract Style"; 212 } 213 setStyleName(String styleName)214 void setStyleName(String styleName) { 215 mStyleName = styleName; 216 } 217 setStyleFileName(String styleFileName)218 void setStyleFileName(String styleFileName) { 219 mStyleFileName = styleFileName; 220 } 221 setChosenAttributes(List<Attr> attributes)222 void setChosenAttributes(List<Attr> attributes) { 223 mChosenAttributes = attributes; 224 } 225 setRemoveExtracted(boolean removeExtracted)226 void setRemoveExtracted(boolean removeExtracted) { 227 mRemoveExtracted = removeExtracted; 228 } 229 setApplyStyle(boolean applyStyle)230 void setApplyStyle(boolean applyStyle) { 231 mApplyStyle = applyStyle; 232 } 233 setRemoveAll(boolean removeAll)234 void setRemoveAll(boolean removeAll) { 235 mRemoveAll = removeAll; 236 } 237 setParent(String parent)238 void setParent(String parent) { 239 mParent = parent; 240 } 241 242 // ---- Actual implementation of Extract Style modification computation ---- 243 244 /** 245 * Returns two items: a map from attribute name to a list of attribute nodes of that 246 * name, and a subset of these attributes that fall within the text selection 247 * (used to drive initial selection in the wizard) 248 */ getAvailableAttributes()249 Pair<Map<String, List<Attr>>, Set<Attr>> getAvailableAttributes() { 250 mAvailableAttributes = new TreeMap<String, List<Attr>>(); 251 Set<Attr> withinSelection = new HashSet<Attr>(); 252 for (Element element : getElements()) { 253 IndexedRegion elementRegion = getRegion(element); 254 boolean allIncluded = 255 (mOriginalSelectionStart <= elementRegion.getStartOffset() && 256 mOriginalSelectionEnd >= elementRegion.getEndOffset()); 257 258 NamedNodeMap attributeMap = element.getAttributes(); 259 for (int i = 0, n = attributeMap.getLength(); i < n; i++) { 260 Attr attribute = (Attr) attributeMap.item(i); 261 262 String name = attribute.getLocalName(); 263 if (!isStylableAttribute(name)) { 264 // Don't offer to extract attributes that don't make sense in 265 // styles (like "id" or "style"), or attributes that the user 266 // probably does not want to define in styles (like layout 267 // attributes such as layout_width, or the label of a button etc). 268 // This makes the options offered listed in the wizard simpler. 269 // In special cases where the user *does* want to set one of these 270 // attributes, they can always do it manually so optimize for 271 // the common case here. 272 continue; 273 } 274 275 // Skip attributes that are in a namespace other than the Android one 276 String namespace = attribute.getNamespaceURI(); 277 if (namespace != null && !ANDROID_URI.equals(namespace)) { 278 continue; 279 } 280 281 if (!allIncluded) { 282 IndexedRegion region = getRegion(attribute); 283 boolean attributeIncluded = mOriginalSelectionStart < region.getEndOffset() && 284 mOriginalSelectionEnd >= region.getStartOffset(); 285 if (attributeIncluded) { 286 withinSelection.add(attribute); 287 } 288 } else { 289 withinSelection.add(attribute); 290 } 291 292 List<Attr> list = mAvailableAttributes.get(name); 293 if (list == null) { 294 list = new ArrayList<Attr>(); 295 mAvailableAttributes.put(name, list); 296 } 297 list.add(attribute); 298 } 299 } 300 301 return Pair.of(mAvailableAttributes, withinSelection); 302 } 303 304 /** 305 * Returns whether the given local attribute name is one the style wizard 306 * should present as a selectable attribute to be extracted. 307 * 308 * @param name the attribute name, not including a namespace prefix 309 * @return true if the name is one that the user can extract 310 */ isStylableAttribute(String name)311 public static boolean isStylableAttribute(String name) { 312 return !(name == null 313 || name.equals(ATTR_ID) 314 || name.startsWith(ATTR_STYLE) 315 || (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) && 316 !name.startsWith(ATTR_LAYOUT_MARGIN)) 317 || name.equals(ATTR_TEXT) 318 || name.equals(ATTR_HINT) 319 || name.equals(ATTR_SRC) 320 || name.equals(ATTR_ON_CLICK)); 321 } 322 getStyleFile(IProject project)323 IFile getStyleFile(IProject project) { 324 return project.getFile(new Path(FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP 325 + mStyleFileName)); 326 } 327 328 @Override computeChanges(IProgressMonitor monitor)329 protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) { 330 List<Change> changes = new ArrayList<Change>(); 331 if (mChosenAttributes.size() == 0) { 332 return changes; 333 } 334 335 IFile file = getStyleFile(mDelegate.getEditor().getProject()); 336 boolean createFile = !file.exists(); 337 int insertAtIndex; 338 String initialIndent = null; 339 if (!createFile) { 340 Pair<Integer, String> context = computeInsertContext(file); 341 insertAtIndex = context.getFirst(); 342 initialIndent = context.getSecond(); 343 } else { 344 insertAtIndex = 0; 345 } 346 347 TextFileChange addFile = new TextFileChange("Create new separate style declaration", file); 348 addFile.setTextType(EXT_XML); 349 changes.add(addFile); 350 String styleString = computeStyleDeclaration(createFile, initialIndent); 351 addFile.setEdit(new InsertEdit(insertAtIndex, styleString)); 352 353 // Remove extracted attributes? 354 MultiTextEdit rootEdit = new MultiTextEdit(); 355 if (mRemoveExtracted || mRemoveAll) { 356 for (Attr attribute : mChosenAttributes) { 357 List<Attr> list = mAvailableAttributes.get(attribute.getLocalName()); 358 for (Attr attr : list) { 359 if (mRemoveAll || attr.getValue().equals(attribute.getValue())) { 360 removeAttribute(rootEdit, attr); 361 } 362 } 363 } 364 } 365 366 // Set the style attribute? 367 if (mApplyStyle) { 368 for (Element element : getElements()) { 369 String value = PREFIX_RESOURCE_REF + REFERENCE_STYLE + mStyleName; 370 setAttribute(rootEdit, element, null, null, ATTR_STYLE, value); 371 } 372 } 373 374 if (rootEdit.hasChildren()) { 375 IFile sourceFile = mDelegate.getEditor().getInputFile(); 376 if (sourceFile == null) { 377 return changes; 378 } 379 TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile); 380 change.setTextType(EXT_XML); 381 changes.add(change); 382 383 if (AdtPrefs.getPrefs().getFormatGuiXml()) { 384 MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT); 385 if (formatted != null) { 386 rootEdit = formatted; 387 } 388 } 389 390 change.setEdit(rootEdit); 391 } 392 393 return changes; 394 } 395 computeStyleDeclaration(boolean createFile, String initialIndent)396 private String computeStyleDeclaration(boolean createFile, String initialIndent) { 397 StringBuilder sb = new StringBuilder(); 398 if (createFile) { 399 sb.append(NewXmlFileWizard.XML_HEADER_LINE); 400 sb.append('<').append(TAG_RESOURCES).append(' '); 401 sb.append(XMLNS_PREFIX).append(ANDROID_NS_NAME).append('=').append('"'); 402 sb.append(ANDROID_URI); 403 sb.append('"').append('>').append('\n'); 404 } 405 406 // Indent. Use the existing indent found for previous <style> elements in 407 // the resource file - but if that indent was 0 (e.g. <style> elements are 408 // at the left margin) only use it to indent the style elements and use a real 409 // nonzero indent for its children. 410 String indent = " "; //$NON-NLS-1$ 411 if (initialIndent == null) { 412 initialIndent = indent; 413 } else if (initialIndent.length() > 0) { 414 indent = initialIndent; 415 } 416 sb.append(initialIndent); 417 String styleTag = "style"; //$NON-NLS-1$ // TODO - use constant in parallel changeset 418 sb.append('<').append(styleTag).append(' ').append(ATTR_NAME).append('=').append('"'); 419 sb.append(mStyleName); 420 sb.append('"'); 421 if (mParent != null) { 422 sb.append(' ').append(ATTR_PARENT).append('=').append('"'); 423 sb.append(mParent); 424 sb.append('"'); 425 } 426 sb.append('>').append('\n'); 427 428 for (Attr attribute : mChosenAttributes) { 429 sb.append(initialIndent).append(indent); 430 sb.append('<').append(TAG_ITEM).append(' ').append(ATTR_NAME).append('=').append('"'); 431 // We've already enforced that regardless of prefix, only attributes with 432 // an Android namespace can be in the set of chosen attributes. Rewrite the 433 // prefix to android here. 434 if (attribute.getPrefix() != null) { 435 sb.append(ANDROID_NS_NAME_PREFIX); 436 } 437 sb.append(attribute.getLocalName()); 438 sb.append('"').append('>'); 439 sb.append(attribute.getValue()); 440 sb.append('<').append('/').append(TAG_ITEM).append('>').append('\n'); 441 } 442 sb.append(initialIndent).append('<').append('/').append(styleTag).append('>').append('\n'); 443 444 if (createFile) { 445 sb.append('<').append('/').append(TAG_RESOURCES).append('>').append('\n'); 446 } 447 String styleString = sb.toString(); 448 return styleString; 449 } 450 451 /** Computes the location in the file to insert the new style element at, as well as 452 * the exact indent string to use to indent the {@code <style>} element. 453 * @param file the styles.xml file to insert into 454 * @return a pair of an insert offset and an indent string 455 */ computeInsertContext(final IFile file)456 private Pair<Integer, String> computeInsertContext(final IFile file) { 457 int insertAtIndex = -1; 458 // Find the insert of the final </resources> item where we will insert 459 // the new style elements. 460 String indent = null; 461 IModelManager modelManager = StructuredModelManager.getModelManager(); 462 IStructuredModel model = null; 463 try { 464 model = modelManager.getModelForRead(file); 465 if (model instanceof IDOMModel) { 466 IDOMModel domModel = (IDOMModel) model; 467 IDOMDocument otherDocument = domModel.getDocument(); 468 Element root = otherDocument.getDocumentElement(); 469 Node lastChild = root.getLastChild(); 470 if (lastChild != null) { 471 if (lastChild instanceof IndexedRegion) { 472 IndexedRegion region = (IndexedRegion) lastChild; 473 insertAtIndex = region.getStartOffset() + region.getLength(); 474 } 475 476 // Compute indent 477 while (lastChild != null) { 478 if (lastChild.getNodeType() == Node.ELEMENT_NODE) { 479 IStructuredDocument document = model.getStructuredDocument(); 480 indent = AndroidXmlEditor.getIndent(document, lastChild); 481 break; 482 } 483 lastChild = lastChild.getPreviousSibling(); 484 } 485 } 486 } 487 } catch (IOException e) { 488 AdtPlugin.log(e, null); 489 } catch (CoreException e) { 490 AdtPlugin.log(e, null); 491 } finally { 492 if (model != null) { 493 model.releaseFromRead(); 494 } 495 } 496 497 if (insertAtIndex == -1) { 498 String contents = AdtPlugin.readFile(file); 499 insertAtIndex = contents.indexOf("</" + TAG_RESOURCES + ">"); //$NON-NLS-1$ 500 if (insertAtIndex == -1) { 501 insertAtIndex = contents.length(); 502 } 503 } 504 505 return Pair.of(insertAtIndex, indent); 506 } 507 508 @Override createWizard()509 VisualRefactoringWizard createWizard() { 510 return new ExtractStyleWizard(this, mDelegate); 511 } 512 513 public static class Descriptor extends VisualRefactoringDescriptor { Descriptor(String project, String description, String comment, Map<String, String> arguments)514 public Descriptor(String project, String description, String comment, 515 Map<String, String> arguments) { 516 super("com.android.ide.eclipse.adt.refactoring.extract.style", //$NON-NLS-1$ 517 project, description, comment, arguments); 518 } 519 520 @Override createRefactoring(Map<String, String> args)521 protected Refactoring createRefactoring(Map<String, String> args) { 522 return new ExtractStyleRefactoring(args); 523 } 524 } 525 526 /** 527 * Determines the parent style to be used for this refactoring 528 * 529 * @return the parent style to be used for this refactoring 530 */ getParentStyle()531 public String getParentStyle() { 532 Set<String> styles = new HashSet<String>(); 533 for (Element element : getElements()) { 534 // Includes "" for elements not setting the style 535 styles.add(element.getAttribute(ATTR_STYLE)); 536 } 537 538 if (styles.size() > 1) { 539 // The elements differ in what style attributes they are set to 540 return null; 541 } 542 543 String style = styles.iterator().next(); 544 if (style != null && style.length() > 0) { 545 return style; 546 } 547 548 // None of the elements set the style -- see if they have the same widget types 549 // and if so offer to extend the theme style for that widget type 550 551 Set<String> types = new HashSet<String>(); 552 for (Element element : getElements()) { 553 types.add(element.getTagName()); 554 } 555 556 if (types.size() == 1) { 557 String view = DescriptorsUtils.getBasename(types.iterator().next()); 558 559 ResourceResolver resolver = mDelegate.getGraphicalEditor().getResourceResolver(); 560 // Look up the theme item name, which for a Button would be "buttonStyle", and so on. 561 String n = Character.toLowerCase(view.charAt(0)) + view.substring(1) 562 + "Style"; //$NON-NLS-1$ 563 ResourceValue value = resolver.findItemInTheme(n); 564 if (value != null) { 565 ResourceValue resolvedValue = resolver.resolveResValue(value); 566 String name = resolvedValue.getName(); 567 if (name != null) { 568 if (resolvedValue.isFramework()) { 569 return PREFIX_ANDROID + name; 570 } else { 571 return name; 572 } 573 } 574 } 575 } 576 577 return null; 578 } 579 } 580