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