diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java | 670 |
1 files changed, 670 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java new file mode 100644 index 000000000..f58ac5501 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java @@ -0,0 +1,670 @@ +/* + * 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_NS_NAME; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; +import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.FD_RES; +import static com.android.SdkConstants.FD_RESOURCES; +import static com.android.SdkConstants.FD_RES_LAYOUT; +import static com.android.SdkConstants.ID_PREFIX; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.VALUE_WRAP_CONTENT; +import static com.android.SdkConstants.VIEW_INCLUDE; +import static com.android.SdkConstants.XMLNS; +import static com.android.SdkConstants.XMLNS_PREFIX; +import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; +import static com.android.resources.ResourceType.LAYOUT; + +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.utils.XmlUtils; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.dialogs.IInputValidator; +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.NullChange; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.swt.widgets.Display; +import org.eclipse.text.edits.InsertEdit; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +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.provisional.document.IDOMDocument; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Extracts the selection and writes it out as a separate layout file, then adds an + * include to that new layout file. Interactively asks the user for a new name for the + * layout. + */ +@SuppressWarnings("restriction") // XML model +public class ExtractIncludeRefactoring extends VisualRefactoring { + private static final String KEY_NAME = "name"; //$NON-NLS-1$ + private static final String KEY_OCCURRENCES = "all-occurrences"; //$NON-NLS-1$ + private String mLayoutName; + private boolean mReplaceOccurrences; + + /** + * This constructor is solely used by {@link Descriptor}, + * to replay a previous refactoring. + * @param arguments argument map created by #createArgumentMap. + */ + ExtractIncludeRefactoring(Map<String, String> arguments) { + super(arguments); + mLayoutName = arguments.get(KEY_NAME); + mReplaceOccurrences = Boolean.parseBoolean(arguments.get(KEY_OCCURRENCES)); + } + + public ExtractIncludeRefactoring( + IFile file, + LayoutEditorDelegate delegate, + ITextSelection selection, + ITreeSelection treeSelection) { + super(file, delegate, selection, treeSelection); + } + + @VisibleForTesting + ExtractIncludeRefactoring(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 extract"); + return status; + } + + // Make sure the selection is contiguous + if (mTreeSelection != null) { + // TODO - don't do this if we based the selection on text. In this case, + // make sure we're -balanced-. + List<CanvasViewInfo> infos = getSelectedViewInfos(); + if (!validateNotEmpty(infos, status)) { + return status; + } + + if (!validateNotRoot(infos, status)) { + return status; + } + + // Disable if you've selected a single include tag + if (infos.size() == 1) { + UiViewElementNode uiNode = infos.get(0).getUiViewNode(); + if (uiNode != null) { + Node xmlNode = uiNode.getXmlNode(); + if (xmlNode.getLocalName().equals(VIEW_INCLUDE)) { + status.addWarning("No point in refactoring a single include tag"); + } + } + } + + // Enforce that the selection is -contiguous- + if (!validateContiguous(infos, status)) { + return status; + } + } + + // This also ensures that we have a valid DOM model: + if (mElements.size() == 0) { + status.addFatalError("Nothing to extract"); + 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_NAME, mLayoutName); + args.put(KEY_OCCURRENCES, Boolean.toString(mReplaceOccurrences)); + + return args; + } + + @Override + public String getName() { + return "Extract as Include"; + } + + void setLayoutName(String layoutName) { + mLayoutName = layoutName; + } + + void setReplaceOccurrences(boolean selection) { + mReplaceOccurrences = selection; + } + + // ---- Actual implementation of Extract as Include modification computation ---- + + @Override + protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) { + String extractedText = getExtractedText(); + + String namespaceDeclarations = computeNamespaceDeclarations(); + + // Insert namespace: + extractedText = insertNamespace(extractedText, namespaceDeclarations); + + StringBuilder sb = new StringBuilder(); + sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$ + sb.append(extractedText); + sb.append('\n'); + + List<Change> changes = new ArrayList<Change>(); + + String newFileName = mLayoutName + DOT_XML; + IProject project = mDelegate.getEditor().getProject(); + IFile sourceFile = mDelegate.getEditor().getInputFile(); + if (sourceFile == null) { + return changes; + } + + // Replace extracted elements by <include> tag + handleIncludingFile(changes, sourceFile, mSelectionStart, mSelectionEnd, + getDomDocument(), getPrimaryElement()); + + // Also extract in other variations of the same file (landscape/portrait, etc) + boolean haveVariations = false; + if (mReplaceOccurrences) { + List<IFile> layouts = getOtherLayouts(sourceFile); + for (IFile file : layouts) { + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredModel model = null; + // We could enhance this with a SubMonitor to make the progress bar move as + // well. + monitor.subTask(String.format("Looking for duplicates in %1$s", + file.getProjectRelativePath())); + if (monitor.isCanceled()) { + throw new OperationCanceledException(); + } + + try { + model = modelManager.getModelForRead(file); + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + IDOMDocument otherDocument = domModel.getDocument(); + List<Element> otherElements = new ArrayList<Element>(); + Element otherPrimary = null; + + for (Element element : getElements()) { + Element other = DomUtilities.findCorresponding(element, + otherDocument); + if (other != null) { + // See if the structure is similar to what we have in this + // document + if (DomUtilities.isEquivalent(element, other)) { + otherElements.add(other); + if (element == getPrimaryElement()) { + otherPrimary = other; + } + } + } + } + + // Only perform extract in the other file if we find a match for + // ALL of elements being extracted, and if they too are contiguous + if (otherElements.size() == getElements().size() && + DomUtilities.isContiguous(otherElements)) { + // Find the range + int begin = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + for (Element element : otherElements) { + // Yes!! Extract this one as well! + IndexedRegion region = getRegion(element); + end = Math.max(end, region.getEndOffset()); + begin = Math.min(begin, region.getStartOffset()); + } + handleIncludingFile(changes, file, begin, + end, otherDocument, otherPrimary); + haveVariations = true; + } + } + } catch (IOException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + } + } + + // Add change to create the new file + IContainer parent = sourceFile.getParent(); + if (haveVariations) { + // If we're extracting from multiple configuration folders, then we need to + // place the extracted include in the base layout folder (if not it goes next to + // the including file) + parent = mProject.getFolder(FD_RES).getFolder(FD_RES_LAYOUT); + } + IPath parentPath = parent.getProjectRelativePath(); + final IFile file = project.getFile(new Path(parentPath + WS_SEP + newFileName)); + TextFileChange addFile = new TextFileChange("Create new separate layout", file); + addFile.setTextType(EXT_XML); + changes.add(addFile); + + String newFile = sb.toString(); + if (AdtPrefs.getPrefs().getFormatGuiXml()) { + newFile = EclipseXmlPrettyPrinter.prettyPrint(newFile, + EclipseXmlFormatPreferences.create(), XmlFormatStyle.LAYOUT, + null /*lineSeparator*/); + } + addFile.setEdit(new InsertEdit(0, newFile)); + + Change finishHook = createFinishHook(file); + changes.add(finishHook); + + return changes; + } + + private void handleIncludingFile(List<Change> changes, + IFile sourceFile, int begin, int end, Document document, Element primary) { + TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile); + MultiTextEdit rootEdit = new MultiTextEdit(); + change.setTextType(EXT_XML); + changes.add(change); + + String referenceId = getReferenceId(); + // Replace existing elements in the source file and insert <include> + String androidNsPrefix = getAndroidNamespacePrefix(document); + String include = computeIncludeString(primary, mLayoutName, androidNsPrefix, referenceId); + int length = end - begin; + ReplaceEdit replace = new ReplaceEdit(begin, length, include); + rootEdit.addChild(replace); + + // Update any layout references to the old id with the new id + if (referenceId != null && primary != null) { + String rootId = getId(primary); + IStructuredModel model = null; + try { + model = StructuredModelManager.getModelManager().getModelForRead(sourceFile); + IStructuredDocument doc = model.getStructuredDocument(); + if (doc != null && rootId != null) { + List<TextEdit> replaceIds = replaceIds(androidNsPrefix, doc, begin, + end, rootId, referenceId); + for (TextEdit edit : replaceIds) { + rootEdit.addChild(edit); + } + + if (AdtPrefs.getPrefs().getFormatGuiXml()) { + MultiTextEdit formatted = reformat(doc.get(), rootEdit, + XmlFormatStyle.LAYOUT); + if (formatted != null) { + rootEdit = formatted; + } + } + } + } catch (IOException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + } + + change.setEdit(rootEdit); + } + + /** + * Returns a list of all the other layouts (in all configurations) in the project other + * than the given source layout where the refactoring was initiated. Never null. + */ + private List<IFile> getOtherLayouts(IFile sourceFile) { + List<IFile> layouts = new ArrayList<IFile>(100); + IPath sourcePath = sourceFile.getProjectRelativePath(); + IFolder resources = mProject.getFolder(FD_RESOURCES); + try { + for (IResource folder : resources.members()) { + if (folder.getName().startsWith(FD_RES_LAYOUT) && + folder instanceof IFolder) { + IFolder layoutFolder = (IFolder) folder; + for (IResource file : layoutFolder.members()) { + if (file.getName().endsWith(EXT_XML) + && file instanceof IFile) { + if (!file.getProjectRelativePath().equals(sourcePath)) { + layouts.add((IFile) file); + } + } + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return layouts; + } + + String getInitialName() { + String defaultName = ""; //$NON-NLS-1$ + Element primary = getPrimaryElement(); + if (primary != null) { + String id = primary.getAttributeNS(ANDROID_URI, ATTR_ID); + // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378 + if (id != null && (id.startsWith(ID_PREFIX) || id.startsWith(NEW_ID_PREFIX))) { + // Use everything following the id/, and make it lowercase since that is + // the convention for layouts (and use Locale.US to ensure that "Image" becomes + // "image" etc) + defaultName = id.substring(id.indexOf('/') + 1).toLowerCase(Locale.US); + + IInputValidator validator = ResourceNameValidator.create(true, mProject, LAYOUT); + + if (validator.isValid(defaultName) != null) { // Already exists? + defaultName = ""; //$NON-NLS-1$ + } + } + } + + return defaultName; + } + + IFile getSourceFile() { + return mFile; + } + + private Change createFinishHook(final IFile file) { + return new NullChange("Open extracted layout and refresh resources") { + @Override + public Change perform(IProgressMonitor pm) throws CoreException { + Display display = AdtPlugin.getDisplay(); + display.asyncExec(new Runnable() { + @Override + public void run() { + openFile(file); + mDelegate.getGraphicalEditor().refreshProjectResources(); + // Save file to trigger include finder scanning (as well as making + // the + // actual show-include feature work since it relies on reading + // files from + // disk, not a live buffer) + IWorkbenchPage page = mDelegate.getEditor().getEditorSite().getPage(); + page.saveEditor(mDelegate.getEditor(), false); + } + }); + + // Not undoable: just return null instead of an undo-change. + return null; + } + }; + } + + private String computeNamespaceDeclarations() { + String androidNsPrefix = null; + String namespaceDeclarations = null; + + StringBuilder sb = new StringBuilder(); + List<Attr> attributeNodes = findNamespaceAttributes(); + for (Node attributeNode : attributeNodes) { + String prefix = attributeNode.getPrefix(); + if (XMLNS.equals(prefix)) { + sb.append(' '); + String name = attributeNode.getNodeName(); + sb.append(name); + sb.append('=').append('"'); + + String value = attributeNode.getNodeValue(); + if (value.equals(ANDROID_URI)) { + androidNsPrefix = name; + if (androidNsPrefix.startsWith(XMLNS_PREFIX)) { + androidNsPrefix = androidNsPrefix.substring(XMLNS_PREFIX.length()); + } + } + sb.append(XmlUtils.toXmlAttributeValue(value)); + sb.append('"'); + } + } + namespaceDeclarations = sb.toString(); + + if (androidNsPrefix == null) { + androidNsPrefix = ANDROID_NS_NAME; + } + + if (namespaceDeclarations.length() == 0) { + sb.setLength(0); + sb.append(' '); + sb.append(XMLNS_PREFIX); + sb.append(androidNsPrefix); + sb.append('=').append('"'); + sb.append(ANDROID_URI); + sb.append('"'); + namespaceDeclarations = sb.toString(); + } + + return namespaceDeclarations; + } + + /** Returns the id to be used for the include tag itself (may be null) */ + private String getReferenceId() { + String rootId = getRootId(); + if (rootId != null) { + return rootId + "_ref"; + } + + return null; + } + + /** + * Compute the actual {@code <include>} string to be inserted in place of the old + * selection + */ + private static String computeIncludeString(Element primaryNode, String newName, + String androidNsPrefix, String referenceId) { + StringBuilder sb = new StringBuilder(); + sb.append("<include layout=\"@layout/"); //$NON-NLS-1$ + sb.append(newName); + sb.append('"'); + sb.append(' '); + + // Create new id for the include itself + if (referenceId != null) { + sb.append(androidNsPrefix); + sb.append(':'); + sb.append(ATTR_ID); + sb.append('=').append('"'); + sb.append(referenceId); + sb.append('"').append(' '); + } + + // Add id string, unless it's a <merge>, since we may need to adjust any layout + // references to apply to the <include> tag instead + + // I should move all the layout_ attributes as well + // I also need to duplicate and modify the id and then replace + // everything else in the file with this new id... + + // HACK: see issue 13494: We must duplicate the width/height attributes on the + // <include> statement for designtime rendering only + String width = null; + String height = null; + if (primaryNode == null) { + // Multiple selection - in that case we will be creating an outer <merge> + // so we need to set our own width/height on it + width = height = VALUE_WRAP_CONTENT; + } else { + if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) { + width = VALUE_WRAP_CONTENT; + } else { + width = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); + } + if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) { + height = VALUE_WRAP_CONTENT; + } else { + height = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); + } + } + if (width != null) { + sb.append(' '); + sb.append(androidNsPrefix); + sb.append(':'); + sb.append(ATTR_LAYOUT_WIDTH); + sb.append('=').append('"'); + sb.append(XmlUtils.toXmlAttributeValue(width)); + sb.append('"'); + } + if (height != null) { + sb.append(' '); + sb.append(androidNsPrefix); + sb.append(':'); + sb.append(ATTR_LAYOUT_HEIGHT); + sb.append('=').append('"'); + sb.append(XmlUtils.toXmlAttributeValue(height)); + sb.append('"'); + } + + // Duplicate all the other layout attributes as well + if (primaryNode != null) { + NamedNodeMap attributes = primaryNode.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Node attr = attributes.item(i); + String name = attr.getLocalName(); + if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) + && ANDROID_URI.equals(attr.getNamespaceURI())) { + if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) { + // Already handled + continue; + } + + sb.append(' '); + sb.append(androidNsPrefix); + sb.append(':'); + sb.append(name); + sb.append('=').append('"'); + sb.append(XmlUtils.toXmlAttributeValue(attr.getNodeValue())); + sb.append('"'); + } + } + } + + sb.append("/>"); + return sb.toString(); + } + + /** Return the text in the document in the range start to end */ + private String getExtractedText() { + String xml = getText(mSelectionStart, mSelectionEnd); + Element primaryNode = getPrimaryElement(); + xml = stripTopLayoutAttributes(primaryNode, mSelectionStart, xml); + xml = dedent(xml); + + // Wrap siblings in <merge>? + if (primaryNode == null) { + StringBuilder sb = new StringBuilder(); + sb.append("<merge>\n"); //$NON-NLS-1$ + // indent an extra level + for (String line : xml.split("\n")) { //$NON-NLS-1$ + sb.append(" "); //$NON-NLS-1$ + sb.append(line).append('\n'); + } + sb.append("</merge>\n"); //$NON-NLS-1$ + xml = sb.toString(); + } + + return xml; + } + + @Override + VisualRefactoringWizard createWizard() { + return new ExtractIncludeWizard(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.extract.include", //$NON-NLS-1$ + project, description, comment, arguments); + } + + @Override + protected Refactoring createRefactoring(Map<String, String> args) { + return new ExtractIncludeRefactoring(args); + } + } +} |