aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java452
1 files changed, 452 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java
new file mode 100644
index 000000000..0e56bdf4d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2012 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.ATTR_DRAWABLE_BOTTOM;
+import static com.android.SdkConstants.ATTR_DRAWABLE_LEFT;
+import static com.android.SdkConstants.ATTR_DRAWABLE_PADDING;
+import static com.android.SdkConstants.ATTR_DRAWABLE_RIGHT;
+import static com.android.SdkConstants.ATTR_DRAWABLE_TOP;
+import static com.android.SdkConstants.ATTR_GRAVITY;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.ATTR_SRC;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.IMAGE_VIEW;
+import static com.android.SdkConstants.LINEAR_LAYOUT;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.TEXT_VIEW;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtUtils;
+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.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.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.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Converts a LinearLayout with exactly a TextView child and an ImageView child into
+ * a single TextView with a compound drawable.
+ */
+@SuppressWarnings("restriction") // XML model
+public class UseCompoundDrawableRefactoring extends VisualRefactoring {
+ /**
+ * Constructs a new {@link UseCompoundDrawableRefactoring}
+ *
+ * @param file the file to refactor in
+ * @param editor the corresponding editor
+ * @param selection the editor selection, or null
+ * @param treeSelection the canvas selection, or null
+ */
+ public UseCompoundDrawableRefactoring(IFile file, LayoutEditorDelegate editor,
+ ITextSelection selection, ITreeSelection treeSelection) {
+ super(file, editor, selection, treeSelection);
+ }
+
+ /**
+ * This constructor is solely used by {@link Descriptor}, to replay a
+ * previous refactoring.
+ *
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ private UseCompoundDrawableRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ }
+
+ @VisibleForTesting
+ UseCompoundDrawableRefactoring(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("Nothing to convert");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ 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 convert");
+ return status;
+ }
+
+ // Ensure that we have selected precisely one LinearLayout
+ if (mElements.size() != 1 ||
+ !(mElements.get(0).getTagName().equals(LINEAR_LAYOUT))) {
+ status.addFatalError("Must select exactly one LinearLayout");
+ return status;
+ }
+
+ Element layout = mElements.get(0);
+ List<Element> children = DomUtilities.getChildren(layout);
+ if (children.size() != 2) {
+ status.addFatalError("The LinearLayout must have exactly two children");
+ return status;
+ }
+ Element first = children.get(0);
+ Element second = children.get(1);
+ boolean haveTextView =
+ first.getTagName().equals(TEXT_VIEW)
+ || second.getTagName().equals(TEXT_VIEW);
+ boolean haveImageView =
+ first.getTagName().equals(IMAGE_VIEW)
+ || second.getTagName().equals(IMAGE_VIEW);
+ if (!(haveTextView && haveImageView)) {
+ status.addFatalError("The LinearLayout must have exactly one TextView child " +
+ "and one ImageView child");
+ 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() {
+ return super.createArgumentMap();
+ }
+
+ @Override
+ public String getName() {
+ return "Convert to Compound Drawable";
+ }
+
+ @Override
+ protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) {
+ 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);
+
+ // (1) Build up the contents of the new TextView. This is identical
+ // to the old contents, but with the addition of a drawableTop/Left/Right/Bottom
+ // attribute (depending on the orientation and order), as well as any layout
+ // params from the LinearLayout.
+ // (2) Delete the linear layout and replace with the text view.
+ // (3) Reformat.
+
+ // checkInitialConditions has already validated that we have exactly a LinearLayout
+ // with an ImageView and a TextView child (in either order)
+ Element layout = mElements.get(0);
+ List<Element> children = DomUtilities.getChildren(layout);
+ Element first = children.get(0);
+ Element second = children.get(1);
+ final Element text;
+ final Element image;
+ if (first.getTagName().equals(TEXT_VIEW)) {
+ text = first;
+ image = second;
+ } else {
+ text = second;
+ image = first;
+ }
+
+ // Horizontal is the default, so if no value is specified it is horizontal.
+ boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
+ ATTR_ORIENTATION));
+
+ // The WST DOM implementation doesn't correctly implement cloneNode: this returns
+ // an empty document instead:
+ // text.getOwnerDocument().cloneNode(false/*deep*/);
+ // Luckily we just need to clone a single element, not a nested structure, so it's
+ // easy enough to do this manually:
+ Document tempDocument = DomUtilities.createEmptyDocument();
+ if (tempDocument == null) {
+ return changes;
+ }
+ Element newTextElement = tempDocument.createElement(text.getTagName());
+ tempDocument.appendChild(newTextElement);
+
+ NamedNodeMap attributes = text.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ String name = attribute.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attribute.getNamespaceURI())
+ && !(name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))) {
+ // Ignore layout params: the parent layout is going away
+ } else {
+ newTextElement.setAttribute(attribute.getName(), attribute.getValue());
+ }
+ }
+
+ // Apply all layout params from the parent (except width and height),
+ // as well as android:gravity
+ List<Attr> layoutAttributes = findLayoutAttributes(layout);
+ 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;
+ }
+ newTextElement.setAttribute(attribute.getName(), attribute.getValue());
+ }
+ String gravity = layout.getAttributeNS(ANDROID_URI, ATTR_GRAVITY);
+ if (gravity.length() > 0) {
+ setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_GRAVITY, gravity);
+ }
+
+ String src = image.getAttributeNS(ANDROID_URI, ATTR_SRC);
+
+ // Set the drawable
+ String drawableAttribute;
+ // The space between the image and the text can have margins/padding, both
+ // from the text's perspective and from the image's perspective. We need to
+ // combine these.
+ String padding1 = null;
+ String padding2 = null;
+ if (isVertical) {
+ if (first == image) {
+ drawableAttribute = ATTR_DRAWABLE_TOP;
+ padding1 = getPadding(image, ATTR_LAYOUT_MARGIN_BOTTOM);
+ padding2 = getPadding(text, ATTR_LAYOUT_MARGIN_TOP);
+ } else {
+ drawableAttribute = ATTR_DRAWABLE_BOTTOM;
+ padding1 = getPadding(text, ATTR_LAYOUT_MARGIN_BOTTOM);
+ padding2 = getPadding(image, ATTR_LAYOUT_MARGIN_TOP);
+ }
+ } else {
+ if (first == image) {
+ drawableAttribute = ATTR_DRAWABLE_LEFT;
+ padding1 = getPadding(image, ATTR_LAYOUT_MARGIN_RIGHT);
+ padding2 = getPadding(text, ATTR_LAYOUT_MARGIN_LEFT);
+ } else {
+ drawableAttribute = ATTR_DRAWABLE_RIGHT;
+ padding1 = getPadding(text, ATTR_LAYOUT_MARGIN_RIGHT);
+ padding2 = getPadding(image, ATTR_LAYOUT_MARGIN_LEFT);
+ }
+ }
+
+ setAndroidAttribute(newTextElement, androidNsPrefix, drawableAttribute, src);
+
+ String padding = combine(padding1, padding2);
+ if (padding != null) {
+ setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_DRAWABLE_PADDING, padding);
+ }
+
+ // If the removed LinearLayout is the root container, transfer its namespace
+ // declaration to the TextView
+ if (layout.getParentNode() instanceof Document) {
+ List<Attr> declarations = findNamespaceAttributes(layout);
+ for (Attr attribute : declarations) {
+ if (attribute instanceof IndexedRegion) {
+ newTextElement.setAttribute(attribute.getName(), attribute.getValue());
+ }
+ }
+ }
+
+ // Update any layout references to the layout to point to the text view
+ String layoutId = getId(layout);
+ if (layoutId.length() > 0) {
+ String id = getId(text);
+ if (id.length() == 0) {
+ id = ensureHasId(rootEdit, text, null, false);
+ setAndroidAttribute(newTextElement, androidNsPrefix, ATTR_ID, id);
+ }
+
+ IStructuredModel model = mDelegate.getEditor().getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ List<TextEdit> replaceIds = replaceIds(androidNsPrefix,
+ doc, mSelectionStart, mSelectionEnd, layoutId, id);
+ for (TextEdit edit : replaceIds) {
+ rootEdit.addChild(edit);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ String xml = EclipseXmlPrettyPrinter.prettyPrint(
+ tempDocument.getDocumentElement(),
+ EclipseXmlFormatPreferences.create(),
+ XmlFormatStyle.LAYOUT, null, false);
+
+ TextEdit replace = new ReplaceEdit(mSelectionStart, mSelectionEnd - mSelectionStart, xml);
+ rootEdit.addChild(replace);
+
+ if (AdtPrefs.getPrefs().getFormatGuiXml()) {
+ MultiTextEdit formatted = reformat(rootEdit, XmlFormatStyle.LAYOUT);
+ if (formatted != null) {
+ rootEdit = formatted;
+ }
+ }
+
+ change.setEdit(rootEdit);
+ changes.add(change);
+ return changes;
+ }
+
+ @Nullable
+ private static String getPadding(@NonNull Element element, @NonNull String attribute) {
+ String padding = element.getAttributeNS(ANDROID_URI, attribute);
+ if (padding != null && padding.isEmpty()) {
+ padding = null;
+ }
+ return padding;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ static String combine(@Nullable String dimension1, @Nullable String dimension2) {
+ if (dimension1 == null || dimension1.isEmpty()) {
+ if (dimension2 != null && dimension2.isEmpty()) {
+ return null;
+ }
+ return dimension2;
+ } else if (dimension2 == null || dimension2.isEmpty()) {
+ if (dimension1 != null && dimension1.isEmpty()) {
+ return null;
+ }
+ return dimension1;
+ } else {
+ // Two dimensions are specified (e.g. marginRight for the left one and marginLeft
+ // for the right one); we have to add these together. We can only do that if
+ // they use the same units, and do not use resources.
+ if (dimension1.startsWith(PREFIX_RESOURCE_REF)
+ || dimension2.startsWith(PREFIX_RESOURCE_REF)) {
+ return null;
+ }
+
+ Pattern p = Pattern.compile("([\\d\\.]+)(.+)"); //$NON-NLS-1$
+ Matcher matcher1 = p.matcher(dimension1);
+ Matcher matcher2 = p.matcher(dimension2);
+ if (matcher1.matches() && matcher2.matches()) {
+ String unit = matcher1.group(2);
+ if (unit.equals(matcher2.group(2))) {
+ float value1 = Float.parseFloat(matcher1.group(1));
+ float value2 = Float.parseFloat(matcher2.group(1));
+ return AdtUtils.formatFloatAttribute(value1 + value2) + unit;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets an Android attribute (in the Android namespace) on an element
+ * without a given namespace prefix. This is done when building a new Element
+ * in a temporary document such that the namespace prefix matches when the element is
+ * formatted and replaced in the target document.
+ */
+ private static void setAndroidAttribute(Element element, String prefix, String name,
+ String value) {
+ element.setAttribute(prefix + ':' + name, value);
+ }
+
+ @Override
+ public VisualRefactoringWizard createWizard() {
+ return new UseCompoundDrawableWizard(this, mDelegate);
+ }
+
+ @SuppressWarnings("javadoc")
+ 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.usecompound", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new UseCompoundDrawableRefactoring(args);
+ }
+ }
+}