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