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 */ 141 public UiElementNode(ElementDescriptor elementDescriptor) { 142 mDescriptor = elementDescriptor; 143 clearContent(); 144 } 145 146 @Override 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 480 public UiElementNode getUiParent() { 481 return mUiParent; 482 } 483 484 /** 485 * Returns the root {@link UiElementNode}. 486 * 487 * @return The root {@link UiElementNode}. 488 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 588 public AndroidTargetData getAndroidTarget() { 589 return getEditor().getTargetData(); 590 } 591 592 /** 593 * @return A read-only version of the children collection. 594 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 // Notify per-node listeners 1088 invokeUiUpdateListeners(UiUpdateState.CREATED); 1089 // Notify global listeners 1090 fireNodeCreated(this, getUiSiblingIndex()); 1091 1092 return mXmlNode; 1093 } 1094 1095 /** 1096 * Removes the XML node corresponding to this UI node if it exists 1097 * and also removes all mirrored information in this UI node (i.e. children, attributes) 1098 * 1099 * @return The removed node or null if it didn't exist in the first place. 1100 */ 1101 public Node deleteXmlNode() { 1102 if (mXmlNode == null) { 1103 return null; 1104 } 1105 1106 int previousIndex = getUiSiblingIndex(); 1107 1108 // First clear the internals of the node and *then* actually deletes the XML 1109 // node (because doing so will generate an update even and this node may be 1110 // revisited via loadFromXmlNode). 1111 Node oldXmlNode = mXmlNode; 1112 clearContent(); 1113 1114 Node xmlParent = oldXmlNode.getParentNode(); 1115 if (xmlParent == null) { 1116 xmlParent = getXmlDocument(); 1117 } 1118 Node previousSibling = oldXmlNode.getPreviousSibling(); 1119 oldXmlNode = xmlParent.removeChild(oldXmlNode); 1120 1121 // We need to remove the text node BEFORE the removed element, since THAT's the 1122 // indentation node for the removed element. 1123 if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE 1124 && previousSibling.getNodeValue().trim().length() == 0) { 1125 xmlParent.removeChild(previousSibling); 1126 } 1127 1128 invokeUiUpdateListeners(UiUpdateState.DELETED); 1129 fireNodeDeleted(this, previousIndex); 1130 1131 return oldXmlNode; 1132 } 1133 1134 /** 1135 * Updates the element list for this UiElementNode. 1136 * At the end, the list of children UiElementNode here will match the one from the 1137 * provided XML {@link Node}: 1138 * <ul> 1139 * <li> Walk both the current ui children list and the xml children list at the same time. 1140 * <li> If we have a new xml child but already reached the end of the ui child list, add the 1141 * new xml node. 1142 * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so, 1143 * move it here. It means the XML child list has been reordered. 1144 * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list. 1145 * <li> At the end, we may have finished walking the xml child list but still have remaining 1146 * ui children, simply delete them as they matching trailing xml nodes that have been 1147 * removed unless they are mandatory ui nodes. 1148 * </ul> 1149 * Note that only the first case is used when populating the ui list the first time. 1150 * 1151 * @param xmlNode The XML node to mirror 1152 * @return True when the XML structure has changed. 1153 */ 1154 protected boolean updateElementList(Node xmlNode) { 1155 boolean structureChanged = false; 1156 boolean hasMandatoryLast = false; 1157 int uiIndex = 0; 1158 Node xmlChild = xmlNode.getFirstChild(); 1159 while (xmlChild != null) { 1160 if (xmlChild.getNodeType() == Node.ELEMENT_NODE) { 1161 String elementName = xmlChild.getNodeName(); 1162 UiElementNode uiNode = null; 1163 if (mUiChildren.size() <= uiIndex) { 1164 // A new node is being added at the end of the list 1165 ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName, 1166 false /* recursive */); 1167 if (desc == null) { 1168 // Unknown node. Create a temporary descriptor for it. 1169 // We'll add unknown attributes to it later. 1170 IUnknownDescriptorProvider p = getUnknownDescriptorProvider(); 1171 desc = p.getDescriptor(elementName); 1172 } 1173 structureChanged = true; 1174 uiNode = appendNewUiChild(desc); 1175 uiIndex++; 1176 } else { 1177 // A new node is being inserted or moved. 1178 // Note: mandatory nodes can be created without an XML node in which case 1179 // getXmlNode() is null. 1180 UiElementNode uiChild; 1181 int n = mUiChildren.size(); 1182 for (int j = uiIndex; j < n; j++) { 1183 uiChild = mUiChildren.get(j); 1184 if (uiChild.getXmlNode() != null && uiChild.getXmlNode() == xmlChild) { 1185 if (j > uiIndex) { 1186 // Found the same XML node at some later index, now move it here. 1187 mUiChildren.remove(j); 1188 mUiChildren.add(uiIndex, uiChild); 1189 structureChanged = true; 1190 } 1191 uiNode = uiChild; 1192 uiIndex++; 1193 break; 1194 } 1195 } 1196 1197 if (uiNode == null) { 1198 // Look for an unused mandatory node with no XML node attached 1199 // referencing the same XML element name 1200 for (int j = uiIndex; j < n; j++) { 1201 uiChild = mUiChildren.get(j); 1202 if (uiChild.getXmlNode() == null && 1203 uiChild.getDescriptor().getMandatory() != 1204 Mandatory.NOT_MANDATORY && 1205 uiChild.getDescriptor().getXmlName().equals(elementName)) { 1206 1207 if (j > uiIndex) { 1208 // Found it, now move it here 1209 mUiChildren.remove(j); 1210 mUiChildren.add(uiIndex, uiChild); 1211 } 1212 // Assign the XML node to this empty mandatory element. 1213 uiChild.mXmlNode = xmlChild; 1214 structureChanged = true; 1215 uiNode = uiChild; 1216 uiIndex++; 1217 } 1218 } 1219 } 1220 1221 if (uiNode == null) { 1222 // Inserting new node 1223 ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName, 1224 false /* recursive */); 1225 if (desc == null) { 1226 // Unknown element. Simply ignore it. 1227 AdtPlugin.log(IStatus.WARNING, 1228 "AndroidManifest: Ignoring unknown '%s' XML element", //$NON-NLS-1$ 1229 elementName); 1230 } else { 1231 structureChanged = true; 1232 uiNode = insertNewUiChild(uiIndex, desc); 1233 uiIndex++; 1234 } 1235 } 1236 } 1237 if (uiNode != null) { 1238 // If we touched an UI Node, even an existing one, refresh its content. 1239 // For new nodes, this will populate them recursively. 1240 structureChanged |= uiNode.loadFromXmlNode(xmlChild); 1241 1242 // Remember if there are any mandatory-last nodes to reorder. 1243 hasMandatoryLast |= 1244 uiNode.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST; 1245 } 1246 } 1247 xmlChild = xmlChild.getNextSibling(); 1248 } 1249 1250 // There might be extra UI nodes at the end if the XML node list got shorter. 1251 for (int index = mUiChildren.size() - 1; index >= uiIndex; --index) { 1252 structureChanged |= removeUiChildAtIndex(index); 1253 } 1254 1255 if (hasMandatoryLast) { 1256 // At least one mandatory-last uiNode was moved. Let's see if we can 1257 // move them back to the last position. That's possible if the only 1258 // thing between these and the end are other mandatory empty uiNodes 1259 // (mandatory uiNodes with no XML attached are pure "virtual" reserved 1260 // slots and it's ok to reorganize them but other can't.) 1261 int n = mUiChildren.size() - 1; 1262 for (int index = n; index >= 0; index--) { 1263 UiElementNode uiChild = mUiChildren.get(index); 1264 Mandatory mand = uiChild.getDescriptor().getMandatory(); 1265 if (mand == Mandatory.MANDATORY_LAST && index < n) { 1266 // Remove it from index and move it back at the end of the list. 1267 mUiChildren.remove(index); 1268 mUiChildren.add(uiChild); 1269 } else if (mand == Mandatory.NOT_MANDATORY || uiChild.getXmlNode() != null) { 1270 // We found at least one non-mandatory or a mandatory node with an actual 1271 // XML attached, so there's nothing we can reorganize past this point. 1272 break; 1273 } 1274 } 1275 } 1276 1277 return structureChanged; 1278 } 1279 1280 /** 1281 * Internal helper to remove an UI child node given by its index in the 1282 * internal child list. 1283 * 1284 * Also invokes the update listener on the node to be deleted *after* the node has 1285 * been removed. 1286 * 1287 * @param uiIndex The index of the UI child to remove, range 0 .. mUiChildren.size()-1 1288 * @return True if the structure has changed 1289 * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you 1290 * know that could never happen unless the computer is on fire or something. 1291 */ 1292 private boolean removeUiChildAtIndex(int uiIndex) { 1293 UiElementNode uiNode = mUiChildren.get(uiIndex); 1294 ElementDescriptor desc = uiNode.getDescriptor(); 1295 1296 try { 1297 if (uiNode.getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) { 1298 // This is a mandatory node. Such a node must exist in the UiNode hierarchy 1299 // even if there's no XML counterpart. However we only need to keep one. 1300 1301 // Check if the parent (e.g. this node) has another similar ui child node. 1302 boolean keepNode = true; 1303 for (UiElementNode child : mUiChildren) { 1304 if (child != uiNode && child.getDescriptor() == desc) { 1305 // We found another child with the same descriptor that is not 1306 // the node we want to remove. This means we have one mandatory 1307 // node so we can safely remove uiNode. 1308 keepNode = false; 1309 break; 1310 } 1311 } 1312 1313 if (keepNode) { 1314 // We can't remove a mandatory node as we need to keep at least one 1315 // mandatory node in the parent. Instead we just clear its content 1316 // (including its XML Node reference). 1317 1318 // A mandatory node with no XML means it doesn't really exist, so it can't be 1319 // deleted. So the structure will change only if the ui node is actually 1320 // associated to an XML node. 1321 boolean xmlExists = (uiNode.getXmlNode() != null); 1322 1323 uiNode.clearContent(); 1324 return xmlExists; 1325 } 1326 } 1327 1328 mUiChildren.remove(uiIndex); 1329 1330 return true; 1331 } finally { 1332 // Tell listeners that a node has been removed. 1333 // The model has already been modified. 1334 invokeUiUpdateListeners(UiUpdateState.DELETED); 1335 } 1336 } 1337 1338 /** 1339 * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor} 1340 * and appends it to the end of the element children list. 1341 * 1342 * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node. 1343 * @return The new UI node that has been appended 1344 */ 1345 public UiElementNode appendNewUiChild(ElementDescriptor descriptor) { 1346 UiElementNode uiNode; 1347 uiNode = descriptor.createUiNode(); 1348 mUiChildren.add(uiNode); 1349 uiNode.setUiParent(this); 1350 uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED); 1351 return uiNode; 1352 } 1353 1354 /** 1355 * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor} 1356 * and inserts it in the element children list at the specified position. 1357 * 1358 * @param index The position where to insert in the element children list. 1359 * Shifts the element currently at that position (if any) and any 1360 * subsequent elements to the right (adds one to their indices). 1361 * Index must >= 0 and <= getUiChildren.size(). 1362 * Using size() means to append to the end of the list. 1363 * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node. 1364 * @return The new UI node. 1365 */ 1366 public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) { 1367 UiElementNode uiNode; 1368 uiNode = descriptor.createUiNode(); 1369 mUiChildren.add(index, uiNode); 1370 uiNode.setUiParent(this); 1371 uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED); 1372 return uiNode; 1373 } 1374 1375 /** 1376 * Updates the {@link UiAttributeNode} list for this {@link UiElementNode} 1377 * using the values from the XML element. 1378 * <p/> 1379 * For a given {@link UiElementNode}, the attribute list always exists in 1380 * full and is totally independent of whether the XML model actually 1381 * has the corresponding attributes. 1382 * <p/> 1383 * For each attribute declared in this {@link UiElementNode}, get 1384 * the corresponding XML attribute. It may not exist, in which case the 1385 * value will be null. We don't really know if a value has changed, so 1386 * the updateValue() is called on the UI attribute in all cases. 1387 * 1388 * @param xmlNode The XML node to mirror 1389 */ 1390 protected void updateAttributeList(Node xmlNode) { 1391 NamedNodeMap xmlAttrMap = xmlNode.getAttributes(); 1392 HashSet<Node> visited = new HashSet<Node>(); 1393 1394 // For all known (i.e. expected) UI attributes, find an existing XML attribute of 1395 // same (uri, local name) and update the internal Ui attribute value. 1396 for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) { 1397 AttributeDescriptor desc = uiAttr.getDescriptor(); 1398 if (!(desc instanceof SeparatorAttributeDescriptor)) { 1399 Node xmlAttr = xmlAttrMap == null ? null : 1400 xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName()); 1401 uiAttr.updateValue(xmlAttr); 1402 visited.add(xmlAttr); 1403 } 1404 } 1405 1406 // Clone the current list of unknown attributes. We'll then remove from this list when 1407 // we find attributes which are still unknown. What will be left are the old unknown 1408 // attributes that have been deleted in the current XML attribute list. 1409 @SuppressWarnings("unchecked") 1410 HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone(); 1411 1412 // We need to ignore hidden attributes. 1413 Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors(); 1414 1415 // Traverse the actual XML attribute list to find unknown attributes 1416 if (xmlAttrMap != null) { 1417 for (int i = 0; i < xmlAttrMap.getLength(); i++) { 1418 Node xmlAttr = xmlAttrMap.item(i); 1419 // Ignore attributes which have actual descriptors 1420 if (visited.contains(xmlAttr)) { 1421 continue; 1422 } 1423 1424 String xmlFullName = xmlAttr.getNodeName(); 1425 1426 // Ignore attributes which are hidden (based on the prefix:localName key) 1427 if (hiddenAttrDesc.containsKey(xmlFullName)) { 1428 continue; 1429 } 1430 1431 String xmlAttrLocalName = xmlAttr.getLocalName(); 1432 String xmlNsUri = xmlAttr.getNamespaceURI(); 1433 1434 UiAttributeNode uiAttr = null; 1435 for (UiAttributeNode a : mUnknownUiAttributes) { 1436 String aLocalName = a.getDescriptor().getXmlLocalName(); 1437 String aNsUri = a.getDescriptor().getNamespaceUri(); 1438 if (aLocalName.equals(xmlAttrLocalName) && 1439 (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) { 1440 // This attribute is still present in the unknown list 1441 uiAttr = a; 1442 // It has not been deleted 1443 deleted.remove(a); 1444 break; 1445 } 1446 } 1447 if (uiAttr == null) { 1448 uiAttr = addUnknownAttribute(xmlFullName, xmlAttrLocalName, xmlNsUri); 1449 } 1450 1451 uiAttr.updateValue(xmlAttr); 1452 } 1453 1454 // Remove from the internal list unknown attributes that have been deleted from the xml 1455 for (UiAttributeNode a : deleted) { 1456 mUnknownUiAttributes.remove(a); 1457 mCachedAllUiAttributes = null; 1458 } 1459 } 1460 } 1461 1462 /** 1463 * Create a new temporary text attribute descriptor for the unknown attribute 1464 * and returns a new {@link UiAttributeNode} associated to this descriptor. 1465 * <p/> 1466 * The attribute is not marked as dirty, doing so is up to the caller. 1467 */ 1468 private UiAttributeNode addUnknownAttribute(String xmlFullName, 1469 String xmlAttrLocalName, String xmlNsUri) { 1470 // Create a new unknown attribute of format string 1471 TextAttributeDescriptor desc = new TextAttributeDescriptor( 1472 xmlAttrLocalName, // xml name 1473 xmlFullName, // ui name 1474 xmlNsUri, // NS uri 1475 "Unknown XML attribute", // tooltip, translatable 1476 new AttributeInfo(xmlAttrLocalName, new Format[] { Format.STRING } ) 1477 ); 1478 UiAttributeNode uiAttr = desc.createUiNode(this); 1479 mUnknownUiAttributes.add(uiAttr); 1480 mCachedAllUiAttributes = null; 1481 return uiAttr; 1482 } 1483 1484 /** 1485 * Invoke all registered {@link IUiUpdateListener} listening on this UI update for this node. 1486 */ 1487 protected void invokeUiUpdateListeners(UiUpdateState state) { 1488 if (mUiUpdateListeners != null) { 1489 for (IUiUpdateListener listener : mUiUpdateListeners) { 1490 try { 1491 listener.uiElementNodeUpdated(this, state); 1492 } catch (Exception e) { 1493 // prevent a crashing listener from crashing the whole invocation chain 1494 AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s", //$NON-NLS-1$ 1495 getBreadcrumbTrailDescription(true), 1496 state.toString()); 1497 } 1498 } 1499 } 1500 } 1501 1502 // --- for derived implementations only --- 1503 1504 @VisibleForTesting 1505 public void setXmlNode(Node xmlNode) { 1506 mXmlNode = xmlNode; 1507 } 1508 1509 public void refreshUi() { 1510 invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED); 1511 } 1512 1513 1514 // ------------- Helpers 1515 1516 /** 1517 * Helper method to commit a single attribute value to XML. 1518 * <p/> 1519 * This method updates the XML regardless of the current XML value. 1520 * Callers should check first if an update is needed. 1521 * If the new value is empty, the XML attribute will be actually removed. 1522 * <p/> 1523 * Note that the caller MUST ensure that modifying the underlying XML model is 1524 * safe and must take care of marking the model as dirty if necessary. 1525 * 1526 * @see AndroidXmlEditor#wrapEditXmlModel(Runnable) 1527 * 1528 * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode. 1529 * @param newValue The new value to set. 1530 * @return True if the XML attribute was modified or removed, false if nothing changed. 1531 */ 1532 public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) { 1533 // Get (or create) the underlying XML element node that contains the attributes. 1534 Node element = prepareCommit(); 1535 if (element != null && uiAttr != null) { 1536 String attrLocalName = uiAttr.getDescriptor().getXmlLocalName(); 1537 String attrNsUri = uiAttr.getDescriptor().getNamespaceUri(); 1538 1539 NamedNodeMap attrMap = element.getAttributes(); 1540 if (newValue == null || newValue.length() == 0) { 1541 // Remove attribute if it's empty 1542 if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) { 1543 attrMap.removeNamedItemNS(attrNsUri, attrLocalName); 1544 return true; 1545 } 1546 } else { 1547 // Add or replace an attribute 1548 Document doc = element.getOwnerDocument(); 1549 if (doc != null) { 1550 Attr attr; 1551 if (attrNsUri != null && attrNsUri.length() > 0) { 1552 attr = (Attr) attrMap.getNamedItemNS(attrNsUri, attrLocalName); 1553 if (attr == null) { 1554 attr = doc.createAttributeNS(attrNsUri, attrLocalName); 1555 attr.setPrefix(lookupNamespacePrefix(element, attrNsUri)); 1556 attrMap.setNamedItemNS(attr); 1557 } 1558 } else { 1559 attr = (Attr) attrMap.getNamedItem(attrLocalName); 1560 if (attr == null) { 1561 attr = doc.createAttribute(attrLocalName); 1562 attrMap.setNamedItem(attr); 1563 } 1564 } 1565 attr.setValue(newValue); 1566 return true; 1567 } 1568 } 1569 } 1570 return false; 1571 } 1572 1573 /** 1574 * Helper method to commit all dirty attributes values to XML. 1575 * <p/> 1576 * This method is useful if {@link #setAttributeValue(String, String, String, boolean)} has 1577 * been called more than once and all the attributes marked as dirty must be committed to 1578 * the XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty 1579 * attribute. 1580 * <p/> 1581 * Note that the caller MUST ensure that modifying the underlying XML model is 1582 * safe and must take care of marking the model as dirty if necessary. 1583 * 1584 * @see AndroidXmlEditor#wrapEditXmlModel(Runnable) 1585 * 1586 * @return True if one or more values were actually modified or removed, 1587 * false if nothing changed. 1588 */ 1589 @SuppressWarnings("null") // Eclipse is confused by the logic and gets it wrong 1590 public boolean commitDirtyAttributesToXml() { 1591 boolean result = false; 1592 List<UiAttributeNode> dirtyAttributes = new ArrayList<UiAttributeNode>(); 1593 for (UiAttributeNode uiAttr : getAllUiAttributes()) { 1594 if (uiAttr.isDirty()) { 1595 String value = uiAttr.getCurrentValue(); 1596 if (value != null && value.length() > 0) { 1597 // Defer the new attributes: set these last and in order 1598 dirtyAttributes.add(uiAttr); 1599 } else { 1600 result |= commitAttributeToXml(uiAttr, value); 1601 uiAttr.setDirty(false); 1602 } 1603 } 1604 } 1605 if (dirtyAttributes.size() > 0) { 1606 result = true; 1607 1608 Collections.sort(dirtyAttributes); 1609 1610 // The Eclipse XML model will *always* append new attributes. 1611 // Therefore, if any of the dirty attributes are new, they will appear 1612 // after any existing, clean attributes on the element. To fix this, 1613 // we need to first remove any of these attributes, then insert them 1614 // back in the right order. 1615 Node element = prepareCommit(); 1616 if (element == null) { 1617 return result; 1618 } 1619 1620 if (AdtPrefs.getPrefs().getFormatGuiXml() && getEditor().supportsFormatOnGuiEdit()) { 1621 // If auto formatting, don't bother with attribute sorting here since the 1622 // order will be corrected as soon as the edit is committed anyway 1623 for (UiAttributeNode uiAttribute : dirtyAttributes) { 1624 commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue()); 1625 uiAttribute.setDirty(false); 1626 } 1627 1628 return result; 1629 } 1630 1631 String firstName = dirtyAttributes.get(0).getDescriptor().getXmlLocalName(); 1632 NamedNodeMap attributes = ((Element) element).getAttributes(); 1633 List<Attr> move = new ArrayList<Attr>(); 1634 for (int i = 0, n = attributes.getLength(); i < n; i++) { 1635 Attr attribute = (Attr) attributes.item(i); 1636 if (UiAttributeNode.compareAttributes(attribute.getLocalName(), firstName) > 0) { 1637 move.add(attribute); 1638 } 1639 } 1640 1641 for (Attr attribute : move) { 1642 if (attribute.getNamespaceURI() != null) { 1643 attributes.removeNamedItemNS(attribute.getNamespaceURI(), 1644 attribute.getLocalName()); 1645 } else { 1646 attributes.removeNamedItem(attribute.getName()); 1647 } 1648 } 1649 1650 // Merge back the removed DOM attribute nodes and the new UI attribute nodes. 1651 // In cases where the attribute DOM name and the UI attribute names equal, 1652 // skip the DOM nodes and just apply the UI attributes. 1653 int domAttributeIndex = 0; 1654 int domAttributeIndexMax = move.size(); 1655 int uiAttributeIndex = 0; 1656 int uiAttributeIndexMax = dirtyAttributes.size(); 1657 1658 while (true) { 1659 Attr domAttribute; 1660 UiAttributeNode uiAttribute; 1661 1662 int compare; 1663 if (uiAttributeIndex < uiAttributeIndexMax) { 1664 if (domAttributeIndex < domAttributeIndexMax) { 1665 domAttribute = move.get(domAttributeIndex); 1666 uiAttribute = dirtyAttributes.get(uiAttributeIndex); 1667 1668 String domAttributeName = domAttribute.getLocalName(); 1669 String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName(); 1670 compare = UiAttributeNode.compareAttributes(domAttributeName, 1671 uiAttributeName); 1672 } else { 1673 compare = 1; 1674 uiAttribute = dirtyAttributes.get(uiAttributeIndex); 1675 domAttribute = null; 1676 } 1677 } else if (domAttributeIndex < domAttributeIndexMax) { 1678 compare = -1; 1679 domAttribute = move.get(domAttributeIndex); 1680 uiAttribute = null; 1681 } else { 1682 break; 1683 } 1684 1685 if (compare < 0) { 1686 if (domAttribute.getNamespaceURI() != null) { 1687 attributes.setNamedItemNS(domAttribute); 1688 } else { 1689 attributes.setNamedItem(domAttribute); 1690 } 1691 domAttributeIndex++; 1692 } else { 1693 assert compare >= 0; 1694 if (compare == 0) { 1695 domAttributeIndex++; 1696 } 1697 commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue()); 1698 uiAttribute.setDirty(false); 1699 uiAttributeIndex++; 1700 } 1701 } 1702 } 1703 1704 return result; 1705 } 1706 1707 /** 1708 * Returns the namespace prefix matching the requested namespace URI. 1709 * If no such declaration is found, returns the default "android" prefix. 1710 * 1711 * @param node The current node. Must not be null. 1712 * @param nsUri The namespace URI of which the prefix is to be found, 1713 * e.g. SdkConstants.NS_RESOURCES 1714 * @return The first prefix declared or the default "android" prefix. 1715 */ 1716 public static String lookupNamespacePrefix(Node node, String nsUri) { 1717 // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java 1718 // The following code emulates this simple call: 1719 // String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES); 1720 1721 // if the requested URI is null, it denotes an attribute with no namespace. 1722 if (nsUri == null) { 1723 return null; 1724 } 1725 1726 // per XML specification, the "xmlns" URI is reserved 1727 if (XMLNS_URI.equals(nsUri)) { 1728 return XMLNS; 1729 } 1730 1731 HashSet<String> visited = new HashSet<String>(); 1732 Document doc = node == null ? null : node.getOwnerDocument(); 1733 1734 // Ask the document about it. This method may not be implemented by the Document. 1735 String nsPrefix = null; 1736 try { 1737 nsPrefix = doc != null ? doc.lookupPrefix(nsUri) : null; 1738 if (nsPrefix != null) { 1739 return nsPrefix; 1740 } 1741 } catch (Throwable t) { 1742 // ignore 1743 } 1744 1745 // If that failed, try to look it up manually. 1746 // This also gathers prefixed in use in the case we want to generate a new one below. 1747 for (; node != null && node.getNodeType() == Node.ELEMENT_NODE; 1748 node = node.getParentNode()) { 1749 NamedNodeMap attrs = node.getAttributes(); 1750 for (int n = attrs.getLength() - 1; n >= 0; --n) { 1751 Node attr = attrs.item(n); 1752 if (XMLNS.equals(attr.getPrefix())) { 1753 String uri = attr.getNodeValue(); 1754 nsPrefix = attr.getLocalName(); 1755 // Is this the URI we are looking for? If yes, we found its prefix. 1756 if (nsUri.equals(uri)) { 1757 return nsPrefix; 1758 } 1759 visited.add(nsPrefix); 1760 } 1761 } 1762 } 1763 1764 // Failed the find a prefix. Generate a new sensible default prefix. 1765 // 1766 // We need to make sure the prefix is not one that was declared in the scope 1767 // visited above. Use a default namespace prefix "android" for the Android resource 1768 // NS and use "ns" for all other custom namespaces. 1769 String prefix = NS_RESOURCES.equals(nsUri) ? ANDROID_NS_NAME : "ns"; //$NON-NLS-1$ 1770 String base = prefix; 1771 for (int i = 1; visited.contains(prefix); i++) { 1772 prefix = base + Integer.toString(i); 1773 } 1774 // Also create & define this prefix/URI in the XML document as an attribute in the 1775 // first element of the document. 1776 if (doc != null) { 1777 node = doc.getFirstChild(); 1778 while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { 1779 node = node.getNextSibling(); 1780 } 1781 if (node != null) { 1782 Attr attr = doc.createAttributeNS(XMLNS_URI, prefix); 1783 attr.setValue(nsUri); 1784 attr.setPrefix(XMLNS); 1785 node.getAttributes().setNamedItemNS(attr); 1786 } 1787 } 1788 1789 return prefix; 1790 } 1791 1792 /** 1793 * Utility method to internally set the value of a text attribute for the current 1794 * UiElementNode. 1795 * <p/> 1796 * This method is a helper. It silently ignores the errors such as the requested 1797 * attribute not being present in the element or attribute not being settable. 1798 * It accepts inherited attributes (such as layout). 1799 * <p/> 1800 * This does not commit to the XML model. It does mark the attribute node as dirty. 1801 * This is up to the caller. 1802 * 1803 * @see #commitAttributeToXml(UiAttributeNode, String) 1804 * @see #commitDirtyAttributesToXml() 1805 * 1806 * @param attrXmlName The XML <em>local</em> name of the attribute to modify 1807 * @param attrNsUri The namespace URI of the attribute. 1808 * Can be null if the attribute uses the global namespace. 1809 * @param value The new value for the attribute. If set to null, the attribute is removed. 1810 * @param override True if the value must be set even if one already exists. 1811 * @return The {@link UiAttributeNode} that has been modified or null. 1812 */ 1813 public UiAttributeNode setAttributeValue( 1814 String attrXmlName, 1815 String attrNsUri, 1816 String value, 1817 boolean override) { 1818 if (value == null) { 1819 value = ""; //$NON-NLS-1$ -- this removes an attribute 1820 } 1821 1822 getEditor().scheduleNodeReformat(this, true); 1823 1824 // Try with all internal attributes 1825 UiAttributeNode uiAttr = setInternalAttrValue( 1826 getAllUiAttributes(), attrXmlName, attrNsUri, value, override); 1827 if (uiAttr != null) { 1828 return uiAttr; 1829 } 1830 1831 if (uiAttr == null) { 1832 // Failed to find the attribute. For non-android attributes that is mostly expected, 1833 // in which case we just create a new custom one. As a side effect, we'll find the 1834 // attribute descriptor via getAllUiAttributes(). 1835 addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri); 1836 1837 // We've created the attribute, but not actually set the value on it, so let's do it. 1838 // Try with the updated internal attributes. 1839 // Implementation detail: we could just do a setCurrentValue + setDirty on the 1840 // uiAttr returned by addUnknownAttribute(); however going through setInternalAttrValue 1841 // means we won't duplicate the logic, at the expense of doing one more lookup. 1842 uiAttr = setInternalAttrValue( 1843 getAllUiAttributes(), attrXmlName, attrNsUri, value, override); 1844 } 1845 1846 return uiAttr; 1847 } 1848 1849 private UiAttributeNode setInternalAttrValue( 1850 Collection<UiAttributeNode> attributes, 1851 String attrXmlName, 1852 String attrNsUri, 1853 String value, 1854 boolean override) { 1855 1856 // For namespace less attributes (like the "layout" attribute of an <include> tag 1857 // we may be passed "" as the namespace (during an attribute copy), and it 1858 // should really be null instead. 1859 if (attrNsUri != null && attrNsUri.length() == 0) { 1860 attrNsUri = null; 1861 } 1862 1863 for (UiAttributeNode uiAttr : attributes) { 1864 AttributeDescriptor uiDesc = uiAttr.getDescriptor(); 1865 1866 if (uiDesc.getXmlLocalName().equals(attrXmlName)) { 1867 // Both NS URI must be either null or equal. 1868 if ((attrNsUri == null && uiDesc.getNamespaceUri() == null) || 1869 (attrNsUri != null && attrNsUri.equals(uiDesc.getNamespaceUri()))) { 1870 1871 // Not all attributes are editable, ignore those which are not. 1872 if (uiAttr instanceof IUiSettableAttributeNode) { 1873 String current = uiAttr.getCurrentValue(); 1874 // Only update (and mark as dirty) if the attribute did not have any 1875 // value or if the value was different. 1876 if (override || current == null || !current.equals(value)) { 1877 ((IUiSettableAttributeNode) uiAttr).setCurrentValue(value); 1878 // mark the attribute as dirty since their internal content 1879 // as been modified, but not the underlying XML model 1880 uiAttr.setDirty(true); 1881 return uiAttr; 1882 } 1883 } 1884 1885 // We found the attribute but it's not settable. Since attributes are 1886 // not duplicated, just abandon here. 1887 break; 1888 } 1889 } 1890 } 1891 1892 return null; 1893 } 1894 1895 /** 1896 * Utility method to retrieve the internal value of an attribute. 1897 * <p/> 1898 * Note that this retrieves the *field* value if the attribute has some UI, and 1899 * not the actual XML value. They may differ if the attribute is dirty. 1900 * 1901 * @param attrXmlName The XML name of the attribute to modify 1902 * @return The current internal value for the attribute or null in case of error. 1903 */ 1904 public String getAttributeValue(String attrXmlName) { 1905 HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); 1906 1907 for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) { 1908 AttributeDescriptor uiDesc = entry.getKey(); 1909 if (uiDesc.getXmlLocalName().equals(attrXmlName)) { 1910 UiAttributeNode uiAttr = entry.getValue(); 1911 return uiAttr.getCurrentValue(); 1912 } 1913 } 1914 return null; 1915 } 1916 1917 // ------ IPropertySource methods 1918 1919 public Object getEditableValue() { 1920 return null; 1921 } 1922 1923 /* 1924 * (non-Javadoc) 1925 * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors() 1926 * 1927 * Returns the property descriptor for this node. Since the descriptors are not linked to the 1928 * data, the AttributeDescriptor are used directly. 1929 */ 1930 public IPropertyDescriptor[] getPropertyDescriptors() { 1931 List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>(); 1932 1933 // get the standard descriptors 1934 HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); 1935 Set<AttributeDescriptor> keys = attributeMap.keySet(); 1936 1937 1938 // we only want the descriptor that do implement the IPropertyDescriptor interface. 1939 for (AttributeDescriptor key : keys) { 1940 if (key instanceof IPropertyDescriptor) { 1941 propDescs.add((IPropertyDescriptor)key); 1942 } 1943 } 1944 1945 // now get the descriptor from the unknown attributes 1946 for (UiAttributeNode unknownNode : mUnknownUiAttributes) { 1947 if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) { 1948 propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor()); 1949 } 1950 } 1951 1952 // TODO cache this maybe, as it's not going to change (except for unknown descriptors) 1953 return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]); 1954 } 1955 1956 /* 1957 * (non-Javadoc) 1958 * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object) 1959 * 1960 * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(), 1961 * which return the AttributeDescriptor itself. 1962 */ 1963 public Object getPropertyValue(Object id) { 1964 HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); 1965 1966 UiAttributeNode attribute = attributeMap.get(id); 1967 1968 if (attribute == null) { 1969 // look for the id in the unknown attributes. 1970 for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { 1971 if (id == unknownAttr.getDescriptor()) { 1972 return unknownAttr; 1973 } 1974 } 1975 } 1976 1977 return attribute; 1978 } 1979 1980 /* 1981 * (non-Javadoc) 1982 * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object) 1983 * 1984 * Returns whether the property is set. In our case this is if the string is non empty. 1985 */ 1986 public boolean isPropertySet(Object id) { 1987 HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); 1988 1989 UiAttributeNode attribute = attributeMap.get(id); 1990 1991 if (attribute != null) { 1992 return attribute.getCurrentValue().length() > 0; 1993 } 1994 1995 // look for the id in the unknown attributes. 1996 for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { 1997 if (id == unknownAttr.getDescriptor()) { 1998 return unknownAttr.getCurrentValue().length() > 0; 1999 } 2000 } 2001 2002 return false; 2003 } 2004 2005 /* 2006 * (non-Javadoc) 2007 * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object) 2008 * 2009 * Reset the property to its default value. For now we simply empty it. 2010 */ 2011 public void resetPropertyValue(Object id) { 2012 HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); 2013 2014 UiAttributeNode attribute = attributeMap.get(id); 2015 if (attribute != null) { 2016 // TODO: reset the value of the attribute 2017 2018 return; 2019 } 2020 2021 // look for the id in the unknown attributes. 2022 for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { 2023 if (id == unknownAttr.getDescriptor()) { 2024 // TODO: reset the value of the attribute 2025 2026 return; 2027 } 2028 } 2029 } 2030 2031 /* 2032 * (non-Javadoc) 2033 * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object) 2034 * 2035 * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the 2036 * AttributeDescriptor itself. Value should be a String. 2037 */ 2038 public void setPropertyValue(Object id, Object value) { 2039 HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); 2040 2041 UiAttributeNode attribute = attributeMap.get(id); 2042 2043 if (attribute == null) { 2044 // look for the id in the unknown attributes. 2045 for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { 2046 if (id == unknownAttr.getDescriptor()) { 2047 attribute = unknownAttr; 2048 break; 2049 } 2050 } 2051 } 2052 2053 if (attribute != null) { 2054 2055 // get the current value and compare it to the new value 2056 String oldValue = attribute.getCurrentValue(); 2057 final String newValue = (String)value; 2058 2059 if (oldValue.equals(newValue)) { 2060 return; 2061 } 2062 2063 final UiAttributeNode fAttribute = attribute; 2064 AndroidXmlEditor editor = getEditor(); 2065 editor.wrapEditXmlModel(new Runnable() { 2066 public void run() { 2067 commitAttributeToXml(fAttribute, newValue); 2068 } 2069 }); 2070 } 2071 } 2072 2073 /** 2074 * Returns true if this node is an ancestor (parent, grandparent, and so on) 2075 * of the given node. Note that a node is not considered an ancestor of 2076 * itself. 2077 * 2078 * @param node the node to test 2079 * @return true if this node is an ancestor of the given node 2080 */ 2081 public boolean isAncestorOf(UiElementNode node) { 2082 node = node.getUiParent(); 2083 while (node != null) { 2084 if (node == this) { 2085 return true; 2086 } 2087 node = node.getUiParent(); 2088 } 2089 return false; 2090 } 2091 2092 /** 2093 * Finds the nearest common parent of the two given nodes (which could be one of the 2094 * two nodes as well) 2095 * 2096 * @param node1 the first node to test 2097 * @param node2 the second node to test 2098 * @return the nearest common parent of the two given nodes 2099 */ 2100 public static UiElementNode getCommonAncestor(UiElementNode node1, UiElementNode node2) { 2101 while (node2 != null) { 2102 UiElementNode current = node1; 2103 while (current != null && current != node2) { 2104 current = current.getUiParent(); 2105 } 2106 if (current == node2) { 2107 return current; 2108 } 2109 node2 = node2.getUiParent(); 2110 } 2111 2112 return null; 2113 } 2114 2115 // ---- Global node create/delete Listeners ---- 2116 2117 /** List of listeners to be notified of newly created nodes, or null */ 2118 private static List<NodeCreationListener> sListeners; 2119 2120 /** Notify listeners that a new node has been created */ 2121 private void fireNodeCreated(UiElementNode newChild, int index) { 2122 // Nothing to do if there aren't any listeners. We don't need to worry about 2123 // the case where one thread is firing node changes while another is adding a listener 2124 // (in that case it's still okay for this node firing not to be heard) so perform 2125 // the check outside of synchronization. 2126 if (sListeners == null) { 2127 return; 2128 } 2129 synchronized (UiElementNode.class) { 2130 if (sListeners != null) { 2131 UiElementNode parent = newChild.getUiParent(); 2132 for (NodeCreationListener listener : sListeners) { 2133 listener.nodeCreated(parent, newChild, index); 2134 } 2135 } 2136 } 2137 } 2138 2139 /** Notify listeners that a new node has been deleted */ 2140 private void fireNodeDeleted(UiElementNode oldChild, int index) { 2141 if (sListeners == null) { 2142 return; 2143 } 2144 synchronized (UiElementNode.class) { 2145 if (sListeners != null) { 2146 UiElementNode parent = oldChild.getUiParent(); 2147 for (NodeCreationListener listener : sListeners) { 2148 listener.nodeDeleted(parent, oldChild, index); 2149 } 2150 } 2151 } 2152 } 2153 2154 /** 2155 * Adds a {@link NodeCreationListener} to be notified when new nodes are created 2156 * 2157 * @param listener the listener to be notified 2158 */ 2159 public static void addNodeCreationListener(NodeCreationListener listener) { 2160 synchronized (UiElementNode.class) { 2161 if (sListeners == null) { 2162 sListeners = new ArrayList<NodeCreationListener>(1); 2163 } 2164 sListeners.add(listener); 2165 } 2166 } 2167 2168 /** 2169 * Removes a {@link NodeCreationListener} from the set of listeners such that it is 2170 * no longer notified when nodes are created. 2171 * 2172 * @param listener the listener to be removed from the notification list 2173 */ 2174 public static void removeNodeCreationListener(NodeCreationListener listener) { 2175 synchronized (UiElementNode.class) { 2176 sListeners.remove(listener); 2177 if (sListeners.size() == 0) { 2178 sListeners = null; 2179 } 2180 } 2181 } 2182 2183 /** Interface implemented by listeners to be notified of newly created nodes */ 2184 public interface NodeCreationListener { 2185 /** 2186 * Called when a new child node is created and added to the given parent 2187 * 2188 * @param parent the parent of the created node 2189 * @param child the newly node 2190 * @param index the index among the siblings of the child <b>after</b> 2191 * insertion 2192 */ 2193 void nodeCreated(UiElementNode parent, UiElementNode child, int index); 2194 2195 /** 2196 * Called when a child node is removed from the given parent 2197 * 2198 * @param parent the parent of the removed node 2199 * @param child the removed node 2200 * @param previousIndex the index among the siblings of the child 2201 * <b>before</b> removal 2202 */ 2203 void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex); 2204 } 2205 } 2206