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