/* * 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_URI; import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX; import static com.android.ide.common.layout.LayoutConstants.ATTR_BASELINE_ALIGNED; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; import static com.android.ide.common.layout.LayoutConstants.FQCN_LINEAR_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.FQCN_RELATIVE_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.FQCN_TABLE_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.TABLE_ROW; import static com.android.ide.common.layout.LayoutConstants.VALUE_FALSE; import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; import static com.android.ide.eclipse.adt.AndroidConstants.EXT_XML; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.IAndroidTarget; 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.wst.sse.core.internal.provisional.IndexedRegion; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Converts the selected layout into a layout of a different type. */ @SuppressWarnings("restriction") // XML model public class ChangeLayoutRefactoring extends VisualRefactoring { private static final String KEY_TYPE = "type"; //$NON-NLS-1$ private String mTypeFqcn; /** * This constructor is solely used by {@link Descriptor}, * to replay a previous refactoring. * @param arguments argument map created by #createArgumentMap. */ ChangeLayoutRefactoring(Map<String, String> arguments) { super(arguments); mTypeFqcn = arguments.get(KEY_TYPE); } public ChangeLayoutRefactoring(IFile file, LayoutEditor editor, ITextSelection selection, ITreeSelection treeSelection) { super(file, editor, selection, treeSelection); } @Override public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException { RefactoringStatus status = new RefactoringStatus(); try { pm.beginTask("Checking preconditions...", 2); if (mSelectionStart == -1 || mSelectionEnd == -1) { status.addFatalError("No selection to convert"); return status; } mElements = getElements(); if (mElements.size() != 1) { status.addFatalError("Select precisely one layout to convert"); 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); return args; } @Override public String getName() { return "Change Layout"; } void setType(String typeFqcn) { mTypeFqcn = typeFqcn; } @Override protected List<Change> computeChanges() { String name = getViewClass(mTypeFqcn); IFile file = mEditor.getInputFile(); List<Change> changes = new ArrayList<Change>(); TextFileChange change = new TextFileChange(file.getName(), file); MultiTextEdit rootEdit = new MultiTextEdit(); change.setEdit(rootEdit); change.setTextType(EXT_XML); changes.add(change); String text = getText(mSelectionStart, mSelectionEnd); Element layout = getPrimaryElement(); String oldName = layout.getNodeName(); int open = text.indexOf(oldName); int close = text.lastIndexOf(oldName); if (open != -1 && close != -1) { int oldLength = oldName.length(); rootEdit.addChild(new ReplaceEdit(mSelectionStart + open, oldLength, name)); if (close != open) { // Gracefully handle <FooLayout/> rootEdit.addChild(new ReplaceEdit(mSelectionStart + close, oldLength, name)); } } String oldType = getOldType(); String newType = mTypeFqcn; if (oldType.equals(FQCN_LINEAR_LAYOUT) && newType.equals(FQCN_RELATIVE_LAYOUT)) { // Hand-coded conversion convertLinearToRelative(rootEdit); } else if (oldType.equals(FQCN_RELATIVE_LAYOUT) && newType.equals(FQCN_LINEAR_LAYOUT)) { convertRelativeToLinear(rootEdit); } else if (oldType.equals(FQCN_LINEAR_LAYOUT) && newType.equals(FQCN_TABLE_LAYOUT)) { convertLinearToTable(rootEdit); } else { convertGeneric(rootEdit, oldType, newType); } removeUndefinedLayoutAttrs(rootEdit, layout); return changes; } /** Hand coded conversion from a LinearLayout to a TableLayout */ private void convertLinearToTable(MultiTextEdit rootEdit) { // This is pretty easy; just switch the root tag (already done by the initial generic // conversion) and then convert all the children into <TableRow> elements. // Finally, get rid of the orientation attribute, if any. Element layout = getPrimaryElement(); removeOrientationAttribute(rootEdit, layout); NodeList children = layout.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node node = children.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element) node; if (node instanceof IndexedRegion) { IndexedRegion region = (IndexedRegion) node; int start = region.getStartOffset(); int end = region.getEndOffset(); String text = getText(start, end); String oldName = child.getNodeName(); if (oldName.equals(LINEAR_LAYOUT)) { removeOrientationAttribute(rootEdit, child); int open = text.indexOf(oldName); int close = text.lastIndexOf(oldName); if (open != -1 && close != -1) { int oldLength = oldName.length(); rootEdit.addChild(new ReplaceEdit(mSelectionStart + open, oldLength, TABLE_ROW)); if (close != open) { // Gracefully handle <FooLayout/> rootEdit.addChild(new ReplaceEdit(mSelectionStart + close, oldLength, TABLE_ROW)); } } } // else: WRAP in TableLayout! } } } } /** Hand coded conversion from a LinearLayout to a RelativeLayout */ private void convertLinearToRelative(MultiTextEdit rootEdit) { // This can be done accurately. Element layout = getPrimaryElement(); // Horizontal is the default, so if no value is specified it is horizontal. boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION)); removeOrientationAttribute(rootEdit, layout); String attributePrefix = getAndroidNamespacePrefix(); // TODO: Consider gravity of each element // TODO: Consider weight of each element // Right now it simply makes a single attachment to keep the order. if (isVertical) { // Align each child to the bottom and left of its parent NodeList children = layout.getChildNodes(); String prevId = null; for (int i = 0, n = children.getLength(); i < n; i++) { Node node = children.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element) node; String id = ensureHasId(rootEdit, child); if (prevId != null) { addAttributeDeclaration(rootEdit, child, attributePrefix, ATTR_LAYOUT_BELOW, prevId); } prevId = id; } } } else { // Align each child to the left NodeList children = layout.getChildNodes(); boolean isBaselineAligned = !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED)); String prevId = null; for (int i = 0, n = children.getLength(); i < n; i++) { Node node = children.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element) node; String id = ensureHasId(rootEdit, child); if (prevId != null) { addAttributeDeclaration(rootEdit, child, attributePrefix, ATTR_LAYOUT_TO_RIGHT_OF, prevId); if (isBaselineAligned) { addAttributeDeclaration(rootEdit, child, attributePrefix, ATTR_LAYOUT_ALIGN_BASELINE, prevId); } } prevId = id; } } } } /** Strips out the android:orientation attribute from the given linear layout element */ private void removeOrientationAttribute(MultiTextEdit rootEdit, Element layout) { assert layout.getTagName().equals(LINEAR_LAYOUT); removeAttribute(rootEdit, layout, ANDROID_URI, ATTR_ORIENTATION); } /** * Hand coded conversion from a RelativeLayout to a LinearLayout * * @param rootEdit the root multi text edit to add edits to */ private void convertRelativeToLinear(MultiTextEdit rootEdit) { // This is going to be lossy... // TODO: Attempt to "order" the items based on their visual positions // and insert them in that order in the LinearLayout. // TODO: Possibly use nesting if necessary, by spatial subdivision, // to accomplish roughly the same layout as the relative layout specifies. } /** * Hand coded -generic- conversion from one layout to another. This is not going to be * an accurate layout transformation; instead it simply migrates the layout attributes * that are supported, and adds defaults for any new required layout attributes. In * addition, it attempts to order the children visually based on where they fit in a * rendering. (Unsupported layout attributes will be removed by the caller at the * end.) * <ul> * <li>Try to handle nesting. Converting a *hierarchy* of layouts into a flatter * layout for powerful layouts that support it, like RelativeLayout. * <li>Try to do automatic "inference" about the layout. I can render it and look at * the ViewInfo positions and sizes. I can render it multiple times, at different * sizes, to infer "stretchiness" and "weight" properties of the children. * <li>Try to do indirect transformations. E.g. if I can go from A to B, and B to C, * then an attempt to go from A to C should perform conversions A to B and then B to * C. * </ul> * * @param rootEdit the root multi text edit to add edits to * @param oldType the fully qualified class name of the layout type we are converting * from * @param newType the fully qualified class name of the layout type we are converting * to */ private void convertGeneric(MultiTextEdit rootEdit, String oldType, String newType) { // TODO: Add hooks for 3rd party conversions getting registered through the // IViewRule interface. // For now we simply go with the default behavior, which is to just strip the // layout attributes that aren't supported. } /** Removes all the unused attributes after a conversion */ private void removeUndefinedLayoutAttrs(MultiTextEdit rootEdit, Element layout) { ViewElementDescriptor descriptor = getLayoutDescriptor(); if (descriptor == null) { return; } Set<String> defined = new HashSet<String>(); AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes(); for (AttributeDescriptor attribute : layoutAttributes) { defined.add(attribute.getXmlLocalName()); } NodeList children = layout.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node node = children.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element) node; List<Attr> attributes = findLayoutAttributes(child); for (Attr attribute : attributes) { String name = attribute.getLocalName(); if (!defined.contains(name)) { // Remove it removeAttribute(rootEdit, child, attribute.getNamespaceURI(), name); } } } } } private ViewElementDescriptor getLayoutDescriptor() { Sdk current = Sdk.getCurrent(); if (current != null) { IAndroidTarget target = current.getTarget(mProject); if (target != null) { AndroidTargetData targetData = current.getTargetData(target); List<ViewElementDescriptor> layouts = targetData.getLayoutDescriptors().getLayoutDescriptors(); for (ViewElementDescriptor descriptor : layouts) { if (mTypeFqcn.equals(descriptor.getFullClassName())) { return descriptor; } } } } return null; } 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.convert", //$NON-NLS-1$ project, description, comment, arguments); } @Override protected Refactoring createRefactoring(Map<String, String> args) { return new ChangeLayoutRefactoring(args); } } 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; } }