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.descriptors; 18 19 import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; 20 import static com.android.SdkConstants.ANDROID_URI; 21 22 import com.android.ide.eclipse.adt.AdtPlugin; 23 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 24 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 25 26 import org.eclipse.jface.resource.ImageDescriptor; 27 import org.eclipse.swt.graphics.Image; 28 29 import java.util.Collection; 30 import java.util.HashSet; 31 import java.util.Set; 32 33 /** 34 * {@link ElementDescriptor} describes the properties expected for a given XML element node. 35 * 36 * {@link ElementDescriptor} have an XML name, UI name, a tooltip, an SDK url, 37 * an attributes list and a children list. 38 * 39 * An UI node can be "mandatory", meaning the UI node is never deleted and it may lack 40 * an actual XML node attached. A non-mandatory UI node MUST have an XML node attached 41 * and it will cease to exist when the XML node ceases to exist. 42 */ 43 public class ElementDescriptor implements Comparable<ElementDescriptor> { 44 private static final String ELEMENT_ICON_FILENAME = "element"; //$NON-NLS-1$ 45 46 /** The XML element node name. Case sensitive. */ 47 protected final String mXmlName; 48 /** The XML element name for the user interface, typically capitalized. */ 49 private final String mUiName; 50 /** The list of allowed attributes. */ 51 private AttributeDescriptor[] mAttributes; 52 /** The list of allowed children */ 53 private ElementDescriptor[] mChildren; 54 /* An optional tooltip. Can be empty. */ 55 private String mTooltip; 56 /** An optional SKD URL. Can be empty. */ 57 private String mSdkUrl; 58 /** Whether this UI node must always exist (even for empty models). */ 59 private final Mandatory mMandatory; 60 61 public enum Mandatory { 62 NOT_MANDATORY, 63 MANDATORY, 64 MANDATORY_LAST 65 } 66 67 /** 68 * Constructs a new {@link ElementDescriptor} based on its XML name, UI name, 69 * tooltip, SDK url, attributes list, children list and mandatory. 70 * 71 * @param xml_name The XML element node name. Case sensitive. 72 * @param ui_name The XML element name for the user interface, typically capitalized. 73 * @param tooltip An optional tooltip. Can be null or empty. 74 * @param sdk_url An optional SKD URL. Can be null or empty. 75 * @param attributes The list of allowed attributes. Can be null or empty. 76 * @param children The list of allowed children. Can be null or empty. 77 * @param mandatory Whether this node must always exist (even for empty models). A mandatory 78 * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory 79 * UI node MUST have an XML node attached and it will cease to exist when the XML node 80 * ceases to exist. 81 */ 82 public ElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url, 83 AttributeDescriptor[] attributes, 84 ElementDescriptor[] children, 85 Mandatory mandatory) { 86 mMandatory = mandatory; 87 mXmlName = xml_name; 88 mUiName = ui_name; 89 mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null; 90 mSdkUrl = (sdk_url != null && sdk_url.length() > 0) ? sdk_url : null; 91 setAttributes(attributes != null ? attributes : new AttributeDescriptor[]{}); 92 mChildren = children != null ? children : new ElementDescriptor[]{}; 93 } 94 95 /** 96 * Constructs a new {@link ElementDescriptor} based on its XML name, UI name, 97 * tooltip, SDK url, attributes list, children list and mandatory. 98 * 99 * @param xml_name The XML element node name. Case sensitive. 100 * @param ui_name The XML element name for the user interface, typically capitalized. 101 * @param tooltip An optional tooltip. Can be null or empty. 102 * @param sdk_url An optional SKD URL. Can be null or empty. 103 * @param attributes The list of allowed attributes. Can be null or empty. 104 * @param children The list of allowed children. Can be null or empty. 105 * @param mandatory Whether this node must always exist (even for empty models). A mandatory 106 * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory 107 * UI node MUST have an XML node attached and it will cease to exist when the XML node 108 * ceases to exist. 109 */ 110 public ElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url, 111 AttributeDescriptor[] attributes, 112 ElementDescriptor[] children, 113 boolean mandatory) { 114 mMandatory = mandatory ? Mandatory.MANDATORY : Mandatory.NOT_MANDATORY; 115 mXmlName = xml_name; 116 mUiName = ui_name; 117 mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null; 118 mSdkUrl = (sdk_url != null && sdk_url.length() > 0) ? sdk_url : null; 119 setAttributes(attributes != null ? attributes : new AttributeDescriptor[]{}); 120 mChildren = children != null ? children : new ElementDescriptor[]{}; 121 } 122 123 /** 124 * Constructs a new {@link ElementDescriptor} based on its XML name and children list. 125 * The UI name is build by capitalizing the XML name. 126 * The UI nodes will be non-mandatory. 127 * 128 * @param xml_name The XML element node name. Case sensitive. 129 * @param children The list of allowed children. Can be null or empty. 130 * @param mandatory Whether this node must always exist (even for empty models). A mandatory 131 * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory 132 * UI node MUST have an XML node attached and it will cease to exist when the XML node 133 * ceases to exist. 134 */ 135 public ElementDescriptor(String xml_name, ElementDescriptor[] children, Mandatory mandatory) { 136 this(xml_name, prettyName(xml_name), null, null, null, children, mandatory); 137 } 138 139 /** 140 * Constructs a new {@link ElementDescriptor} based on its XML name and children list. 141 * The UI name is build by capitalizing the XML name. 142 * The UI nodes will be non-mandatory. 143 * 144 * @param xml_name The XML element node name. Case sensitive. 145 * @param children The list of allowed children. Can be null or empty. 146 */ 147 public ElementDescriptor(String xml_name, ElementDescriptor[] children) { 148 this(xml_name, prettyName(xml_name), null, null, null, children, false); 149 } 150 151 /** 152 * Constructs a new {@link ElementDescriptor} based on its XML name. 153 * The UI name is build by capitalizing the XML name. 154 * The UI nodes will be non-mandatory. 155 * 156 * @param xml_name The XML element node name. Case sensitive. 157 */ 158 public ElementDescriptor(String xml_name) { 159 this(xml_name, prettyName(xml_name), null, null, null, null, false); 160 } 161 162 /** Returns whether this node must always exist (even for empty models) */ 163 public Mandatory getMandatory() { 164 return mMandatory; 165 } 166 167 @Override 168 public String toString() { 169 return String.format("%s [%s, attr %d, children %d%s]", //$NON-NLS-1$ 170 this.getClass().getSimpleName(), 171 mXmlName, 172 mAttributes != null ? mAttributes.length : 0, 173 mChildren != null ? mChildren.length : 0, 174 mMandatory != Mandatory.NOT_MANDATORY ? ", " + mMandatory.toString() : "" //$NON-NLS-1$ //$NON-NLS-2$ 175 ); 176 } 177 178 /** 179 * Returns the XML element node local name (case sensitive) 180 */ 181 public final String getXmlLocalName() { 182 int pos = mXmlName.indexOf(':'); 183 if (pos != -1) { 184 return mXmlName.substring(pos+1); 185 } 186 return mXmlName; 187 } 188 189 /** 190 * Returns the XML element node name, including the prefix. 191 * Case sensitive. 192 * <p/> 193 * In Android resources, the element node name for Android resources typically does not 194 * have a prefix and is typically the simple Java class name (e.g. "View"), whereas for 195 * custom views it is generally the fully qualified class name of the view (e.g. 196 * "com.mycompany.myapp.MyView"). 197 * <p/> 198 * Most of the time you'll probably want to use {@link #getXmlLocalName()} to get a local 199 * name guaranteed without a prefix. 200 * <p/> 201 * Note that the prefix that <em>may</em> be available in this descriptor has nothing to 202 * do with the actual prefix the node might have (or needs to have) in the actual XML file 203 * since descriptors are fixed and do not depend on any current namespace defined in the 204 * target XML. 205 */ 206 public String getXmlName() { 207 return mXmlName; 208 } 209 210 /** 211 * Returns the namespace of the attribute. 212 */ 213 public final String getNamespace() { 214 // For now we hard-code the prefix as being "android" 215 if (mXmlName.startsWith(ANDROID_NS_NAME_PREFIX)) { 216 return ANDROID_URI; 217 } 218 219 return ""; //$NON-NLs-1$ 220 } 221 222 223 /** Returns the XML element name for the user interface, typically capitalized. */ 224 public String getUiName() { 225 return mUiName; 226 } 227 228 /** 229 * Returns an icon for the element. 230 * This icon is generic, that is all element descriptors have the same icon 231 * no matter what they represent. 232 * 233 * @return An icon for this element or null. 234 * @see #getCustomizedIcon() 235 */ 236 public Image getGenericIcon() { 237 return IconFactory.getInstance().getIcon(ELEMENT_ICON_FILENAME); 238 } 239 240 /** 241 * Returns an optional icon for the element, typically to be used in XML form trees. 242 * <p/> 243 * This icon is customized to the given descriptor, that is different elements 244 * will have different icons. 245 * <p/> 246 * By default this tries to return an icon based on the XML name of the element. 247 * If this fails, it tries to return the default Android logo as defined in the 248 * plugin. If all fails, it returns null. 249 * 250 * @return An icon for this element. This is never null. 251 */ 252 public Image getCustomizedIcon() { 253 IconFactory factory = IconFactory.getInstance(); 254 int color = hasChildren() ? IconFactory.COLOR_BLUE 255 : IconFactory.COLOR_GREEN; 256 int shape = hasChildren() ? IconFactory.SHAPE_RECT 257 : IconFactory.SHAPE_CIRCLE; 258 String name = mXmlName; 259 260 int pos = name.lastIndexOf('.'); 261 if (pos != -1) { 262 // If the user uses a fully qualified name, such as 263 // "android.gesture.GestureOverlayView" in their XML, we need to 264 // look up only by basename 265 name = name.substring(pos + 1); 266 } 267 Image icon = factory.getIcon(name, color, shape); 268 if (icon == null) { 269 icon = getGenericIcon(); 270 } 271 if (icon == null) { 272 icon = AdtPlugin.getAndroidLogo(); 273 } 274 return icon; 275 } 276 277 /** 278 * Returns an optional ImageDescriptor for the element. 279 * <p/> 280 * By default this tries to return an image based on the XML name of the element. 281 * If this fails, it tries to return the default Android logo as defined in the 282 * plugin. If all fails, it returns null. 283 * 284 * @return An ImageDescriptor for this element or null. 285 */ 286 public ImageDescriptor getImageDescriptor() { 287 IconFactory factory = IconFactory.getInstance(); 288 int color = hasChildren() ? IconFactory.COLOR_BLUE : IconFactory.COLOR_GREEN; 289 int shape = hasChildren() ? IconFactory.SHAPE_RECT : IconFactory.SHAPE_CIRCLE; 290 ImageDescriptor id = factory.getImageDescriptor(mXmlName, color, shape); 291 return id != null ? id : AdtPlugin.getAndroidLogoDesc(); 292 } 293 294 /* Returns the list of allowed attributes. */ 295 public AttributeDescriptor[] getAttributes() { 296 return mAttributes; 297 } 298 299 /** Sets the list of allowed attributes. */ 300 public void setAttributes(AttributeDescriptor[] attributes) { 301 mAttributes = attributes; 302 for (AttributeDescriptor attribute : attributes) { 303 attribute.setParent(this); 304 } 305 } 306 307 /** Returns the list of allowed children */ 308 public ElementDescriptor[] getChildren() { 309 return mChildren; 310 } 311 312 /** @return True if this descriptor has children available */ 313 public boolean hasChildren() { 314 return mChildren.length > 0; 315 } 316 317 /** 318 * Checks whether this descriptor can accept the given descriptor type 319 * as a direct child. 320 * 321 * @return True if this descriptor can accept children of the given descriptor type. 322 * False if not accepted, no children allowed, or target is null. 323 */ 324 public boolean acceptChild(ElementDescriptor target) { 325 if (target != null && mChildren.length > 0) { 326 String targetXmlName = target.getXmlName(); 327 for (ElementDescriptor child : mChildren) { 328 if (child.getXmlName().equals(targetXmlName)) { 329 return true; 330 } 331 } 332 } 333 334 return false; 335 } 336 337 /** Sets the list of allowed children. */ 338 public void setChildren(ElementDescriptor[] newChildren) { 339 mChildren = newChildren; 340 } 341 342 /** 343 * Sets the list of allowed children. 344 * <p/> 345 * This is just a convenience method that converts a Collection into an array and 346 * calls {@link #setChildren(ElementDescriptor[])}. 347 * <p/> 348 * This means a <em>copy</em> of the collection is made. The collection is not 349 * stored by the recipient and can thus be altered by the caller. 350 */ 351 public void setChildren(Collection<ElementDescriptor> newChildren) { 352 setChildren(newChildren.toArray(new ElementDescriptor[newChildren.size()])); 353 } 354 355 /** 356 * Returns an optional tooltip. Will be null if not present. 357 * <p/> 358 * The tooltip is based on the Javadoc of the element and already processed via 359 * {@link DescriptorsUtils#formatTooltip(String)} to be displayed right away as 360 * a UI tooltip. 361 */ 362 public String getTooltip() { 363 return mTooltip; 364 } 365 366 /** Returns an optional SKD URL. Will be null if not present. */ 367 public String getSdkUrl() { 368 return mSdkUrl; 369 } 370 371 /** Sets the optional tooltip. Can be null or empty. */ 372 public void setTooltip(String tooltip) { 373 mTooltip = tooltip; 374 } 375 376 /** Sets the optional SDK URL. Can be null or empty. */ 377 public void setSdkUrl(String sdkUrl) { 378 mSdkUrl = sdkUrl; 379 } 380 381 /** 382 * @return A new {@link UiElementNode} linked to this descriptor. 383 */ 384 public UiElementNode createUiNode() { 385 return new UiElementNode(this); 386 } 387 388 /** 389 * Returns the first children of this descriptor that describes the given XML element name. 390 * <p/> 391 * In recursive mode, searches the direct children first before descending in the hierarchy. 392 * 393 * @return The ElementDescriptor matching the requested XML node element name or null. 394 */ 395 public ElementDescriptor findChildrenDescriptor(String element_name, boolean recursive) { 396 return findChildrenDescriptorInternal(element_name, recursive, null); 397 } 398 399 private ElementDescriptor findChildrenDescriptorInternal(String element_name, 400 boolean recursive, 401 Set<ElementDescriptor> visited) { 402 if (recursive && visited == null) { 403 visited = new HashSet<ElementDescriptor>(); 404 } 405 406 for (ElementDescriptor e : getChildren()) { 407 if (e.getXmlName().equals(element_name)) { 408 return e; 409 } 410 } 411 412 if (visited != null) { 413 visited.add(this); 414 } 415 416 if (recursive) { 417 for (ElementDescriptor e : getChildren()) { 418 if (visited != null) { 419 if (!visited.add(e)) { // Set.add() returns false if element is already present 420 continue; 421 } 422 } 423 ElementDescriptor f = e.findChildrenDescriptorInternal(element_name, 424 recursive, visited); 425 if (f != null) { 426 return f; 427 } 428 } 429 } 430 431 return null; 432 } 433 434 /** 435 * Utility helper than pretty-formats an XML Name for the UI. 436 * This is used by the simplified constructor that takes only an XML element name. 437 * 438 * @param xml_name The XML name to convert. 439 * @return The XML name with dashes replaced by spaces and capitalized. 440 */ 441 private static String prettyName(String xml_name) { 442 char c[] = xml_name.toCharArray(); 443 if (c.length > 0) { 444 c[0] = Character.toUpperCase(c[0]); 445 } 446 return new String(c).replace("-", " "); //$NON-NLS-1$ //$NON-NLS-2$ 447 } 448 449 /** 450 * Returns true if this node defines the given attribute 451 * 452 * @param namespaceUri the namespace URI of the target attribute 453 * @param attributeName the attribute name 454 * @return true if this element defines an attribute of the given name and namespace 455 */ 456 public boolean definesAttribute(String namespaceUri, String attributeName) { 457 for (AttributeDescriptor desc : mAttributes) { 458 if (desc.getXmlLocalName().equals(attributeName) && 459 desc.getNamespaceUri().equals(namespaceUri)) { 460 return true; 461 } 462 } 463 464 return false; 465 } 466 467 // Implements Comparable<ElementDescriptor>: 468 @Override 469 public int compareTo(ElementDescriptor o) { 470 return mUiName.compareToIgnoreCase(o.mUiName); 471 } 472 473 /** 474 * Ensures that this view descriptor's attribute list is up to date. This is 475 * always the case for all the builtin descriptors, but for example for a 476 * custom view, it could be changing dynamically so caches may have to be 477 * recomputed. This method will return true if nothing changed, and false if 478 * it recomputed its info. 479 * 480 * @return true if the attributes are already up to date and nothing changed 481 */ 482 public boolean syncAttributes() { 483 return true; 484 } 485 } 486