Home | History | Annotate | Download | only in refactoring
      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      */
    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 
    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
    156     ExtractStyleRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
    157         super(selectedElements, editor);
    158     }
    159 
    160     @Override
    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
    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
    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
    210     public String getName() {
    211         return "Extract Style";
    212     }
    213 
    214     void setStyleName(String styleName) {
    215         mStyleName = styleName;
    216     }
    217 
    218     void setStyleFileName(String styleFileName) {
    219         mStyleFileName = styleFileName;
    220     }
    221 
    222     void setChosenAttributes(List<Attr> attributes) {
    223         mChosenAttributes = attributes;
    224     }
    225 
    226     void setRemoveExtracted(boolean removeExtracted) {
    227         mRemoveExtracted = removeExtracted;
    228     }
    229 
    230     void setApplyStyle(boolean applyStyle) {
    231         mApplyStyle = applyStyle;
    232     }
    233 
    234     void setRemoveAll(boolean removeAll) {
    235         mRemoveAll = removeAll;
    236     }
    237 
    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      */
    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      */
    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 
    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
    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 
    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      */
    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
    509     VisualRefactoringWizard createWizard() {
    510         return new ExtractStyleWizard(this, mDelegate);
    511     }
    512 
    513     public static class Descriptor extends VisualRefactoringDescriptor {
    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
    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      */
    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