• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
UiElementNode(ElementDescriptor elementDescriptor)143     public UiElementNode(ElementDescriptor elementDescriptor) {
144         mDescriptor = elementDescriptor;
145         clearContent();
146     }
147 
148     @Override
toString()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      */
clearContent()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      */
clearAttributes()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      */
getInternalUiAttributes()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      */
getShortDescription()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 */
getDescAttribute()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      */
getStyledDescription()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      */
_Element_getAttributeNS(Element element, String namespaceURI, String localName)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      */
getBreadcrumbTrailDescription(boolean includeRoot)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      */
setXmlDocument(Document xmlDoc)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      */
getXmlDocument()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      */
getXmlNode()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      */
getDescriptor()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      */
getAttributeDescriptors()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      */
getHiddenAttributeDescriptors()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      */
setUiParent(UiElementNode parent)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      */
getUiParent()482     public UiElementNode getUiParent() {
483         return mUiParent;
484     }
485 
486     /**
487      * Returns the root {@link UiElementNode}.
488      *
489      * @return The root {@link UiElementNode}.
490      */
getUiRoot()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      */
getUiSiblingIndex()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      */
getUiPreviousSibling()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      */
getUiNextSibling()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      */
setEditor(AndroidXmlEditor editor)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      */
getEditor()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      */
getAndroidTarget()590     public AndroidTargetData getAndroidTarget() {
591         return getEditor().getTargetData();
592     }
593 
594     /**
595      * @return A read-only version of the children collection.
596      */
getUiChildren()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      */
getAllUiAttributes()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      */
getUnknownUiAttributes()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      */
setHasError(boolean errorFlag)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      */
hasError()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      */
getUnknownDescriptorProvider()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      */
setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider)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      */
addUpdateListener(IUiUpdateListener listener)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      */
removeUpdateListener(IUiUpdateListener listener)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      */
findUiChildNode(String path)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      */
findXmlNode(Node xmlNode)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      */
findUiAttribute(AttributeDescriptor attrDesc)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      */
loadFromXmlNode(Node xmlNode)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      */
reloadFromXmlNode(Node xmlNode)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      */
prepareCommit()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      */
commit()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      */
isDirty()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      */
createXmlNode()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      */
deleteXmlNode()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      */
updateElementList(Node xmlNode)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      */
removeUiChildAtIndex(int uiIndex)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      */
appendNewUiChild(ElementDescriptor descriptor)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      */
insertNewUiChild(int index, ElementDescriptor descriptor)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      */
updateAttributeList(Node xmlNode)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      */
addUnknownAttribute(String xmlFullName, String xmlAttrLocalName, String xmlNsUri)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      */
invokeUiUpdateListeners(UiUpdateState state)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
setXmlNode(Node xmlNode)1530     public void setXmlNode(Node xmlNode) {
1531         mXmlNode = xmlNode;
1532     }
1533 
refreshUi()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      */
commitAttributeToXml(UiAttributeNode uiAttr, String newValue)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
commitDirtyAttributesToXml()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      */
setAttributeValue( String attrXmlName, String attrNsUri, String value, boolean override)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 
setInternalAttrValue( Collection<UiAttributeNode> attributes, String attrXmlName, String attrNsUri, String value, boolean override)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      */
getAttributeValue(String attrXmlName)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
getEditableValue()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
getPropertyDescriptors()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
getPropertyValue(Object id)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
isPropertySet(Object id)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
resetPropertyValue(Object id)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
setPropertyValue(Object id, Object value)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      */
isAncestorOf(UiElementNode node)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      */
getCommonAncestor(UiElementNode node1, UiElementNode node2)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 */
fireNodeCreated(UiElementNode newChild, int index)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 */
fireNodeDeleted(UiElementNode oldChild, int index)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      */
addNodeCreationListener(NodeCreationListener listener)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      */
removeNodeCreationListener(NodeCreationListener listener)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          */
nodeCreated(UiElementNode parent, UiElementNode child, int index)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          */
nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex)2158         void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex);
2159     }
2160 }
2161