blob: 9b1770d82751c316b12cad1b78d6ecc3a0b050c9 [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;
import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_HINT;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_ON_CLICK;
import static com.android.SdkConstants.ATTR_PARENT;
import static com.android.SdkConstants.ATTR_SRC;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.EXT_XML;
import static com.android.SdkConstants.FD_RESOURCES;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.SdkConstants.PREFIX_ANDROID;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.REFERENCE_STYLE;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_RESOURCES;
import static com.android.SdkConstants.XMLNS_PREFIX;
import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.ResourceResolver;
import com.android.ide.common.xml.XmlFormatStyle;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard;
import com.android.utils.Pair;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
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.InsertEdit;
import org.eclipse.text.edits.MultiTextEdit;
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.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* 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.
* <p>
* Remaining work to do / Possible enhancements:
* <ul>
* <li>Optionally look in other files in the project and attempt to set style attributes
* in other cases where the style attributes match?
* <li>If the elements we are extracting from already contain a style attribute, set that
* style as the parent style of the current style?
* <li>Add a parent-style picker to the wizard (initialized with the above if applicable)
* <li>Pick up indentation settings from the XML module
* <li>Integrate with themes somehow -- make an option to have the extracted style go into
* the theme instead
* </ul>
*/
@SuppressWarnings("restriction") // XML model
public class ExtractStyleRefactoring extends VisualRefactoring {
private static final String KEY_NAME = "name"; //$NON-NLS-1$
private static final String KEY_REMOVE_EXTRACTED = "removeextracted"; //$NON-NLS-1$
private static final String KEY_REMOVE_ALL = "removeall"; //$NON-NLS-1$
private static final String KEY_APPLY_STYLE = "applystyle"; //$NON-NLS-1$
private static final String KEY_PARENT = "parent"; //$NON-NLS-1$
private String mStyleName;
/** The name of the file in res/values/ that the style will be added to. Normally
* res/values/styles.xml - but unit tests pick other names */
private String mStyleFileName = "styles.xml";
/** Set a style reference on the extracted elements? */
private boolean mApplyStyle;
/** Remove the attributes that were extracted? */
private boolean mRemoveExtracted;
/** List of attributes chosen by the user to be extracted */
private List<Attr> mChosenAttributes = new ArrayList<Attr>();
/** Remove all attributes that match the extracted attributes names, regardless of value */
private boolean mRemoveAll;
/** The parent style to extend */
private String mParent;
/** The full list of available attributes in the refactoring */
private Map<String, List<Attr>> mAvailableAttributes;
/**
* This constructor is solely used by {@link Descriptor},
* to replay a previous refactoring.
* @param arguments argument map created by #createArgumentMap.
*/
ExtractStyleRefactoring(Map<String, String> arguments) {
super(arguments);
mStyleName = arguments.get(KEY_NAME);
mRemoveExtracted = Boolean.parseBoolean(arguments.get(KEY_REMOVE_EXTRACTED));
mRemoveAll = Boolean.parseBoolean(arguments.get(KEY_REMOVE_ALL));
mApplyStyle = Boolean.parseBoolean(arguments.get(KEY_APPLY_STYLE));
mParent = arguments.get(KEY_PARENT);
if (mParent != null && mParent.length() == 0) {
mParent = null;
}
}
public ExtractStyleRefactoring(
IFile file,
LayoutEditorDelegate delegate,
ITextSelection selection,
ITreeSelection treeSelection) {
super(file, delegate, selection, treeSelection);
}
@VisibleForTesting
ExtractStyleRefactoring(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;
}
// 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, mStyleName);
args.put(KEY_REMOVE_EXTRACTED, Boolean.toString(mRemoveExtracted));
args.put(KEY_REMOVE_ALL, Boolean.toString(mRemoveAll));
args.put(KEY_APPLY_STYLE, Boolean.toString(mApplyStyle));
args.put(KEY_PARENT, mParent != null ? mParent : "");
return args;
}
@Override
public String getName() {
return "Extract Style";
}
void setStyleName(String styleName) {
mStyleName = styleName;
}
void setStyleFileName(String styleFileName) {
mStyleFileName = styleFileName;
}
void setChosenAttributes(List<Attr> attributes) {
mChosenAttributes = attributes;
}
void setRemoveExtracted(boolean removeExtracted) {
mRemoveExtracted = removeExtracted;
}
void setApplyStyle(boolean applyStyle) {
mApplyStyle = applyStyle;
}
void setRemoveAll(boolean removeAll) {
mRemoveAll = removeAll;
}
void setParent(String parent) {
mParent = parent;
}
// ---- Actual implementation of Extract Style modification computation ----
/**
* Returns two items: a map from attribute name to a list of attribute nodes of that
* name, and a subset of these attributes that fall within the text selection
* (used to drive initial selection in the wizard)
*/
Pair<Map<String, List<Attr>>, Set<Attr>> getAvailableAttributes() {
mAvailableAttributes = new TreeMap<String, List<Attr>>();
Set<Attr> withinSelection = new HashSet<Attr>();
for (Element element : getElements()) {
IndexedRegion elementRegion = getRegion(element);
boolean allIncluded =
(mOriginalSelectionStart <= elementRegion.getStartOffset() &&
mOriginalSelectionEnd >= elementRegion.getEndOffset());
NamedNodeMap attributeMap = element.getAttributes();
for (int i = 0, n = attributeMap.getLength(); i < n; i++) {
Attr attribute = (Attr) attributeMap.item(i);
String name = attribute.getLocalName();
if (!isStylableAttribute(name)) {
// Don't offer to extract attributes that don't make sense in
// styles (like "id" or "style"), or attributes that the user
// probably does not want to define in styles (like layout
// attributes such as layout_width, or the label of a button etc).
// This makes the options offered listed in the wizard simpler.
// In special cases where the user *does* want to set one of these
// attributes, they can always do it manually so optimize for
// the common case here.
continue;
}
// Skip attributes that are in a namespace other than the Android one
String namespace = attribute.getNamespaceURI();
if (namespace != null && !ANDROID_URI.equals(namespace)) {
continue;
}
if (!allIncluded) {
IndexedRegion region = getRegion(attribute);
boolean attributeIncluded = mOriginalSelectionStart < region.getEndOffset() &&
mOriginalSelectionEnd >= region.getStartOffset();
if (attributeIncluded) {
withinSelection.add(attribute);
}
} else {
withinSelection.add(attribute);
}
List<Attr> list = mAvailableAttributes.get(name);
if (list == null) {
list = new ArrayList<Attr>();
mAvailableAttributes.put(name, list);
}
list.add(attribute);
}
}
return Pair.of(mAvailableAttributes, withinSelection);
}
/**
* Returns whether the given local attribute name is one the style wizard
* should present as a selectable attribute to be extracted.
*
* @param name the attribute name, not including a namespace prefix
* @return true if the name is one that the user can extract
*/
public static boolean isStylableAttribute(String name) {
return !(name == null
|| name.equals(ATTR_ID)
|| name.startsWith(ATTR_STYLE)
|| (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) &&
!name.startsWith(ATTR_LAYOUT_MARGIN))
|| name.equals(ATTR_TEXT)
|| name.equals(ATTR_HINT)
|| name.equals(ATTR_SRC)
|| name.equals(ATTR_ON_CLICK));
}
IFile getStyleFile(IProject project) {
return project.getFile(new Path(FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP
+ mStyleFileName));
}
@Override
protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
List<Change> changes = new ArrayList<Change>();
if (mChosenAttributes.size() == 0) {
return changes;
}
IFile file = getStyleFile(mDelegate.getEditor().getProject());
boolean createFile = !file.exists();
int insertAtIndex;
String initialIndent = null;
if (!createFile) {
Pair<Integer, String> context = computeInsertContext(file);
insertAtIndex = context.getFirst();
initialIndent = context.getSecond();
} else {
insertAtIndex = 0;
}
TextFileChange addFile = new TextFileChange("Create new separate style declaration", file);
addFile.setTextType(EXT_XML);
changes.add(addFile);
String styleString = computeStyleDeclaration(createFile, initialIndent);
addFile.setEdit(new InsertEdit(insertAtIndex, styleString));
// Remove extracted attributes?
MultiTextEdit rootEdit = new MultiTextEdit();
if (mRemoveExtracted || mRemoveAll) {
for (Attr attribute : mChosenAttributes) {
List<Attr> list = mAvailableAttributes.get(attribute.getLocalName());
for (Attr attr : list) {
if (mRemoveAll || attr.getValue().equals(attribute.getValue())) {
removeAttribute(rootEdit, attr);
}
}
}
}
// Set the style attribute?
if (mApplyStyle) {
for (Element element : getElements()) {
String value = PREFIX_RESOURCE_REF + REFERENCE_STYLE + mStyleName;
setAttribute(rootEdit, element, null, null, ATTR_STYLE, value);
}
}
if (rootEdit.hasChildren()) {
IFile sourceFile = mDelegate.getEditor().getInputFile();
if (sourceFile == null) {
return changes;
}
TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile);
change.setTextType(EXT_XML);
changes.add(change);
if (AdtPrefs.getPrefs().getFormatGuiXml()) {
MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
if (formatted != null) {
rootEdit = formatted;
}
}
change.setEdit(rootEdit);
}
return changes;
}
private String computeStyleDeclaration(boolean createFile, String initialIndent) {
StringBuilder sb = new StringBuilder();
if (createFile) {
sb.append(NewXmlFileWizard.XML_HEADER_LINE);
sb.append('<').append(TAG_RESOURCES).append(' ');
sb.append(XMLNS_PREFIX).append(ANDROID_NS_NAME).append('=').append('"');
sb.append(ANDROID_URI);
sb.append('"').append('>').append('\n');
}
// Indent. Use the existing indent found for previous <style> elements in
// the resource file - but if that indent was 0 (e.g. <style> elements are
// at the left margin) only use it to indent the style elements and use a real
// nonzero indent for its children.
String indent = " "; //$NON-NLS-1$
if (initialIndent == null) {
initialIndent = indent;
} else if (initialIndent.length() > 0) {
indent = initialIndent;
}
sb.append(initialIndent);
String styleTag = "style"; //$NON-NLS-1$ // TODO - use constant in parallel changeset
sb.append('<').append(styleTag).append(' ').append(ATTR_NAME).append('=').append('"');
sb.append(mStyleName);
sb.append('"');
if (mParent != null) {
sb.append(' ').append(ATTR_PARENT).append('=').append('"');
sb.append(mParent);
sb.append('"');
}
sb.append('>').append('\n');
for (Attr attribute : mChosenAttributes) {
sb.append(initialIndent).append(indent);
sb.append('<').append(TAG_ITEM).append(' ').append(ATTR_NAME).append('=').append('"');
// We've already enforced that regardless of prefix, only attributes with
// an Android namespace can be in the set of chosen attributes. Rewrite the
// prefix to android here.
if (attribute.getPrefix() != null) {
sb.append(ANDROID_NS_NAME_PREFIX);
}
sb.append(attribute.getLocalName());
sb.append('"').append('>');
sb.append(attribute.getValue());
sb.append('<').append('/').append(TAG_ITEM).append('>').append('\n');
}
sb.append(initialIndent).append('<').append('/').append(styleTag).append('>').append('\n');
if (createFile) {
sb.append('<').append('/').append(TAG_RESOURCES).append('>').append('\n');
}
String styleString = sb.toString();
return styleString;
}
/** Computes the location in the file to insert the new style element at, as well as
* the exact indent string to use to indent the {@code <style>} element.
* @param file the styles.xml file to insert into
* @return a pair of an insert offset and an indent string
*/
private Pair<Integer, String> computeInsertContext(final IFile file) {
int insertAtIndex = -1;
// Find the insert of the final </resources> item where we will insert
// the new style elements.
String indent = null;
IModelManager modelManager = StructuredModelManager.getModelManager();
IStructuredModel model = null;
try {
model = modelManager.getModelForRead(file);
if (model instanceof IDOMModel) {
IDOMModel domModel = (IDOMModel) model;
IDOMDocument otherDocument = domModel.getDocument();
Element root = otherDocument.getDocumentElement();
Node lastChild = root.getLastChild();
if (lastChild != null) {
if (lastChild instanceof IndexedRegion) {
IndexedRegion region = (IndexedRegion) lastChild;
insertAtIndex = region.getStartOffset() + region.getLength();
}
// Compute indent
while (lastChild != null) {
if (lastChild.getNodeType() == Node.ELEMENT_NODE) {
IStructuredDocument document = model.getStructuredDocument();
indent = AndroidXmlEditor.getIndent(document, lastChild);
break;
}
lastChild = lastChild.getPreviousSibling();
}
}
}
} catch (IOException e) {
AdtPlugin.log(e, null);
} catch (CoreException e) {
AdtPlugin.log(e, null);
} finally {
if (model != null) {
model.releaseFromRead();
}
}
if (insertAtIndex == -1) {
String contents = AdtPlugin.readFile(file);
insertAtIndex = contents.indexOf("</" + TAG_RESOURCES + ">"); //$NON-NLS-1$
if (insertAtIndex == -1) {
insertAtIndex = contents.length();
}
}
return Pair.of(insertAtIndex, indent);
}
@Override
VisualRefactoringWizard createWizard() {
return new ExtractStyleWizard(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.style", //$NON-NLS-1$
project, description, comment, arguments);
}
@Override
protected Refactoring createRefactoring(Map<String, String> args) {
return new ExtractStyleRefactoring(args);
}
}
/**
* Determines the parent style to be used for this refactoring
*
* @return the parent style to be used for this refactoring
*/
public String getParentStyle() {
Set<String> styles = new HashSet<String>();
for (Element element : getElements()) {
// Includes "" for elements not setting the style
styles.add(element.getAttribute(ATTR_STYLE));
}
if (styles.size() > 1) {
// The elements differ in what style attributes they are set to
return null;
}
String style = styles.iterator().next();
if (style != null && style.length() > 0) {
return style;
}
// None of the elements set the style -- see if they have the same widget types
// and if so offer to extend the theme style for that widget type
Set<String> types = new HashSet<String>();
for (Element element : getElements()) {
types.add(element.getTagName());
}
if (types.size() == 1) {
String view = DescriptorsUtils.getBasename(types.iterator().next());
ResourceResolver resolver = mDelegate.getGraphicalEditor().getResourceResolver();
// Look up the theme item name, which for a Button would be "buttonStyle", and so on.
String n = Character.toLowerCase(view.charAt(0)) + view.substring(1)
+ "Style"; //$NON-NLS-1$
ResourceValue value = resolver.findItemInTheme(n);
if (value != null) {
ResourceValue resolvedValue = resolver.resolveResValue(value);
String name = resolvedValue.getName();
if (name != null) {
if (resolvedValue.isFramework()) {
return PREFIX_ANDROID + name;
} else {
return name;
}
}
}
}
return null;
}
}