/*
* 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.ide.common.layout.LayoutConstants.ANDROID_NS_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_COLON;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
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.gle2.GraphicalEditorPart;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.util.Pair;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
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.core.runtime.QualifiedName;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.text.edits.DeleteEdit;
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.IEditorPart;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.ide.IDE;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
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.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Parent class for the various visual refactoring operations; contains shared
* implementations needed by most of them
*/
@SuppressWarnings("restriction") // XML model
public abstract class VisualRefactoring extends Refactoring {
protected static final String KEY_FILE = "file"; //$NON-NLS-1$
protected static final String KEY_PROJECT = "proj"; //$NON-NLS-1$
protected static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$
protected static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$
protected IFile mFile;
protected LayoutEditor mEditor;
protected IProject mProject;
protected int mSelectionStart = -1;
protected int mSelectionEnd = -1;
protected List<Element> mElements = null;
protected ITreeSelection mTreeSelection;
protected ITextSelection mSelection;
protected List<Change> mChanges;
private String mAndroidNamespacePrefix;
/**
* This constructor is solely used by {@link VisualRefactoringDescriptor},
* to replay a previous refactoring.
* @param arguments argument map created by #createArgumentMap.
*/
VisualRefactoring(Map<String, String> arguments) {
IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
path = Path.fromPortableString(arguments.get(KEY_FILE));
mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START));
mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END));
mEditor = null;
}
public VisualRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
ITreeSelection treeSelection) {
mFile = file;
mEditor = editor;
mProject = file.getProject();
mSelection = selection;
mTreeSelection = treeSelection;
// Initialize mSelectionStart and mSelectionEnd based on the selection context, which
// is either a treeSelection (when invoked from the layout editor or the outline), or
// a selection (when invoked from an XML editor)
if (treeSelection != null) {
int end = Integer.MIN_VALUE;
int start = Integer.MAX_VALUE;
for (TreePath path : treeSelection.getPaths()) {
Object lastSegment = path.getLastSegment();
if (lastSegment instanceof CanvasViewInfo) {
CanvasViewInfo viewInfo = (CanvasViewInfo) lastSegment;
UiViewElementNode uiNode = viewInfo.getUiViewNode();
if (uiNode == null) {
continue;
}
Node xmlNode = uiNode.getXmlNode();
if (xmlNode instanceof IndexedRegion) {
IndexedRegion region = (IndexedRegion) xmlNode;
start = Math.min(start, region.getStartOffset());
end = Math.max(end, region.getEndOffset());
}
}
}
if (start >= 0) {
mSelectionStart = start;
mSelectionEnd = end;
}
} else if (selection != null) {
// TODO: update selection to boundaries!
mSelectionStart = selection.getOffset();
mSelectionEnd = mSelectionStart + selection.getLength();
}
}
@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 = new ArrayList<CanvasViewInfo>();
for (TreePath path : mTreeSelection.getPaths()) {
Object lastSegment = path.getLastSegment();
if (lastSegment instanceof CanvasViewInfo) {
infos.add((CanvasViewInfo) lastSegment);
}
}
if (infos.size() == 0) {
status.addFatalError("No selection to extract");
return status;
}
// Can't extract the root -- wouldn't that be pointless? (or maybe not
// always)
for (CanvasViewInfo info : infos) {
if (info.isRoot()) {
status.addFatalError("Cannot refactor the root");
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(LayoutDescriptors.VIEW_INCLUDE)) {
status.addWarning("No point in refactoring a single include tag");
}
}
}
// Enforce that the selection is -contiguous-
if (infos.size() > 1) {
// All elements must be siblings (e.g. same parent)
List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(infos
.size());
for (CanvasViewInfo info : infos) {
UiViewElementNode node = info.getUiViewNode();
if (node != null) {
nodes.add(node);
}
}
if (nodes.size() == 0) {
status.addFatalError("No selected views");
return status;
}
UiElementNode parent = nodes.get(0).getUiParent();
for (UiViewElementNode node : nodes) {
if (parent != node.getUiParent()) {
status.addFatalError("The selected elements must be adjacent");
return status;
}
}
// Ensure that the siblings are contiguous; no gaps.
// If we've selected all the children of the parent then we don't need
// to look.
List<UiElementNode> siblings = parent.getUiChildren();
if (siblings.size() != nodes.size()) {
Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes);
boolean inRange = false;
int remaining = nodes.size();
for (UiElementNode node : siblings) {
boolean in = nodeSet.contains(node);
if (in) {
remaining--;
if (remaining == 0) {
break;
}
inRange = true;
} else if (inRange) {
status.addFatalError("The selected elements must be adjacent");
return status;
}
}
}
}
}
// This also ensures that we have a valid DOM model:
mElements = getElements();
if (mElements.size() == 0) {
status.addFatalError("Nothing to extract");
return status;
}
pm.worked(1);
return status;
} finally {
pm.done();
}
}
protected abstract List<Change> computeChanges();
@Override
public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) throws CoreException,
OperationCanceledException {
RefactoringStatus status = new RefactoringStatus();
mChanges = new ArrayList<Change>();
try {
monitor.beginTask("Checking post-conditions...", 5);
List<Change> changes = computeChanges();
mChanges.addAll(changes);
monitor.worked(1);
} finally {
monitor.done();
}
return status;
}
@Override
public Change createChange(IProgressMonitor monitor) throws CoreException,
OperationCanceledException {
try {
monitor.beginTask("Applying changes...", 1);
CompositeChange change = new CompositeChange(
getName(),
mChanges.toArray(new Change[mChanges.size()])) {
@Override
public ChangeDescriptor getDescriptor() {
VisualRefactoringDescriptor desc = createDescriptor();
return new RefactoringChangeDescriptor(desc);
}
};
monitor.worked(1);
return change;
} finally {
monitor.done();
}
}
protected abstract VisualRefactoringDescriptor createDescriptor();
protected Map<String, String> createArgumentMap() {
HashMap<String, String> args = new HashMap<String, String>();
args.put(KEY_PROJECT, mProject.getFullPath().toPortableString());
args.put(KEY_FILE, mFile.getFullPath().toPortableString());
args.put(KEY_SEL_START, Integer.toString(mSelectionStart));
args.put(KEY_SEL_END, Integer.toString(mSelectionEnd));
return args;
}
// ---- Shared functionality ----
protected void openFile(IFile file) {
GraphicalEditorPart graphicalEditor = mEditor.getGraphicalEditor();
IFile leavingFile = graphicalEditor.getEditedFile();
try {
// Duplicate the current state into the newly created file
QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE;
String state = AdtPlugin.getFileProperty(leavingFile, qname);
// TODO: Look for a ".NoTitleBar.Fullscreen" theme version of the current
// theme to show.
file.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state);
} catch (CoreException e) {
// pass
}
/* TBD: "Show Included In" if supported.
* Not sure if this is a good idea.
if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
try {
Reference include = Reference.create(graphicalEditor.getEditedFile());
file.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, include);
} catch (CoreException e) {
// pass - worst that can happen is that we don't start with inclusion
}
}
*/
try {
IEditorPart part = IDE.openEditor(mEditor.getEditorSite().getPage(), file);
if (part instanceof AndroidXmlEditor && AdtPrefs.getPrefs().getFormatXml()) {
AndroidXmlEditor newEditor = (AndroidXmlEditor) part;
newEditor.reformatDocument();
}
} catch (PartInitException e) {
AdtPlugin.log(e, "Can't open new included layout");
}
}
/** Produce a list of edits to replace references to the given id with the given new id */
protected List<TextEdit> replaceIds(IStructuredDocument doc, int skipStart, int skipEnd,
String rootId, String referenceId) {
if (rootId == null) {
return Collections.emptyList();
}
// We need to search for either @+id/ or @id/
String match1 = rootId;
String match2;
if (match1.startsWith(ID_PREFIX)) {
match2 = '"' + NEW_ID_PREFIX + match1.substring(ID_PREFIX.length()) + '"';
match1 = '"' + match1 + '"';
} else if (match1.startsWith(NEW_ID_PREFIX)) {
match2 = '"' + ID_PREFIX + match1.substring(NEW_ID_PREFIX.length()) + '"';
match1 = '"' + match1 + '"';
} else {
return Collections.emptyList();
}
String namePrefix = getAndroidNamespacePrefix() + ':' + ATTR_LAYOUT_PREFIX;
List<TextEdit> edits = new ArrayList<TextEdit>();
IStructuredDocumentRegion region = doc.getFirstStructuredDocumentRegion();
for (; region != null; region = region.getNext()) {
ITextRegionList list = region.getRegions();
int regionStart = region.getStart();
// Look at all attribute values and look for an id reference match
String attributeName = ""; //$NON-NLS-1$
for (int j = 0; j < region.getNumberOfRegions(); j++) {
ITextRegion subRegion = list.get(j);
String type = subRegion.getType();
if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
attributeName = region.getText(subRegion);
} else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
// Only replace references in layout attributes
if (!attributeName.startsWith(namePrefix)) {
continue;
}
// Skip occurrences in the given skip range
int subRegionStart = regionStart + subRegion.getStart();
if (subRegionStart >= skipStart && subRegionStart <= skipEnd) {
continue;
}
String attributeValue = region.getText(subRegion);
if (attributeValue.equals(match1) || attributeValue.equals(match2)) {
int start = subRegionStart + 1; // skip quote
int end = start + rootId.length();
edits.add(new ReplaceEdit(start, end - start, referenceId));
}
}
}
}
return edits;
}
/** Get the id of the root selected element, if any */
protected String getRootId() {
Element primary = getPrimaryElement();
if (primary != null) {
String oldId = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
// id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
if (oldId != null && oldId.length() > 0) {
return oldId;
}
}
return null;
}
protected String getAndroidNamespacePrefix() {
if (mAndroidNamespacePrefix == null) {
List<Attr> attributeNodes = findNamespaceAttributes();
for (Node attributeNode : attributeNodes) {
String prefix = attributeNode.getPrefix();
if (XMLNS.equals(prefix)) {
String name = attributeNode.getNodeName();
String value = attributeNode.getNodeValue();
if (value.equals(ANDROID_URI)) {
mAndroidNamespacePrefix = name;
if (mAndroidNamespacePrefix.startsWith(XMLNS_COLON)) {
mAndroidNamespacePrefix =
mAndroidNamespacePrefix.substring(XMLNS_COLON.length());
}
}
}
}
if (mAndroidNamespacePrefix == null) {
mAndroidNamespacePrefix = ANDROID_NS_PREFIX;
}
}
return mAndroidNamespacePrefix;
}
protected List<Attr> findNamespaceAttributes() {
Document document = getDomDocument();
if (document != null) {
Element root = document.getDocumentElement();
return findNamespaceAttributes(root);
}
return Collections.emptyList();
}
protected List<Attr> findNamespaceAttributes(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 prefix = attributeNode.getPrefix();
if (XMLNS.equals(prefix)) {
result.add((Attr) attributeNode);
}
}
return result;
}
protected List<Attr> findLayoutAttributes(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_PREFIX)
&& ANDROID_URI.equals(attributeNode.getNamespaceURI())) {
result.add((Attr) attributeNode);
}
}
return result;
}
protected String insertNamespace(String xmlText, String namespaceDeclarations) {
// Insert namespace declarations into the extracted XML fragment
int firstSpace = xmlText.indexOf(' ');
int elementEnd = xmlText.indexOf('>');
int insertAt;
if (firstSpace != -1 && firstSpace < elementEnd) {
insertAt = firstSpace;
} else {
insertAt = elementEnd;
}
xmlText = xmlText.substring(0, insertAt) + namespaceDeclarations
+ xmlText.substring(insertAt);
return xmlText;
}
/** Remove sections of the document that correspond to top level layout attributes;
* these are placed on the include element instead */
protected String stripTopLayoutAttributes(Element primary, int start, String xml) {
if (primary != null) {
// List of attributes to remove
List<IndexedRegion> skip = new ArrayList<IndexedRegion>();
NamedNodeMap attributes = primary.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_PREFIX)
&& ANDROID_URI.equals(attr.getNamespaceURI())) {
if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
// These are special and are left in
continue;
}
if (attr instanceof IndexedRegion) {
skip.add((IndexedRegion) attr);
}
}
}
if (skip.size() > 0) {
Collections.sort(skip, new Comparator<IndexedRegion>() {
// Sort in start order
public int compare(IndexedRegion r1, IndexedRegion r2) {
return r1.getStartOffset() - r2.getStartOffset();
}
});
// Successively cut out the various layout attributes
// TODO remove adjacent whitespace too (but not newlines, unless they
// are newly adjacent)
StringBuilder sb = new StringBuilder(xml.length());
int nextStart = 0;
// Copy out all the sections except the skip sections
for (IndexedRegion r : skip) {
int regionStart = r.getStartOffset();
// Adjust to string offsets since we've copied the string out of
// the document
regionStart -= start;
sb.append(xml.substring(nextStart, regionStart));
nextStart = regionStart + r.getLength();
}
if (nextStart < xml.length()) {
sb.append(xml.substring(nextStart));
}
return sb.toString();
}
}
return xml;
}
protected static String getIndent(String line, int max) {
int i = 0;
int n = Math.min(max, line.length());
for (; i < n; i++) {
char c = line.charAt(i);
if (!Character.isWhitespace(c)) {
return line.substring(0, i);
}
}
if (n < line.length()) {
return line.substring(0, n);
} else {
return line;
}
}
protected static String dedent(String xml) {
String[] lines = xml.split("\n"); //$NON-NLS-1$
if (lines.length < 2) {
// The first line never has any indentation since we copy it out from the
// element start index
return xml;
}
String indentPrefix = getIndent(lines[1], lines[1].length());
for (int i = 2, n = lines.length; i < n; i++) {
String line = lines[i];
// Ignore blank lines
if (line.trim().length() == 0) {
continue;
}
indentPrefix = getIndent(line, indentPrefix.length());
if (indentPrefix.length() == 0) {
return xml;
}
}
StringBuilder sb = new StringBuilder();
for (String line : lines) {
if (line.startsWith(indentPrefix)) {
sb.append(line.substring(indentPrefix.length()));
} else {
sb.append(line);
}
sb.append('\n');
}
return sb.toString();
}
protected String getText(int start, int end) {
try {
IStructuredDocument document = mEditor.getStructuredDocument();
return document.get(start, end - start);
} catch (BadLocationException e) {
// the region offset was invalid. ignore.
return null;
}
}
protected List<Element> getElements() {
if (mElements == null) {
List<Element> nodes = new ArrayList<Element>();
AndroidXmlEditor editor = mEditor;
IStructuredDocument doc = editor.getStructuredDocument();
Pair<Element, Element> range = DomUtilities.getElementRange(doc,
mSelectionStart, mSelectionEnd);
if (range != null) {
Element first = range.getFirst();
Element last = range.getSecond();
if (first == last) {
nodes.add(first);
} else if (first.getParentNode() == last.getParentNode()) {
// Add the range
Node node = first;
while (node != null) {
if (node instanceof Element) {
nodes.add((Element) node);
}
if (node == last) {
break;
}
node = node.getNextSibling();
}
} else {
// Different parents: this means we have an uneven selection, selecting
// elements from different levels. We can't extract ranges like that.
}
}
mElements = nodes;
}
return mElements;
}
protected Element getPrimaryElement() {
List<Element> elements = getElements();
if (elements != null && elements.size() == 1) {
return elements.get(0);
}
return null;
}
protected Document getDomDocument() {
return mEditor.getUiRootNode().getXmlDocument();
}
protected List<CanvasViewInfo> getSelectedViewInfos() {
List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
if (mTreeSelection != null) {
for (TreePath path : mTreeSelection.getPaths()) {
Object lastSegment = path.getLastSegment();
if (lastSegment instanceof CanvasViewInfo) {
infos.add((CanvasViewInfo) lastSegment);
}
}
}
return infos;
}
protected boolean validateNotEmpty(List<CanvasViewInfo> infos, RefactoringStatus status) {
if (infos.size() == 0) {
status.addFatalError("No selection to extract");
return false;
}
return true;
}
protected boolean validateNotRoot(List<CanvasViewInfo> infos, RefactoringStatus status) {
for (CanvasViewInfo info : infos) {
if (info.isRoot()) {
status.addFatalError("Cannot refactor the root");
return false;
}
}
return true;
}
protected boolean validateContiguous(List<CanvasViewInfo> infos, RefactoringStatus status) {
if (infos.size() > 1) {
// All elements must be siblings (e.g. same parent)
List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(infos
.size());
for (CanvasViewInfo info : infos) {
UiViewElementNode node = info.getUiViewNode();
if (node != null) {
nodes.add(node);
}
}
if (nodes.size() == 0) {
status.addFatalError("No selected views");
return false;
}
UiElementNode parent = nodes.get(0).getUiParent();
for (UiViewElementNode node : nodes) {
if (parent != node.getUiParent()) {
status.addFatalError("The selected elements must be adjacent");
return false;
}
}
// Ensure that the siblings are contiguous; no gaps.
// If we've selected all the children of the parent then we don't need
// to look.
List<UiElementNode> siblings = parent.getUiChildren();
if (siblings.size() != nodes.size()) {
Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes);
boolean inRange = false;
int remaining = nodes.size();
for (UiElementNode node : siblings) {
boolean in = nodeSet.contains(node);
if (in) {
remaining--;
if (remaining == 0) {
break;
}
inRange = true;
} else if (inRange) {
status.addFatalError("The selected elements must be adjacent");
return false;
}
}
}
}
return true;
}
protected IndexedRegion getRegion(Node node) {
if (node instanceof IndexedRegion) {
return (IndexedRegion) node;
}
return null;
}
protected String ensureHasId(MultiTextEdit rootEdit, Element element) {
if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
String id = DomUtilities.getFreeWidgetId(element);
id = NEW_ID_PREFIX + id;
addAttributeDeclaration(rootEdit, element, getAndroidNamespacePrefix(), ATTR_ID, id);
return id;
}
return getId(element);
}
protected int getFirstAttributeOffset(Element element) {
IndexedRegion region = getRegion(element);
if (region != null) {
int startOffset = region.getStartOffset();
int endOffset = region.getEndOffset();
String text = getText(startOffset, endOffset);
String name = element.getLocalName();
int nameOffset = text.indexOf(name);
if (nameOffset != -1) {
return startOffset + nameOffset + name.length();
}
}
return -1;
}
protected static String getId(Element element) {
return element.getAttributeNS(ANDROID_URI, ATTR_ID);
}
protected String ensureNewId(String id) {
if (id != null && id.length() > 0) {
if (id.startsWith(ID_PREFIX)) {
id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
} else if (!id.startsWith(NEW_ID_PREFIX)) {
id = NEW_ID_PREFIX + id;
}
} else {
id = null;
}
return id;
}
protected String getViewClass(String fqcn) {
// Don't include android.widget. as a package prefix in layout files
if (fqcn.startsWith(ANDROID_WIDGET_PREFIX)) {
fqcn = fqcn.substring(ANDROID_WIDGET_PREFIX.length());
}
return fqcn;
}
protected void addAttributeDeclaration(MultiTextEdit rootEdit, Element element,
String attributePrefix, String attributeName, String attributeValue) {
int offset = getFirstAttributeOffset(element);
if (offset != -1) {
addAttributeDeclaration(rootEdit, offset, attributePrefix, attributeName,
attributeValue);
}
}
protected void addAttributeDeclaration(MultiTextEdit rootEdit, int offset,
String attributePrefix, String attributeName, String attributeValue) {
StringBuilder sb = new StringBuilder();
sb.append(' ').append(attributePrefix).append(':');
sb.append(attributeName).append('=').append('"');
sb.append(attributeValue).append('"');
InsertEdit setAttribute = new InsertEdit(offset, sb.toString());
rootEdit.addChild(setAttribute);
}
/** Strips out the given attribute, if defined */
protected void removeAttribute(MultiTextEdit rootEdit, Element element, String uri,
String attributeName) {
if (element.hasAttributeNS(uri, attributeName)) {
Attr attribute = element.getAttributeNodeNS(uri, attributeName);
IndexedRegion region = getRegion(attribute);
if (region != null) {
int startOffset = region.getStartOffset();
int endOffset = region.getEndOffset();
DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
rootEdit.addChild(deletion);
}
}
}
public abstract static class VisualRefactoringDescriptor extends RefactoringDescriptor {
private final Map<String, String> mArguments;
public VisualRefactoringDescriptor(
String id, String project, String description, String comment,
Map<String, String> arguments) {
super(id, project, description, comment, STRUCTURAL_CHANGE | MULTI_CHANGE);
mArguments = arguments;
}
public Map<String, String> getArguments() {
return mArguments;
}
protected abstract Refactoring createRefactoring(Map<String, String> args);
@Override
public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
try {
return createRefactoring(mArguments);
} catch (NullPointerException e) {
status.addFatalError("Failed to recreate refactoring from descriptor");
return null;
}
}
}
}