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