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_PREFIX;
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
     21 import static com.android.SdkConstants.ATTR_ID;
     22 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
     23 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
     24 import static com.android.SdkConstants.EXT_XML;
     25 import static com.android.SdkConstants.VALUE_FILL_PARENT;
     26 import static com.android.SdkConstants.VALUE_MATCH_PARENT;
     27 import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
     28 
     29 import com.android.annotations.NonNull;
     30 import com.android.annotations.VisibleForTesting;
     31 import com.android.ide.common.xml.XmlFormatStyle;
     32 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     33 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     34 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
     35 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     36 
     37 import org.eclipse.core.resources.IFile;
     38 import org.eclipse.core.runtime.CoreException;
     39 import org.eclipse.core.runtime.IProgressMonitor;
     40 import org.eclipse.core.runtime.OperationCanceledException;
     41 import org.eclipse.jface.text.ITextSelection;
     42 import org.eclipse.jface.viewers.ITreeSelection;
     43 import org.eclipse.ltk.core.refactoring.Change;
     44 import org.eclipse.ltk.core.refactoring.Refactoring;
     45 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
     46 import org.eclipse.ltk.core.refactoring.TextFileChange;
     47 import org.eclipse.text.edits.DeleteEdit;
     48 import org.eclipse.text.edits.InsertEdit;
     49 import org.eclipse.text.edits.MultiTextEdit;
     50 import org.eclipse.text.edits.TextEdit;
     51 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     52 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     53 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     54 import org.w3c.dom.Attr;
     55 import org.w3c.dom.Element;
     56 
     57 import java.util.ArrayList;
     58 import java.util.List;
     59 import java.util.Map;
     60 
     61 /**
     62  * Inserts a new layout surrounding the current selection, migrates namespace
     63  * attributes (if wrapping the root node), and optionally migrates layout
     64  * attributes and updates references elsewhere.
     65  */
     66 @SuppressWarnings("restriction") // XML model
     67 public class WrapInRefactoring extends VisualRefactoring {
     68     private static final String KEY_ID = "name";                           //$NON-NLS-1$
     69     private static final String KEY_TYPE = "type";                         //$NON-NLS-1$
     70 
     71     private String mId;
     72     private String mTypeFqcn;
     73     private String mInitializedAttributes;
     74 
     75     /**
     76      * This constructor is solely used by {@link Descriptor},
     77      * to replay a previous refactoring.
     78      * @param arguments argument map created by #createArgumentMap.
     79      */
     80     WrapInRefactoring(Map<String, String> arguments) {
     81         super(arguments);
     82         mId = arguments.get(KEY_ID);
     83         mTypeFqcn = arguments.get(KEY_TYPE);
     84     }
     85 
     86     public WrapInRefactoring(
     87             IFile file,
     88             LayoutEditorDelegate delegate,
     89             ITextSelection selection,
     90             ITreeSelection treeSelection) {
     91         super(file, delegate, selection, treeSelection);
     92     }
     93 
     94     @VisibleForTesting
     95     WrapInRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) {
     96         super(selectedElements, editor);
     97     }
     98 
     99     @Override
    100     public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
    101             OperationCanceledException {
    102         RefactoringStatus status = new RefactoringStatus();
    103 
    104         try {
    105             pm.beginTask("Checking preconditions...", 6);
    106 
    107             if (mSelectionStart == -1 || mSelectionEnd == -1) {
    108                 status.addFatalError("No selection to wrap");
    109                 return status;
    110             }
    111 
    112             // Make sure the selection is contiguous
    113             if (mTreeSelection != null) {
    114                 // TODO - don't do this if we based the selection on text. In this case,
    115                 // make sure we're -balanced-.
    116 
    117                 List<CanvasViewInfo> infos = getSelectedViewInfos();
    118                 if (!validateNotEmpty(infos, status)) {
    119                     return status;
    120                 }
    121 
    122                 // Enforce that the selection is -contiguous-
    123                 if (!validateContiguous(infos, status)) {
    124                     return status;
    125                 }
    126             }
    127 
    128             // Ensures that we have a valid DOM model:
    129             if (mElements.size() == 0) {
    130                 status.addFatalError("Nothing to wrap");
    131                 return status;
    132             }
    133 
    134             pm.worked(1);
    135             return status;
    136 
    137         } finally {
    138             pm.done();
    139         }
    140     }
    141 
    142     @Override
    143     protected VisualRefactoringDescriptor createDescriptor() {
    144         String comment = getName();
    145         return new Descriptor(
    146                 mProject.getName(), //project
    147                 comment, //description
    148                 comment, //comment
    149                 createArgumentMap());
    150     }
    151 
    152     @Override
    153     protected Map<String, String> createArgumentMap() {
    154         Map<String, String> args = super.createArgumentMap();
    155         args.put(KEY_TYPE, mTypeFqcn);
    156         args.put(KEY_ID, mId);
    157 
    158         return args;
    159     }
    160 
    161     @Override
    162     public String getName() {
    163         return "Wrap in Container";
    164     }
    165 
    166     void setId(String id) {
    167         mId = id;
    168     }
    169 
    170     void setType(String typeFqcn) {
    171         mTypeFqcn = typeFqcn;
    172     }
    173 
    174     void setInitializedAttributes(String initializedAttributes) {
    175         mInitializedAttributes = initializedAttributes;
    176     }
    177 
    178     @Override
    179     protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
    180         // (1) Insert the new container in front of the beginning of the
    181         //      first wrapped view
    182         // (2) If the container is the new root, transfer namespace declarations
    183         //      to it
    184         // (3) Insert the closing tag of the new container at the end of the
    185         //      last wrapped view
    186         // (4) Reindent the wrapped views
    187         // (5) If the user requested it, update all layout references to the
    188         //      wrapped views with the new container?
    189         //   For that matter, does RelativeLayout even require it? Probably not,
    190         //   it can point inside the current layout...
    191 
    192         // Add indent to all lines between mSelectionStart and mEnd
    193         // TODO: Figure out the indentation amount?
    194         // For now, use 4 spaces
    195         String indentUnit = "    "; //$NON-NLS-1$
    196         boolean separateAttributes = true;
    197         IStructuredDocument document = mDelegate.getEditor().getStructuredDocument();
    198         String startIndent = AndroidXmlEditor.getIndentAtOffset(document, mSelectionStart);
    199 
    200         String viewClass = getViewClass(mTypeFqcn);
    201         String androidNsPrefix = getAndroidNamespacePrefix();
    202 
    203 
    204         IFile file = mDelegate.getEditor().getInputFile();
    205         List<Change> changes = new ArrayList<Change>();
    206         if (file == null) {
    207             return changes;
    208         }
    209         TextFileChange change = new TextFileChange(file.getName(), file);
    210         MultiTextEdit rootEdit = new MultiTextEdit();
    211         change.setTextType(EXT_XML);
    212 
    213         String id = ensureNewId(mId);
    214 
    215         // Update any layout references to the old id with the new id
    216         if (id != null) {
    217             String rootId = getRootId();
    218             IStructuredModel model = mDelegate.getEditor().getModelForRead();
    219             try {
    220                 IStructuredDocument doc = model.getStructuredDocument();
    221                 if (doc != null) {
    222                     List<TextEdit> replaceIds = replaceIds(androidNsPrefix,
    223                             doc, mSelectionStart, mSelectionEnd, rootId, id);
    224                     for (TextEdit edit : replaceIds) {
    225                         rootEdit.addChild(edit);
    226                     }
    227                 }
    228             } finally {
    229                 model.releaseFromRead();
    230             }
    231         }
    232 
    233         // Insert namespace elements?
    234         StringBuilder namespace = null;
    235         List<DeleteEdit> deletions = new ArrayList<DeleteEdit>();
    236         Element primary = getPrimaryElement();
    237         if (primary != null && getDomDocument().getDocumentElement() == primary) {
    238             namespace = new StringBuilder();
    239 
    240             List<Attr> declarations = findNamespaceAttributes(primary);
    241             for (Attr attribute : declarations) {
    242                 if (attribute instanceof IndexedRegion) {
    243                     // Delete the namespace declaration in the node which is no longer the root
    244                     IndexedRegion region = (IndexedRegion) attribute;
    245                     int startOffset = region.getStartOffset();
    246                     int endOffset = region.getEndOffset();
    247                     String text = getText(startOffset, endOffset);
    248                     DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
    249                     deletions.add(deletion);
    250                     rootEdit.addChild(deletion);
    251                     text = text.trim();
    252 
    253                     // Insert the namespace declaration in the new root
    254                     if (separateAttributes) {
    255                         namespace.append('\n').append(startIndent).append(indentUnit);
    256                     } else {
    257                         namespace.append(' ');
    258                     }
    259                     namespace.append(text);
    260                 }
    261             }
    262         }
    263 
    264         // Insert begin tag: <type ...>
    265         StringBuilder sb = new StringBuilder();
    266         sb.append('<');
    267         sb.append(viewClass);
    268 
    269         if (namespace != null) {
    270             sb.append(namespace);
    271         }
    272 
    273         // Set the ID if any
    274         if (id != null) {
    275             if (separateAttributes) {
    276                 sb.append('\n').append(startIndent).append(indentUnit);
    277             } else {
    278                 sb.append(' ');
    279             }
    280             sb.append(androidNsPrefix).append(':');
    281             sb.append(ATTR_ID).append('=').append('"').append(id).append('"');
    282         }
    283 
    284         // If any of the elements are fill/match parent, use that instead
    285         String width = VALUE_WRAP_CONTENT;
    286         String height = VALUE_WRAP_CONTENT;
    287 
    288         for (Element element : getElements()) {
    289             String oldWidth = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
    290             String oldHeight = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
    291 
    292             if (VALUE_MATCH_PARENT.equals(oldWidth) || VALUE_FILL_PARENT.equals(oldWidth)) {
    293                 width = oldWidth;
    294             }
    295             if (VALUE_MATCH_PARENT.equals(oldHeight) || VALUE_FILL_PARENT.equals(oldHeight)) {
    296                 height = oldHeight;
    297             }
    298         }
    299 
    300         // Add in width/height.
    301         if (separateAttributes) {
    302             sb.append('\n').append(startIndent).append(indentUnit);
    303         } else {
    304             sb.append(' ');
    305         }
    306         sb.append(androidNsPrefix).append(':');
    307         sb.append(ATTR_LAYOUT_WIDTH).append('=').append('"').append(width).append('"');
    308 
    309         if (separateAttributes) {
    310             sb.append('\n').append(startIndent).append(indentUnit);
    311         } else {
    312             sb.append(' ');
    313         }
    314         sb.append(androidNsPrefix).append(':');
    315         sb.append(ATTR_LAYOUT_HEIGHT).append('=').append('"').append(height).append('"');
    316 
    317         if (mInitializedAttributes != null && mInitializedAttributes.length() > 0) {
    318             for (String s : mInitializedAttributes.split(",")) { //$NON-NLS-1$
    319                 sb.append(' ');
    320                 String[] nameValue = s.split("="); //$NON-NLS-1$
    321                 String name = nameValue[0];
    322                 String value = nameValue[1];
    323                 if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
    324                     name = name.substring(ANDROID_NS_NAME_PREFIX.length());
    325                     sb.append(androidNsPrefix).append(':');
    326                 }
    327                 sb.append(name).append('=').append('"').append(value).append('"');
    328             }
    329         }
    330 
    331         // Transfer layout_ attributes (other than width and height)
    332         if (primary != null) {
    333             List<Attr> layoutAttributes = findLayoutAttributes(primary);
    334             for (Attr attribute : layoutAttributes) {
    335                 String name = attribute.getLocalName();
    336                 if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))
    337                         && ANDROID_URI.equals(attribute.getNamespaceURI())) {
    338                     // Already handled specially
    339                     continue;
    340                 }
    341 
    342                 if (attribute instanceof IndexedRegion) {
    343                     IndexedRegion region = (IndexedRegion) attribute;
    344                     int startOffset = region.getStartOffset();
    345                     int endOffset = region.getEndOffset();
    346                     String text = getText(startOffset, endOffset);
    347                     DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
    348                     rootEdit.addChild(deletion);
    349                     deletions.add(deletion);
    350 
    351                     if (separateAttributes) {
    352                         sb.append('\n').append(startIndent).append(indentUnit);
    353                     } else {
    354                         sb.append(' ');
    355                     }
    356                     sb.append(text.trim());
    357                 }
    358             }
    359         }
    360 
    361         // Finish open tag:
    362         sb.append('>');
    363         sb.append('\n').append(startIndent).append(indentUnit);
    364 
    365         InsertEdit beginEdit = new InsertEdit(mSelectionStart, sb.toString());
    366         rootEdit.addChild(beginEdit);
    367 
    368         String nested = getText(mSelectionStart, mSelectionEnd);
    369         int index = 0;
    370         while (index != -1) {
    371             index = nested.indexOf('\n', index);
    372             if (index != -1) {
    373                 index++;
    374                 InsertEdit newline = new InsertEdit(mSelectionStart + index, indentUnit);
    375                 // Some of the deleted namespaces may have had newlines - be careful
    376                 // not to overlap edits
    377                 boolean covered = false;
    378                 for (DeleteEdit deletion : deletions) {
    379                     if (deletion.covers(newline)) {
    380                         covered = true;
    381                         break;
    382                     }
    383                 }
    384                 if (!covered) {
    385                     rootEdit.addChild(newline);
    386                 }
    387             }
    388         }
    389 
    390         // Insert end tag: </type>
    391         sb.setLength(0);
    392         sb.append('\n').append(startIndent);
    393         sb.append('<').append('/').append(viewClass).append('>');
    394         InsertEdit endEdit = new InsertEdit(mSelectionEnd, sb.toString());
    395         rootEdit.addChild(endEdit);
    396 
    397         if (AdtPrefs.getPrefs().getFormatGuiXml()) {
    398             MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
    399             if (formatted != null) {
    400                 rootEdit = formatted;
    401             }
    402         }
    403 
    404         change.setEdit(rootEdit);
    405         changes.add(change);
    406         return changes;
    407     }
    408 
    409     String getOldType() {
    410         Element primary = getPrimaryElement();
    411         if (primary != null) {
    412             String oldType = primary.getTagName();
    413             if (oldType.indexOf('.') == -1) {
    414                 oldType = ANDROID_WIDGET_PREFIX + oldType;
    415             }
    416             return oldType;
    417         }
    418 
    419         return null;
    420     }
    421 
    422     @Override
    423     VisualRefactoringWizard createWizard() {
    424         return new WrapInWizard(this, mDelegate);
    425     }
    426 
    427     public static class Descriptor extends VisualRefactoringDescriptor {
    428         public Descriptor(String project, String description, String comment,
    429                 Map<String, String> arguments) {
    430             super("com.android.ide.eclipse.adt.refactoring.wrapin", //$NON-NLS-1$
    431                     project, description, comment, arguments);
    432         }
    433 
    434         @Override
    435         protected Refactoring createRefactoring(Map<String, String> args) {
    436             return new WrapInRefactoring(args);
    437         }
    438     }
    439 }
    440