OSDN Git Service

Add autoformatting of XML, and improved manual formatting
authorTor Norbye <tnorbye@google.com>
Fri, 5 Nov 2010 20:56:04 +0000 (13:56 -0700)
committerTor Norbye <tnorbye@google.com>
Mon, 8 Nov 2010 19:24:03 +0000 (11:24 -0800)
This changeset improves the formatting of XML edited by the layout
editor in two ways:

(1) It improves the way the layout editor handles insertion and
deletion into the XML document; it looks up the indentation of the
sibling and parent elements and attempts to correctly indent new
elements based on the existing surrounding formatting, and it also
attempts to clean things up correctly on element deletion.

(2) It adds a new user option for turning on automatic XML
formatting. When this is on, it will invoke the Eclipse XML formatter
on portions of the XML after each edit. This will ensure that the
document adheres to the user's preferred formatting settings (maximum
line width, tabs versus spaces, line breaks before attributes, etc.

Change-Id: I74f9a4240a8c5ca4295c01f3b55751ef10b1c1b0

eclipse/dictionary.txt
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java

index b3c0fba..a010809 100644 (file)
@@ -30,6 +30,7 @@ configurability
 coords
 ddms
 debuggable
+dedent
 deprecated
 deselect
 deselects
index 1e745b3..5ffc17e 100644 (file)
@@ -39,7 +39,9 @@ import org.eclipse.jface.action.IAction;
 import org.eclipse.jface.dialogs.ErrorDialog;
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
 import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.swt.custom.StyledText;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.ui.IActionBars;
 import org.eclipse.ui.IEditorInput;
@@ -67,6 +69,7 @@ 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.sse.ui.StructuredTextEditor;
+import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
 import org.eclipse.wst.xml.core.internal.document.NodeContainer;
 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
 import org.w3c.dom.Document;
