blob: 07b00b8da29a57b2d7b5fd1494f7ba3992790be1 [file] [log] [blame]
/*
* 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_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.EXT_XML;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.xml.XmlFormatStyle;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
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.preferences.AdtPrefs;
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.DeleteEdit;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MultiTextEdit;
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.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Inserts a new layout surrounding the current selection, migrates namespace
* attributes (if wrapping the root node), and optionally migrates layout
* attributes and updates references elsewhere.
*/
@SuppressWarnings("restriction") // XML model
public class WrapInRefactoring extends VisualRefactoring {
private static final String KEY_ID = "name"; //$NON-NLS-1$
private static final String KEY_TYPE = "type"; //$NON-NLS-1$
private String mId;
private String mTypeFqcn;
private String mInitializedAttributes;
/**
* This constructor is solely used by {@link Descriptor},
* to replay a previous refactoring.
* @param arguments argument map created by #createArgumentMap.
*/
WrapInRefactoring(Map<String, String> arguments) {
super(arguments);
mId = arguments.get(KEY_ID);
mTypeFqcn = arguments.get(KEY_TYPE);
}
public WrapInRefactoring(
IFile file,
LayoutEditorDelegate delegate,
ITextSelection selection,
ITreeSelection treeSelection) {
super(file, delegate, selection, treeSelection);
}
@VisibleForTesting
WrapInRefactoring(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 wrap");
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;
}
// Enforce that the selection is -contiguous-
if (!validateContiguous(infos, status)) {
return status;
}
}
// Ensures that we have a valid DOM model:
if (mElements.size() == 0) {
status.addFatalError("Nothing to wrap");
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);
args.put(KEY_ID, mId);
return args;
}
@Override
public String getName() {
return "Wrap in Container";
}
void setId(String id) {
mId = id;
}
void setType(String typeFqcn) {
mTypeFqcn = typeFqcn;
}
void setInitializedAttributes(String initializedAttributes) {
mInitializedAttributes = initializedAttributes;
}
@Override
protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
// (1) Insert the new container in front of the beginning of the
// first wrapped view
// (2) If the container is the new root, transfer namespace declarations
// to it
// (3) Insert the closing tag of the new container at the end of the
// last wrapped view
// (4) Reindent the wrapped views
// (5) If the user requested it, update all layout references to the
// wrapped views with the new container?
// For that matter, does RelativeLayout even require it? Probably not,
// it can point inside the current layout...
// Add indent to all lines between mSelectionStart and mEnd
// TODO: Figure out the indentation amount?
// For now, use 4 spaces
String indentUnit = " "; //$NON-NLS-1$
boolean separateAttributes = true;
IStructuredDocument document = mDelegate.getEditor().getStructuredDocument();
String startIndent = AndroidXmlEditor.getIndentAtOffset(document, mSelectionStart);
String viewClass = getViewClass(mTypeFqcn);
String androidNsPrefix = getAndroidNamespacePrefix();
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.setTextType(EXT_XML);
String id = ensureNewId(mId);
// Update any layout references to the old id with the new id
if (id != null) {
String rootId = getRootId();
IStructuredModel model = mDelegate.getEditor().getModelForRead();
try {
IStructuredDocument doc = model.getStructuredDocument();
if (doc != null) {
List<TextEdit> replaceIds = replaceIds(androidNsPrefix,
doc, mSelectionStart, mSelectionEnd, rootId, id);
for (TextEdit edit : replaceIds) {
rootEdit.addChild(edit);
}
}
} finally {
model.releaseFromRead();
}
}
// Insert namespace elements?
StringBuilder namespace = null;
List<DeleteEdit> deletions = new ArrayList<DeleteEdit>();
Element primary = getPrimaryElement();
if (primary != null && getDomDocument().getDocumentElement() == primary) {
namespace = new StringBuilder();
List<Attr> declarations = findNamespaceAttributes(primary);
for (Attr attribute : declarations) {
if (attribute instanceof IndexedRegion) {
// Delete the namespace declaration in the node which is no longer the root
IndexedRegion region = (IndexedRegion) attribute;
int startOffset = region.getStartOffset();
int endOffset = region.getEndOffset();
String text = getText(startOffset, endOffset);
DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
deletions.add(deletion);
rootEdit.addChild(deletion);
text = text.trim();
// Insert the namespace declaration in the new root
if (separateAttributes) {
namespace.append('\n').append(startIndent).append(indentUnit);
} else {
namespace.append(' ');
}
namespace.append(text);
}
}
}
// Insert begin tag: <type ...>
StringBuilder sb = new StringBuilder();
sb.append('<');
sb.append(viewClass);
if (namespace != null) {
sb.append(namespace);
}
// Set the ID if any
if (id != null) {
if (separateAttributes) {
sb.append('\n').append(startIndent).append(indentUnit);
} else {
sb.append(' ');
}
sb.append(androidNsPrefix).append(':');
sb.append(ATTR_ID).append('=').append('"').append(id).append('"');
}
// If any of the elements are fill/match parent, use that instead
String width = VALUE_WRAP_CONTENT;
String height = VALUE_WRAP_CONTENT;
for (Element element : getElements()) {
String oldWidth = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
String oldHeight = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
if (VALUE_MATCH_PARENT.equals(oldWidth) || VALUE_FILL_PARENT.equals(oldWidth)) {
width = oldWidth;
}
if (VALUE_MATCH_PARENT.equals(oldHeight) || VALUE_FILL_PARENT.equals(oldHeight)) {
height = oldHeight;
}
}
// Add in width/height.
if (separateAttributes) {
sb.append('\n').append(startIndent).append(indentUnit);
} else {
sb.append(' ');
}
sb.append(androidNsPrefix).append(':');
sb.append(ATTR_LAYOUT_WIDTH).append('=').append('"').append(width).append('"');
if (separateAttributes) {
sb.append('\n').append(startIndent).append(indentUnit);
} else {
sb.append(' ');
}
sb.append(androidNsPrefix).append(':');
sb.append(ATTR_LAYOUT_HEIGHT).append('=').append('"').append(height).append('"');
if (mInitializedAttributes != null && mInitializedAttributes.length() > 0) {
for (String s : mInitializedAttributes.split(",")) { //$NON-NLS-1$
sb.append(' ');
String[] nameValue = s.split("="); //$NON-NLS-1$
String name = nameValue[0];
String value = nameValue[1];
if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
name = name.substring(ANDROID_NS_NAME_PREFIX.length());
sb.append(androidNsPrefix).append(':');
}
sb.append(name).append('=').append('"').append(value).append('"');
}
}
// Transfer layout_ attributes (other than width and height)
if (primary != null) {
List<Attr> layoutAttributes = findLayoutAttributes(primary);
for (Attr attribute : layoutAttributes) {
String name = attribute.getLocalName();
if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))
&& ANDROID_URI.equals(attribute.getNamespaceURI())) {
// Already handled specially
continue;
}
if (attribute instanceof IndexedRegion) {
IndexedRegion region = (IndexedRegion) attribute;
int startOffset = region.getStartOffset();
int endOffset = region.getEndOffset();
String text = getText(startOffset, endOffset);
DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
rootEdit.addChild(deletion);
deletions.add(deletion);
if (separateAttributes) {
sb.append('\n').append(startIndent).append(indentUnit);
} else {
sb.append(' ');
}
sb.append(text.trim());
}
}
}
// Finish open tag:
sb.append('>');
sb.append('\n').append(startIndent).append(indentUnit);
InsertEdit beginEdit = new InsertEdit(mSelectionStart, sb.toString());
rootEdit.addChild(beginEdit);
String nested = getText(mSelectionStart, mSelectionEnd);
int index = 0;
while (index != -1) {
index = nested.indexOf('\n', index);
if (index != -1) {
index++;
InsertEdit newline = new InsertEdit(mSelectionStart + index, indentUnit);
// Some of the deleted namespaces may have had newlines - be careful
// not to overlap edits
boolean covered = false;
for (DeleteEdit deletion : deletions) {
if (deletion.covers(newline)) {
covered = true;
break;
}
}
if (!covered) {
rootEdit.addChild(newline);
}
}
}
// Insert end tag: </type>
sb.setLength(0);
sb.append('\n').append(startIndent);
sb.append('<').append('/').append(viewClass).append('>');
InsertEdit endEdit = new InsertEdit(mSelectionEnd, sb.toString());
rootEdit.addChild(endEdit);
if (AdtPrefs.getPrefs().getFormatGuiXml()) {
MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
if (formatted != null) {
rootEdit = formatted;
}
}
change.setEdit(rootEdit);
changes.add(change);
return changes;
}
String getOldType() {
Element primary = getPrimaryElement();
if (primary != null) {
String oldType = primary.getTagName();
if (oldType.indexOf('.') == -1) {
oldType = ANDROID_WIDGET_PREFIX + oldType;
}
return oldType;
}
return null;
}
@Override
VisualRefactoringWizard createWizard() {
return new WrapInWizard(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.wrapin", //$NON-NLS-1$
project, description, comment, arguments);
}
@Override
protected Refactoring createRefactoring(Map<String, String> args) {
return new WrapInRefactoring(args);
}
}
}