Home | History | Annotate | Download | only in uimodel
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.uimodel;
     18 
     19 import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
     20 import static com.android.SdkConstants.ANDROID_SUPPORT_PKG_PREFIX;
     21 import static com.android.SdkConstants.ATTR_CLASS;
     22 import static com.android.SdkConstants.ID_PREFIX;
     23 import static com.android.SdkConstants.NEW_ID_PREFIX;
     24 
     25 import com.android.SdkConstants;
     26 import com.android.annotations.VisibleForTesting;
     27 import com.android.ide.common.api.IAttributeInfo.Format;
     28 import com.android.ide.common.resources.platform.AttributeInfo;
     29 import com.android.ide.common.xml.XmlAttributeSortOrder;
     30 import com.android.ide.eclipse.adt.AdtPlugin;
     31 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     32 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     33 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     34 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
     35 import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
     36 import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
     37 import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
     38 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
     39 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
     40 import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
     41 import com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors.OtherXmlDescriptors;
     42 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
     43 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     44 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     45 import com.android.utils.SdkUtils;
     46 import com.android.utils.XmlUtils;
     47 
     48 import org.eclipse.jface.text.TextUtilities;
     49 import org.eclipse.jface.viewers.StyledString;
     50 import org.eclipse.ui.views.properties.IPropertyDescriptor;
     51 import org.eclipse.ui.views.properties.IPropertySource;
     52 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     53 import org.eclipse.wst.xml.core.internal.document.ElementImpl;
     54 import org.w3c.dom.Attr;
     55 import org.w3c.dom.Document;
     56 import org.w3c.dom.Element;
     57 import org.w3c.dom.NamedNodeMap;
     58 import org.w3c.dom.Node;
     59 import org.w3c.dom.Text;
     60 
     61 import java.util.ArrayList;
     62 import java.util.Collection;
     63 import java.util.Collections;
     64 import java.util.HashMap;
     65 import java.util.HashSet;
     66 import java.util.List;
     67 import java.util.Locale;
     68 import java.util.Map;
     69 import java.util.Map.Entry;
     70 import java.util.Set;
     71 
     72 /**
     73  * Represents an XML node that can be modified by the user interface in the XML editor.
     74  * <p/>
     75  * Each tree viewer used in the application page's parts needs to keep a model representing
     76  * each underlying node in the tree. This interface represents the base type for such a node.
     77  * <p/>
     78  * Each node acts as an intermediary model between the actual XML model (the real data support)
     79  * and the tree viewers or the corresponding page parts.
     80  * <p/>
     81  * Element nodes don't contain data per se. Their data is contained in their attributes
     82  * as well as their children's attributes, see {@link UiAttributeNode}.
     83  * <p/>
     84  * The structure of a given {@link UiElementNode} is declared by a corresponding
     85  * {@link ElementDescriptor}.
     86  * <p/>
     87  * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when
     88  * an element is selected. The {@link AttributeDescriptor} are used property descriptors.
     89  */
     90 @SuppressWarnings("restriction") // XML model
     91 public class UiElementNode implements IPropertySource {
     92 
     93     /** List of prefixes removed from android:id strings when creating short descriptions. */
     94     private static String[] ID_PREFIXES = {
     95         "@android:id/", //$NON-NLS-1$
     96         NEW_ID_PREFIX, ID_PREFIX, "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$
     97 
     98     /** The element descriptor for the node. Always present, never null. */
     99     private ElementDescriptor mDescriptor;
    100     /** The parent element node in the UI model. It is null for a root element or until
    101      *  the node is attached to its parent. */
    102     private UiElementNode mUiParent;
    103     /** The {@link AndroidXmlEditor} handling the UI hierarchy. This is defined only for the
    104      *  root node. All children have the value set to null and query their parent. */
    105     private AndroidXmlEditor mEditor;
    106     /** The XML {@link Document} model that is being mirror by the UI model. This is defined
    107      *  only for the root node. All children have the value set to null and query their parent. */
    108     private Document mXmlDocument;
    109     /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which
    110      *  have no corresponding XML node or for new UI nodes before their XML node is set. */
    111     private Node mXmlNode;
    112     /** The list of all UI children nodes. Can be empty but never null. There's one UI children
    113      *  node per existing XML children node. */
    114     private ArrayList<UiElementNode> mUiChildren;
    115     /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}.
    116      *  The list is always defined and never null. Unlike the UiElementNode children list, this
    117      *  is always defined, even for attributes that do not exist in the XML model - that's because
    118      *  "missing" attributes in the XML model simply mean a default value is used. Also note that
    119      *  the underlying collection is a map, so order is not respected. To get the desired attribute
    120      *  order, iterate through the {@link ElementDescriptor}'s attribute list. */
    121     private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes;
    122     private HashSet<UiAttributeNode> mUnknownUiAttributes;
    123     /** A read-only view of the UI children node collection. */
    124     private List<UiElementNode> mReadOnlyUiChildren;
    125     /** A read-only view of the UI attributes collection. */
    126     private Collection<UiAttributeNode> mCachedAllUiAttributes;
    127     /** A map of hidden attribute descriptors. Key is the XML name. */
    128     private Map<String, AttributeDescriptor> mCachedHiddenAttributes;
    129     /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any
    130      *  listeners attached, so the list is only created on demand and can be null. */
    131     private List<IUiUpdateListener> mUiUpdateListeners;
    132     /** A provider that knows how to create {@link ElementDescriptor} from unmapped XML names.
    133      *  The default is to have one that creates new {@link ElementDescriptor}. */
    134     private IUnknownDescriptorProvider mUnknownDescProvider;
    135     /** Error Flag */
    136     private boolean mHasError;
    137 
    138     /**
    139      * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
    140      *
    141      * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
    142      */
    143     public UiElementNode(ElementDescriptor elementDescriptor) {
    144         mDescriptor = elementDescriptor;
    145         clearContent();
    146     }
    147 
    148     @Override
    149     public String toString() {
    150       return String.format("%s [desc: %s, parent: %s, children: %d]",         //$NON-NLS-1$
    151               this.getClass().getSimpleName(),
    152               mDescriptor,
    153               mUiParent != null ? mUiParent.toString() : "none",              //$NON-NLS-1$
    154                       mUiChildren != null ? mUiChildren.size() : 0
    155       );
    156     }
    157 
    158     /**
    159      * Clears the {@link UiElementNode} by resetting the children list and
    160      * the {@link UiAttributeNode}s list.
    161      * Also resets the attached XML node, document, editor if any.
    162      * <p/>
    163      * The parent {@link UiElementNode} node is not reset so that it's position
    164      * in the hierarchy be left intact, if any.
    165      */
    166     /* package */ void clearContent() {
    167         mXmlNode = null;
    168         mXmlDocument = null;
    169         mEditor = null;
    170         clearAttributes();
    171         mReadOnlyUiChildren = null;
    172         if (mUiChildren == null) {
    173             mUiChildren = new ArrayList<UiElementNode>();
    174         } else {
    175             // We can't remove mandatory nodes, we just clear them.
    176             for (int i = mUiChildren.size() - 1; i >= 0; --i) {
    177                 removeUiChildAtIndex(i);
    178             }
    179         }
    180     }
    181 
    182     /**
    183      * Clears the internal list of attributes, the read-only cached version of it
    184      * and the read-only cached hidden attribute list.
    185      */
    186     private void clearAttributes() {
    187         mUiAttributes = null;
    188         mCachedAllUiAttributes = null;
    189         mCachedHiddenAttributes = null;
    190         mUnknownUiAttributes = new HashSet<UiAttributeNode>();
    191     }
    192 
    193     /**
    194      * Gets or creates the internal UiAttributes list.
    195      * <p/>
    196      * When the descriptor derives from ViewElementDescriptor, this list depends on the
    197      * current UiParent node.
    198      *
    199      * @return A new set of {@link UiAttributeNode} that matches the expected
    200      *         attributes for this node.
    201      */
    202     private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() {
    203         if (mUiAttributes == null) {
    204             AttributeDescriptor[] attrList = getAttributeDescriptors();
    205             mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attrList.length);
    206             for (AttributeDescriptor desc : attrList) {
    207                 UiAttributeNode uiNode = desc.createUiNode(this);
    208                 if (uiNode != null) {  // Some AttributeDescriptors do not have UI associated
    209                     mUiAttributes.put(desc, uiNode);
    210                 }
    211             }
    212         }
    213         return mUiAttributes;
    214     }
    215 
    216     /**
    217      * Computes a short string describing the UI node suitable for tree views.
    218      * Uses the element's attribute "android:name" if present, or the "android:label" one
    219      * followed by the element's name if not repeated.
    220      *
    221      * @return A short string describing the UI node suitable for tree views.
    222      */
    223     public String getShortDescription() {
    224         String name = mDescriptor.getUiName();
    225         String attr = getDescAttribute();
    226         if (attr != null) {
    227             // If the ui name is repeated in the attribute value, don't use it.
    228             // Typical case is to avoid ".pkg.MyActivity (Activity)".
    229             if (attr.contains(name)) {
    230                 return attr;
    231             } else {
    232                 return String.format("%1$s (%2$s)", attr, name);
    233             }
    234         }
    235 
    236         return name;
    237     }
    238 
    239     /** Returns the key attribute that can be used to describe this node, or null */
    240     private String getDescAttribute() {
    241         if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) {
    242             // Application and Manifest nodes have a special treatment: they are unique nodes
    243             // so we don't bother trying to differentiate their strings and we fall back to
    244             // just using the UI name below.
    245             Element elem = (Element) mXmlNode;
    246 
    247             String attr = _Element_getAttributeNS(elem,
    248                                 SdkConstants.NS_RESOURCES,
    249                                 AndroidManifestDescriptors.ANDROID_NAME_ATTR);
    250             if (attr == null || attr.length() == 0) {
    251                 attr = _Element_getAttributeNS(elem,
    252                                 SdkConstants.NS_RESOURCES,
    253                                 AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
    254             } else if (mXmlNode.getNodeName().equals(SdkConstants.VIEW_FRAGMENT)) {
    255                 attr = attr.substring(attr.lastIndexOf('.') + 1);
    256             }
    257             if (attr == null || attr.length() == 0) {
    258                 attr = _Element_getAttributeNS(elem,
    259                                 SdkConstants.NS_RESOURCES,
    260                                 OtherXmlDescriptors.PREF_KEY_ATTR);
    261             }
    262             if (attr == null || attr.length() == 0) {
    263                 attr = _Element_getAttributeNS(elem,
    264                                 null, // no namespace
    265                                 SdkConstants.ATTR_NAME);
    266             }
    267             if (attr == null || attr.length() == 0) {
    268                 attr = _Element_getAttributeNS(elem,
    269                                 SdkConstants.NS_RESOURCES,
    270                                 SdkConstants.ATTR_ID);
    271 
    272                 if (attr != null && attr.length() > 0) {
    273                     for (String prefix : ID_PREFIXES) {
    274                         if (attr.startsWith(prefix)) {
    275                             attr = attr.substring(prefix.length());
    276                             break;
    277                         }
    278                     }
    279                 }
    280             }
    281             if (attr != null && attr.length() > 0) {
    282                 return attr;
    283             }
    284         }
    285 
    286         return null;
    287     }
    288 
    289     /**
    290      * Computes a styled string describing the UI node suitable for tree views.
    291      * Similar to {@link #getShortDescription()} but styles the Strings.
    292      *
    293      * @return A styled string describing the UI node suitable for tree views.
    294      */
    295     public StyledString getStyledDescription() {
    296         String uiName = mDescriptor.getUiName();
    297 
    298         // Special case: for <view>, show the class attribute value instead.
    299         // This is done here rather than in the descriptor since this depends on
    300         // node instance data.
    301         if (SdkConstants.VIEW_TAG.equals(uiName) && mXmlNode instanceof Element) {
    302             Element element = (Element) mXmlNode;
    303             String cls = element.getAttribute(ATTR_CLASS);
    304             if (cls != null) {
    305                 uiName = cls.substring(cls.lastIndexOf('.') + 1);
    306             }
    307         }
    308 
    309         StyledString styledString = new StyledString();
    310         String attr = getDescAttribute();
    311         if (attr != null) {
    312             // Don't append the two when it's a repeat, e.g. Button01 (Button),
    313             // only when the ui name is not part of the attribute
    314             if (attr.toLowerCase(Locale.US).indexOf(uiName.toLowerCase(Locale.US)) == -1) {
    315                 styledString.append(attr);
    316                 styledString.append(String.format(" (%1$s)", uiName),
    317                         StyledString.DECORATIONS_STYLER);
    318             } else {
    319                 styledString.append(attr);
    320             }
    321         }
    322 
    323         if (styledString.length() == 0) {
    324             styledString.append(uiName);
    325         }
    326 
    327         return styledString;
    328     }
    329 
    330     /**
    331      * Retrieves an attribute value by local name and namespace URI.
    332      * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>]
    333      * , applications must use the value <code>null</code> as the
    334      * <code>namespaceURI</code> parameter for methods if they wish to have
    335      * no namespace.
    336      * <p/>
    337      * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}.
    338      * In some versions of webtools, the getAttributeNS implementation crashes with an NPE.
    339      * This wrapper will return an empty string instead.
    340      *
    341      * @see Element#getAttributeNS(String, String)
    342      * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a>
    343      * @return The result from {@link Element#getAttributeNS(String, String)} or an empty string.
    344      */
    345     private String _Element_getAttributeNS(Element element,
    346             String namespaceURI,
    347             String localName) {
    348         try {
    349             return element.getAttributeNS(namespaceURI, localName);
    350         } catch (Exception ignore) {
    351             return "";
    352         }
    353     }
    354 
    355     /**
    356      * Computes a "breadcrumb trail" description for this node.
    357      * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter"
    358      *
    359      * @param includeRoot Whether to include the root (e.g. "Manifest") or not. Has no effect
    360      *                     when called on the root node itself.
    361      * @return The "breadcrumb trail" description for this node.
    362      */
    363     public String getBreadcrumbTrailDescription(boolean includeRoot) {
    364         StringBuilder sb = new StringBuilder(getShortDescription());
    365 
    366         for (UiElementNode uiNode = getUiParent();
    367                 uiNode != null;
    368                 uiNode = uiNode.getUiParent()) {
    369             if (!includeRoot && uiNode.getUiParent() == null) {
    370                 break;
    371             }
    372             sb.insert(0, String.format("%1$s > ", uiNode.getShortDescription())); //$NON-NLS-1$
    373         }
    374 
    375         return sb.toString();
    376     }
    377 
    378     /**
    379      * Sets the XML {@link Document}.
    380      * <p/>
    381      * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the
    382      * UI root element node (this method takes care of that.)
    383      * @param xmlDoc The new XML document to associate this node with.
    384      */
    385     public void setXmlDocument(Document xmlDoc) {
    386         if (mUiParent == null) {
    387             mXmlDocument = xmlDoc;
    388         } else {
    389             mUiParent.setXmlDocument(xmlDoc);
    390         }
    391     }
    392 
    393     /**
    394      * Returns the XML {@link Document}.
    395      * <p/>
    396      * The value is initially null until the UI node is attached to its UI parent -- the value
    397      * of the document is then propagated.
    398      *
    399      * @return the XML {@link Document} or the parent's XML {@link Document} or null.
    400      */
    401     public Document getXmlDocument() {
    402         if (mXmlDocument != null) {
    403             return mXmlDocument;
    404         } else if (mUiParent != null) {
    405             return mUiParent.getXmlDocument();
    406         }
    407         return null;
    408     }
    409 
    410     /**
    411      * Returns the XML node associated with this UI node.
    412      * <p/>
    413      * Some {@link ElementDescriptor} are declared as being "mandatory". This means the
    414      * corresponding UI node will exist even if there is no corresponding XML node. Such structure
    415      * is created and enforced by the parent of the tree, not the element themselves. However
    416      * such nodes will likely not have an XML node associated, so getXmlNode() can return null.
    417      *
    418      * @return The associated XML node. Can be null for mandatory nodes.
    419      */
    420     public Node getXmlNode() {
    421         return mXmlNode;
    422     }
    423 
    424     /**
    425      * Returns the {@link ElementDescriptor} for this node. This is never null.
    426      * <p/>
    427      * Do not use this to call getDescriptor().getAttributes(), instead call
    428      * getAttributeDescriptors() which can be overridden by derived classes.
    429      * @return The {@link ElementDescriptor} for this node. This is never null.
    430      */
    431     public ElementDescriptor getDescriptor() {
    432         return mDescriptor;
    433     }
    434 
    435     /**
    436      * Returns the {@link AttributeDescriptor} array for the descriptor of this node.
    437      * <p/>
    438      * Use this instead of getDescriptor().getAttributes() -- derived classes can override
    439      * this to manipulate the attribute descriptor list depending on the current UI node.
    440      * @return The {@link AttributeDescriptor} array for the descriptor of this node.
    441      */
    442     public AttributeDescriptor[] getAttributeDescriptors() {
    443         return mDescriptor.getAttributes();
    444     }
    445 
    446     /**
    447      * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node.
    448      * This is a subset of the getAttributeDescriptors() list.
    449      * <p/>
    450      * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes
    451      * could override this to manipulate the attribute descriptor list depending on the current
    452      * UI node. There's no need for it right now so keep it private.
    453      */
    454     private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() {
    455         if (mCachedHiddenAttributes == null) {
    456             mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>();
    457             for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
    458                 if (attrDesc instanceof XmlnsAttributeDescriptor) {
    459                     mCachedHiddenAttributes.put(
    460                             ((XmlnsAttributeDescriptor) attrDesc).getXmlNsName(),
    461                             attrDesc);
    462                 }
    463             }
    464         }
    465         return mCachedHiddenAttributes;
    466     }
    467 
    468     /**
    469      * Sets the parent of this UiElementNode.
    470      * <p/>
    471      * The root node has no parent.
    472      */
    473     protected void setUiParent(UiElementNode parent) {
    474         mUiParent = parent;
    475         // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent.
    476         clearAttributes();
    477     }
    478 
    479     /**
    480      * @return The parent {@link UiElementNode} or null if this is the root node.
    481      */
    482     public UiElementNode getUiParent() {
    483         return mUiParent;
    484     }
    485 
    486     /**
    487      * Returns the root {@link UiElementNode}.
    488      *
    489      * @return The root {@link UiElementNode}.
    490      */
    491     public UiElementNode getUiRoot() {
    492         UiElementNode root = this;
    493         while (root.mUiParent != null) {
    494             root = root.mUiParent;
    495         }
    496 
    497         return root;
    498     }
    499 
    500     /**
    501      * Returns the index of this sibling (where the first child has index 0, the second child
    502      * has index 1, and so on.)
    503      *
    504      * @return The sibling index of this node
    505      */
    506     public int getUiSiblingIndex() {
    507         if (mUiParent != null) {
    508             int index = 0;
    509             for (UiElementNode node : mUiParent.getUiChildren()) {
    510                 if (node == this) {
    511                     break;
    512                 }
    513                 index++;
    514             }
    515             return index;
    516         }
    517 
    518         return 0;
    519     }
    520 
    521     /**
    522      * Returns the previous UI sibling of this UI node. If the node does not have a previous
    523      * sibling, returns null.
    524      *
    525      * @return The previous UI sibling of this UI node, or null if not applicable.
    526      */
    527     public UiElementNode getUiPreviousSibling() {
    528         if (mUiParent != null) {
    529             List<UiElementNode> childlist = mUiParent.getUiChildren();
    530             if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) {
    531                 int index = childlist.indexOf(this);
    532                 return index > 0 ? childlist.get(index - 1) : null;
    533             }
    534         }
    535         return null;
    536     }
    537 
    538     /**
    539      * Returns the next UI sibling of this UI node.
    540      * If the node does not have a next sibling, returns null.
    541      *
    542      * @return The next UI sibling of this UI node, or null.
    543      */
    544     public UiElementNode getUiNextSibling() {
    545         if (mUiParent != null) {
    546             List<UiElementNode> childlist = mUiParent.getUiChildren();
    547             if (childlist != null) {
    548                 int size = childlist.size();
    549                 if (size > 1 && childlist.get(size - 1) != this) {
    550                     int index = childlist.indexOf(this);
    551                     return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null;
    552                 }
    553             }
    554         }
    555         return null;
    556     }
    557 
    558     /**
    559      * Sets the {@link AndroidXmlEditor} handling this {@link UiElementNode} hierarchy.
    560      * <p/>
    561      * The editor must always be set on the root node. This method takes care of that.
    562      *
    563      * @param editor The editor to associate this node with.
    564      */
    565     public void setEditor(AndroidXmlEditor editor) {
    566         if (mUiParent == null) {
    567             mEditor = editor;
    568         } else {
    569             mUiParent.setEditor(editor);
    570         }
    571     }
    572 
    573     /**
    574      * Returns the {@link AndroidXmlEditor} that embeds this {@link UiElementNode}.
    575      * <p/>
    576      * The value is initially null until the node is attached to its parent -- the value
    577      * of the root node is then propagated.
    578      *
    579      * @return The embedding {@link AndroidXmlEditor} or null.
    580      */
    581     public AndroidXmlEditor getEditor() {
    582         return mUiParent == null ? mEditor : mUiParent.getEditor();
    583     }
    584 
    585     /**
    586      * Returns the Android target data for the file being edited.
    587      *
    588      * @return The Android target data for the file being edited.
    589      */
    590     public AndroidTargetData getAndroidTarget() {
    591         return getEditor().getTargetData();
    592     }
    593 
    594     /**
    595      * @return A read-only version of the children collection.
    596      */
    597     public List<UiElementNode> getUiChildren() {
    598         if (mReadOnlyUiChildren == null) {
    599             mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren);
    600         }
    601         return mReadOnlyUiChildren;
    602     }
    603 
    604     /**
    605      * Returns a collection containing all the known attributes as well as
    606      * all the unknown ui attributes.
    607      *
    608      * @return A read-only version of the attributes collection.
    609      */
    610     public Collection<UiAttributeNode> getAllUiAttributes() {
    611         if (mCachedAllUiAttributes == null) {
    612 
    613             List<UiAttributeNode> allValues =
    614                 new ArrayList<UiAttributeNode>(getInternalUiAttributes().values());
    615             allValues.addAll(mUnknownUiAttributes);
    616 
    617             mCachedAllUiAttributes = Collections.unmodifiableCollection(allValues);
    618         }
    619         return mCachedAllUiAttributes;
    620     }
    621 
    622     /**
    623      * Returns all the unknown ui attributes, that is those we found defined in the
    624      * actual XML but that we don't have descriptors for.
    625      *
    626      * @return A read-only version of the unknown attributes collection.
    627      */
    628     public Collection<UiAttributeNode> getUnknownUiAttributes() {
    629         return Collections.unmodifiableCollection(mUnknownUiAttributes);
    630     }
    631 
    632     /**
    633      * Sets the error flag value.
    634      *
    635      * @param errorFlag the error flag
    636      */
    637     public final void setHasError(boolean errorFlag) {
    638         mHasError = errorFlag;
    639     }
    640 
    641     /**
    642      * Returns whether this node, its attributes, or one of the children nodes (and attributes)
    643      * has errors.
    644      *
    645      * @return True if this node, its attributes, or one of the children nodes (and attributes)
    646      * has errors.
    647      */
    648     public final boolean hasError() {
    649         if (mHasError) {
    650             return true;
    651         }
    652 
    653         // get the error value from the attributes.
    654         for (UiAttributeNode attribute : getAllUiAttributes()) {
    655             if (attribute.hasError()) {
    656                 return true;
    657             }
    658         }
    659 
    660         // and now from the children.
    661         for (UiElementNode child : mUiChildren) {
    662             if (child.hasError()) {
    663                 return true;
    664             }
    665         }
    666 
    667         return false;
    668     }
    669 
    670     /**
    671      * Returns the provider that knows how to create {@link ElementDescriptor} from unmapped
    672      * XML names.
    673      * <p/>
    674      * The default is to have one that creates new {@link ElementDescriptor}.
    675      * <p/>
    676      * There is only one such provider in any UI model tree, attached to the root node.
    677      *
    678      * @return An instance of {@link IUnknownDescriptorProvider}. Can never be null.
    679      */
    680     public IUnknownDescriptorProvider getUnknownDescriptorProvider() {
    681         if (mUiParent != null) {
    682             return mUiParent.getUnknownDescriptorProvider();
    683         }
    684         if (mUnknownDescProvider == null) {
    685             // Create the default one on demand.
    686             mUnknownDescProvider = new IUnknownDescriptorProvider() {
    687 
    688                 private final HashMap<String, ElementDescriptor> mMap =
    689                     new HashMap<String, ElementDescriptor>();
    690 
    691                 /**
    692                  * The default is to create a new ElementDescriptor wrapping
    693                  * the unknown XML local name and reuse previously created descriptors.
    694                  */
    695                 @Override
    696                 public ElementDescriptor getDescriptor(String xmlLocalName) {
    697 
    698                     ElementDescriptor desc = mMap.get(xmlLocalName);
    699 
    700                     if (desc == null) {
    701                         desc = new ElementDescriptor(xmlLocalName);
    702                         mMap.put(xmlLocalName, desc);
    703                     }
    704 
    705                     return desc;
    706                 }
    707             };
    708         }
    709         return mUnknownDescProvider;
    710     }
    711 
    712     /**
    713      * Sets the provider that knows how to create {@link ElementDescriptor} from unmapped
    714      * XML names.
    715      * <p/>
    716      * The default is to have one that creates new {@link ElementDescriptor}.
    717      * <p/>
    718      * There is only one such provider in any UI model tree, attached to the root node.
    719      *
    720      * @param unknownDescProvider The new provider to use. Must not be null.
    721      */
    722     public void setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider) {
    723         if (mUiParent == null) {
    724             mUnknownDescProvider = unknownDescProvider;
    725         } else {
    726             mUiParent.setUnknownDescriptorProvider(unknownDescProvider);
    727         }
    728     }
    729 
    730     /**
    731      * Adds a new {@link IUiUpdateListener} to the internal update listener list.
    732      *
    733      * @param listener The listener to add.
    734      */
    735     public void addUpdateListener(IUiUpdateListener listener) {
    736        if (mUiUpdateListeners == null) {
    737            mUiUpdateListeners = new ArrayList<IUiUpdateListener>();
    738        }
    739        if (!mUiUpdateListeners.contains(listener)) {
    740            mUiUpdateListeners.add(listener);
    741        }
    742     }
    743 
    744     /**
    745      * Removes an existing {@link IUiUpdateListener} from the internal update listener list.
    746      * Does nothing if the list is empty or the listener is not registered.
    747      *
    748      * @param listener The listener to remove.
    749      */
    750     public void removeUpdateListener(IUiUpdateListener listener) {
    751        if (mUiUpdateListeners != null) {
    752            mUiUpdateListeners.remove(listener);
    753        }
    754     }
    755 
    756     /**
    757      * Finds a child node relative to this node using a path-like expression.
    758      * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and
    759      * returns the latter. If there are multiple nodes with the same name at the same
    760      * level, always uses the first one found.
    761      *
    762      * @param path The path like expression to select a child node.
    763      * @return The ui node found or null.
    764      */
    765     public UiElementNode findUiChildNode(String path) {
    766         String[] items = path.split("/");  //$NON-NLS-1$
    767         UiElementNode uiNode = this;
    768         for (String item : items) {
    769             boolean nextSegment = false;
    770             for (UiElementNode c : uiNode.mUiChildren) {
    771                 if (c.getDescriptor().getXmlName().equals(item)) {
    772                     uiNode = c;
    773                     nextSegment = true;
    774                     break;
    775                 }
    776             }
    777             if (!nextSegment) {
    778                 return null;
    779             }
    780         }
    781         return uiNode;
    782     }
    783 
    784     /**
    785      * Finds an {@link UiElementNode} which contains the give XML {@link Node}.
    786      * Looks recursively in all children UI nodes.
    787      *
    788      * @param xmlNode The XML node to look for.
    789      * @return The {@link UiElementNode} that contains xmlNode or null if not found,
    790      */
    791     public UiElementNode findXmlNode(Node xmlNode) {
    792         if (xmlNode == null) {
    793             return null;
    794         }
    795         if (getXmlNode() == xmlNode) {
    796             return this;
    797         }
    798 
    799         for (UiElementNode uiChild : mUiChildren) {
    800             UiElementNode found = uiChild.findXmlNode(xmlNode);
    801             if (found != null) {
    802                 return found;
    803             }
    804         }
    805 
    806         return null;
    807     }
    808 
    809     /**
    810      * Returns the {@link UiAttributeNode} matching this attribute descriptor or
    811      * null if not found.
    812      *
    813      * @param attrDesc The {@link AttributeDescriptor} to match.
    814      * @return the {@link UiAttributeNode} matching this attribute descriptor or null
    815      *         if not found.
    816      */
    817     public UiAttributeNode findUiAttribute(AttributeDescriptor attrDesc) {
    818         return getInternalUiAttributes().get(attrDesc);
    819     }
    820 
    821     /**
    822      * Populate this element node with all values from the given XML node.
    823      *
    824      * This fails if the given XML node has a different element name -- it won't change the
    825      * type of this ui node.
    826      *
    827      * This method can be both used for populating values the first time and updating values
    828      * after the XML model changed.
    829      *
    830      * @param xmlNode The XML node to mirror
    831      * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
    832      */
    833     public boolean loadFromXmlNode(Node xmlNode) {
    834         boolean structureChanged = (mXmlNode != xmlNode);
    835         mXmlNode = xmlNode;
    836         if (xmlNode != null) {
    837             updateAttributeList(xmlNode);
    838             structureChanged |= updateElementList(xmlNode);
    839             invokeUiUpdateListeners(structureChanged ? UiUpdateState.CHILDREN_CHANGED
    840                                                       : UiUpdateState.ATTR_UPDATED);
    841         }
    842         return structureChanged;
    843     }
    844 
    845     /**
    846      * Clears the UI node and reload it from the given XML node.
    847      * <p/>
    848      * This works by clearing all references to any previous XML or UI nodes and
    849      * then reloads the XML document from scratch. The editor reference is kept.
    850      * <p/>
    851      * This is used in the special case where the ElementDescriptor structure has changed.
    852      * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother
    853      * and reload everything. This is not subtle and should be used very rarely.
    854      *
    855      * @param xmlNode The XML node or document to reload. Can be null.
    856      */
    857     public void reloadFromXmlNode(Node xmlNode) {
    858         // The editor needs to be preserved, it is not affected by an XML change.
    859         AndroidXmlEditor editor = getEditor();
    860         clearContent();
    861         setEditor(editor);
    862         if (xmlNode != null) {
    863             setXmlDocument(xmlNode.getOwnerDocument());
    864         }
    865         // This will reload all the XML and recreate the UI structure from scratch.
    866         loadFromXmlNode(xmlNode);
    867     }
    868 
    869     /**
    870      * Called by attributes when they want to commit their value
    871      * to an XML node.
    872      * <p/>
    873      * For mandatory nodes, this makes sure the underlying XML element node
    874      * exists in the model. If not, it is created and assigned as the underlying
    875      * XML node.
    876      * </br>
    877      * For non-mandatory nodes, simply return the underlying XML node, which
    878      * must always exists.
    879      *
    880      * @return The XML node matching this {@link UiElementNode} or null.
    881      */
    882     public Node prepareCommit() {
    883         if (getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
    884             createXmlNode();
    885             // The new XML node has been created.
    886             // We don't need to refresh using loadFromXmlNode() since there are
    887             // no attributes or elements that need to be loading into this node.
    888         }
    889         return getXmlNode();
    890     }
    891 
    892     /**
    893      * Commits the attributes (all internal, inherited from UI parent & unknown attributes).
    894      * This is called by the UI when the embedding part needs to be committed.
    895      */
    896     public void commit() {
    897         for (UiAttributeNode uiAttr : getAllUiAttributes()) {
    898             uiAttr.commit();
    899         }
    900     }
    901 
    902     /**
    903      * Returns true if the part has been modified with respect to the data
    904      * loaded from the model.
    905      * @return True if the part has been modified with respect to the data
    906      * loaded from the model.
    907      */
    908     public boolean isDirty() {
    909         for (UiAttributeNode uiAttr : getAllUiAttributes()) {
    910             if (uiAttr.isDirty()) {
    911                 return true;
    912             }
    913         }
    914 
    915         return false;
    916     }
    917 
    918     /**
    919      * Creates the underlying XML element node for this UI node if it doesn't already
    920      * exists.
    921      *
    922      * @return The new value of getXmlNode() (can be null if creation failed)
    923      */
    924     public Node createXmlNode() {
    925         if (mXmlNode != null) {
    926             return null;
    927         }
    928         Node parentXmlNode = null;
    929         if (mUiParent != null) {
    930             parentXmlNode = mUiParent.prepareCommit();
    931             if (parentXmlNode == null) {
    932                 // The parent failed to create its own backing XML node. Abort.
    933                 // No need to throw an exception, the parent will most likely
    934                 // have done so itself.
    935                 return null;
    936             }
    937         }
    938 
    939         String elementName = getDescriptor().getXmlName();
    940         Document doc = getXmlDocument();
    941 
    942         // We *must* have a root node. If not, we need to abort.
    943         if (doc == null) {
    944             throw new RuntimeException(
    945                     String.format("Missing XML document for %1$s XML node.", elementName));
    946         }
    947 
    948         // If we get here and parentXmlNode is null, the node is to be created
    949         // as the root node of the document (which can't be null, cf. check above).
    950         if (parentXmlNode == null) {
    951             parentXmlNode = doc;
    952         }
    953 
    954         mXmlNode = doc.createElement(elementName);
    955 
    956         // If this element does not have children, mark it as an empty tag
    957         // such that the XML looks like <tag/> instead of <tag></tag>
    958         if (!mDescriptor.hasChildren()) {
    959             if (mXmlNode instanceof ElementImpl) {
    960                 ElementImpl element = (ElementImpl) mXmlNode;
    961                 element.setEmptyTag(true);
    962             }
    963         }
    964 
    965         Node xmlNextSibling = null;
    966 
    967         UiElementNode uiNextSibling = getUiNextSibling();
    968         if (uiNextSibling != null) {
    969             xmlNextSibling = uiNextSibling.getXmlNode();
    970         }
    971 
    972         Node previousTextNode = null;
    973         if (xmlNextSibling != null) {
    974             Node previousNode = xmlNextSibling.getPreviousSibling();
    975             if (previousNode != null && previousNode.getNodeType() == Node.TEXT_NODE) {
    976                 previousTextNode = previousNode;
    977             }
    978         } else {
    979             Node lastChild = parentXmlNode.getLastChild();
    980             if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) {
    981                 previousTextNode = lastChild;
    982             }
    983         }
    984 
    985         String insertAfter = null;
    986 
    987         // Try to figure out the indentation node to insert. Even in auto-formatting
    988         // we need to do this, because it turns out the XML editor's formatter does
    989         // not do a very good job with completely botched up XML; it does a much better
    990         // job if the new XML is already mostly well formatted. Thus, the main purpose
    991         // of applying the real XML formatter after our own indentation attempts here is
    992         // to make it apply its own tab-versus-spaces indentation properties, have it
    993         // insert line breaks before attributes (if the user has configured that), etc.
    994 
    995         // First figure out the indentation level of the newly inserted element;
    996         // this is either the same as the previous sibling, or if there is no sibling,
    997         // it's the indentation of the parent plus one indentation level.
    998         boolean isFirstChild = getUiPreviousSibling() == null
    999                 || parentXmlNode.getFirstChild() == null;
   1000         AndroidXmlEditor editor = getEditor();
   1001         String indent;
   1002         String parentIndent = ""; //$NON-NLS-1$
   1003         if (isFirstChild) {
   1004             indent = parentIndent = editor.getIndent(parentXmlNode);
   1005             // We need to add one level of indentation. Are we using tabs?
   1006             // Can't get to formatting settings so let's just look at the
   1007             // parent indentation and see if we can guess
   1008             if (indent.length() > 0 && indent.charAt(indent.length()-1) == '\t') {
   1009                 indent = indent + '\t';
   1010             } else {
   1011                 // Not using tabs, or we can't figure it out (because parent had no
   1012                 // indentation). In that case, indent with 4 spaces, as seems to
   1013                 // be the Android default.
   1014                 indent = indent + "    "; //$NON-NLS-1$
   1015             }
   1016         } else {
   1017             // Find out the indent of the previous sibling
   1018             indent = editor.getIndent(getUiPreviousSibling().getXmlNode());
   1019         }
   1020 
   1021         // We want to insert the new element BEFORE the text node which precedes
   1022         // the next element, since that text node is the next element's indentation!
   1023         if (previousTextNode != null) {
   1024             xmlNextSibling = previousTextNode;
   1025         } else {
   1026             // If there's no previous text node, we are probably inside an
   1027             // empty element (<LinearLayout>|</LinearLayout>) and in that case we need
   1028             // to not only insert a newline and indentation before the new element, but
   1029             // after it as well.
   1030             insertAfter = parentIndent;
   1031         }
   1032 
   1033         // Insert indent text node before the new element
   1034         IStructuredDocument document = editor.getStructuredDocument();
   1035         String newLine;
   1036         if (document != null) {
   1037             newLine = TextUtilities.getDefaultLineDelimiter(document);
   1038         } else {
   1039             newLine = SdkUtils.getLineSeparator();
   1040         }
   1041         Text indentNode = doc.createTextNode(newLine + indent);
   1042         parentXmlNode.insertBefore(indentNode, xmlNextSibling);
   1043 
   1044         // Insert the element itself
   1045         parentXmlNode.insertBefore(mXmlNode, xmlNextSibling);
   1046 
   1047         // Insert a separator after the tag. We only do this when we've inserted
   1048         // a tag into an area where there was no whitespace before
   1049         // (e.g. a new child of <LinearLayout></LinearLayout>).
   1050         if (insertAfter != null) {
   1051             Text sep = doc.createTextNode(newLine + insertAfter);
   1052             parentXmlNode.insertBefore(sep, xmlNextSibling);
   1053         }
   1054 
   1055         // Set all initial attributes in the XML node if they are not empty.
   1056         // Iterate on the descriptor list to get the desired order and then use the
   1057         // internal values, if any.
   1058         List<UiAttributeNode> addAttributes = new ArrayList<UiAttributeNode>();
   1059 
   1060         for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
   1061             if (attrDesc instanceof XmlnsAttributeDescriptor) {
   1062                 XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attrDesc;
   1063                 Attr attr = doc.createAttributeNS(SdkConstants.XMLNS_URI,
   1064                         desc.getXmlNsName());
   1065                 attr.setValue(desc.getValue());
   1066                 attr.setPrefix(desc.getXmlNsPrefix());
   1067                 mXmlNode.getAttributes().setNamedItemNS(attr);
   1068             } else {
   1069                 UiAttributeNode uiAttr = getInternalUiAttributes().get(attrDesc);
   1070 
   1071                 // Don't apply the attribute immediately, instead record this attribute
   1072                 // such that we can gather all attributes and sort them first.
   1073                 // This is necessary because the XML model will *append* all attributes
   1074                 // so we want to add them in a particular order.
   1075                 // (Note that we only have to worry about UiAttributeNodes with non null
   1076                 // values, since this is a new node and we therefore don't need to attempt
   1077                 // to remove existing attributes)
   1078                 String value = uiAttr.getCurrentValue();
   1079                 if (value != null && value.length() > 0) {
   1080                     addAttributes.add(uiAttr);
   1081                 }
   1082             }
   1083         }
   1084 
   1085         // Sort and apply the attributes in order, because the Eclipse XML model will always
   1086         // append the XML attributes, so by inserting them in our desired order they will
   1087         // appear that way in the XML
   1088         Collections.sort(addAttributes);
   1089 
   1090         for (UiAttributeNode node : addAttributes) {
   1091             commitAttributeToXml(node, node.getCurrentValue());
   1092             node.setDirty(false);
   1093         }
   1094 
   1095         getEditor().scheduleNodeReformat(this, false);
   1096 
   1097         // Notify per-node listeners
   1098         invokeUiUpdateListeners(UiUpdateState.CREATED);
   1099         // Notify global listeners
   1100         fireNodeCreated(this, getUiSiblingIndex());
   1101 
   1102         return mXmlNode;
   1103     }
   1104 
   1105     /**
   1106      * Removes the XML node corresponding to this UI node if it exists
   1107      * and also removes all mirrored information in this UI node (i.e. children, attributes)
   1108      *
   1109      * @return The removed node or null if it didn't exist in the first place.
   1110      */
   1111     public Node deleteXmlNode() {
   1112         if (mXmlNode == null) {
   1113             return null;
   1114         }
   1115 
   1116         int previousIndex = getUiSiblingIndex();
   1117 
   1118         // First clear the internals of the node and *then* actually deletes the XML
   1119         // node (because doing so will generate an update even and this node may be
   1120         // revisited via loadFromXmlNode).
   1121         Node oldXmlNode = mXmlNode;
   1122         clearContent();
   1123 
   1124         Node xmlParent = oldXmlNode.getParentNode();
   1125         if (xmlParent == null) {
   1126             xmlParent = getXmlDocument();
   1127         }
   1128         Node previousSibling = oldXmlNode.getPreviousSibling();
   1129         oldXmlNode = xmlParent.removeChild(oldXmlNode);
   1130 
   1131         // We need to remove the text node BEFORE the removed element, since THAT's the
   1132         // indentation node for the removed element.
   1133         if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE
   1134                 && previousSibling.getNodeValue().trim().length() == 0) {
   1135             xmlParent.removeChild(previousSibling);
   1136         }
   1137 
   1138         invokeUiUpdateListeners(UiUpdateState.DELETED);
   1139         fireNodeDeleted(this, previousIndex);
   1140 
   1141         return oldXmlNode;
   1142     }
   1143 
   1144     /**
   1145      * Updates the element list for this UiElementNode.
   1146      * At the end, the list of children UiElementNode here will match the one from the
   1147      * provided XML {@link Node}:
   1148      * <ul>
   1149      * <li> Walk both the current ui children list and the xml children list at the same time.
   1150      * <li> If we have a new xml child but already reached the end of the ui child list, add the
   1151      *      new xml node.
   1152      * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so,
   1153      *      move it here. It means the XML child list has been reordered.
   1154      * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list.
   1155      * <li> At the end, we may have finished walking the xml child list but still have remaining
   1156      *      ui children, simply delete them as they matching trailing xml nodes that have been
   1157      *      removed unless they are mandatory ui nodes.
   1158      * </ul>
   1159      * Note that only the first case is used when populating the ui list the first time.
   1160      *
   1161      * @param xmlNode The XML node to mirror
   1162      * @return True when the XML structure has changed.
   1163      */
   1164     protected boolean updateElementList(Node xmlNode) {
   1165         boolean structureChanged = false;
   1166         boolean hasMandatoryLast = false;
   1167         int uiIndex = 0;
   1168         Node xmlChild = xmlNode.getFirstChild();
   1169         while (xmlChild != null) {
   1170             if (xmlChild.getNodeType() == Node.ELEMENT_NODE) {
   1171                 String elementName = xmlChild.getNodeName();
   1172                 UiElementNode uiNode = null;
   1173                 CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
   1174                 if (mUiChildren.size() <= uiIndex) {
   1175                     // A new node is being added at the end of the list
   1176                     ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
   1177                             false /* recursive */);
   1178                     if (desc == null && elementName.indexOf('.') != -1 &&
   1179                             (!elementName.startsWith(ANDROID_PKG_PREFIX)
   1180                                     || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) {
   1181                         AndroidXmlEditor editor = getEditor();
   1182                         if (editor != null && editor.getProject() != null) {
   1183                             desc = service.getDescriptor(editor.getProject(), elementName);
   1184                         }
   1185                     }
   1186                     if (desc == null) {
   1187                         // Unknown node. Create a temporary descriptor for it.
   1188                         // We'll add unknown attributes to it later.
   1189                         IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
   1190                         desc = p.getDescriptor(elementName);
   1191                     }
   1192                     structureChanged = true;
   1193                     uiNode = appendNewUiChild(desc);
   1194                     uiIndex++;
   1195                 } else {
   1196                     // A new node is being inserted or moved.
   1197                     // Note: mandatory nodes can be created without an XML node in which case
   1198                     // getXmlNode() is null.
   1199                     UiElementNode uiChild;
   1200                     int n = mUiChildren.size();
   1201                     for (int j = uiIndex; j < n; j++) {
   1202                         uiChild = mUiChildren.get(j);
   1203                         if (uiChild.getXmlNode() != null && uiChild.getXmlNode() == xmlChild) {
   1204                             if (j > uiIndex) {
   1205                                 // Found the same XML node at some later index, now move it here.
   1206                                 mUiChildren.remove(j);
   1207                                 mUiChildren.add(uiIndex, uiChild);
   1208                                 structureChanged = true;
   1209                             }
   1210                             uiNode = uiChild;
   1211                             uiIndex++;
   1212                             break;
   1213                         }
   1214                     }
   1215 
   1216                     if (uiNode == null) {
   1217                         // Look for an unused mandatory node with no XML node attached
   1218                         // referencing the same XML element name
   1219                         for (int j = uiIndex; j < n; j++) {
   1220                             uiChild = mUiChildren.get(j);
   1221                             if (uiChild.getXmlNode() == null &&
   1222                                     uiChild.getDescriptor().getMandatory() !=
   1223                                                                 Mandatory.NOT_MANDATORY &&
   1224                                     uiChild.getDescriptor().getXmlName().equals(elementName)) {
   1225 
   1226                                 if (j > uiIndex) {
   1227                                     // Found it, now move it here
   1228                                     mUiChildren.remove(j);
   1229                                     mUiChildren.add(uiIndex, uiChild);
   1230                                 }
   1231                                 // Assign the XML node to this empty mandatory element.
   1232                                 uiChild.mXmlNode = xmlChild;
   1233                                 structureChanged = true;
   1234                                 uiNode = uiChild;
   1235                                 uiIndex++;
   1236                             }
   1237                         }
   1238                     }
   1239 
   1240                     if (uiNode == null) {
   1241                         // Inserting new node
   1242                         ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
   1243                                 false /* recursive */);
   1244                         if (desc == null && elementName.indexOf('.') != -1 &&
   1245                                 (!elementName.startsWith(ANDROID_PKG_PREFIX)
   1246                                         || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) {
   1247                             AndroidXmlEditor editor = getEditor();
   1248                             if (editor != null && editor.getProject() != null) {
   1249                                 desc = service.getDescriptor(editor.getProject(), elementName);
   1250                             }
   1251                         }
   1252                         if (desc == null) {
   1253                             // Unknown node. Create a temporary descriptor for it.
   1254                             // We'll add unknown attributes to it later.
   1255                             IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
   1256                             desc = p.getDescriptor(elementName);
   1257                         } else {
   1258                             structureChanged = true;
   1259                             uiNode = insertNewUiChild(uiIndex, desc);
   1260                             uiIndex++;
   1261                         }
   1262                     }
   1263                 }
   1264                 if (uiNode != null) {
   1265                     // If we touched an UI Node, even an existing one, refresh its content.
   1266                     // For new nodes, this will populate them recursively.
   1267                     structureChanged |= uiNode.loadFromXmlNode(xmlChild);
   1268 
   1269                     // Remember if there are any mandatory-last nodes to reorder.
   1270                     hasMandatoryLast |=
   1271                         uiNode.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST;
   1272                 }
   1273             }
   1274             xmlChild = xmlChild.getNextSibling();
   1275         }
   1276 
   1277         // There might be extra UI nodes at the end if the XML node list got shorter.
   1278         for (int index = mUiChildren.size() - 1; index >= uiIndex; --index) {
   1279              structureChanged |= removeUiChildAtIndex(index);
   1280         }
   1281 
   1282         if (hasMandatoryLast) {
   1283             // At least one mandatory-last uiNode was moved. Let's see if we can
   1284             // move them back to the last position. That's possible if the only
   1285             // thing between these and the end are other mandatory empty uiNodes
   1286             // (mandatory uiNodes with no XML attached are pure "virtual" reserved
   1287             // slots and it's ok to reorganize them but other can't.)
   1288             int n = mUiChildren.size() - 1;
   1289             for (int index = n; index >= 0; index--) {
   1290                 UiElementNode uiChild = mUiChildren.get(index);
   1291                 Mandatory mand = uiChild.getDescriptor().getMandatory();
   1292                 if (mand == Mandatory.MANDATORY_LAST && index < n) {
   1293                     // Remove it from index and move it back at the end of the list.
   1294                     mUiChildren.remove(index);
   1295                     mUiChildren.add(uiChild);
   1296                 } else if (mand == Mandatory.NOT_MANDATORY || uiChild.getXmlNode() != null) {
   1297                     // We found at least one non-mandatory or a mandatory node with an actual
   1298                     // XML attached, so there's nothing we can reorganize past this point.
   1299                     break;
   1300                 }
   1301             }
   1302         }
   1303 
   1304         return structureChanged;
   1305     }
   1306 
   1307     /**
   1308      * Internal helper to remove an UI child node given by its index in the
   1309      * internal child list.
   1310      *
   1311      * Also invokes the update listener on the node to be deleted *after* the node has
   1312      * been removed.
   1313      *
   1314      * @param uiIndex The index of the UI child to remove, range 0 .. mUiChildren.size()-1
   1315      * @return True if the structure has changed
   1316      * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you
   1317      *         know that could never happen unless the computer is on fire or something.
   1318      */
   1319     private boolean removeUiChildAtIndex(int uiIndex) {
   1320         UiElementNode uiNode = mUiChildren.get(uiIndex);
   1321         ElementDescriptor desc = uiNode.getDescriptor();
   1322 
   1323         try {
   1324             if (uiNode.getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
   1325                 // This is a mandatory node. Such a node must exist in the UiNode hierarchy
   1326                 // even if there's no XML counterpart. However we only need to keep one.
   1327 
   1328                 // Check if the parent (e.g. this node) has another similar ui child node.
   1329                 boolean keepNode = true;
   1330                 for (UiElementNode child : mUiChildren) {
   1331                     if (child != uiNode && child.getDescriptor() == desc) {
   1332                         // We found another child with the same descriptor that is not
   1333                         // the node we want to remove. This means we have one mandatory
   1334                         // node so we can safely remove uiNode.
   1335                         keepNode = false;
   1336                         break;
   1337                     }
   1338                 }
   1339 
   1340                 if (keepNode) {
   1341                     // We can't remove a mandatory node as we need to keep at least one
   1342                     // mandatory node in the parent. Instead we just clear its content
   1343                     // (including its XML Node reference).
   1344 
   1345                     // A mandatory node with no XML means it doesn't really exist, so it can't be
   1346                     // deleted. So the structure will change only if the ui node is actually
   1347                     // associated to an XML node.
   1348                     boolean xmlExists = (uiNode.getXmlNode() != null);
   1349 
   1350                     uiNode.clearContent();
   1351                     return xmlExists;
   1352                 }
   1353             }
   1354 
   1355             mUiChildren.remove(uiIndex);
   1356 
   1357             return true;
   1358         } finally {
   1359             // Tell listeners that a node has been removed.
   1360             // The model has already been modified.
   1361             invokeUiUpdateListeners(UiUpdateState.DELETED);
   1362         }
   1363     }
   1364 
   1365     /**
   1366      * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
   1367      * and appends it to the end of the element children list.
   1368      *
   1369      * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
   1370      * @return The new UI node that has been appended
   1371      */
   1372     public UiElementNode appendNewUiChild(ElementDescriptor descriptor) {
   1373         UiElementNode uiNode;
   1374         uiNode = descriptor.createUiNode();
   1375         mUiChildren.add(uiNode);
   1376         uiNode.setUiParent(this);
   1377         uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
   1378         return uiNode;
   1379     }
   1380 
   1381     /**
   1382      * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
   1383      * and inserts it in the element children list at the specified position.
   1384      *
   1385      * @param index The position where to insert in the element children list.
   1386      *              Shifts the element currently at that position (if any) and any
   1387      *              subsequent elements to the right (adds one to their indices).
   1388      *              Index must >= 0 and <= getUiChildren.size().
   1389      *              Using size() means to append to the end of the list.
   1390      * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
   1391      * @return The new UI node.
   1392      */
   1393     public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) {
   1394         UiElementNode uiNode;
   1395         uiNode = descriptor.createUiNode();
   1396         mUiChildren.add(index, uiNode);
   1397         uiNode.setUiParent(this);
   1398         uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
   1399         return uiNode;
   1400     }
   1401 
   1402     /**
   1403      * Updates the {@link UiAttributeNode} list for this {@link UiElementNode}
   1404      * using the values from the XML element.
   1405      * <p/>
   1406      * For a given {@link UiElementNode}, the attribute list always exists in
   1407      * full and is totally independent of whether the XML model actually
   1408      * has the corresponding attributes.
   1409      * <p/>
   1410      * For each attribute declared in this {@link UiElementNode}, get
   1411      * the corresponding XML attribute. It may not exist, in which case the
   1412      * value will be null. We don't really know if a value has changed, so
   1413      * the updateValue() is called on the UI attribute in all cases.
   1414      *
   1415      * @param xmlNode The XML node to mirror
   1416      */
   1417     protected void updateAttributeList(Node xmlNode) {
   1418         NamedNodeMap xmlAttrMap = xmlNode.getAttributes();
   1419         HashSet<Node> visited = new HashSet<Node>();
   1420 
   1421         // For all known (i.e. expected) UI attributes, find an existing XML attribute of
   1422         // same (uri, local name) and update the internal Ui attribute value.
   1423         for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) {
   1424             AttributeDescriptor desc = uiAttr.getDescriptor();
   1425             if (!(desc instanceof SeparatorAttributeDescriptor)) {
   1426                 Node xmlAttr = xmlAttrMap == null ? null :
   1427                     xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName());
   1428                 uiAttr.updateValue(xmlAttr);
   1429                 visited.add(xmlAttr);
   1430             }
   1431         }
   1432 
   1433         // Clone the current list of unknown attributes. We'll then remove from this list when
   1434         // we find attributes which are still unknown. What will be left are the old unknown
   1435         // attributes that have been deleted in the current XML attribute list.
   1436         @SuppressWarnings("unchecked")
   1437         HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone();
   1438 
   1439         // We need to ignore hidden attributes.
   1440         Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors();
   1441 
   1442         // Traverse the actual XML attribute list to find unknown attributes
   1443         if (xmlAttrMap != null) {
   1444             for (int i = 0; i < xmlAttrMap.getLength(); i++) {
   1445                 Node xmlAttr = xmlAttrMap.item(i);
   1446                 // Ignore attributes which have actual descriptors
   1447                 if (visited.contains(xmlAttr)) {
   1448                     continue;
   1449                 }
   1450 
   1451                 String xmlFullName = xmlAttr.getNodeName();
   1452 
   1453                 // Ignore attributes which are hidden (based on the prefix:localName key)
   1454                 if (hiddenAttrDesc.containsKey(xmlFullName)) {
   1455                     continue;
   1456                 }
   1457 
   1458                 String xmlAttrLocalName = xmlAttr.getLocalName();
   1459                 String xmlNsUri = xmlAttr.getNamespaceURI();
   1460 
   1461                 UiAttributeNode uiAttr = null;
   1462                 for (UiAttributeNode a : mUnknownUiAttributes) {
   1463                     String aLocalName = a.getDescriptor().getXmlLocalName();
   1464                     String aNsUri = a.getDescriptor().getNamespaceUri();
   1465                     if (aLocalName.equals(xmlAttrLocalName) &&
   1466                             (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) {
   1467                         // This attribute is still present in the unknown list
   1468                         uiAttr = a;
   1469                         // It has not been deleted
   1470                         deleted.remove(a);
   1471                         break;
   1472                     }
   1473                 }
   1474                 if (uiAttr == null) {
   1475                     uiAttr = addUnknownAttribute(xmlFullName, xmlAttrLocalName, xmlNsUri);
   1476                 }
   1477 
   1478                 uiAttr.updateValue(xmlAttr);
   1479             }
   1480 
   1481             // Remove from the internal list unknown attributes that have been deleted from the xml
   1482             for (UiAttributeNode a : deleted) {
   1483                 mUnknownUiAttributes.remove(a);
   1484                 mCachedAllUiAttributes = null;
   1485             }
   1486         }
   1487     }
   1488 
   1489     /**
   1490      * Create a new temporary text attribute descriptor for the unknown attribute
   1491      * and returns a new {@link UiAttributeNode} associated to this descriptor.
   1492      * <p/>
   1493      * The attribute is not marked as dirty, doing so is up to the caller.
   1494      */
   1495     private UiAttributeNode addUnknownAttribute(String xmlFullName,
   1496             String xmlAttrLocalName, String xmlNsUri) {
   1497         // Create a new unknown attribute of format string
   1498         TextAttributeDescriptor desc = new TextAttributeDescriptor(
   1499                 xmlAttrLocalName,           // xml name
   1500                 xmlNsUri,                // ui name
   1501                 new AttributeInfo(xmlAttrLocalName, Format.STRING_SET)
   1502                 );
   1503         UiAttributeNode uiAttr = desc.createUiNode(this);
   1504         mUnknownUiAttributes.add(uiAttr);
   1505         mCachedAllUiAttributes = null;
   1506         return uiAttr;
   1507     }
   1508 
   1509     /**
   1510      * Invoke all registered {@link IUiUpdateListener} listening on this UI update for this node.
   1511      */
   1512     protected void invokeUiUpdateListeners(UiUpdateState state) {
   1513         if (mUiUpdateListeners != null) {
   1514             for (IUiUpdateListener listener : mUiUpdateListeners) {
   1515                 try {
   1516                     listener.uiElementNodeUpdated(this, state);
   1517                 } catch (Exception e) {
   1518                     // prevent a crashing listener from crashing the whole invocation chain
   1519                     AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s",  //$NON-NLS-1$
   1520                             getBreadcrumbTrailDescription(true),
   1521                             state.toString());
   1522                 }
   1523             }
   1524         }
   1525     }
   1526 
   1527     // --- for derived implementations only ---
   1528 
   1529     @VisibleForTesting
   1530     public void setXmlNode(Node xmlNode) {
   1531         mXmlNode = xmlNode;
   1532     }
   1533 
   1534     public void refreshUi() {
   1535         invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED);
   1536     }
   1537 
   1538 
   1539     // ------------- Helpers
   1540 
   1541     /**
   1542      * Helper method to commit a single attribute value to XML.
   1543      * <p/>
   1544      * This method updates the XML regardless of the current XML value.
   1545      * Callers should check first if an update is needed.
   1546      * If the new value is empty, the XML attribute will be actually removed.
   1547      * <p/>
   1548      * Note that the caller MUST ensure that modifying the underlying XML model is
   1549      * safe and must take care of marking the model as dirty if necessary.
   1550      *
   1551      * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
   1552      *
   1553      * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode.
   1554      * @param newValue The new value to set.
   1555      * @return True if the XML attribute was modified or removed, false if nothing changed.
   1556      */
   1557     public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) {
   1558         // Get (or create) the underlying XML element node that contains the attributes.
   1559         Node element = prepareCommit();
   1560         if (element != null && uiAttr != null) {
   1561             String attrLocalName = uiAttr.getDescriptor().getXmlLocalName();
   1562             String attrNsUri = uiAttr.getDescriptor().getNamespaceUri();
   1563 
   1564             NamedNodeMap attrMap = element.getAttributes();
   1565             if (newValue == null || newValue.length() == 0) {
   1566                 // Remove attribute if it's empty
   1567                 if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) {
   1568                     attrMap.removeNamedItemNS(attrNsUri, attrLocalName);
   1569                     return true;
   1570                 }
   1571             } else {
   1572                 // Add or replace an attribute
   1573                 Document doc = element.getOwnerDocument();
   1574                 if (doc != null) {
   1575                     Attr attr;
   1576                     if (attrNsUri != null && attrNsUri.length() > 0) {
   1577                         attr = (Attr) attrMap.getNamedItemNS(attrNsUri, attrLocalName);
   1578                         if (attr == null) {
   1579                             attr = doc.createAttributeNS(attrNsUri, attrLocalName);
   1580                             attr.setPrefix(XmlUtils.lookupNamespacePrefix(element, attrNsUri));
   1581                             attrMap.setNamedItemNS(attr);
   1582                         }
   1583                     } else {
   1584                         attr = (Attr) attrMap.getNamedItem(attrLocalName);
   1585                         if (attr == null) {
   1586                             attr = doc.createAttribute(attrLocalName);
   1587                             attrMap.setNamedItem(attr);
   1588                         }
   1589                     }
   1590                     attr.setValue(newValue);
   1591                     return true;
   1592                 }
   1593             }
   1594         }
   1595         return false;
   1596     }
   1597 
   1598     /**
   1599      * Helper method to commit all dirty attributes values to XML.
   1600      * <p/>
   1601      * This method is useful if {@link #setAttributeValue(String, String, String, boolean)} has
   1602      * been called more than once and all the attributes marked as dirty must be committed to
   1603      * the XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty
   1604      * attribute.
   1605      * <p/>
   1606      * Note that the caller MUST ensure that modifying the underlying XML model is
   1607      * safe and must take care of marking the model as dirty if necessary.
   1608      *
   1609      * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
   1610      *
   1611      * @return True if one or more values were actually modified or removed,
   1612      *         false if nothing changed.
   1613      */
   1614     @SuppressWarnings("null") // Eclipse is confused by the logic and gets it wrong
   1615     public boolean commitDirtyAttributesToXml() {
   1616         boolean result = false;
   1617         List<UiAttributeNode> dirtyAttributes = new ArrayList<UiAttributeNode>();
   1618         for (UiAttributeNode uiAttr : getAllUiAttributes()) {
   1619             if (uiAttr.isDirty()) {
   1620                 String value = uiAttr.getCurrentValue();
   1621                 if (value != null && value.length() > 0) {
   1622                     // Defer the new attributes: set these last and in order
   1623                     dirtyAttributes.add(uiAttr);
   1624                 } else {
   1625                     result |= commitAttributeToXml(uiAttr, value);
   1626                     uiAttr.setDirty(false);
   1627                 }
   1628             }
   1629         }
   1630         if (dirtyAttributes.size() > 0) {
   1631             result = true;
   1632 
   1633             Collections.sort(dirtyAttributes);
   1634 
   1635             // The Eclipse XML model will *always* append new attributes.
   1636             // Therefore, if any of the dirty attributes are new, they will appear
   1637             // after any existing, clean attributes on the element. To fix this,
   1638             // we need to first remove any of these attributes, then insert them
   1639             // back in the right order.
   1640             Node element = prepareCommit();
   1641             if (element == null) {
   1642                 return result;
   1643             }
   1644 
   1645             if (AdtPrefs.getPrefs().getFormatGuiXml() && getEditor().supportsFormatOnGuiEdit()) {
   1646                 // If auto formatting, don't bother with attribute sorting here since the
   1647                 // order will be corrected as soon as the edit is committed anyway
   1648                 for (UiAttributeNode uiAttribute : dirtyAttributes) {
   1649                     commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
   1650                     uiAttribute.setDirty(false);
   1651                 }
   1652 
   1653                 return result;
   1654             }
   1655 
   1656             AttributeDescriptor descriptor = dirtyAttributes.get(0).getDescriptor();
   1657             String firstName = descriptor.getXmlLocalName();
   1658             String firstNamePrefix = null;
   1659             String namespaceUri = descriptor.getNamespaceUri();
   1660             if (namespaceUri != null) {
   1661                 firstNamePrefix = XmlUtils.lookupNamespacePrefix(element, namespaceUri);
   1662             }
   1663             NamedNodeMap attributes = ((Element) element).getAttributes();
   1664             List<Attr> move = new ArrayList<Attr>();
   1665             for (int i = 0, n = attributes.getLength(); i < n; i++) {
   1666                 Attr attribute = (Attr) attributes.item(i);
   1667                 if (XmlAttributeSortOrder.compareAttributes(
   1668                         attribute.getPrefix(), attribute.getLocalName(),
   1669                         firstNamePrefix, firstName) > 0) {
   1670                     move.add(attribute);
   1671                 }
   1672             }
   1673 
   1674             for (Attr attribute : move) {
   1675                 if (attribute.getNamespaceURI() != null) {
   1676                     attributes.removeNamedItemNS(attribute.getNamespaceURI(),
   1677                             attribute.getLocalName());
   1678                 } else {
   1679                     attributes.removeNamedItem(attribute.getName());
   1680                 }
   1681             }
   1682 
   1683             // Merge back the removed DOM attribute nodes and the new UI attribute nodes.
   1684             // In cases where the attribute DOM name and the UI attribute names equal,
   1685             // skip the DOM nodes and just apply the UI attributes.
   1686             int domAttributeIndex = 0;
   1687             int domAttributeIndexMax = move.size();
   1688             int uiAttributeIndex = 0;
   1689             int uiAttributeIndexMax = dirtyAttributes.size();
   1690 
   1691             while (true) {
   1692                 Attr domAttribute;
   1693                 UiAttributeNode uiAttribute;
   1694 
   1695                 int compare;
   1696                 if (uiAttributeIndex < uiAttributeIndexMax) {
   1697                     if (domAttributeIndex < domAttributeIndexMax) {
   1698                         domAttribute = move.get(domAttributeIndex);
   1699                         uiAttribute = dirtyAttributes.get(uiAttributeIndex);
   1700 
   1701                         String domAttributeName = domAttribute.getLocalName();
   1702                         String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName();
   1703                         compare = XmlAttributeSortOrder.compareAttributes(domAttributeName,
   1704                                 uiAttributeName);
   1705                     } else {
   1706                         compare = 1;
   1707                         uiAttribute = dirtyAttributes.get(uiAttributeIndex);
   1708                         domAttribute = null;
   1709                     }
   1710                 } else if (domAttributeIndex < domAttributeIndexMax) {
   1711                     compare = -1;
   1712                     domAttribute = move.get(domAttributeIndex);
   1713                     uiAttribute = null;
   1714                 } else {
   1715                     break;
   1716                 }
   1717 
   1718                 if (compare < 0) {
   1719                     if (domAttribute.getNamespaceURI() != null) {
   1720                         attributes.setNamedItemNS(domAttribute);
   1721                     } else {
   1722                         attributes.setNamedItem(domAttribute);
   1723                     }
   1724                     domAttributeIndex++;
   1725                 } else {
   1726                     assert compare >= 0;
   1727                     if (compare == 0) {
   1728                         domAttributeIndex++;
   1729                     }
   1730                     commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
   1731                     uiAttribute.setDirty(false);
   1732                     uiAttributeIndex++;
   1733                 }
   1734             }
   1735         }
   1736 
   1737         return result;
   1738     }
   1739 
   1740     /**
   1741      * Utility method to internally set the value of a text attribute for the current
   1742      * UiElementNode.
   1743      * <p/>
   1744      * This method is a helper. It silently ignores the errors such as the requested
   1745      * attribute not being present in the element or attribute not being settable.
   1746      * It accepts inherited attributes (such as layout).
   1747      * <p/>
   1748      * This does not commit to the XML model. It does mark the attribute node as dirty.
   1749      * This is up to the caller.
   1750      *
   1751      * @see #commitAttributeToXml(UiAttributeNode, String)
   1752      * @see #commitDirtyAttributesToXml()
   1753      *
   1754      * @param attrXmlName The XML <em>local</em> name of the attribute to modify
   1755      * @param attrNsUri The namespace URI of the attribute.
   1756      *                  Can be null if the attribute uses the global namespace.
   1757      * @param value The new value for the attribute. If set to null, the attribute is removed.
   1758      * @param override True if the value must be set even if one already exists.
   1759      * @return The {@link UiAttributeNode} that has been modified or null.
   1760      */
   1761     public UiAttributeNode setAttributeValue(
   1762             String attrXmlName,
   1763             String attrNsUri,
   1764             String value,
   1765             boolean override) {
   1766         if (value == null) {
   1767             value = ""; //$NON-NLS-1$ -- this removes an attribute
   1768         }
   1769 
   1770         getEditor().scheduleNodeReformat(this, true);
   1771 
   1772         // Try with all internal attributes
   1773         UiAttributeNode uiAttr = setInternalAttrValue(
   1774                 getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
   1775         if (uiAttr != null) {
   1776             return uiAttr;
   1777         }
   1778 
   1779         if (uiAttr == null) {
   1780             // Failed to find the attribute. For non-android attributes that is mostly expected,
   1781             // in which case we just create a new custom one. As a side effect, we'll find the
   1782             // attribute descriptor via getAllUiAttributes().
   1783             addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri);
   1784 
   1785             // We've created the attribute, but not actually set the value on it, so let's do it.
   1786             // Try with the updated internal attributes.
   1787             // Implementation detail: we could just do a setCurrentValue + setDirty on the
   1788             // uiAttr returned by addUnknownAttribute(); however going through setInternalAttrValue
   1789             // means we won't duplicate the logic, at the expense of doing one more lookup.
   1790             uiAttr = setInternalAttrValue(
   1791                     getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
   1792         }
   1793 
   1794         return uiAttr;
   1795     }
   1796 
   1797     private UiAttributeNode setInternalAttrValue(
   1798             Collection<UiAttributeNode> attributes,
   1799             String attrXmlName,
   1800             String attrNsUri,
   1801             String value,
   1802             boolean override) {
   1803 
   1804         // For namespace less attributes (like the "layout" attribute of an <include> tag
   1805         // we may be passed "" as the namespace (during an attribute copy), and it
   1806         // should really be null instead.
   1807         if (attrNsUri != null && attrNsUri.length() == 0) {
   1808             attrNsUri = null;
   1809         }
   1810 
   1811         for (UiAttributeNode uiAttr : attributes) {
   1812             AttributeDescriptor uiDesc = uiAttr.getDescriptor();
   1813 
   1814             if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
   1815                 // Both NS URI must be either null or equal.
   1816                 if ((attrNsUri == null && uiDesc.getNamespaceUri() == null) ||
   1817                         (attrNsUri != null && attrNsUri.equals(uiDesc.getNamespaceUri()))) {
   1818 
   1819                     // Not all attributes are editable, ignore those which are not.
   1820                     if (uiAttr instanceof IUiSettableAttributeNode) {
   1821                         String current = uiAttr.getCurrentValue();
   1822                         // Only update (and mark as dirty) if the attribute did not have any
   1823                         // value or if the value was different.
   1824                         if (override || current == null || !current.equals(value)) {
   1825                             ((IUiSettableAttributeNode) uiAttr).setCurrentValue(value);
   1826                             // mark the attribute as dirty since their internal content
   1827                             // as been modified, but not the underlying XML model
   1828                             uiAttr.setDirty(true);
   1829                             return uiAttr;
   1830                         }
   1831                     }
   1832 
   1833                     // We found the attribute but it's not settable. Since attributes are
   1834                     // not duplicated, just abandon here.
   1835                     break;
   1836                 }
   1837             }
   1838         }
   1839 
   1840         return null;
   1841     }
   1842 
   1843     /**
   1844      * Utility method to retrieve the internal value of an attribute.
   1845      * <p/>
   1846      * Note that this retrieves the *field* value if the attribute has some UI, and
   1847      * not the actual XML value. They may differ if the attribute is dirty.
   1848      *
   1849      * @param attrXmlName The XML name of the attribute to modify
   1850      * @return The current internal value for the attribute or null in case of error.
   1851      */
   1852     public String getAttributeValue(String attrXmlName) {
   1853         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
   1854 
   1855         for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
   1856             AttributeDescriptor uiDesc = entry.getKey();
   1857             if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
   1858                 UiAttributeNode uiAttr = entry.getValue();
   1859                 return uiAttr.getCurrentValue();
   1860             }
   1861         }
   1862         return null;
   1863     }
   1864 
   1865     // ------ IPropertySource methods
   1866 
   1867     @Override
   1868     public Object getEditableValue() {
   1869         return null;
   1870     }
   1871 
   1872     /*
   1873      * (non-Javadoc)
   1874      * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors()
   1875      *
   1876      * Returns the property descriptor for this node. Since the descriptors are not linked to the
   1877      * data, the AttributeDescriptor are used directly.
   1878      */
   1879     @Override
   1880     public IPropertyDescriptor[] getPropertyDescriptors() {
   1881         List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>();
   1882 
   1883         // get the standard descriptors
   1884         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
   1885         Set<AttributeDescriptor> keys = attributeMap.keySet();
   1886 
   1887 
   1888         // we only want the descriptor that do implement the IPropertyDescriptor interface.
   1889         for (AttributeDescriptor key : keys) {
   1890             if (key instanceof IPropertyDescriptor) {
   1891                 propDescs.add((IPropertyDescriptor)key);
   1892             }
   1893         }
   1894 
   1895         // now get the descriptor from the unknown attributes
   1896         for (UiAttributeNode unknownNode : mUnknownUiAttributes) {
   1897             if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) {
   1898                 propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor());
   1899             }
   1900         }
   1901 
   1902         // TODO cache this maybe, as it's not going to change (except for unknown descriptors)
   1903         return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]);
   1904     }
   1905 
   1906     /*
   1907      * (non-Javadoc)
   1908      * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object)
   1909      *
   1910      * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(),
   1911      * which return the AttributeDescriptor itself.
   1912      */
   1913     @Override
   1914     public Object getPropertyValue(Object id) {
   1915         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
   1916 
   1917         UiAttributeNode attribute = attributeMap.get(id);
   1918 
   1919         if (attribute == null) {
   1920             // look for the id in the unknown attributes.
   1921             for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
   1922                 if (id == unknownAttr.getDescriptor()) {
   1923                     return unknownAttr;
   1924                 }
   1925             }
   1926         }
   1927 
   1928         return attribute;
   1929     }
   1930 
   1931     /*
   1932      * (non-Javadoc)
   1933      * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object)
   1934      *
   1935      * Returns whether the property is set. In our case this is if the string is non empty.
   1936      */
   1937     @Override
   1938     public boolean isPropertySet(Object id) {
   1939         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
   1940 
   1941         UiAttributeNode attribute = attributeMap.get(id);
   1942 
   1943         if (attribute != null) {
   1944             return attribute.getCurrentValue().length() > 0;
   1945         }
   1946 
   1947         // look for the id in the unknown attributes.
   1948         for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
   1949             if (id == unknownAttr.getDescriptor()) {
   1950                 return unknownAttr.getCurrentValue().length() > 0;
   1951             }
   1952         }
   1953 
   1954         return false;
   1955     }
   1956 
   1957     /*
   1958      * (non-Javadoc)
   1959      * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object)
   1960      *
   1961      * Reset the property to its default value. For now we simply empty it.
   1962      */
   1963     @Override
   1964     public void resetPropertyValue(Object id) {
   1965         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
   1966 
   1967         UiAttributeNode attribute = attributeMap.get(id);
   1968         if (attribute != null) {
   1969             // TODO: reset the value of the attribute
   1970 
   1971             return;
   1972         }
   1973 
   1974         // look for the id in the unknown attributes.
   1975         for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
   1976             if (id == unknownAttr.getDescriptor()) {
   1977                 // TODO: reset the value of the attribute
   1978 
   1979                 return;
   1980             }
   1981         }
   1982     }
   1983 
   1984     /*
   1985      * (non-Javadoc)
   1986      * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object)
   1987      *
   1988      * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the
   1989      * AttributeDescriptor itself. Value should be a String.
   1990      */
   1991     @Override
   1992     public void setPropertyValue(Object id, Object value) {
   1993         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
   1994 
   1995         UiAttributeNode attribute = attributeMap.get(id);
   1996 
   1997         if (attribute == null) {
   1998             // look for the id in the unknown attributes.
   1999             for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
   2000                 if (id == unknownAttr.getDescriptor()) {
   2001                     attribute = unknownAttr;
   2002                     break;
   2003                 }
   2004             }
   2005         }
   2006 
   2007         if (attribute != null) {
   2008 
   2009             // get the current value and compare it to the new value
   2010             String oldValue = attribute.getCurrentValue();
   2011             final String newValue = (String)value;
   2012 
   2013             if (oldValue.equals(newValue)) {
   2014                 return;
   2015             }
   2016 
   2017             final UiAttributeNode fAttribute = attribute;
   2018             AndroidXmlEditor editor = getEditor();
   2019             editor.wrapEditXmlModel(new Runnable() {
   2020                 @Override
   2021                 public void run() {
   2022                     commitAttributeToXml(fAttribute, newValue);
   2023                 }
   2024             });
   2025         }
   2026     }
   2027 
   2028     /**
   2029      * Returns true if this node is an ancestor (parent, grandparent, and so on)
   2030      * of the given node. Note that a node is not considered an ancestor of
   2031      * itself.
   2032      *
   2033      * @param node the node to test
   2034      * @return true if this node is an ancestor of the given node
   2035      */
   2036     public boolean isAncestorOf(UiElementNode node) {
   2037         node = node.getUiParent();
   2038         while (node != null) {
   2039             if (node == this) {
   2040                 return true;
   2041             }
   2042             node = node.getUiParent();
   2043         }
   2044         return false;
   2045     }
   2046 
   2047     /**
   2048      * Finds the nearest common parent of the two given nodes (which could be one of the
   2049      * two nodes as well)
   2050      *
   2051      * @param node1 the first node to test
   2052      * @param node2 the second node to test
   2053      * @return the nearest common parent of the two given nodes
   2054      */
   2055     public static UiElementNode getCommonAncestor(UiElementNode node1, UiElementNode node2) {
   2056         while (node2 != null) {
   2057             UiElementNode current = node1;
   2058             while (current != null && current != node2) {
   2059                 current = current.getUiParent();
   2060             }
   2061             if (current == node2) {
   2062                 return current;
   2063             }
   2064             node2 = node2.getUiParent();
   2065         }
   2066 
   2067         return null;
   2068     }
   2069 
   2070     // ---- Global node create/delete Listeners ----
   2071 
   2072     /** List of listeners to be notified of newly created nodes, or null */
   2073     private static List<NodeCreationListener> sListeners;
   2074 
   2075     /** Notify listeners that a new node has been created */
   2076     private void fireNodeCreated(UiElementNode newChild, int index) {
   2077         // Nothing to do if there aren't any listeners. We don't need to worry about
   2078         // the case where one thread is firing node changes while another is adding a listener
   2079         // (in that case it's still okay for this node firing not to be heard) so perform
   2080         // the check outside of synchronization.
   2081         if (sListeners == null) {
   2082             return;
   2083         }
   2084         synchronized (UiElementNode.class) {
   2085             if (sListeners != null) {
   2086                 UiElementNode parent = newChild.getUiParent();
   2087                 for (NodeCreationListener listener : sListeners) {
   2088                     listener.nodeCreated(parent, newChild, index);
   2089                 }
   2090             }
   2091         }
   2092     }
   2093 
   2094     /** Notify listeners that a new node has been deleted */
   2095     private void fireNodeDeleted(UiElementNode oldChild, int index) {
   2096         if (sListeners == null) {
   2097             return;
   2098         }
   2099         synchronized (UiElementNode.class) {
   2100             if (sListeners != null) {
   2101                 UiElementNode parent = oldChild.getUiParent();
   2102                 for (NodeCreationListener listener : sListeners) {
   2103                     listener.nodeDeleted(parent, oldChild, index);
   2104                 }
   2105             }
   2106         }
   2107     }
   2108 
   2109     /**
   2110      * Adds a {@link NodeCreationListener} to be notified when new nodes are created
   2111      *
   2112      * @param listener the listener to be notified
   2113      */
   2114     public static void addNodeCreationListener(NodeCreationListener listener) {
   2115         synchronized (UiElementNode.class) {
   2116             if (sListeners == null) {
   2117                 sListeners = new ArrayList<NodeCreationListener>(1);
   2118             }
   2119             sListeners.add(listener);
   2120         }
   2121     }
   2122 
   2123     /**
   2124      * Removes a {@link NodeCreationListener} from the set of listeners such that it is
   2125      * no longer notified when nodes are created.
   2126      *
   2127      * @param listener the listener to be removed from the notification list
   2128      */
   2129     public static void removeNodeCreationListener(NodeCreationListener listener) {
   2130         synchronized (UiElementNode.class) {
   2131             sListeners.remove(listener);
   2132             if (sListeners.size() == 0) {
   2133                 sListeners = null;
   2134             }
   2135         }
   2136     }
   2137 
   2138     /** Interface implemented by listeners to be notified of newly created nodes */
   2139     public interface NodeCreationListener {
   2140         /**
   2141          * Called when a new child node is created and added to the given parent
   2142          *
   2143          * @param parent the parent of the created node
   2144          * @param child the newly node
   2145          * @param index the index among the siblings of the child <b>after</b>
   2146          *            insertion
   2147          */
   2148         void nodeCreated(UiElementNode parent, UiElementNode child, int index);
   2149 
   2150         /**
   2151          * Called when a child node is removed from the given parent
   2152          *
   2153          * @param parent the parent of the removed node
   2154          * @param child the removed node
   2155          * @param previousIndex the index among the siblings of the child
   2156          *            <b>before</b> removal
   2157          */
   2158         void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex);
   2159     }
   2160 }
   2161