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