• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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