| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.ide.eclipse.adt.internal.editors.layout.refactoring; |
| |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; |
| import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; |
| import static com.android.SdkConstants.ATTR_TEXT; |
| import static com.android.SdkConstants.EXT_XML; |
| import static com.android.SdkConstants.VIEW_FRAGMENT; |
| import static com.android.SdkConstants.VIEW_INCLUDE; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; |
| import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.jface.text.ITextSelection; |
| import org.eclipse.jface.viewers.ITreeSelection; |
| import org.eclipse.ltk.core.refactoring.Change; |
| import org.eclipse.ltk.core.refactoring.Refactoring; |
| import org.eclipse.ltk.core.refactoring.RefactoringStatus; |
| import org.eclipse.ltk.core.refactoring.TextFileChange; |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.text.edits.ReplaceEdit; |
| import org.eclipse.text.edits.TextEdit; |
| import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.xml.core.internal.document.ElementImpl; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Changes the type of the given widgets to the given target type |
| * and updates the attributes if necessary |
| */ |
| @SuppressWarnings("restriction") // XML model |
| public class ChangeViewRefactoring extends VisualRefactoring { |
| private static final String KEY_TYPE = "type"; //$NON-NLS-1$ |
| private String mTypeFqcn; |
| |
| /** |
| * This constructor is solely used by {@link Descriptor}, |
| * to replay a previous refactoring. |
| * @param arguments argument map created by #createArgumentMap. |
| */ |
| ChangeViewRefactoring(Map<String, String> arguments) { |
| super(arguments); |
| mTypeFqcn = arguments.get(KEY_TYPE); |
| } |
| |
| public ChangeViewRefactoring( |
| IFile file, |
| LayoutEditorDelegate delegate, |
| ITextSelection selection, |
| ITreeSelection treeSelection) { |
| super(file, delegate, selection, treeSelection); |
| } |
| |
| @VisibleForTesting |
| ChangeViewRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) { |
| super(selectedElements, editor); |
| } |
| |
| @Override |
| public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, |
| OperationCanceledException { |
| RefactoringStatus status = new RefactoringStatus(); |
| |
| try { |
| pm.beginTask("Checking preconditions...", 6); |
| |
| if (mSelectionStart == -1 || mSelectionEnd == -1) { |
| status.addFatalError("No selection to convert"); |
| return status; |
| } |
| |
| // Make sure the selection is contiguous |
| if (mTreeSelection != null) { |
| List<CanvasViewInfo> infos = getSelectedViewInfos(); |
| if (!validateNotEmpty(infos, status)) { |
| return status; |
| } |
| } |
| |
| // Ensures that we have a valid DOM model: |
| if (mElements.size() == 0) { |
| status.addFatalError("Nothing to convert"); |
| return status; |
| } |
| |
| pm.worked(1); |
| return status; |
| |
| } finally { |
| pm.done(); |
| } |
| } |
| |
| @Override |
| protected VisualRefactoringDescriptor createDescriptor() { |
| String comment = getName(); |
| return new Descriptor( |
| mProject.getName(), //project |
| comment, //description |
| comment, //comment |
| createArgumentMap()); |
| } |
| |
| @Override |
| protected Map<String, String> createArgumentMap() { |
| Map<String, String> args = super.createArgumentMap(); |
| args.put(KEY_TYPE, mTypeFqcn); |
| |
| return args; |
| } |
| |
| @Override |
| public String getName() { |
| return "Change Widget Type"; |
| } |
| |
| void setType(String typeFqcn) { |
| mTypeFqcn = typeFqcn; |
| } |
| |
| @Override |
| protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) { |
| String name = getViewClass(mTypeFqcn); |
| |
| IFile file = mDelegate.getEditor().getInputFile(); |
| List<Change> changes = new ArrayList<Change>(); |
| if (file == null) { |
| return changes; |
| } |
| TextFileChange change = new TextFileChange(file.getName(), file); |
| MultiTextEdit rootEdit = new MultiTextEdit(); |
| change.setEdit(rootEdit); |
| change.setTextType(EXT_XML); |
| changes.add(change); |
| |
| for (Element element : getElements()) { |
| IndexedRegion region = getRegion(element); |
| String text = getText(region.getStartOffset(), region.getEndOffset()); |
| String oldName = element.getNodeName(); |
| int open = text.indexOf(oldName); |
| int close = text.lastIndexOf(oldName); |
| if (element instanceof ElementImpl && ((ElementImpl) element).isEmptyTag()) { |
| close = -1; |
| } |
| |
| if (open != -1) { |
| int oldLength = oldName.length(); |
| rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + open, |
| oldLength, name)); |
| } |
| if (close != -1 && close != open) { |
| int oldLength = oldName.length(); |
| rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + close, oldLength, |
| name)); |
| } |
| |
| // Change tag type |
| String oldId = getId(element); |
| String newId = ensureIdMatchesType(element, mTypeFqcn, rootEdit); |
| // Update any layout references to the old id with the new id |
| if (oldId != null && newId != null) { |
| IStructuredModel model = mDelegate.getEditor().getModelForRead(); |
| try { |
| IStructuredDocument doc = model.getStructuredDocument(); |
| if (doc != null) { |
| IndexedRegion range = getRegion(element); |
| int skipStart = range.getStartOffset(); |
| int skipEnd = range.getEndOffset(); |
| List<TextEdit> replaceIds = replaceIds(getAndroidNamespacePrefix(), doc, |
| skipStart, skipEnd, |
| oldId, newId); |
| for (TextEdit edit : replaceIds) { |
| rootEdit.addChild(edit); |
| } |
| } |
| } finally { |
| model.releaseFromRead(); |
| } |
| } |
| |
| // Strip out attributes that no longer make sense |
| removeUndefinedAttrs(rootEdit, element); |
| } |
| |
| return changes; |
| } |
| |
| /** Removes all the unused attributes after a conversion */ |
| private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element element) { |
| ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn); |
| if (descriptor == null) { |
| return; |
| } |
| |
| Set<String> defined = new HashSet<String>(); |
| AttributeDescriptor[] layoutAttributes = descriptor.getAttributes(); |
| for (AttributeDescriptor attribute : layoutAttributes) { |
| defined.add(attribute.getXmlLocalName()); |
| } |
| |
| List<Attr> attributes = findAttributes(element); |
| for (Attr attribute : attributes) { |
| String name = attribute.getLocalName(); |
| if (!defined.contains(name)) { |
| // Remove it |
| removeAttribute(rootEdit, element, attribute.getNamespaceURI(), name); |
| } |
| } |
| |
| // Set text attribute if it's defined |
| if (defined.contains(ATTR_TEXT) && !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) { |
| setAttribute(rootEdit, element, ANDROID_URI, getAndroidNamespacePrefix(), |
| ATTR_TEXT, descriptor.getUiName()); |
| } |
| } |
| |
| protected List<Attr> findAttributes(Node root) { |
| List<Attr> result = new ArrayList<Attr>(); |
| NamedNodeMap attributes = root.getAttributes(); |
| for (int i = 0, n = attributes.getLength(); i < n; i++) { |
| Node attributeNode = attributes.item(i); |
| |
| String name = attributeNode.getLocalName(); |
| if (!name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) |
| && ANDROID_URI.equals(attributeNode.getNamespaceURI())) { |
| result.add((Attr) attributeNode); |
| } |
| } |
| |
| return result; |
| } |
| |
| List<String> getOldTypes() { |
| List<String> types = new ArrayList<String>(); |
| for (Element primary : getElements()) { |
| String oldType = primary.getTagName(); |
| if (oldType.indexOf('.') == -1 |
| && !oldType.equals(VIEW_INCLUDE) && !oldType.equals(VIEW_FRAGMENT)) { |
| oldType = ANDROID_WIDGET_PREFIX + oldType; |
| } |
| types.add(oldType); |
| } |
| |
| return types; |
| } |
| |
| @Override |
| VisualRefactoringWizard createWizard() { |
| return new ChangeViewWizard(this, mDelegate); |
| } |
| |
| public static class Descriptor extends VisualRefactoringDescriptor { |
| public Descriptor(String project, String description, String comment, |
| Map<String, String> arguments) { |
| super("com.android.ide.eclipse.adt.refactoring.changeview", //$NON-NLS-1$ |
| project, description, comment, arguments); |
| } |
| |
| @Override |
| protected Refactoring createRefactoring(Map<String, String> args) { |
| return new ChangeViewRefactoring(args); |
| } |
| } |
| } |