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