/*
 * Copyright (C) 2007 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.uimodel;

import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME;
import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS;
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_URI;
import static com.android.sdklib.SdkConstants.NS_RESOURCES;

import com.android.annotations.VisibleForTesting;
import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.resources.platform.AttributeInfo;
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.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
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.sdk.AndroidTargetData;
import com.android.sdklib.SdkConstants;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.wst.xml.core.internal.document.ElementImpl;
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 org.w3c.dom.Text;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Represents an XML node that can be modified by the user interface in the XML editor.
 * <p/>
 * Each tree viewer used in the application page's parts needs to keep a model representing
 * each underlying node in the tree. This interface represents the base type for such a node.
 * <p/>
 * Each node acts as an intermediary model between the actual XML model (the real data support)
 * and the tree viewers or the corresponding page parts.
 * <p/>
 * Element nodes don't contain data per se. Their data is contained in their attributes
 * as well as their children's attributes, see {@link UiAttributeNode}.
 * <p/>
 * The structure of a given {@link UiElementNode} is declared by a corresponding
 * {@link ElementDescriptor}.
 * <p/>
 * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when
 * an element is selected. The {@link AttributeDescriptor} are used property descriptors.
 */
@SuppressWarnings("restriction") // XML model
public class UiElementNode implements IPropertySource {

    /** List of prefixes removed from android:id strings when creating short descriptions. */
    private static String[] ID_PREFIXES = {
        "@android:id/", //$NON-NLS-1$
        NEW_ID_PREFIX, ID_PREFIX, "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$

    /** The element descriptor for the node. Always present, never null. */
    private ElementDescriptor mDescriptor;
    /** The parent element node in the UI model. It is null for a root element or until
     *  the node is attached to its parent. */
    private UiElementNode mUiParent;
    /** The {@link AndroidXmlEditor} handling the UI hierarchy. This is defined only for the
     *  root node. All children have the value set to null and query their parent. */
    private AndroidXmlEditor mEditor;
    /** The XML {@link Document} model that is being mirror by the UI model. This is defined
     *  only for the root node. All children have the value set to null and query their parent. */
    private Document mXmlDocument;
    /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which
     *  have no corresponding XML node or for new UI nodes before their XML node is set. */
    private Node mXmlNode;
    /** The list of all UI children nodes. Can be empty but never null. There's one UI children
     *  node per existing XML children node. */
    private ArrayList<UiElementNode> mUiChildren;
    /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}.
     *  The list is always defined and never null. Unlike the UiElementNode children list, this
     *  is always defined, even for attributes that do not exist in the XML model - that's because
     *  "missing" attributes in the XML model simply mean a default value is used. Also note that
     *  the underlying collection is a map, so order is not respected. To get the desired attribute
     *  order, iterate through the {@link ElementDescriptor}'s attribute list. */
    private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes;
    private HashSet<UiAttributeNode> mUnknownUiAttributes;
    /** A read-only view of the UI children node collection. */
    private List<UiElementNode> mReadOnlyUiChildren;
    /** A read-only view of the UI attributes collection. */
    private Collection<UiAttributeNode> mCachedAllUiAttributes;
    /** A map of hidden attribute descriptors. Key is the XML name. */
    private Map<String, AttributeDescriptor> mCachedHiddenAttributes;
    /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any
     *  listeners attached, so the list is only created on demand and can be null. */
    private ArrayList<IUiUpdateListener> mUiUpdateListeners;
    /** A provider that knows how to create {@link ElementDescriptor} from unmapped XML names.
     *  The default is to have one that creates new {@link ElementDescriptor}. */
    private IUnknownDescriptorProvider mUnknownDescProvider;
    /** Error Flag */
    private boolean mHasError;

    /**
     * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
     *
     * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
     */
    public UiElementNode(ElementDescriptor elementDescriptor) {
        mDescriptor = elementDescriptor;
        clearContent();
    }

    @Override
    public String toString() {
      return String.format("%s [desc: %s, parent: %s, children: %d]",         //$NON-NLS-1$
              this.getClass().getSimpleName(),
              mDescriptor,
              mUiParent != null ? mUiParent.toString() : "none",              //$NON-NLS-1$
                      mUiChildren != null ? mUiChildren.size() : 0
      );
    }

    /**
     * Clears the {@link UiElementNode} by resetting the children list and
     * the {@link UiAttributeNode}s list.
     * Also resets the attached XML node, document, editor if any.
     * <p/>
     * The parent {@link UiElementNode} node is not reset so that it's position
     * in the hierarchy be left intact, if any.
     */
    /* package */ void clearContent() {
        mXmlNode = null;
        mXmlDocument = null;
        mEditor = null;
        clearAttributes();
        mReadOnlyUiChildren = null;
        if (mUiChildren == null) {
            mUiChildren = new ArrayList<UiElementNode>();
        } else {
            // We can't remove mandatory nodes, we just clear them.
            for (int i = mUiChildren.size() - 1; i >= 0; --i) {
                removeUiChildAtIndex(i);
            }
        }
    }

    /**
     * Clears the internal list of attributes, the read-only cached version of it
     * and the read-only cached hidden attribute list.
     */
    private void clearAttributes() {
        mUiAttributes = null;
        mCachedAllUiAttributes = null;
        mCachedHiddenAttributes = null;
        mUnknownUiAttributes = new HashSet<UiAttributeNode>();
    }

