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