Home | History | Annotate | Download | only in refactoring
      1 /*
      2  * Copyright (C) 2012 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_URI;
     19 import static com.android.SdkConstants.ATTR_DRAWABLE_BOTTOM;
     20 import static com.android.SdkConstants.ATTR_DRAWABLE_LEFT;
     21 import static com.android.SdkConstants.ATTR_DRAWABLE_PADDING;
     22 import static com.android.SdkConstants.ATTR_DRAWABLE_RIGHT;
     23 import static com.android.SdkConstants.ATTR_DRAWABLE_TOP;
     24 import static com.android.SdkConstants.ATTR_GRAVITY;
     25 import static com.android.SdkConstants.ATTR_ID;
     26 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
     27 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
     28 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
     29 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
     30 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
     31 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
     32 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
     33 import static com.android.SdkConstants.ATTR_ORIENTATION;
     34 import static com.android.SdkConstants.ATTR_SRC;
     35 import static com.android.SdkConstants.EXT_XML;
     36 import static com.android.SdkConstants.IMAGE_VIEW;
     37 import static com.android.SdkConstants.LINEAR_LAYOUT;
     38 import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
     39 import static com.android.SdkConstants.TEXT_VIEW;
     40 import static com.android.SdkConstants.VALUE_VERTICAL;
     41 
     42 import com.android.annotations.NonNull;
     43 import com.android.annotations.Nullable;
     44 import com.android.annotations.VisibleForTesting;
     45 import com.android.ide.common.xml.XmlFormatStyle;
     46 import com.android.ide.eclipse.adt.AdtUtils;
     47 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
     48 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
     49 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     50 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
     51 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     52 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     53 
     54 import org.eclipse.core.resources.IFile;
     55 import org.eclipse.core.runtime.CoreException;
     56 import org.eclipse.core.runtime.IProgressMonitor;
     57 import org.eclipse.core.runtime.OperationCanceledException;
     58 import org.eclipse.jface.text.ITextSelection;
     59 import org.eclipse.jface.viewers.ITreeSelection;
     60 import org.eclipse.ltk.core.refactoring.Change;
     61 import org.eclipse.ltk.core.refactoring.Refactoring;
     62 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
     63 import org.eclipse.ltk.core.refactoring.TextFileChange;
     64 import org.eclipse.text.edits.MultiTextEdit;
     65 import org.eclipse.text.edits.ReplaceEdit;
     66 import org.eclipse.text.edits.TextEdit;
     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.w3c.dom.Attr;
     71 import org.w3c.dom.Document;
     72 import org.w3c.dom.Element;
     73 import org.w3c.dom.NamedNodeMap;
     74 
     75 import java.util.ArrayList;
     76 import java.util.List;
     77 import java.util.Map;
     78 import java.util.regex.Matcher;
     79 import java.util.regex.Pattern;
     80 
     81 /**
     82  * Converts a LinearLayout with exactly a TextView child and an ImageView child into
     83  * a single TextView with a compound drawable.
     84  */
     85 @SuppressWarnings("restriction") // XML model
     86 public class UseCompoundDrawableRefactoring extends VisualRefactoring {
     87     /**
     88      * Constructs a new {@link UseCompoundDrawableRefactoring}
     89      *
     90      * @param file the file to refactor in
     91      * @param editor the corresponding editor
     92      * @param selection the editor selection, or null
     93      * @param treeSelection the canvas selection, or null
     94      */
     95     public UseCompoundDrawableRefactoring(IFile file, LayoutEditorDelegate editor,
     96             ITextSelection selection, ITreeSelection treeSelection) {
     97         super(file, editor, selection, treeSelection);
     98     }
     99 
    100     /**
    101      * This constructor is solely used by {@link Descriptor}, to replay a
    102      * previous refactoring.
    103      *
    104      * @param arguments argument map created by #createArgumentMap.
    105      */
    106     private UseCompoundDrawableRefactoring(Map<String, String> arguments) {
    107         super(arguments);
    108     }
    109 
    110     @VisibleForTesting
    111     UseCompoundDrawableRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
    112         super(selectedElements, editor);
    113     }
    114 
    115     @Override
    116     public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
    117             OperationCanceledException {
    118         RefactoringStatus status = new RefactoringStatus();
    119 
    120         try {
    121             pm.beginTask("Checking preconditions...", 6);
    122 
    123             if (mSelectionStart == -1 || mSelectionEnd == -1) {
    124                 status.addFatalError("Nothing to convert");
    125                 return status;
    126             }
    127 
    128             // Make sure the selection is contiguous
    129             if (mTreeSelection != null) {
    130                 List<CanvasViewInfo> infos = getSelectedViewInfos();
    131                 if (!validateNotEmpty(infos, status)) {
    132                     return status;
    133                 }
    134 
    135                 // Enforce that the selection is -contiguous-
    136                 if (!validateContiguous(infos, status)) {
    137                     return status;
    138                 }
    139             }
    140 
    141             // Ensures that we have a valid DOM model:
    142             if (mElements.size() == 0) {
    143                 status.addFatalError("Nothing to convert");
    144                 return status;
    145             }
    146 
    147             // Ensure that we have selected precisely one LinearLayout
    148             if (mElements.size() != 1 ||
    149                     !(mElements.get(0).getTagName().equals(LINEAR_LAYOUT))) {
    150                 status.addFatalError("Must select exactly one LinearLayout");
    151                 return status;
    152             }
    153 
    154             Element layout = mElements.get(0);
    155             List<Element> children = DomUtilities.getChildren(layout);
    156             if (children.size() != 2) {
    157                 status.addFatalError("The LinearLayout must have exactly two children");
    158                 return status;
    159             }
    160             Element first = children.get(0);
    161             Element second = children.get(1);
    162             boolean haveTextView =
    163                     first.getTagName().equals(TEXT_VIEW)
    164                     || second.getTagName().equals(TEXT_VIEW);
    165             boolean haveImageView =
    166                     first.getTagName().equals(IMAGE_VIEW)
    167                     || second.getTagName().equals(IMAGE_VIEW);
    168             if (!(haveTextView && haveImageView)) {
    169                 status.addFatalError("The LinearLayout must have exactly one TextView child " +
    170                         "and one ImageView child");
    171                 return status;
    172             }
    173 
    174             pm.worked(1);
    175             return status;
    176 
    177         } finally {
    178             pm.done();
    179         }
    180     }
    181 
    182     @Override
    183     protected VisualRefactoringDescriptor createDescriptor() {
    184         String comment = getName();
    185         return new Descriptor(
    186                 mProject.getName(), //project
    187                 comment, //description
    188                 comment, //comment
    189                 createArgumentMap());
    190     }
    191 
    192     @Override
    193     protected Map<String, String> createArgumentMap() {
    194         return super.createArgumentMap();
    195     }
    196 
    197     @Override
    198     public String getName() {
    199         return "Convert to Compound Drawable";
    200     }
    201 
    202     @Override
    203     protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
    204         String androidNsPrefix = getAndroidNamespacePrefix();
    205         IFile file = mDelegate.getEditor().getInputFile();
    206         List<Change> changes = new ArrayList<Change>();
    207         if (file == null) {
    208             return changes;
    209         }
    210         TextFileChange change = new TextFileChange(file.getName(), file);
    211         MultiTextEdit rootEdit = new MultiTextEdit();
    212         change.setTextType(EXT_XML);
    213 
    214         // (1) Build up the contents of the new TextView. This is identical
    215         //     to the old contents, but with the addition of a drawableTop/Left/Right/Bottom
    216         //     attribute (depending on the orientation and order), as well as any layout
    217         //     params from the LinearLayout.
    218         // (2) Delete the linear layout and replace with the text view.
    219         // (3) Reformat.
    220 
    221         // checkInitialConditions has already validated that we have exactly a LinearLayout
    222         // with an ImageView and a TextView child (in either order)
    223         Element layout = mElements.get(0);
    224         List<Element> children = DomUtilities.getChildren(layout);
    225         Element first = children.get(0);
    226         Element second = children.get(1);
    227         final Element text;
    228         final Element image;
    229         if (first.getTagName().equals(TEXT_VIEW)) {
    230             text = first;
    231             image = second;
    232         } else {
    233             text = second;
    234             image = first;
    235         }
    236 
    237         // Horizontal is the default, so if no value is specified it is horizontal.
    238         boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
    239                 ATTR_ORIENTATION));
    240 
    241         // The WST DOM implementation doesn't correctly implement cloneNode: this returns
    242         // an empty document instead:
    243         //   text.getOwnerDocument().cloneNode(false/*deep*/);
    244         // Luckily we just need to clone a single element, not a nested structure, so it's
    245         // easy enough to do this manually:
    246         Document tempDocument = DomUtilities.createEmptyDocument();
    247         if (tempDocument == null) {
    248             return changes;
    249         }
    250         Element newTextElement = tempDocument.createElement(text.getTagName());
    251         tempDocument.appendChild(newTextElement);
    252 
    253         NamedNodeMap attributes =  text.getAttributes();
    254         for (int i = 0, n = attributes.getLength(); i < n; i++) {
    255             Attr attribute = (Attr) attributes.item(i);
    256             String name = attribute.getLocalName();
    257             if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
    258                     && ANDROID_URI.equals(attribute.getNamespaceURI())
    259                     && !(name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))) {
    260                 // Ignore layout params: the parent layout is going away
    261             } else {
    262                 newTextElement.setAttribute(attribute.getName(), attribute.getValue());
    263             }
    264         }
    265 
    266         // Apply all layout params from the parent (except width and height),
    267         // as well as android:gravity
    268         List<Attr> layoutAttributes = findLayoutAttributes(layout);
    269         for (Attr attribute : layoutAttributes) {
    270             String name = attribute.getLocalName();
    271             if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))
    272                     && ANDROID_URI.equals(attribute.getNamespaceURI())) {
    273                 // Already handled specially
    274                 continue;
    275             }
    276             newTextElement.setAttribute(attribute.getName(), attribute.getValue());
    277         }
    278         String gravity = layout.getAttributeNS(ANDROID_URI, ATTR_GRAVITY);
    279         if (gravity.length() > 0) {
    280             setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_GRAVITY, gravity);
    281         }
    282 
    283         String src = image.getAttributeNS(ANDROID_URI, ATTR_SRC);
    284 
    285         // Set the drawable
    286         String drawableAttribute;
    287         // The space between the image and the text can have margins/padding, both
    288         // from the text's perspective and from the image's perspective. We need to
    289         // combine these.
    290         String padding1 = null;
    291         String padding2 = null;
    292         if (isVertical) {
    293             if (first == image) {
    294                 drawableAttribute = ATTR_DRAWABLE_TOP;
    295                 padding1 = getPadding(image, ATTR_LAYOUT_MARGIN_BOTTOM);
    296                 padding2 = getPadding(text, ATTR_LAYOUT_MARGIN_TOP);
    297             } else {
    298                 drawableAttribute = ATTR_DRAWABLE_BOTTOM;
    299                 padding1 = getPadding(text, ATTR_LAYOUT_MARGIN_BOTTOM);
    300                 padding2 = getPadding(image, ATTR_LAYOUT_MARGIN_TOP);
    301             }
    302         } else {
    303             if (first == image) {
    304                 drawableAttribute = ATTR_DRAWABLE_LEFT;
    305                 padding1 = getPadding(image, ATTR_LAYOUT_MARGIN_RIGHT);
    306                 padding2 = getPadding(text, ATTR_LAYOUT_MARGIN_LEFT);
    307             } else {
    308                 drawableAttribute = ATTR_DRAWABLE_RIGHT;
    309                 padding1 = getPadding(text, ATTR_LAYOUT_MARGIN_RIGHT);
    310                 padding2 = getPadding(image, ATTR_LAYOUT_MARGIN_LEFT);
    311             }
    312         }
    313 
    314         setAndroidAttribute(newTextElement, androidNsPrefix, drawableAttribute, src);
    315 
    316         String padding = combine(padding1, padding2);
    317         if (padding != null) {
    318             setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_DRAWABLE_PADDING, padding);
    319         }
    320 
    321         // If the removed LinearLayout is the root container, transfer its namespace
    322         // declaration to the TextView
    323         if (layout.getParentNode() instanceof Document) {
    324             List<Attr> declarations = findNamespaceAttributes(layout);
    325             for (Attr attribute : declarations) {
    326                 if (attribute instanceof IndexedRegion) {
    327                     newTextElement.setAttribute(attribute.getName(), attribute.getValue());
    328                 }
    329             }
    330         }
    331 
    332         // Update any layout references to the layout to point to the text view
    333         String layoutId = getId(layout);
    334         if (layoutId.length() > 0) {
    335             String id = getId(text);
    336             if (id.length() == 0) {
    337                 id = ensureHasId(rootEdit, text, null, false);
    338                 setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_ID, id);
    339             }
    340 
    341             IStructuredModel model = mDelegate.getEditor().getModelForRead();
    342             try {
    343                 IStructuredDocument doc = model.getStructuredDocument();
    344                 if (doc != null) {
    345                     List<TextEdit> replaceIds = replaceIds(androidNsPrefix,
    346                             doc, mSelectionStart, mSelectionEnd, layoutId, id);
    347                     for (TextEdit edit : replaceIds) {
    348                         rootEdit.addChild(edit);
    349                     }
    350                 }
    351             } finally {
    352                 model.releaseFromRead();
    353             }
    354         }
    355 
    356         String xml = EclipseXmlPrettyPrinter.prettyPrint(
    357                 tempDocument.getDocumentElement(),
    358                 EclipseXmlFormatPreferences.create(),
    359                 XmlFormatStyle.LAYOUT, null, false);
    360 
    361         TextEdit replace = new ReplaceEdit(mSelectionStart, mSelectionEnd - mSelectionStart, xml);
    362         rootEdit.addChild(replace);
    363 
    364         if (AdtPrefs.getPrefs().getFormatGuiXml()) {
    365             MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
    366             if (formatted != null) {
    367                 rootEdit = formatted;
    368             }
    369         }
    370 
    371         change.setEdit(rootEdit);
    372         changes.add(change);
    373         return changes;
    374     }
    375 
    376     @Nullable
    377     private static String getPadding(@NonNull Element element, @NonNull String attribute) {
    378         String padding = element.getAttributeNS(ANDROID_URI, attribute);
    379         if (padding != null && padding.isEmpty()) {
    380             padding = null;
    381         }
    382         return padding;
    383     }
    384 
    385     @VisibleForTesting
    386     @Nullable
    387     static String combine(@Nullable String dimension1, @Nullable String dimension2) {
    388         if (dimension1 == null || dimension1.isEmpty()) {
    389             if (dimension2 != null && dimension2.isEmpty()) {
    390                 return null;
    391             }
    392             return dimension2;
    393         } else if (dimension2 == null || dimension2.isEmpty()) {
    394             if (dimension1 != null && dimension1.isEmpty()) {
    395                 return null;
    396             }
    397             return dimension1;
    398         } else {
    399             // Two dimensions are specified (e.g. marginRight for the left one and marginLeft
    400             // for the right one); we have to add these together. We can only do that if
    401             // they use the same units, and do not use resources.
    402             if (dimension1.startsWith(PREFIX_RESOURCE_REF)
    403                     || dimension2.startsWith(PREFIX_RESOURCE_REF)) {
    404                 return null;
    405             }
    406 
    407             Pattern p = Pattern.compile("([\\d\\.]+)(.+)"); //$NON-NLS-1$
    408             Matcher matcher1 = p.matcher(dimension1);
    409             Matcher matcher2 = p.matcher(dimension2);
    410             if (matcher1.matches() && matcher2.matches()) {
    411                 String unit = matcher1.group(2);
    412                 if (unit.equals(matcher2.group(2))) {
    413                     float value1 = Float.parseFloat(matcher1.group(1));
    414                     float value2 = Float.parseFloat(matcher2.group(1));
    415                     return AdtUtils.formatFloatAttribute(value1 + value2) + unit;
    416                 }
    417             }
    418         }
    419 
    420         return null;
    421     }
    422 
    423     /**
    424      * Sets an Android attribute (in the Android namespace) on an element
    425      * without a given namespace prefix. This is done when building a new Element
    426      * in a temporary document such that the namespace prefix matches when the element is
    427      * formatted and replaced in the target document.
    428      */
    429     private static void setAndroidAttribute(Element element, String prefix, String name,
    430             String value) {
    431         element.setAttribute(prefix + ':' + name, value);
    432     }
    433 
    434     @Override
    435     public VisualRefactoringWizard createWizard() {
    436         return new UseCompoundDrawableWizard(this, mDelegate);
    437     }
    438 
    439     @SuppressWarnings("javadoc")
    440     public static class Descriptor extends VisualRefactoringDescriptor {
    441         public Descriptor(String project, String description, String comment,
    442                 Map<String, String> arguments) {
    443             super("com.android.ide.eclipse.adt.refactoring.usecompound", //$NON-NLS-1$
    444                     project, description, comment, arguments);
    445         }
    446 
    447         @Override
    448         protected Refactoring createRefactoring(Map<String, String> args) {
    449             return new UseCompoundDrawableRefactoring(args);
    450         }
    451     }
    452 }
    453