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