    /**
     * Gets or creates the internal UiAttributes list.
     * <p/>
     * When the descriptor derives from ViewElementDescriptor, this list depends on the
     * current UiParent node.
     *
     * @return A new set of {@link UiAttributeNode} that matches the expected
     *         attributes for this node.
     */
    private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() {
        if (mUiAttributes == null) {
            AttributeDescriptor[] attrList = getAttributeDescriptors();
            mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attrList.length);
            for (AttributeDescriptor desc : attrList) {
                UiAttributeNode uiNode = desc.createUiNode(this);
                if (uiNode != null) {  // Some AttributeDescriptors do not have UI associated
                    mUiAttributes.put(desc, uiNode);
                }
            }
        }
        return mUiAttributes;
    }

    /**
     * Computes a short string describing the UI node suitable for tree views.
     * Uses the element's attribute "android:name" if present, or the "android:label" one
     * followed by the element's name if not repeated.
     *
     * @return A short string describing the UI node suitable for tree views.
     */
    public String getShortDescription() {
        String name = mDescriptor.getUiName();
        String attr = getDescAttribute();
        if (attr != null) {
            // If the ui name is repeated in the attribute value, don't use it.
            // Typical case is to avoid ".pkg.MyActivity (Activity)".
            if (attr.contains(name)) {
                return attr;
            } else {
                return String.format("%1$s (%2$s)", attr, name);
            }
        }

        return name;
    }

    /** Returns the key attribute that can be used to describe this node, or null */
    private String getDescAttribute() {
        if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) {
            // Application and Manifest nodes have a special treatment: they are unique nodes
            // so we don't bother trying to differentiate their strings and we fall back to
            // just using the UI name below.
            Element elem = (Element) mXmlNode;

            String attr = _Element_getAttributeNS(elem,
                                SdkConstants.NS_RESOURCES,
                                AndroidManifestDescriptors.ANDROID_NAME_ATTR);
            if (attr == null || attr.length() == 0) {
                attr = _Element_getAttributeNS(elem,
                                SdkConstants.NS_RESOURCES,
                                AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
            } else if (mXmlNode.getNodeName().equals(LayoutDescriptors.VIEW_FRAGMENT)) {
                attr = attr.substring(attr.lastIndexOf('.') + 1);
            }
            if (attr == null || attr.length() == 0) {
                attr = _Element_getAttributeNS(elem,
                                SdkConstants.NS_RESOURCES,
                                XmlDescriptors.PREF_KEY_ATTR);
            }
            if (attr == null || attr.length() == 0) {
                attr = _Element_getAttributeNS(elem,
                                null, // no namespace
                                ResourcesDescriptors.NAME_ATTR);
            }
            if (attr == null || attr.length() == 0) {
                attr = _Element_getAttributeNS(elem,
                                SdkConstants.NS_RESOURCES,
                                LayoutDescriptors.ID_ATTR);

                if (attr != null && attr.length() > 0) {
                    for (String prefix : ID_PREFIXES) {
                        if (attr.startsWith(prefix)) {
                            attr = attr.substring(prefix.length());
                            break;
                        }
                    }
                }
            }
            if (attr != null && attr.length() > 0) {
                return attr;
            }
        }

        return null;
    }

    /**
     * Computes a styled string describing the UI node suitable for tree views.
     * Similar to {@link #getShortDescription()} but styles the Strings.
     *
     * @return A styled string describing the UI node suitable for tree views.
     */
    public StyledString getStyledDescription() {
        String uiName = mDescriptor.getUiName();

        // Special case: for <view>, show the class attribute value instead.
        // This is done here rather than in the descriptor since this depends on
        // node instance data.
        if (LayoutDescriptors.VIEW_VIEWTAG.equals(uiName) && mXmlNode instanceof Element) {
            Element element = (Element) mXmlNode;
            String cls = element.getAttribute(ATTR_CLASS);
            if (cls != null) {
                uiName = cls.substring(cls.lastIndexOf('.') + 1);
            }
        }

        StyledString styledString = new StyledString();
        String attr = getDescAttribute();
        if (attr != null) {
            // Don't append the two when it's a repeat, e.g. Button01 (Button),
            // only when the ui name is not part of the attribute
            if (attr.toLowerCase().indexOf(uiName.toLowerCase()) == -1) {
                styledString.append(attr);
                styledString.append(String.format(" (%1$s)", uiName),
                        StyledString.DECORATIONS_STYLER);
            } else {
                styledString.append(attr);
            }
        }

        if (styledString.length() == 0) {
            styledString.append(uiName);
        }

        return styledString;
    }

    /**
     * Retrieves an attribute value by local name and namespace URI.
     * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>]
     * , applications must use the value <code>null</code> as the
     * <code>namespaceURI</code> parameter for methods if they wish to have
     * no namespace.
     * <p/>
     * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}.
     * In some versions of webtools, the getAttributeNS implementation crashes with an NPE.
     * This wrapper will return null instead.
     *
     * @see Element#getAttributeNS(String, String)
     * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a>
     * @return The result from {@link Element#getAttributeNS(String, String)} or an empty string.
     */
    private String _Element_getAttributeNS(Element element,
            String namespaceURI,
            String localName) {
        try {
            return element.getAttributeNS(namespaceURI, localName);
        } catch (Exception ignore) {
            return "";
        }
    }

    /**
     * Computes a "breadcrumb trail" description for this node.
     * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter"
     *
     * @param includeRoot Whether to include the root (e.g. "Manifest") or not. Has no effect
     *                     when called on the root node itself.
     * @return The "breadcrumb trail" description for this node.
     */
    public String getBreadcrumbTrailDescription(boolean includeRoot) {
        StringBuilder sb = new StringBuilder(getShortDescription());

        for (UiElementNode uiNode = getUiParent();
                uiNode != null;
                uiNode = uiNode.getUiParent()) {
            if (!includeRoot && uiNode.getUiParent() == null) {
                break;
            }
            sb.insert(0, String.format("%1$s > ", uiNode.getShortDescription())); //$NON-NLS-1$
        }

        return sb.toString();
    }

    /**
     * Sets the XML {@link Document}.
     * <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) {
            mXmlDocument = xmlDoc;
        } else {
            mUiParent.setXmlDocument(xmlDoc);
        }
    }

    /**
     * Returns the XML {@link Document}.
     * <p/>
     * The value is initially null until the UI node is attached to its UI parent -- the value
     * of the document is then propagated.
     *
     * @return the XML {@link Document} or the parent's XML {@link Document} or null.
     */
    public Document getXmlDocument() {
        if (mXmlDocument != null) {
            return mXmlDocument;
        } else if (mUiParent != null) {
            return mUiParent.getXmlDocument();
        }
        return null;
    }

    /**
     * Returns the XML node associated with this UI node.
     * <p/>
     * Some {@link ElementDescriptor} are declared as being "mandatory". This means the
     * corresponding UI node will exist even if there is no corresponding XML node. Such structure
     * is created and enforced by the parent of the tree, not the element themselves. However
     * such nodes will likely not have an XML node associated, so getXmlNode() can return null.
     *
     * @return The associated XML node. Can be null for mandatory nodes.
     */
    public Node getXmlNode() {
        return mXmlNode;
    }

    /**
     * Returns the {@link ElementDescriptor} for this node. This is never null.
     * <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;
    }

    /**
     * Returns the {@link AttributeDescriptor} array for the descriptor of this node.
     * <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();
    }

    /**
     * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node.
     * This is a subset of the getAttributeDescriptors() list.
     * <p/>
     * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes
     * could override this to manipulate the attribute descriptor list depending on the current
     * UI node. There's no need for it right now so keep it private.
     */
    private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() {
        if (mCachedHiddenAttributes == null) {
            mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>();
            for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
                if (attrDesc instanceof XmlnsAttributeDescriptor) {
                    mCachedHiddenAttributes.put(
                            ((XmlnsAttributeDescriptor) attrDesc).getXmlNsName(),
                            attrDesc);
                }
            }
        }
        return mCachedHiddenAttributes;
    }

    /**
     * Sets the parent of this UiElementNode.
     * <p/>
     * The root node has no parent.
     */
    protected void setUiParent(UiElementNode parent) {
        mUiParent = parent;
        // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent.
        clearAttributes();
    }

    /**
     * @return The parent {@link UiElementNode} or null if this is the root node.
     */
    public UiElementNode getUiParent() {
        return mUiParent;
    }

    /**
     * Returns the root {@link UiElementNode}.
     *
     * @return The root {@link UiElementNode}.
     */
    public UiElementNode getUiRoot() {
        UiElementNode root = this;
        while (root.mUiParent != null) {
            root = root.mUiParent;
        }

        return root;
    }

    /**
     * Returns the index of this sibling (where the first child has index 0, the second child
     * has index 1, and so on.)
     *
     * @return The sibling index of this node
     */
    public int getUiSiblingIndex() {
        if (mUiParent != null) {
            int index = 0;
            for (UiElementNode node : mUiParent.getUiChildren()) {
                if (node == this) {
                    break;
                }
                index++;
            }
            return index;
        }

        return 0;
    }

    /**
     * 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) {
            List<UiElementNode> childlist = mUiParent.getUiChildren();
            if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) {
                int index = childlist.indexOf(this);
                return index > 0 ? childlist.get(index - 1) : null;
            }
        }
        return null;
    }

    /**
     * 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) {
            List<UiElementNode> childlist = mUiParent.getUiChildren();
            if (childlist != null) {
                int size = childlist.size();
                if (size > 1 && childlist.get(size - 1) != this) {
                    int index = childlist.indexOf(this);
                    return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null;
                }
            }
        }
        return null;
    }

    /**
     * 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) {
            mEditor = editor;
        } else {
            mUiParent.setEditor(editor);
        }
    }

    /**
     * Returns the {@link AndroidXmlEditor} that embeds this {@link UiElementNode}.
     * <p/>
     * The value is initially null until the node is attached to its parent -- the value
     * of the root node is then propagated.
     *
     * @return The embedding {@link AndroidXmlEditor} or null.
     */
    public AndroidXmlEditor getEditor() {
        return mUiParent == null ? mEditor : mUiParent.getEditor();
    }

    /**
     * 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();
    }

    /**
     * @return A read-only version of the children collection.
     */
    public List<UiElementNode> getUiChildren() {
        if (mReadOnlyUiChildren == null) {
            mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren);
        }
        return mReadOnlyUiChildren;
    }

    /**
     * Returns a collection containing all the known attributes as well as
     * all the unknown ui attributes.
     *
     * @return A read-only version of the attributes collection.
     */
    public Collection<UiAttributeNode> getAllUiAttributes() {
        if (mCachedAllUiAttributes == null) {

            List<UiAttributeNode> allValues =
                new ArrayList<UiAttributeNode>(getInternalUiAttributes().values());
            allValues.addAll(mUnknownUiAttributes);

            mCachedAllUiAttributes = Collections.unmodifiableCollection(allValues);
        }
        return mCachedAllUiAttributes;
    }

    /**
     * Returns all the unknown ui attributes, that is those we found defined in the
     * actual XML but that we don't have descriptors for.
     *
     * @return A read-only version of the unknown attributes collection.
     */
    public Collection<UiAttributeNode> getUnknownUiAttributes() {
        return Collections.unmodifiableCollection(mUnknownUiAttributes);
    }

    /**
     * Sets the error flag value.
     *
     * @param errorFlag the error flag
     */
    public final void setHasError(boolean errorFlag) {
        mHasError = errorFlag;
    }

    /**
     * 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) {
            return true;
        }

        // get the error value from the attributes.
        for (UiAttributeNode attribute : getAllUiAttributes()) {
            if (attribute.hasError()) {
                return true;
            }
        }

        // and now from the children.
        for (UiElementNode child : mUiChildren) {
            if (child.hasError()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the provider that knows how to create {@link ElementDescriptor} from unmapped
     * XML names.
     * <p/>
     * The default is to have one that creates new {@link ElementDescriptor}.
     * <p/>
     * There is only one such provider in any UI model tree, attached to the root node.
     *
     * @return An instance of {@link IUnknownDescriptorProvider}. Can never be null.
     */
    public IUnknownDescriptorProvider getUnknownDescriptorProvider() {
        if (mUiParent != null) {
            return mUiParent.getUnknownDescriptorProvider();
        }
        if (mUnknownDescProvider == null) {
            // Create the default one on demand.
            mUnknownDescProvider = new IUnknownDescriptorProvider() {

                private final HashMap<String, ElementDescriptor> mMap =
                    new HashMap<String, ElementDescriptor>();

                /**
                 * The default is to create a new ElementDescriptor wrapping
                 * the unknown XML local name and reuse previously created descriptors.
                 */
                public ElementDescriptor getDescriptor(String xmlLocalName) {

                    ElementDescriptor desc = mMap.get(xmlLocalName);

                    if (desc == null) {
                        desc = new ElementDescriptor(xmlLocalName);
                        mMap.put(xmlLocalName, desc);
                    }

                    return desc;
                }
            };
        }
        return mUnknownDescProvider;
    }

    /**
     * Sets the provider that knows how to create {@link ElementDescriptor} from unmapped
     * XML names.
     * <p/>
     * The default is to have one that creates new {@link ElementDescriptor}.
     * <p/>
     * There is only one such provider in any UI model tree, attached to the root node.
     *
     * @param unknownDescProvider The new provider to use. Must not be null.
     */
    public void setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider) {
        if (mUiParent == null) {
            mUnknownDescProvider = unknownDescProvider;
        } else {
            mUiParent.setUnknownDescriptorProvider(unknownDescProvider);
        }
    }

    /**
     * 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) {
           mUiUpdateListeners = new ArrayList<IUiUpdateListener>();
       }
       if (!mUiUpdateListeners.contains(listener)) {
           mUiUpdateListeners.add(listener);
       }
    }

    /**
     * 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) {
           mUiUpdateListeners.remove(listener);
       }
    }

    /**
     * Finds a child node relative to this node using a path-like expression.
     * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and
     * returns the latter. If there are multiple nodes with the same name at the same
     * level, always uses the first one found.
     *
     * @param path The path like expression to select a child node.
     * @return The ui node found or null.
     */
    public UiElementNode findUiChildNode(String path) {
        String[] items = path.split("/");  //$NON-NLS-1$
        UiElementNode uiNode = this;
        for (String item : items) {
            boolean nextSegment = false;
            for (UiElementNode c : uiNode.mUiChildren) {
                if (c.getDescriptor().getXmlName().equals(item)) {
                    uiNode = c;
                    nextSegment = true;
                    break;
                }
            }
            if (!nextSegment) {
                return null;
            }
        }
        return uiNode;
    }

    /**
     * Finds an {@link UiElementNode} which contains the give XML {@link Node}.
     * Looks recursively in all children UI nodes.
     *
     * @param xmlNode The XML node to look for.
     * @return The {@link UiElementNode} that contains xmlNode or null if not found,
     */
    public UiElementNode findXmlNode(Node xmlNode) {
        if (xmlNode == null) {
            return null;
        }
        if (getXmlNode() == xmlNode) {
            return this;
        }

        for (UiElementNode uiChild : mUiChildren) {
            UiElementNode found = uiChild.findXmlNode(xmlNode);
            if (found != null) {
                return found;
            }
        }

        return null;
    }

    /**
     * Returns the {@link UiAttributeNode} matching this attribute descriptor or
     * null if not found.
     *
     * @param attrDesc The {@link AttributeDescriptor} to match.
     * @return the {@link UiAttributeNode} matching this attribute descriptor or null
     *         if not found.
     */
    public UiAttributeNode findUiAttribute(AttributeDescriptor attrDesc) {
        return getInternalUiAttributes().get(attrDesc);
    }

    /**
     * Populate this element node with all values from the given XML node.
     *
     * This fails if the given XML node has a different element name -- it won't change the
     * type of this ui node.
     *
     * This method can be both used for populating values the first time and updating values
     * after the XML model changed.
     *
     * @param xmlNode The XML node to mirror
     * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
     */
    public boolean loadFromXmlNode(Node xmlNode) {
        boolean structureChanged = (mXmlNode != xmlNode);
        mXmlNode = xmlNode;
        if (xmlNode != null) {
            updateAttributeList(xmlNode);
            structureChanged |= updateElementList(xmlNode);
            invokeUiUpdateListeners(structureChanged ? UiUpdateState.CHILDREN_CHANGED
                                                      : UiUpdateState.ATTR_UPDATED);
        }
        return structureChanged;
    }

    /**
     * Clears the UI node and reload it from the given XML node.
     * <p/>
     * This works by clearing all references to any previous XML or UI nodes and
     * then reloads the XML document from scratch. The editor reference is kept.
     * <p/>
     * This is used in the special case where the ElementDescriptor structure has changed.
     * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother
     * and reload everything. This is not subtle and should be used very rarely.
     *
     * @param xmlNode The XML node or document to reload. Can be null.
     */
    public void reloadFromXmlNode(Node xmlNode) {
        // The editor needs to be preserved, it is not affected by an XML change.
        AndroidXmlEditor editor = getEditor();
        clearContent();
        setEditor(editor);
        if (xmlNode != null) {
            setXmlDocument(xmlNode.getOwnerDocument());
        }
        // This will reload all the XML and recreate the UI structure from scratch.
        loadFromXmlNode(xmlNode);
    }

    /**
     * Called by attributes when they want to commit their value
     * to an XML node.
     * <p/>
     * For mandatory nodes, this makes sure the underlying XML element node
     * exists in the model. If not, it is created and assigned as the underlying
     * XML node.
     * </br>
     * For non-mandatory nodes, simply return the underlying XML node, which
     * must always exists.
     *
     * @return The XML node matching this {@link UiElementNode} or null.
     */
    public Node prepareCommit() {
        if (getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
            createXmlNode();
            // The new XML node has been created.
            // We don't need to refresh using loadFromXmlNode() since there are
            // no attributes or elements that need to be loading into this node.
        }
        return getXmlNode();
    }

    /**
     * Commits the attributes (all internal, inherited from UI parent & unknown attributes).
     * This is called by the UI when the embedding part needs to be committed.
     */
    public void commit() {
        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
            uiAttr.commit();
        }
    }

    /**
     * Returns true if the part has been modified with respect to the data
     * loaded from the model.
     * @return True if the part has been modified with respect to the data
     * loaded from the model.
     */
    public boolean isDirty() {
        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
            if (uiAttr.isDirty()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Creates the underlying XML element node for this UI node if it doesn't already
     * exists.
     *
     * @return The new value of getXmlNode() (can be null if creation failed)
     */
    public Node createXmlNode() {
        if (mXmlNode != null) {
            return null;
        }
        Node parentXmlNode = null;
        if (mUiParent != null) {
            parentXmlNode = mUiParent.prepareCommit();
            if (parentXmlNode == null) {
                // The parent failed to create its own backing XML node. Abort.
                // No need to throw an exception, the parent will most likely
                // have done so itself.
                return null;
            }
        }

        String elementName = getDescriptor().getXmlName();
        Document doc = getXmlDocument();

        // We *must* have a root node. If not, we need to abort.
        if (doc == null) {
            throw new RuntimeException(
                    String.format("Missing XML document for %1$s XML node.", elementName));
        }

        // If we get here and parentXmlNode is null, the node is to be created
        // as the root node of the document (which can't be null, cf. check above).
        if (parentXmlNode == null) {
            parentXmlNode = doc;
        }

        mXmlNode = doc.createElement(elementName);

        // If this element does not have children, mark it as an empty tag
        // such that the XML looks like <tag/> instead of <tag></tag>
        if (!mDescriptor.hasChildren()) {
            if (mXmlNode instanceof ElementImpl) {
                ElementImpl element = (ElementImpl) mXmlNode;
                element.setEmptyTag(true);
            }
        }

        Node xmlNextSibling = null;

        UiElementNode uiNextSibling = getUiNextSibling();
        if (uiNextSibling != null) {
            xmlNextSibling = uiNextSibling.getXmlNode();
        }

        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. 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
        // internal values, if any.
        List<UiAttributeNode> addAttributes = new ArrayList<UiAttributeNode>();

        for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
            if (attrDesc instanceof XmlnsAttributeDescriptor) {
                XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attrDesc;
                Attr attr = doc.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI,
                        desc.getXmlNsName());
                attr.setValue(desc.getValue());
                attr.setPrefix(desc.getXmlNsPrefix());
                mXmlNode.getAttributes().setNamedItemNS(attr);
            } else {
                UiAttributeNode uiAttr = getInternalUiAttributes().get(attrDesc);

                // Don't apply the attribute immediately, instead record this attribute
                // such that we can gather all attributes and sort them first.
                // This is necessary because the XML model will *append* all attributes
                // so we want to add them in a particular order.
                // (Note that we only have to worry about UiAttributeNodes with non null
                // values, since this is a new node and we therefore don't need to attempt
                // to remove existing attributes)
                String value = uiAttr.getCurrentValue();
                if (value != null && value.length() > 0) {
                    addAttributes.add(uiAttr);
                }
            }
        }

        // Sort and apply the attributes in order, because the Eclipse XML model will always
        // append the XML attributes, so by inserting them in our desired order they will
        // appear that way in the XML
        Collections.sort(addAttributes);

        for (UiAttributeNode node : addAttributes) {
            commitAttributeToXml(node, node.getCurrentValue());
            node.setDirty(false);
        }

        if (mUiParent != null) {
            mUiParent.formatOnInsert(this);
        }

        invokeUiUpdateListeners(UiUpdateState.CREATED);
        return mXmlNode;
    }

    /**
     * Removes the XML node corresponding to this UI node if it exists
     * and also removes all mirrored information in this UI node (i.e. children, attributes)
     *
     * @return The removed node or null if it didn't exist in the first place.
     */
    public Node deleteXmlNode() {
        if (mXmlNode == null) {
            return null;
        }

        // First clear the internals of the node and *then* actually deletes the XML
        // node (because doing so will generate an update even and this node may be
        // revisited via loadFromXmlNode).
        Node oldXmlNode = mXmlNode;
        clearContent();

        Node xmlParent = oldXmlNode.getParentNode();
        if (xmlParent == null) {
            xmlParent = getXmlDocument();
        }
        Node previousSibling = oldXmlNode.getPreviousSibling();
        oldXmlNode = xmlParent.removeChild(oldXmlNode);

        // 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);
        return oldXmlNode;
    }

    /**
     * Updates the element list for this UiElementNode.
     * At the end, the list of children UiElementNode here will match the one from the
     * provided XML {@link Node}:
     * <ul>
     * <li> Walk both the current ui children list and the xml children list at the same time.
     * <li> If we have a new xml child but already reached the end of the ui child list, add the
     *      new xml node.
     * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so,
     *      move it here. It means the XML child list has been reordered.
     * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list.
     * <li> At the end, we may have finished walking the xml child list but still have remaining
     *      ui children, simply delete them as they matching trailing xml nodes that have been
     *      removed unless they are mandatory ui nodes.
     * </ul>
     * Note that only the first case is used when populating the ui list the first time.
     *
     * @param xmlNode The XML node to mirror
     * @return True when the XML structure has changed.
     */
    protected boolean updateElementList(Node xmlNode) {
        boolean structureChanged = false;
        boolean hasMandatoryLast = false;
        int uiIndex = 0;
        Node xmlChild = xmlNode.getFirstChild();
        while (xmlChild != null) {
            if (xmlChild.getNodeType() == Node.ELEMENT_NODE) {
                String elementName = xmlChild.getNodeName();
                UiElementNode uiNode = null;
                if (mUiChildren.size() <= uiIndex) {
                    // A new node is being added at the end of the list
                    ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
                            false /* recursive */);
                    if (desc == null) {
                        // Unknown node. Create a temporary descriptor for it.
                        // We'll add unknown attributes to it later.
                        IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
                        desc = p.getDescriptor(elementName);
                    }
                    structureChanged = true;
                    uiNode = appendNewUiChild(desc);
                    uiIndex++;
                } else {
                    // A new node is being inserted or moved.
                    // Note: mandatory nodes can be created without an XML node in which case
                    // getXmlNode() is null.
                    UiElementNode uiChild;
                    int n = mUiChildren.size();
                    for (int j = uiIndex; j < n; j++) {
                        uiChild = mUiChildren.get(j);
                        if (uiChild.getXmlNode() != null && uiChild.getXmlNode() == xmlChild) {
                            if (j > uiIndex) {
                                // Found the same XML node at some later index, now move it here.
                                mUiChildren.remove(j);
                                mUiChildren.add(uiIndex, uiChild);
                                structureChanged = true;
                            }
                            uiNode = uiChild;
                            uiIndex++;
                            break;
                        }
                    }

                    if (uiNode == null) {
                        // Look for an unused mandatory node with no XML node attached
                        // referencing the same XML element name
                        for (int j = uiIndex; j < n; j++) {
                            uiChild = mUiChildren.get(j);
                            if (uiChild.getXmlNode() == null &&
                                    uiChild.getDescriptor().getMandatory() !=
                                                                Mandatory.NOT_MANDATORY &&
                                    uiChild.getDescriptor().getXmlName().equals(elementName)) {

                                if (j > uiIndex) {
                                    // Found it, now move it here
                                    mUiChildren.remove(j);
                                    mUiChildren.add(uiIndex, uiChild);
                                }
                                // Assign the XML node to this empty mandatory element.
                                uiChild.mXmlNode = xmlChild;
                                structureChanged = true;
                                uiNode = uiChild;
                                uiIndex++;
                            }
                        }
                    }

                    if (uiNode == null) {
                        // Inserting new node
                        ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
                                false /* recursive */);
                        if (desc == null) {
                            // Unknown element. Simply ignore it.
                            AdtPlugin.log(IStatus.WARNING,
                                    "AndroidManifest: Ignoring unknown '%s' XML element", //$NON-NLS-1$
                                    elementName);
                        } else {
                            structureChanged = true;
                            uiNode = insertNewUiChild(uiIndex, desc);
                            uiIndex++;
                        }
                    }
                }
                if (uiNode != null) {
                    // If we touched an UI Node, even an existing one, refresh its content.
                    // For new nodes, this will populate them recursively.
                    structureChanged |= uiNode.loadFromXmlNode(xmlChild);

                    // Remember if there are any mandatory-last nodes to reorder.
                    hasMandatoryLast |=
                        uiNode.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST;
                }
            }
            xmlChild = xmlChild.getNextSibling();
        }

        // There might be extra UI nodes at the end if the XML node list got shorter.
        for (int index = mUiChildren.size() - 1; index >= uiIndex; --index) {
             structureChanged |= removeUiChildAtIndex(index);
        }

        if (hasMandatoryLast) {
            // At least one mandatory-last uiNode was moved. Let's see if we can
            // move them back to the last position. That's possible if the only
            // thing between these and the end are other mandatory empty uiNodes
            // (mandatory uiNodes with no XML attached are pure "virtual" reserved
            // slots and it's ok to reorganize them but other can't.)
            int n = mUiChildren.size() - 1;
            for (int index = n; index >= 0; index--) {
                UiElementNode uiChild = mUiChildren.get(index);
                Mandatory mand = uiChild.getDescriptor().getMandatory();
                if (mand == Mandatory.MANDATORY_LAST && index < n) {
                    // Remove it from index and move it back at the end of the list.
                    mUiChildren.remove(index);
                    mUiChildren.add(uiChild);
                } else if (mand == Mandatory.NOT_MANDATORY || uiChild.getXmlNode() != null) {
                    // We found at least one non-mandatory or a mandatory node with an actual
                    // XML attached, so there's nothing we can reorganize past this point.
                    break;
                }
            }
        }

        return structureChanged;
    }

    /**
     * Internal helper to remove an UI child node given by its index in the
     * internal child list.
     *
     * Also invokes the update listener on the node to be deleted *after* the node has
     * been removed.
     *
     * @param uiIndex The index of the UI child to remove, range 0 .. mUiChildren.size()-1
     * @return True if the structure has changed
     * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you
     *         know that could never happen unless the computer is on fire or something.
     */
    private boolean removeUiChildAtIndex(int uiIndex) {
        UiElementNode uiNode = mUiChildren.get(uiIndex);
        ElementDescriptor desc = uiNode.getDescriptor();

        try {
            if (uiNode.getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
                // This is a mandatory node. Such a node must exist in the UiNode hierarchy
                // even if there's no XML counterpart. However we only need to keep one.

                // Check if the parent (e.g. this node) has another similar ui child node.
                boolean keepNode = true;
                for (UiElementNode child : mUiChildren) {
                    if (child != uiNode && child.getDescriptor() == desc) {
                        // We found another child with the same descriptor that is not
                        // the node we want to remove. This means we have one mandatory
                        // node so we can safely remove uiNode.
                        keepNode = false;
                        break;
                    }
                }

                if (keepNode) {
                    // We can't remove a mandatory node as we need to keep at least one
                    // mandatory node in the parent. Instead we just clear its content
                    // (including its XML Node reference).

                    // A mandatory node with no XML means it doesn't really exist, so it can't be
                    // deleted. So the structure will change only if the ui node is actually
                    // associated to an XML node.
                    boolean xmlExists = (uiNode.getXmlNode() != null);

                    uiNode.clearContent();
                    return xmlExists;
                }
            }

            mUiChildren.remove(uiIndex);

            return true;
        } finally {
            // Tell listeners that a node has been removed.
            // The model has already been modified.
            invokeUiUpdateListeners(UiUpdateState.DELETED);
        }
    }

    /**
     * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
     * and appends it to the end of the element children list.
     *
     * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
     * @return The new UI node that has been appended
     */
    public UiElementNode appendNewUiChild(ElementDescriptor descriptor) {
        UiElementNode uiNode;
        uiNode = descriptor.createUiNode();
        mUiChildren.add(uiNode);
        uiNode.setUiParent(this);
        uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
        return uiNode;
    }

    /**
     * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
     * and inserts it in the element children list at the specified position.
     *
     * @param index The position where to insert in the element children list.
     *              Shifts the element currently at that position (if any) and any
     *              subsequent elements to the right (adds one to their indices).
     *              Index must >= 0 and <= getUiChildren.size().
     *              Using size() means to append to the end of the list.
     * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
     * @return The new UI node.
     */
    public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) {
        UiElementNode uiNode;
        uiNode = descriptor.createUiNode();
        mUiChildren.add(index, uiNode);
        uiNode.setUiParent(this);
        uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
        return uiNode;
    }

    /**
     * Updates the {@link UiAttributeNode} list for this {@link UiElementNode}.
     * <p/>
     * For a given {@link UiElementNode}, the attribute list always exists in
     * full and is totally independent of whether the XML model actually
     * has the corresponding attributes.
     * <p/>
     * For each attribute declared in this {@link UiElementNode}, get
     * the corresponding XML attribute. It may not exist, in which case the
     * value will be null. We don't really know if a value has changed, so
     * the updateValue() is called on the UI attribute in all cases.
     *
     * @param xmlNode The XML node to mirror
     */
    protected void updateAttributeList(Node xmlNode) {
        NamedNodeMap xmlAttrMap = xmlNode.getAttributes();
        HashSet<Node> visited = new HashSet<Node>();

        // For all known (i.e. expected) UI attributes, find an existing XML attribute of
        // same (uri, local name) and update the internal Ui attribute value.
        for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) {
            AttributeDescriptor desc = uiAttr.getDescriptor();
            if (!(desc instanceof SeparatorAttributeDescriptor)) {
                Node xmlAttr = xmlAttrMap == null ? null :
                    xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName());
                uiAttr.updateValue(xmlAttr);
                visited.add(xmlAttr);
            }
        }

        // Clone the current list of unknown attributes. We'll then remove from this list when
        // we find attributes which are still unknown. What will be left are the old unknown
        // attributes that have been deleted in the current XML attribute list.
        @SuppressWarnings("unchecked")
        HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone();

        // We need to ignore hidden attributes.
        Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors();

        // Traverse the actual XML attribute list to find unknown attributes
        if (xmlAttrMap != null) {
            for (int i = 0; i < xmlAttrMap.getLength(); i++) {
                Node xmlAttr = xmlAttrMap.item(i);
                // Ignore attributes which have actual descriptors
                if (visited.contains(xmlAttr)) {
                    continue;
                }

                String xmlFullName = xmlAttr.getNodeName();

                // Ignore attributes which are hidden (based on the prefix:localName key)
                if (hiddenAttrDesc.containsKey(xmlFullName)) {
                    continue;
                }

                String xmlAttrLocalName = xmlAttr.getLocalName();
                String xmlNsUri = xmlAttr.getNamespaceURI();

                UiAttributeNode uiAttr = null;
                for (UiAttributeNode a : mUnknownUiAttributes) {
                    String aLocalName = a.getDescriptor().getXmlLocalName();
                    String aNsUri = a.getDescriptor().getNamespaceUri();
                    if (aLocalName.equals(xmlAttrLocalName) &&
                            (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) {
                        // This attribute is still present in the unknown list
                        uiAttr = a;
                        // It has not been deleted
                        deleted.remove(a);
                        break;
                    }
                }
                if (uiAttr == null) {
                    uiAttr = addUnknownAttribute(xmlFullName, xmlAttrLocalName, xmlNsUri);
                }

                uiAttr.updateValue(xmlAttr);
            }

            // Remove from the internal list unknown attributes that have been deleted from the xml
            for (UiAttributeNode a : deleted) {
                mUnknownUiAttributes.remove(a);
                mCachedAllUiAttributes = null;
            }
        }
    }

    private UiAttributeNode addUnknownAttribute(String xmlFullName,
            String xmlAttrLocalName, String xmlNsUri) {
        // Create a new unknown attribute of format string
        TextAttributeDescriptor desc = new TextAttributeDescriptor(
                xmlAttrLocalName,           // xml name
                xmlFullName,                // ui name
                xmlNsUri,                   // NS uri
                "Unknown XML attribute",    // tooltip, translatable
                new AttributeInfo(xmlAttrLocalName, new Format[] { Format.STRING } )
                );
        UiAttributeNode uiAttr = desc.createUiNode(this);
        uiAttr.setDirty(true);
        mUnknownUiAttributes.add(uiAttr);
        mCachedAllUiAttributes = null;
        return uiAttr;
    }

    /**
     * Invoke all registered {@link IUiUpdateListener} listening on this UI update for this node.
     */
    protected void invokeUiUpdateListeners(UiUpdateState state) {
        if (mUiUpdateListeners != null) {
            for (IUiUpdateListener listener : mUiUpdateListeners) {
                try {
                    listener.uiElementNodeUpdated(this, state);
                } catch (Exception e) {
                    // prevent a crashing listener from crashing the whole invocation chain
                    AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s",  //$NON-NLS-1$
                            getBreadcrumbTrailDescription(true),
                            state.toString());
                }
            }
        }
    }

    // --- for derived implementations only ---

    @VisibleForTesting
    public void setXmlNode(Node xmlNode) {
        mXmlNode = xmlNode;
    }

    public void refreshUi() {
        invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED);
    }


    // ------------- Helpers

    /**
     * Helper method to commit a single attribute value to XML.
     * <p/>
     * This method updates the XML regardless of the current XML value.
     * Callers should check first if an update is needed.
     * If the new value is empty, the XML attribute will be actually removed.
     * <p/>
     * Note that the caller MUST ensure that modifying the underlying XML model is
     * safe and must take care of marking the model as dirty if necessary.
     *
     * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
     *
     * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode.
     * @param newValue The new value to set.
     * @return True if the XML attribute was modified or removed, false if nothing changed.
     */
    public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) {
        // Get (or create) the underlying XML element node that contains the attributes.
        Node element = prepareCommit();
        if (element != null && uiAttr != null) {
            String attrLocalName = uiAttr.getDescriptor().getXmlLocalName();
            String attrNsUri = uiAttr.getDescriptor().getNamespaceUri();

            NamedNodeMap attrMap = element.getAttributes();
            if (newValue == null || newValue.length() == 0) {
                // Remove attribute if it's empty
                if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) {
                    attrMap.removeNamedItemNS(attrNsUri, attrLocalName);
                    return true;
                }
            } else {
                // Add or replace an attribute
                Document doc = element.getOwnerDocument();
                if (doc != null) {
                    Attr attr;
                    if (attrNsUri != null && attrNsUri.length() > 0) {
                        attr = (Attr) attrMap.getNamedItemNS(attrNsUri, attrLocalName);
                        if (attr == null) {
                            attr = doc.createAttributeNS(attrNsUri, attrLocalName);
                            attr.setPrefix(lookupNamespacePrefix(element, attrNsUri));
                            attrMap.setNamedItemNS(attr);
                        }
                    } else {
                        attr = (Attr) attrMap.getNamedItem(attrLocalName);
                        if (attr == null) {
                            attr = doc.createAttribute(attrLocalName);
                            attrMap.setNamedItem(attr);
                        }
                    }
                    attr.setValue(newValue);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Helper method to commit all dirty attributes values to XML.
     * <p/>
     * This method is useful if {@link #setAttributeValue(String, String, String, boolean)} has
     * been called more than once and all the attributes marked as dirty must be committed to
     * the XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty
     * attribute.
     * <p/>
     * Note that the caller MUST ensure that modifying the underlying XML model is
     * safe and must take care of marking the model as dirty if necessary.
     *
     * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
     *
     * @return True if one or more values were actually modified or removed,
     *         false if nothing changed.
     */
    @SuppressWarnings("null") // Eclipse is confused by the logic and gets it wrong
    public boolean commitDirtyAttributesToXml() {
        boolean result = false;
        List<UiAttributeNode> dirtyAttributes = new ArrayList<UiAttributeNode>();
        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
            if (uiAttr.isDirty()) {
                String value = uiAttr.getCurrentValue();
                if (value != null && value.length() > 0) {
                    // Defer the new attributes: set these last and in order
                    dirtyAttributes.add(uiAttr);
                } else {
                    result |= commitAttributeToXml(uiAttr, value);
                    uiAttr.setDirty(false);
                }
            }
        }
        if (dirtyAttributes.size() > 0) {
            result = true;

            Collections.sort(dirtyAttributes);

            // The Eclipse XML model will *always* append new attributes.
            // Therefore, if any of the dirty attributes are new, they will appear
            // after any existing, clean attributes on the element. To fix this,
            // we need to first remove any of these attributes, then insert them
            // back in the right order.
            Node element = prepareCommit();
            if (element == null) {
                return result;
            }

            String firstName = dirtyAttributes.get(0).getDescriptor().getXmlLocalName();
            NamedNodeMap attributes = ((Element) element).getAttributes();
            List<Attr> move = new ArrayList<Attr>();
            for (int i = 0, n = attributes.getLength(); i < n; i++) {
                Attr attribute = (Attr) attributes.item(i);
                if (UiAttributeNode.compareAttributes(attribute.getLocalName(), firstName) > 0) {
                    move.add(attribute);
                }
            }

            for (Attr attribute : move) {
                if (attribute.getNamespaceURI() != null) {
                    attributes.removeNamedItemNS(attribute.getNamespaceURI(),
                            attribute.getLocalName());
                } else {
                    attributes.removeNamedItem(attribute.getName());
                }
            }

            // Merge back the removed DOM attribute nodes and the new UI attribute nodes.
            // In cases where the attribute DOM name and the UI attribute names equal,
            // skip the DOM nodes and just apply the UI attributes.
            int domAttributeIndex = 0;
            int domAttributeIndexMax = move.size();
            int uiAttributeIndex = 0;
            int uiAttributeIndexMax = dirtyAttributes.size();

            while (true) {
                Attr domAttribute;
                UiAttributeNode uiAttribute;

                int compare;
                if (uiAttributeIndex < uiAttributeIndexMax) {
                    if (domAttributeIndex < domAttributeIndexMax) {
                        domAttribute = move.get(domAttributeIndex);
                        uiAttribute = dirtyAttributes.get(uiAttributeIndex);

                        String domAttributeName = domAttribute.getLocalName();
                        String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName();
                        compare = UiAttributeNode.compareAttributes(domAttributeName,
                                uiAttributeName);
                    } else {
                        compare = 1;
                        uiAttribute = dirtyAttributes.get(uiAttributeIndex);
                        domAttribute = null;
                    }
                } else if (domAttributeIndex < domAttributeIndexMax) {
                    compare = -1;
                    domAttribute = move.get(domAttributeIndex);
                    uiAttribute = null;
                } else {
                    break;
                }

                if (compare < 0) {
                    if (domAttribute.getNamespaceURI() != null) {
                        attributes.setNamedItemNS(domAttribute);
                    } else {
                        attributes.setNamedItem(domAttribute);
                    }
                    domAttributeIndex++;
                } else {
                    assert compare >= 0;
                    if (compare == 0) {
                        domAttributeIndex++;
                    }
                    commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
                    uiAttribute.setDirty(false);
                    uiAttributeIndex++;
                }
            }
        }

        return result;
    }

    /**
     * Returns the namespace prefix matching the requested namespace URI.
     * If no such declaration is found, returns the default "android" prefix.
     *
     * @param node The current node. Must not be null.
     * @param nsUri The namespace URI of which the prefix is to be found,
     *              e.g. SdkConstants.NS_RESOURCES
     * @return The first prefix declared or the default "android" prefix.
     */
    public static String lookupNamespacePrefix(Node node, String nsUri) {
        // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java
        // The following code emulates this simple call:
        //   String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES);

        // if the requested URI is null, it denotes an attribute with no namespace.
        if (nsUri == null) {
            return null;
        }

        // per XML specification, the "xmlns" URI is reserved
        if (XMLNS_URI.equals(nsUri)) {
            return XMLNS;
        }

        HashSet<String> visited = new HashSet<String>();
        Document doc = node == null ? null : node.getOwnerDocument();

        // Ask the document about it. This method may not be implemented by the Document.
        String nsPrefix = null;
        try {
            nsPrefix = doc != null ? doc.lookupPrefix(nsUri) : null;
            if (nsPrefix != null) {
                return nsPrefix;
            }
        } catch (Throwable t) {
            // ignore
        }

        // If that failed, try to look it up manually.
        // This also gathers prefixed in use in the case we want to generate a new one below.
        for (; node != null && node.getNodeType() == Node.ELEMENT_NODE;
               node = node.getParentNode()) {
            NamedNodeMap attrs = node.getAttributes();
            for (int n = attrs.getLength() - 1; n >= 0; --n) {
                Node attr = attrs.item(n);
                if (XMLNS.equals(attr.getPrefix())) {
                    String uri = attr.getNodeValue();
                    nsPrefix = attr.getLocalName();
                    // Is this the URI we are looking for? If yes, we found its prefix.
                    if (nsUri.equals(uri)) {
                        return nsPrefix;
                    }
                    visited.add(nsPrefix);
                }
            }
        }

        // Failed the find a prefix. Generate a new sensible default prefix.
        //
        // We need to make sure the prefix is not one that was declared in the scope
        // visited above. Use a default namespace prefix "android" for the Android resource
        // NS and use "ns" for all other custom namespaces.
        String prefix = NS_RESOURCES.equals(nsUri) ? ANDROID_NS_NAME : "ns"; //$NON-NLS-1$
        String base = prefix;
        for (int i = 1; visited.contains(prefix); i++) {
            prefix = base + Integer.toString(i);
        }
        // Also create & define this prefix/URI in the XML document as an attribute in the
        // first element of the document.
        if (doc != null) {
            node = doc.getFirstChild();
            while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
                node = node.getNextSibling();
            }
            if (node != null) {
                Attr attr = doc.createAttributeNS(XMLNS_URI, prefix);
                attr.setValue(nsUri);
                attr.setPrefix(XMLNS);
                node.getAttributes().setNamedItemNS(attr);
            }
        }

        return prefix;
    }

    /**
     * Utility method to internally set the value of a text attribute for the current
     * UiElementNode.
     * <p/>
     * This method is a helper. It silently ignores the errors such as the requested
     * attribute not being present in the element or attribute not being settable.
     * It accepts inherited attributes (such as layout).
     * <p/>
     * This does not commit to the XML model. It does mark the attribute node as dirty.
     * This is up to the caller.
     *
     * @see #commitAttributeToXml(UiAttributeNode, String)
     * @see #commitDirtyAttributesToXml()
     *
     * @param attrXmlName The XML <em>local</em> name of the attribute to modify
     * @param attrNsUri The namespace URI of the attribute.
     *                  Can be null if the attribute uses the global namespace.
     * @param value The new value for the attribute. If set to null, the attribute is removed.
     * @param override True if the value must be set even if one already exists.
     * @return The {@link UiAttributeNode} that has been modified or null.
     */
    public UiAttributeNode setAttributeValue(
            String attrXmlName,
            String attrNsUri,
            String value,
            boolean override) {
        if (value == null) {
            value = ""; //$NON-NLS-1$ -- this removes an attribute
        }

        // Try with all internal attributes
        UiAttributeNode uiAttr = setInternalAttrValue(
                getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
        if (uiAttr != null) {
            return uiAttr;
        }

        if (uiAttr == null) {
            // Failed to find the attribute. For non-android attributes that is mostly expected,
            // in which case we just create a new custom one. As a side effect, we'll find the
            // attribute descriptor via getAllUiAttributes().
            addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri);

            // We've created the attribute, but not actually set the value on it, so let's do it.
            // Try with the updated internal attributes.
            uiAttr = setInternalAttrValue(
                    getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
        }

        return uiAttr;
    }

    private UiAttributeNode setInternalAttrValue(
            Collection<UiAttributeNode> attributes,
            String attrXmlName,
            String attrNsUri,
            String value,
            boolean override) {

        // For namespace less attributes (like the "layout" attribute of an <include> tag
        // we may be passed "" as the namespace (during an attribute copy), and it
        // should really be null instead.
        if (attrNsUri != null && attrNsUri.length() == 0) {
            attrNsUri = null;
        }

        for (UiAttributeNode uiAttr : attributes) {
            AttributeDescriptor uiDesc = uiAttr.getDescriptor();

            if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
                // Both NS URI must be either null or equal.
                if ((attrNsUri == null && uiDesc.getNamespaceUri() == null) ||
                        (attrNsUri != null && attrNsUri.equals(uiDesc.getNamespaceUri()))) {

                    // Not all attributes are editable, ignore those which are not.
                    if (uiAttr instanceof IUiSettableAttributeNode) {
                        String current = uiAttr.getCurrentValue();
                        // Only update (and mark as dirty) if the attribute did not have any
                        // value or if the value was different.
                        if (override || current == null || !current.equals(value)) {
                            ((IUiSettableAttributeNode) uiAttr).setCurrentValue(value);
                            // mark the attribute as dirty since their internal content
                            // as been modified, but not the underlying XML model
                            uiAttr.setDirty(true);
                            return uiAttr;
                        }
                    }

                    // We found the attribute but it's not settable. Since attributes are
                    // not duplicated, just abandon here.
                    break;
                }
            }
        }

        return null;
    }

    /**
     * Utility method to retrieve the internal value of an attribute.
     * <p/>
     * Note that this retrieves the *field* value if the attribute has some UI, and
     * not the actual XML value. They may differ if the attribute is dirty.
     *
     * @param attrXmlName The XML name of the attribute to modify
     * @return The current internal value for the attribute or null in case of error.
     */
    public String getAttributeValue(String attrXmlName) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
            AttributeDescriptor uiDesc = entry.getKey();
            if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
                UiAttributeNode uiAttr = entry.getValue();
                return uiAttr.getCurrentValue();
            }
        }
        return null;
    }

    // ------ IPropertySource methods

    public Object getEditableValue() {
        return null;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors()
     *
     * Returns the property descriptor for this node. Since the descriptors are not linked to the
     * data, the AttributeDescriptor are used directly.
     */
    public IPropertyDescriptor[] getPropertyDescriptors() {
        List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>();

        // get the standard descriptors
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
        Set<AttributeDescriptor> keys = attributeMap.keySet();


        // we only want the descriptor that do implement the IPropertyDescriptor interface.
        for (AttributeDescriptor key : keys) {
            if (key instanceof IPropertyDescriptor) {
                propDescs.add((IPropertyDescriptor)key);
            }
        }

        // now get the descriptor from the unknown attributes
        for (UiAttributeNode unknownNode : mUnknownUiAttributes) {
            if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) {
                propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor());
            }
        }

        // TODO cache this maybe, as it's not going to change (except for unknown descriptors)
        return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object)
     *
     * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(),
     * which return the AttributeDescriptor itself.
     */
    public Object getPropertyValue(Object id) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        UiAttributeNode attribute = attributeMap.get(id);

        if (attribute == null) {
            // look for the id in the unknown attributes.
            for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
                if (id == unknownAttr.getDescriptor()) {
                    return unknownAttr;
                }
            }
        }

        return attribute;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object)
     *
     * Returns whether the property is set. In our case this is if the string is non empty.
     */
    public boolean isPropertySet(Object id) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        UiAttributeNode attribute = attributeMap.get(id);

        if (attribute != null) {
            return attribute.getCurrentValue().length() > 0;
        }

        // look for the id in the unknown attributes.
        for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
            if (id == unknownAttr.getDescriptor()) {
                return unknownAttr.getCurrentValue().length() > 0;
            }
        }

        return false;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object)
     *
     * Reset the property to its default value. For now we simply empty it.
     */
    public void resetPropertyValue(Object id) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        UiAttributeNode attribute = attributeMap.get(id);
        if (attribute != null) {
            // TODO: reset the value of the attribute

            return;
        }

        // look for the id in the unknown attributes.
        for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
            if (id == unknownAttr.getDescriptor()) {
                // TODO: reset the value of the attribute

                return;
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object)
     *
     * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the
     * AttributeDescriptor itself. Value should be a String.
     */
    public void setPropertyValue(Object id, Object value) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        UiAttributeNode attribute = attributeMap.get(id);

        if (attribute == null) {
            // look for the id in the unknown attributes.
            for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
                if (id == unknownAttr.getDescriptor()) {
                    attribute = unknownAttr;
                    break;
                }
            }
        }

        if (attribute != null) {

            // get the current value and compare it to the new value
            String oldValue = attribute.getCurrentValue();
            final String newValue = (String)value;

            if (oldValue.equals(newValue)) {
                return;
            }

            final UiAttributeNode fAttribute = attribute;
            AndroidXmlEditor editor = getEditor();
            editor.wrapEditXmlModel(new Runnable() {
                public void run() {
                    commitAttributeToXml(fAttribute, newValue);
                }
            });
        }
    }

    /** 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().getFormatGuiXml()) {
            return;
        }

        AndroidXmlEditor editor = getEditor();
        if (editor != null && mXmlNode != null) {
            editor.reformatNode(mXmlNode);
        }
    }
}