@@ -87,7 +90,7 @@ import java.net.URL;
 public abstract class AndroidXmlEditor extends FormEditor implements IResourceChangeListener {
 
     /** Preference name for the current page of this file */
-    private static final String PREF_CURRENT_PAGE = "_current_page";
+    private static final String PREF_CURRENT_PAGE = "_current_page"; // $NON-NLS-1$
 
     /** Id string used to create the Android SDK browser */
     private static String BROWSER_ID = "android"; // $NON-NLS-1$
@@ -946,7 +949,7 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
      * Get the XML text directly from the editor.
      *
      * @param xmlNode The node whose XML text we want to obtain.
-     * @return The XML representation of the {@link Node}.
+     * @return The XML representation of the {@link Node}, or null if there was an error.
      */
     public String getXmlText(Node xmlNode) {
         String data = null;
@@ -975,6 +978,135 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
     }
 
     /**
+     * Formats the text around the given caret range, using the current Eclipse
+     * XML formatter settings.
+     *
+     * @param begin The starting offset of the range to be reformatted.
+     * @param end The ending offset of the range to be reformatted.
+     */
+    public void reformatRegion(int begin, int end) {
+        ISourceViewer textViewer = getStructuredSourceViewer();
+
+        // Clamp text range to valid offsets.
+        IDocument document = textViewer.getDocument();
+        int documentLength = document.getLength();
+        end = Math.min(end, documentLength);
+        begin = Math.min(begin, end);
+
+        // It turns out the XML formatter does *NOT* format things correctly if you
+        // select just a region of text. You *MUST* also include the leading whitespace
+        // on the line, or it will dedent all the content to column 0. Therefore,
+        // we must figure out the offset of the start of the line that contains the
+        // beginning of the tag.
+        try {
+            IRegion lineInformation = document.getLineInformationOfOffset(begin);
+            if (lineInformation != null) {
+                int lineBegin = lineInformation.getOffset();
+                if (lineBegin != begin) {
+                    begin = lineBegin;
+                } else if (begin > 0) {
+                    // Trick #2: It turns out that, if an XML element starts in column 0,
+                    // then the XML formatter will NOT indent it (even if its parent is
+                    // indented). If you on the other hand include the end of the previous
+                    // line (the newline), THEN the formatter also correctly inserts the
+                    // element. Therefore, we adjust the beginning range to include the
+                    // previous line (if we are not already in column 0 of the first line)
+                    // in the case where the element starts the line.
+                    begin--;
+                }
+            }
+        } catch (BadLocationException e) {
+            // This cannot happen because we already clamped the offsets
+            AdtPlugin.log(e, e.toString()); // $NON-NLS-1$
+        }
+
+        if (textViewer instanceof StructuredTextViewer) {
+            StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
+            int operation = ISourceViewer.FORMAT;
+            boolean canFormat = structuredTextViewer.canDoOperation(operation);
+            if (canFormat) {
+                StyledText textWidget = textViewer.getTextWidget();
+                textWidget.setSelection(begin, end);
+                structuredTextViewer.doOperation(operation);
+            }
+        }
+    }
+
+    /**
+     * Formats the XML region corresponding to the given node.
+     *
+     * @param node The node to be formatted.
+     */
+    public void reformatNode(Node node) {
+        if (mIsCreatingPage) {
+            return;
+        }
+
+        if (node instanceof IndexedRegion) {
+            IndexedRegion region = (IndexedRegion) node;
+            int begin = region.getStartOffset();
+            int end = region.getEndOffset();
+            reformatRegion(begin, end);
+        }
+    }
+
+    /**
+     * Formats the XML document according to the user's XML formatting settings.
+     */
+    public void reformatDocument() {
+        ISourceViewer textViewer = getStructuredSourceViewer();
+        if (textViewer instanceof StructuredTextViewer) {
+            StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
+            int operation = StructuredTextViewer.FORMAT_DOCUMENT;
+            boolean canFormat = structuredTextViewer.canDoOperation(operation);
+            if (canFormat) {
+                structuredTextViewer.doOperation(operation);
+            }
+        }
+    }
+
+    /**
+     * Returns the indentation String of the given node.
+     *
+     * @param xmlNode The node whose indentation we want.
+     * @return The indent-string of the given node, or "" if the indentation for some reason could
+     *         not be computed.
+     */
+    public String getIndent(Node xmlNode) {
+        assert xmlNode.getNodeType() == Node.ELEMENT_NODE;
+
+        if (xmlNode instanceof IndexedRegion) {
+            IndexedRegion region = (IndexedRegion)xmlNode;
+            IDocument document = getStructuredSourceViewer().getDocument();
+            int startOffset = region.getStartOffset();
+            try {
+                IRegion lineInformation = document.getLineInformationOfOffset(startOffset);
+                if (lineInformation != null) {
+                    int lineBegin = lineInformation.getOffset();
+                    if (lineBegin != startOffset) {
+                        String prefix = document.get(lineBegin, startOffset - lineBegin);
+
+                        // It's possible that the tag whose indentation we seek is not
+                        // at the beginning of the line. In that case we'll just return
+                        // the indentation of the line itself.
+                        for (int i = 0; i < prefix.length(); i++) {
+                            if (!Character.isWhitespace(prefix.charAt(i))) {
+                                return prefix.substring(0, i);
+                            }
+                        }
+
+                        return prefix;
+                    }
+                }
+            } catch (BadLocationException e) {
+                AdtPlugin.log(e, "Could not obtain indentation"); // $NON-NLS-1$
+            }
+        }
+
+        return ""; // $NON-NLS-1$
+    }
+
+    /**
      * Listen to changes in the underlying XML model in the structured editor.
      */
     private class XmlModelStateListener implements IModelStateListener {
index c23a4d4..180c329 100644 (file)
@@ -543,7 +543,7 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput,
             if (layoutDesc != null) {
                 DocumentDescriptor docDesc = layoutDesc.getDescriptor();
                 if (docDesc != null) {
-                    desc = internalFindFqcnViewDescritor(fqcn, docDesc.getChildren(), null);
+                    desc = internalFindFqcnViewDescriptor(fqcn, docDesc.getChildren(), null);
                 }
             }
         }
@@ -567,7 +567,7 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput,
      *  necessary since the view descriptor hierarchy is cyclic.
      * @return Either a matching {@link ViewElementDescriptor} or null.
      */
-    private ViewElementDescriptor internalFindFqcnViewDescritor(String fqcn,
+    private ViewElementDescriptor internalFindFqcnViewDescriptor(String fqcn,
             ElementDescriptor[] descriptors,
             Set<ElementDescriptor> visited) {
         if (visited == null) {
@@ -587,7 +587,7 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput,
 
                     // Visit its children
                     ViewElementDescriptor vd =
-                        internalFindFqcnViewDescritor(fqcn, desc.getChildren(), visited);
+                        internalFindFqcnViewDescriptor(fqcn, desc.getChildren(), visited);
                     if (vd != null) {
                         return vd;
                     }
index 530be06..5f5af7c 100755 (executable)
@@ -198,7 +198,7 @@ public class NodeProxy implements INode {
         checkEditOK();
 
         // Find the descriptor for this FQCN
-        ViewElementDescriptor vd = getFqcnViewDescritor(viewFqcn);
+        ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn);
         if (vd == null) {
             warnPrintf("Can't create a new %s element", viewFqcn);
             return null;
@@ -230,7 +230,7 @@ public class NodeProxy implements INode {
         checkEditOK();
 
         // Find the descriptor for this FQCN
-        ViewElementDescriptor vd = getFqcnViewDescritor(viewFqcn);
+        ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn);
         if (vd == null) {
             warnPrintf("Can't create a new %s element", viewFqcn);
             return null;
@@ -364,7 +364,7 @@ public class NodeProxy implements INode {
      * (which shouldn't really happen since at this point the SDK should be fully loaded and
      * isn't reloading, or we wouldn't be here editing XML for a layout rule.)
      */
-    private ViewElementDescriptor getFqcnViewDescritor(String fqcn) {
+    private ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
         AndroidXmlEditor editor = mNode.getEditor();
         if (editor instanceof LayoutEditor) {
             return ((LayoutEditor) editor).getFqcnViewDescriptor(fqcn);
index 71d8e8f..121dd3f 100644 (file)
@@ -30,6 +30,7 @@ import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.Android
 import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors;
 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
 import com.android.ide.eclipse.adt.internal.editors.xml.descriptors.XmlDescriptors;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
 import com.android.ide.eclipse.adt.internal.resources.AttributeInfo;
 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
 import com.android.sdklib.SdkConstants;
@@ -295,6 +296,7 @@ public class UiElementNode implements IPropertySource {
      * <p/>
      * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the
      * UI root element node (this method takes care of that.)
+     * @param xmlDoc The new XML document to associate this node with.
      */
     public void setXmlDocument(Document xmlDoc) {
         if (mUiParent == null) {
@@ -340,6 +342,7 @@ public class UiElementNode implements IPropertySource {
      * <p/>
      * Do not use this to call getDescriptor().getAttributes(), instead call
      * getAttributeDescriptors() which can be overridden by derived classes.
+     * @return The {@link ElementDescriptor} for this node. This is never null.
      */
     public ElementDescriptor getDescriptor() {
         return mDescriptor;
@@ -350,6 +353,7 @@ public class UiElementNode implements IPropertySource {
      * <p/>
      * Use this instead of getDescriptor().getAttributes() -- derived classes can override
      * this to manipulate the attribute descriptor list depending on the current UI node.
+     * @return The {@link AttributeDescriptor} array for the descriptor of this node.
      */
     public AttributeDescriptor[] getAttributeDescriptors() {
         return mDescriptor.getAttributes();
@@ -396,7 +400,9 @@ public class UiElementNode implements IPropertySource {
     }
 
     /**
-     * Returns The root {@link UiElementNode}.
+     * Returns the root {@link UiElementNode}.
+     *
+     * @return The root {@link UiElementNode}.
      */
     public UiElementNode getUiRoot() {
         UiElementNode root = this;
@@ -408,8 +414,10 @@ public class UiElementNode implements IPropertySource {
     }
 
     /**
-     * Returns the previous UI sibling of this UI node.
-     * If the node does not have a previous sibling, returns null.
+     * Returns the previous UI sibling of this UI node. If the node does not have a previous
+     * sibling, returns null.
+     *
+     * @return The previous UI sibling of this UI node, or null if not applicable.
      */
     public UiElementNode getUiPreviousSibling() {
         if (mUiParent != null) {
@@ -425,6 +433,8 @@ public class UiElementNode implements IPropertySource {
     /**
      * Returns the next UI sibling of this UI node.
      * If the node does not have a next sibling, returns null.
+     *
+     * @return The next UI sibling of this UI node, or null.
      */
     public UiElementNode getUiNextSibling() {
         if (mUiParent != null) {
@@ -444,6 +454,8 @@ public class UiElementNode implements IPropertySource {
      * Sets the {@link AndroidXmlEditor} handling this {@link UiElementNode} hierarchy.
      * <p/>
      * The editor must always be set on the root node. This method takes care of that.
+     *
+     * @param editor The editor to associate this node with.
      */
     public void setEditor(AndroidXmlEditor editor) {
         if (mUiParent == null) {
@@ -467,6 +479,8 @@ public class UiElementNode implements IPropertySource {
 
     /**
      * Returns the Android target data for the file being edited.
+     *
+     * @return The Android target data for the file being edited.
      */
     public AndroidTargetData getAndroidTarget() {
         return getEditor().getTargetData();
@@ -502,6 +516,7 @@ public class UiElementNode implements IPropertySource {
 
     /**
      * Sets the error flag value.
+     *
      * @param errorFlag the error flag
      */
     public final void setHasError(boolean errorFlag) {
@@ -511,6 +526,9 @@ public class UiElementNode implements IPropertySource {
     /**
      * Returns whether this node, its attributes, or one of the children nodes (and attributes)
      * has errors.
+     *
+     * @return True if this node, its attributes, or one of the children nodes (and attributes)
+     * has errors.
      */
     public final boolean hasError() {
         if (mHasError) {
@@ -596,6 +614,8 @@ public class UiElementNode implements IPropertySource {
 
     /**
      * Adds a new {@link IUiUpdateListener} to the internal update listener list.
+     *
+     * @param listener The listener to add.
      */
     public void addUpdateListener(IUiUpdateListener listener) {
        if (mUiUpdateListeners == null) {
@@ -609,6 +629,8 @@ public class UiElementNode implements IPropertySource {
     /**
      * Removes an existing {@link IUiUpdateListener} from the internal update listener list.
      * Does nothing if the list is empty or the listener is not registered.
+     *
+     * @param listener The listener to remove.
      */
     public void removeUpdateListener(IUiUpdateListener listener) {
        if (mUiUpdateListeners != null) {
@@ -833,20 +855,81 @@ public class UiElementNode implements IPropertySource {
             xmlNextSibling = uiNextSibling.getXmlNode();
         }
 
-        // If this is the first element we are adding into a new element,
-        // we need to insert a newline at the beginning too. Unless it's already
-        // there, which is the case for the root element created for the .xml template
-        // files - but this is why we use the xml node list rather than the element
-        // count.
-        if (parentXmlNode.getChildNodes().getLength() == 0) {
-            parentXmlNode.insertBefore(doc.createTextNode("\n"), xmlNextSibling); //$NON-NLS-1$
+        Node previousTextNode = null;
+        if (xmlNextSibling != null) {
+            Node previousNode = xmlNextSibling.getPreviousSibling();
+            if (previousNode != null && previousNode.getNodeType() == Node.TEXT_NODE) {
+                previousTextNode = previousNode;
+            }
+        } else {
+            Node lastChild = parentXmlNode.getLastChild();
+            if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) {
+                previousTextNode = lastChild;
+            }
         }
 
+        String insertAfter = null;
+
+        // Try to figure out the indentation node to insert. Even in auto-formatting
+        // we need to do this, because it turns out the XML editor's formatter does
+        // not do a very good job with completely botched up XML; it does a much better
+        // job if the new XML is already mostly well formatted. Thus, the main purpose
+        // of applying the real XML formatter after our own indentation attempts here is
+        // to make it apply its own tab-versus-spaces indentation properties, have it
+        // insert line breaks before attributes (if the user has configured that), etc.
+
+        // First figure out the indentation level of the newly inserted element;
+        // this is either the same as the previous sibling, or if there is no sibling,
+        // it's the indentation of the parent plus one indentation level.
+        boolean isFirstChild = getUiPreviousSibling() == null
+                || parentXmlNode.getFirstChild() == null;
+        AndroidXmlEditor editor = getEditor();
+        String indent;
+        String parentIndent = ""; //$NON-NLS-1$
+        if (isFirstChild) {
+            indent = parentIndent = editor.getIndent(parentXmlNode);
+            // We need to add one level of indentation. Are we using tabs?
+            // Can't get to formatting settings so let's just look at the
+            // parent indentation and see if we can guess
+            if (indent.length() > 0 && indent.charAt(indent.length()-1) == '\t') {
+                indent = indent + '\t';
+            } else {
+                // Not using tabs, or we can't figure it out (because parent had no
+                // indentation). In that case, indent with 4 spaces, as seems to
+                // be the Android default.
+                indent = indent + "    "; //$NON-NLS-1$
+            }
+        } else {
+            // Find out the indent of the previous sibling
+            indent = editor.getIndent(getUiPreviousSibling().getXmlNode());
+        }
+
+        // We want to insert the new element BEFORE the text node which precedes
+        // the next element, since that text node is the next element's indentation!
+        if (previousTextNode != null) {
+            xmlNextSibling = previousTextNode;
+        } else {
+            // If there's no previous text node, we are probably inside an
+            // empty element (<LinearLayout>|</LinearLayout>) and in that case we need
+            // to not only insert a newline and indentation before the new element, but
+            // after it as well.
+            insertAfter = parentIndent;
+        }
+
+        // Insert indent text node before the new element
+        Text indentNode = doc.createTextNode("\n" + indent); //$NON-NLS-1$
+        parentXmlNode.insertBefore(indentNode, xmlNextSibling);
+
+        // Insert the element itself
         parentXmlNode.insertBefore(mXmlNode, xmlNextSibling);
 
-        // Insert a separator after the tag, to make it easier to read
-        Text sep = doc.createTextNode("\n"); //$NON-NLS-1$
-        parentXmlNode.insertBefore(sep, xmlNextSibling);
+        // Insert a separator after the tag. We only do this when we've inserted
+        // a tag into an area where there was no whitespace before
+        // (e.g. a new child of <LinearLayout></LinearLayout>).
+        if (insertAfter != null) {
+            Text sep = doc.createTextNode("\n" + insertAfter); //$NON-NLS-1$
+            parentXmlNode.insertBefore(sep, xmlNextSibling);
+        }
 
         // Set all initial attributes in the XML node if they are not empty.
         // Iterate on the descriptor list to get the desired order and then use the
@@ -865,6 +948,10 @@ public class UiElementNode implements IPropertySource {
             }
         }
 
+        if (mUiParent != null) {
+            mUiParent.formatOnInsert(this);
+        }
+
         invokeUiUpdateListeners(UiUpdateState.CREATED);
         return mXmlNode;
     }
@@ -890,14 +977,18 @@ public class UiElementNode implements IPropertySource {
         if (xmlParent == null) {
             xmlParent = getXmlDocument();
         }
-        Node nextSibling = oldXmlNode.getNextSibling();
+        Node previousSibling = oldXmlNode.getPreviousSibling();
         oldXmlNode = xmlParent.removeChild(oldXmlNode);
 
-        // Remove following text node if it's just blank space, to account for
-        // the fact what we add these when we insert nodes.
-        if (nextSibling != null && nextSibling.getNodeType() == Node.TEXT_NODE
-                && nextSibling.getNodeValue().trim().length() == 0) {
-            xmlParent.removeChild(nextSibling);
+        // We need to remove the text node BEFORE the removed element, since THAT's the
+        // indentation node for the removed element.
+        if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE
+                && previousSibling.getNodeValue().trim().length() == 0) {
+            xmlParent.removeChild(previousSibling);
+        }
+
+        if (mUiParent != null) {
+            mUiParent.formatOnDeletion(this);
         }
 
         invokeUiUpdateListeners(UiUpdateState.DELETED);
@@ -1070,6 +1161,7 @@ public class UiElementNode implements IPropertySource {
             }
 
             mUiChildren.remove(uiIndex);
+
             return true;
         } finally {
             // Tell listeners that a node has been removed.
@@ -1667,4 +1759,76 @@ public class UiElementNode implements IPropertySource {
             });
         }
     }
+
+    /** Handles reformatting of the XML buffer when a given node has been inserted.
+     *
+     * @param node The node that was inserted.
+     */
+    private void formatOnInsert(UiElementNode node) {
+        // Reformat parent if it's the first child (such that it for example can force
+        // children into their own lines.)
+        if (mUiChildren.size() == 1) {
+            reformat();
+        } else {
+            // In theory, we should ONLY have to reformat the node itself:
+            // uiNode.reformat();
+            //
+            // However, the XML formatter does not correctly handle this; in particular
+            // it will -dedent- a correctly indented child. Here's an example:
+            //
+            // @formatter:off
+            //    <?xml version="1.0" encoding="utf-8"?>
+            //    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+            //        android:layout_width="fill_parent" android:layout_height="fill_parent"
+            //        android:orientation="vertical">
+            //        <LinearLayout android:id="@+id/LinearLayout01"
+            //            android:layout_width="wrap_content" android:layout_height="wrap_content">
+            //            <Button android:id="@+id/Button03"></Button>
+            //        </LinearLayout>
+            //    </LinearLayout>
+            // @formatter:on
+            //
+            // If we  have just inserted the button inside the nested LinearLayout, and
+            // attempt to format it, it will incorrectly dedent the button to be flush with
+            // its parent.
+            //
+            // Therefore, for now, in this case, format the PARENT on insert. This means that
+            // siblings can be formatted as well, but that can't be helped.
+
+            // This should be "uiNode.reformat();" instead of "reformat()" if formatting
+            // worked correctly:
+            reformat();
+        }
+    }
+
+    /**
+     * Handles reformatting of the XML buffer when a given node has been removed.
+     *
+     * @param node The node that was removed.
+     */
+    private void formatOnDeletion(UiElementNode node) {
+        // Reformat parent if it's the last child removed, such that we can for example
+        // place the closing element back on the same line as the opening tag (if the
+        // user has that mode configured in the formatting options.)
+        if (mUiChildren.size() <= 1) {
+            // <= 1 instead of == 0: turns out the parent hasn't always deleted
+            // this child from its its children list yet.
+            reformat();
+        }
+    }
+
+    /**
+     * Reformats the XML corresponding to the given XML node. This will do nothing if we have
+     * errors, or if the user has turned off XML auto-formatting.
+     */
+    private void reformat() {
+        if (mHasError || !AdtPrefs.getPrefs().getFormatXml()) {
+            return;
+        }
+
+        AndroidXmlEditor editor = getEditor();
+        if (editor != null && mXmlNode != null) {
+            editor.reformatNode(mXmlNode);
+        }
+    }
 }
index 11210e3..de5847b 100644 (file)
@@ -46,6 +46,8 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
 
     public final static String PREFS_MONITOR_DENSITY = AdtPlugin.PLUGIN_ID + ".monitorDensity"; //$NON-NLS-1$
 
+    public final static String PREFS_FORMAT_XML = AdtPlugin.PLUGIN_ID + ".formatXml"; //$NON-NLS-1$
+
     /** singleton instance */
     private final static AdtPrefs sThis = new AdtPrefs();
 
@@ -60,6 +62,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
 
     private boolean mBuildForceResResfresh = false;
     private boolean mBuildForceErrorOnNativeLibInJar = true;
+    private boolean mFormatXml = false;
     private float mMonitorDensity = 0.f;
 
     public static enum BuildVerbosity {
@@ -139,6 +142,10 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
         if (property == null || PREFS_MONITOR_DENSITY.equals(property)) {
             mMonitorDensity = mStore.getFloat(PREFS_MONITOR_DENSITY);
         }
+
+        if (property == null || PREFS_FORMAT_XML.equals(property)) {
+            mFormatXml = mStore.getBoolean(PREFS_FORMAT_XML);
+        }
     }
 
     /**
@@ -153,10 +160,14 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
         return mBuildVerbosity;
     }
 
-    public  boolean getBuildForceResResfresh() {
+    public boolean getBuildForceResResfresh() {
         return mBuildForceResResfresh;
     }
 
+    public boolean getFormatXml() {
+        return mFormatXml;
+    }
+
     public boolean getBuildForceErrorOnNativeLibInJar() {
         return mBuildForceErrorOnNativeLibInJar;
     }
@@ -185,6 +196,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
         store.setDefault(PREFS_HOME_PACKAGE, "android.process.acore"); //$NON-NLS-1$
 
         store.setDefault(PREFS_MONITOR_DENSITY, 0.f);
+        store.setDefault(PREFS_FORMAT_XML, false);
 
         try {
             store.setDefault(PREFS_DEFAULT_DEBUG_KEYSTORE,
index 472b15b..140520f 100644 (file)
@@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.preferences;
 import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog;
 
+import org.eclipse.jface.preference.BooleanFieldEditor;
 import org.eclipse.jface.preference.FieldEditorPreferencePage;
 import org.eclipse.jface.preference.StringButtonFieldEditor;
 import org.eclipse.jface.window.Window;
@@ -44,6 +45,9 @@ public class EditorsPage extends FieldEditorPreferencePage implements IWorkbench
     protected void createFieldEditors() {
         addField(new DensityFieldEditor(AdtPrefs.PREFS_MONITOR_DENSITY,
                 "Monitor Density", getFieldEditorParent()));
+        addField(new BooleanFieldEditor(AdtPrefs.PREFS_FORMAT_XML,
+                "Automatically format the XML edited by the visual layout editor",
+                getFieldEditorParent()));
     }
 
     /**