Home | History | Annotate | Download | only in descriptors
      1 /*
      2  * Copyright (C) 2008 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_URI;
     20 import static com.android.SdkConstants.ATTR_ID;
     21 import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
     22 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
     23 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
     24 import static com.android.SdkConstants.ATTR_TEXT;
     25 import static com.android.SdkConstants.EDIT_TEXT;
     26 import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
     27 import static com.android.SdkConstants.FQCN_ADAPTER_VIEW;
     28 import static com.android.SdkConstants.GALLERY;
     29 import static com.android.SdkConstants.GRID_LAYOUT;
     30 import static com.android.SdkConstants.GRID_VIEW;
     31 import static com.android.SdkConstants.GT_ENTITY;
     32 import static com.android.SdkConstants.ID_PREFIX;
     33 import static com.android.SdkConstants.LIST_VIEW;
     34 import static com.android.SdkConstants.LT_ENTITY;
     35 import static com.android.SdkConstants.NEW_ID_PREFIX;
     36 import static com.android.SdkConstants.RELATIVE_LAYOUT;
     37 import static com.android.SdkConstants.REQUEST_FOCUS;
     38 import static com.android.SdkConstants.SPACE;
     39 import static com.android.SdkConstants.VALUE_FILL_PARENT;
     40 import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
     41 import static com.android.SdkConstants.VIEW_INCLUDE;
     42 import static com.android.SdkConstants.VIEW_MERGE;
     43 
     44 import com.android.SdkConstants;
     45 import com.android.annotations.NonNull;
     46 import com.android.ide.common.api.IAttributeInfo.Format;
     47 import com.android.ide.common.resources.platform.AttributeInfo;
     48 import com.android.ide.eclipse.adt.AdtConstants;
     49 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
     50 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     51 import com.android.resources.ResourceType;
     52 
     53 import org.eclipse.swt.graphics.Image;
     54 
     55 import java.util.ArrayList;
     56 import java.util.EnumSet;
     57 import java.util.HashSet;
     58 import java.util.List;
     59 import java.util.Locale;
     60 import java.util.Map;
     61 import java.util.Map.Entry;
     62 import java.util.Set;
     63 import java.util.regex.Matcher;
     64 import java.util.regex.Pattern;
     65 
     66 
     67 /**
     68  * Utility methods related to descriptors handling.
     69  */
     70 public final class DescriptorsUtils {
     71     private static final String DEFAULT_WIDGET_PREFIX = "widget";
     72 
     73     private static final int JAVADOC_BREAK_LENGTH = 60;
     74 
     75     /**
     76      * The path in the online documentation for the manifest description.
     77      * <p/>
     78      * This is NOT a complete URL. To be used, it needs to be appended
     79      * to {@link AdtConstants#CODESITE_BASE_URL} or to the local SDK
     80      * documentation.
     81      */
     82     public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#";  //$NON-NLS-1$
     83 
     84     public static final String IMAGE_KEY = "image"; //$NON-NLS-1$
     85 
     86     private static final String CODE  = "$code";  //$NON-NLS-1$
     87     private static final String LINK  = "$link";  //$NON-NLS-1$
     88     private static final String ELEM  = "$elem";  //$NON-NLS-1$
     89     private static final String BREAK = "$break"; //$NON-NLS-1$
     90 
     91     /**
     92      * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
     93      *
     94      * @param attributes The list of {@link AttributeDescriptor} to append to
     95      * @param elementXmlName Optional XML local name of the element to which attributes are
     96      *              being added. When not null, this is used to filter overrides.
     97      * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
     98      *              See {@link SdkConstants#NS_RESOURCES} for a common value.
     99      * @param infos The array of {@link AttributeInfo} to read and append to attributes
    100      * @param requiredAttributes An optional set of attributes to mark as "required" (i.e. append
    101      *        a "*" to their UI name as a hint for the user.) If not null, must contains
    102      *        entries in the form "elem-name/attr-name". Elem-name can be "*".
    103      * @param overrides A map [attribute name => ITextAttributeCreator creator].
    104      */
    105     public static void appendAttributes(List<AttributeDescriptor> attributes,
    106             String elementXmlName,
    107             String nsUri, AttributeInfo[] infos,
    108             Set<String> requiredAttributes,
    109             Map<String, ITextAttributeCreator> overrides) {
    110         for (AttributeInfo info : infos) {
    111             boolean required = false;
    112             if (requiredAttributes != null) {
    113                 String attr_name = info.getName();
    114                 if (requiredAttributes.contains("*/" + attr_name) ||
    115                         requiredAttributes.contains(elementXmlName + "/" + attr_name)) {
    116                     required = true;
    117                 }
    118             }
    119             appendAttribute(attributes, elementXmlName, nsUri, info, required, overrides);
    120         }
    121     }
    122 
    123     /**
    124      * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
    125      *
    126      * @param attributes The list of {@link AttributeDescriptor} to append to
    127      * @param elementXmlName Optional XML local name of the element to which attributes are
    128      *              being added. When not null, this is used to filter overrides.
    129      * @param info The {@link AttributeInfo} to append to attributes
    130      * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
    131      *              See {@link SdkConstants#NS_RESOURCES} for a common value.
    132      * @param required True if the attribute is to be marked as "required" (i.e. append
    133      *        a "*" to its UI name as a hint for the user.)
    134      * @param overrides A map [attribute name => ITextAttributeCreator creator].
    135      */
    136     public static void appendAttribute(List<AttributeDescriptor> attributes,
    137             String elementXmlName,
    138             String nsUri,
    139             AttributeInfo info, boolean required,
    140             Map<String, ITextAttributeCreator> overrides) {
    141         TextAttributeDescriptor attr = null;
    142 
    143         String xmlLocalName = info.getName();
    144 
    145         // Add the known types to the tooltip
    146         EnumSet<Format> formats_set = info.getFormats();
    147         int flen = formats_set.size();
    148         if (flen > 0) {
    149             // Create a specialized attribute if we can
    150             if (overrides != null) {
    151                 for (Entry<String, ITextAttributeCreator> entry: overrides.entrySet()) {
    152                     // The override key can have the following formats:
    153                     //   */xmlLocalName
    154                     //   element/xmlLocalName
    155                     //   element1,element2,...,elementN/xmlLocalName
    156                     String key = entry.getKey();
    157                     String elements[] = key.split("/");          //$NON-NLS-1$
    158                     String overrideAttrLocalName = null;
    159                     if (elements.length < 1) {
    160                         continue;
    161                     } else if (elements.length == 1) {
    162                         overrideAttrLocalName = elements[0];
    163                         elements = null;
    164                     } else {
    165                         overrideAttrLocalName = elements[elements.length - 1];
    166                         elements = elements[0].split(",");       //$NON-NLS-1$
    167                     }
    168 
    169                     if (overrideAttrLocalName == null ||
    170                             !overrideAttrLocalName.equals(xmlLocalName)) {
    171                         continue;
    172                     }
    173 
    174                     boolean ok_element = elements != null && elements.length < 1;
    175                     if (!ok_element && elements != null) {
    176                         for (String element : elements) {
    177                             if (element.equals("*")              //$NON-NLS-1$
    178                                     || element.equals(elementXmlName)) {
    179                                 ok_element = true;
    180                                 break;
    181                             }
    182                         }
    183                     }
    184 
    185                     if (!ok_element) {
    186                         continue;
    187                     }
    188 
    189                     ITextAttributeCreator override = entry.getValue();
    190                     if (override != null) {
    191                         attr = override.create(xmlLocalName, nsUri, info);
    192                     }
    193                 }
    194             } // if overrides
    195 
    196             // Create a specialized descriptor if we can, based on type
    197             if (attr == null) {
    198                 if (formats_set.contains(Format.REFERENCE)) {
    199                     // This is either a multi-type reference or a generic reference.
    200                     attr = new ReferenceAttributeDescriptor(
    201                             xmlLocalName, nsUri, info);
    202                 } else if (formats_set.contains(Format.ENUM)) {
    203                     attr = new ListAttributeDescriptor(
    204                             xmlLocalName, nsUri, info);
    205                 } else if (formats_set.contains(Format.FLAG)) {
    206                     attr = new FlagAttributeDescriptor(
    207                             xmlLocalName, nsUri, info);
    208                 } else if (formats_set.contains(Format.BOOLEAN)) {
    209                     attr = new BooleanAttributeDescriptor(
    210                             xmlLocalName, nsUri, info);
    211                 } else if (formats_set.contains(Format.STRING)) {
    212                     attr = new ReferenceAttributeDescriptor(
    213                             ResourceType.STRING, xmlLocalName, nsUri, info);
    214                 }
    215             }
    216         }
    217 
    218         // By default a simple text field is used
    219         if (attr == null) {
    220             attr = new TextAttributeDescriptor(xmlLocalName, nsUri, info);
    221         }
    222 
    223         if (required) {
    224             attr.setRequired(true);
    225         }
    226 
    227         attributes.add(attr);
    228     }
    229 
    230     /**
    231      * Indicates the the given {@link AttributeInfo} already exists in the ArrayList of
    232      * {@link AttributeDescriptor}. This test for the presence of a descriptor with the same
    233      * XML name.
    234      *
    235      * @param attributes The list of {@link AttributeDescriptor} to compare to.
    236      * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
    237      *              See {@link SdkConstants#NS_RESOURCES} for a common value.
    238      * @param info The {@link AttributeInfo} to know whether it is included in the above list.
    239      * @return True if this {@link AttributeInfo} is already present in
    240      *         the {@link AttributeDescriptor} list.
    241      */
    242     public static boolean containsAttribute(ArrayList<AttributeDescriptor> attributes,
    243             String nsUri,
    244             AttributeInfo info) {
    245         String xmlLocalName = info.getName();
    246         for (AttributeDescriptor desc : attributes) {
    247             if (desc.getXmlLocalName().equals(xmlLocalName)) {
    248                 if (nsUri == desc.getNamespaceUri() ||
    249                         (nsUri != null && nsUri.equals(desc.getNamespaceUri()))) {
    250                     return true;
    251                 }
    252             }
    253         }
    254         return false;
    255     }
    256 
    257     /**
    258      * Create a pretty attribute UI name from an XML name.
    259      * <p/>
    260      * The original xml name starts with a lower case and is camel-case,
    261      * e.g. "maxWidthForView". The pretty name starts with an upper case
    262      * and has space separators, e.g. "Max width for view".
    263      */
    264     public static String prettyAttributeUiName(String name) {
    265         if (name.length() < 1) {
    266             return name;
    267         }
    268         StringBuilder buf = new StringBuilder(2 * name.length());
    269 
    270         char c = name.charAt(0);
    271         // Use upper case initial letter
    272         buf.append(Character.toUpperCase(c));
    273         int len = name.length();
    274         for (int i = 1; i < len; i++) {
    275             c = name.charAt(i);
    276             if (Character.isUpperCase(c)) {
    277                 // Break camel case into separate words
    278                 buf.append(' ');
    279                 // Use a lower case initial letter for the next word, except if the
    280                 // word is solely X, Y or Z.
    281                 if (c >= 'X' && c <= 'Z' &&
    282                         (i == len-1 ||
    283                             (i < len-1 && Character.isUpperCase(name.charAt(i+1))))) {
    284                     buf.append(c);
    285                 } else {
    286                     buf.append(Character.toLowerCase(c));
    287                 }
    288             } else if (c == '_') {
    289                 buf.append(' ');
    290             } else {
    291                 buf.append(c);
    292             }
    293         }
    294 
    295         name = buf.toString();
    296 
    297         name = replaceAcronyms(name);
    298 
    299         return name;
    300     }
    301 
    302     /**
    303      * Similar to {@link #prettyAttributeUiName(String)}, but it will capitalize
    304      * all words, not just the first one.
    305      * <p/>
    306      * The original xml name starts with a lower case and is camel-case, e.g.
    307      * "maxWidthForView". The corresponding return value is
    308      * "Max Width For View".
    309      *
    310      * @param name the attribute name, which should be a camel case name, e.g.
    311      *            "maxWidth"
    312      * @return the corresponding display name, e.g. "Max Width"
    313      */
    314     @NonNull
    315     public static String capitalize(@NonNull String name) {
    316         if (name.isEmpty()) {
    317             return name;
    318         }
    319         StringBuilder buf = new StringBuilder(2 * name.length());
    320 
    321         char c = name.charAt(0);
    322         // Use upper case initial letter
    323         buf.append(Character.toUpperCase(c));
    324         int len = name.length();
    325         for (int i = 1; i < len; i++) {
    326             c = name.charAt(i);
    327             if (Character.isUpperCase(c)) {
    328                 // Break camel case into separate words
    329                 buf.append(' ');
    330                 // Use a lower case initial letter for the next word, except if the
    331                 // word is solely X, Y or Z.
    332                 buf.append(c);
    333             } else if (c == '_') {
    334                 buf.append(' ');
    335                 if (i < len -1 && Character.isLowerCase(name.charAt(i + 1))) {
    336                     buf.append(Character.toUpperCase(name.charAt(i + 1)));
    337                     i++;
    338                 }
    339             } else {
    340                 buf.append(c);
    341             }
    342         }
    343 
    344         name = buf.toString();
    345 
    346         name = replaceAcronyms(name);
    347 
    348         return name;
    349     }
    350 
    351     private static String replaceAcronyms(String name) {
    352         // Replace these acronyms by upper-case versions
    353         // - (?<=^| ) means "if preceded by a space or beginning of string"
    354         // - (?=$| )  means "if followed by a space or end of string"
    355         if (name.contains("sdk") || name.contains("Sdk")) {
    356             name = name.replaceAll("(?<=^| )[sS]dk(?=$| )", "SDK");
    357         }
    358         if (name.contains("uri") || name.contains("Uri")) {
    359             name = name.replaceAll("(?<=^| )[uU]ri(?=$| )", "URI");
    360         }
    361         if (name.contains("ime") || name.contains("Ime")) {
    362             name = name.replaceAll("(?<=^| )[iI]me(?=$| )", "IME");
    363         }
    364         if (name.contains("vm") || name.contains("Vm")) {
    365             name = name.replaceAll("(?<=^| )[vV]m(?=$| )", "VM");
    366         }
    367         if (name.contains("ui") || name.contains("Ui")) {
    368             name = name.replaceAll("(?<=^| )[uU]i(?=$| )", "UI");
    369         }
    370         return name;
    371     }
    372 
    373     /**
    374      * Formats the javadoc tooltip to be usable in a tooltip.
    375      */
    376     public static String formatTooltip(String javadoc) {
    377         ArrayList<String> spans = scanJavadoc(javadoc);
    378 
    379         StringBuilder sb = new StringBuilder();
    380         boolean needBreak = false;
    381 
    382         for (int n = spans.size(), i = 0; i < n; ++i) {
    383             String s = spans.get(i);
    384             if (CODE.equals(s)) {
    385                 s = spans.get(++i);
    386                 if (s != null) {
    387                     sb.append('"').append(s).append('"');
    388                 }
    389             } else if (LINK.equals(s)) {
    390                 String base   = spans.get(++i);
    391                 String anchor = spans.get(++i);
    392                 String text   = spans.get(++i);
    393 
    394                 if (base != null) {
    395                     base = base.trim();
    396                 }
    397                 if (anchor != null) {
    398                     anchor = anchor.trim();
    399                 }
    400                 if (text != null) {
    401                     text = text.trim();
    402                 }
    403 
    404                 // If there's no text, use the anchor if there's one
    405                 if (text == null || text.length() == 0) {
    406                     text = anchor;
    407                 }
    408 
    409                 if (base != null && base.length() > 0) {
    410                     if (text == null || text.length() == 0) {
    411                         // If we still have no text, use the base as text
    412                         text = base;
    413                     }
    414                 }
    415 
    416                 if (text != null) {
    417                     sb.append(text);
    418                 }
    419 
    420             } else if (ELEM.equals(s)) {
    421                 s = spans.get(++i);
    422                 if (s != null) {
    423                     sb.append(s);
    424                 }
    425             } else if (BREAK.equals(s)) {
    426                 needBreak = true;
    427             } else if (s != null) {
    428                 if (needBreak && s.trim().length() > 0) {
    429                     sb.append('\n');
    430                 }
    431                 sb.append(s);
    432                 needBreak = false;
    433             }
    434         }
    435 
    436         return sb.toString();
    437     }
    438 
    439     /**
    440      * Formats the javadoc tooltip to be usable in a FormText.
    441      * <p/>
    442      * If the descriptor can provide an icon, the caller should provide
    443      * elementsDescriptor.getIcon() as "image" to FormText, e.g.:
    444      * <code>formText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());</code>
    445      *
    446      * @param javadoc The javadoc to format. Cannot be null.
    447      * @param elementDescriptor The element descriptor parent of the javadoc. Cannot be null.
    448      * @param androidDocBaseUrl The base URL for the documentation. Cannot be null. Should be
    449      *   <code>FrameworkResourceManager.getInstance().getDocumentationBaseUrl()</code>
    450      */
    451     public static String formatFormText(String javadoc,
    452             ElementDescriptor elementDescriptor,
    453             String androidDocBaseUrl) {
    454         ArrayList<String> spans = scanJavadoc(javadoc);
    455 
    456         String fullSdkUrl = androidDocBaseUrl + MANIFEST_SDK_URL;
    457         String sdkUrl = elementDescriptor.getSdkUrl();
    458         if (sdkUrl != null && sdkUrl.startsWith(MANIFEST_SDK_URL)) {
    459             fullSdkUrl = androidDocBaseUrl + sdkUrl;
    460         }
    461 
    462         StringBuilder sb = new StringBuilder();
    463 
    464         Image icon = elementDescriptor.getCustomizedIcon();
    465         if (icon != null) {
    466             sb.append("<form><li style=\"image\" value=\"" +        //$NON-NLS-1$
    467                     IMAGE_KEY + "\">");                             //$NON-NLS-1$
    468         } else {
    469             sb.append("<form><p>");                                 //$NON-NLS-1$
    470         }
    471 
    472         for (int n = spans.size(), i = 0; i < n; ++i) {
    473             String s = spans.get(i);
    474             if (CODE.equals(s)) {
    475                 s = spans.get(++i);
    476                 if (elementDescriptor.getXmlName().equals(s) && fullSdkUrl != null) {
    477                     sb.append("<a href=\"");                        //$NON-NLS-1$
    478                     sb.append(fullSdkUrl);
    479                     sb.append("\">");                               //$NON-NLS-1$
    480                     sb.append(s);
    481                     sb.append("</a>");                              //$NON-NLS-1$
    482                 } else if (s != null) {
    483                     sb.append('"').append(s).append('"');
    484                 }
    485             } else if (LINK.equals(s)) {
    486                 String base   = spans.get(++i);
    487                 String anchor = spans.get(++i);
    488                 String text   = spans.get(++i);
    489 
    490                 if (base != null) {
    491                     base = base.trim();
    492                 }
    493                 if (anchor != null) {
    494                     anchor = anchor.trim();
    495                 }
    496                 if (text != null) {
    497                     text = text.trim();
    498                 }
    499 
    500                 // If there's no text, use the anchor if there's one
    501                 if (text == null || text.length() == 0) {
    502                     text = anchor;
    503                 }
    504 
    505                 // TODO specialize with a base URL for views, menus & other resources
    506                 // Base is empty for a local page anchor, in which case we'll replace it
    507                 // by the element SDK URL if it exists.
    508                 if ((base == null || base.length() == 0) && fullSdkUrl != null) {
    509                     base = fullSdkUrl;
    510                 }
    511 
    512                 String url = null;
    513                 if (base != null && base.length() > 0) {
    514                     if (base.startsWith("http")) {                  //$NON-NLS-1$
    515                         // If base looks an URL, use it, with the optional anchor
    516                         url = base;
    517                         if (anchor != null && anchor.length() > 0) {
    518                             // If the base URL already has an anchor, it needs to be
    519                             // removed first. If there's no anchor, we need to add "#"
    520                             int pos = url.lastIndexOf('#');
    521                             if (pos < 0) {
    522                                 url += "#";                         //$NON-NLS-1$
    523                             } else if (pos < url.length() - 1) {
    524                                 url = url.substring(0, pos + 1);
    525                             }
    526 
    527                             url += anchor;
    528                         }
    529                     } else if (text == null || text.length() == 0) {
    530                         // If we still have no text, use the base as text
    531                         text = base;
    532                     }
    533                 }
    534 
    535                 if (url != null && text != null) {
    536                     sb.append("<a href=\"");                        //$NON-NLS-1$
    537                     sb.append(url);
    538                     sb.append("\">");                               //$NON-NLS-1$
    539                     sb.append(text);
    540                     sb.append("</a>");                              //$NON-NLS-1$
    541                 } else if (text != null) {
    542                     sb.append("<b>").append(text).append("</b>");   //$NON-NLS-1$ //$NON-NLS-2$
    543                 }
    544 
    545             } else if (ELEM.equals(s)) {
    546                 s = spans.get(++i);
    547                 if (sdkUrl != null && s != null) {
    548                     sb.append("<a href=\"");                        //$NON-NLS-1$
    549                     sb.append(sdkUrl);
    550                     sb.append("\">");                               //$NON-NLS-1$
    551                     sb.append(s);
    552                     sb.append("</a>");                              //$NON-NLS-1$
    553                 } else if (s != null) {
    554                     sb.append("<b>").append(s).append("</b>");      //$NON-NLS-1$ //$NON-NLS-2$
    555                 }
    556             } else if (BREAK.equals(s)) {
    557                 // ignore line breaks in pseudo-HTML rendering
    558             } else if (s != null) {
    559                 sb.append(s);
    560             }
    561         }
    562 
    563         if (icon != null) {
    564             sb.append("</li></form>");                              //$NON-NLS-1$
    565         } else {
    566             sb.append("</p></form>");                               //$NON-NLS-1$
    567         }
    568         return sb.toString();
    569     }
    570 
    571     private static ArrayList<String> scanJavadoc(String javadoc) {
    572         ArrayList<String> spans = new ArrayList<String>();
    573 
    574         // Standardize all whitespace in the javadoc to single spaces.
    575         if (javadoc != null) {
    576             javadoc = javadoc.replaceAll("[ \t\f\r\n]+", " "); //$NON-NLS-1$ //$NON-NLS-2$
    577         }
    578 
    579         // Detects {@link <base>#<name> <text>} where all 3 are optional
    580         Pattern p_link = Pattern.compile("\\{@link\\s+([^#\\}\\s]*)(?:#([^\\s\\}]*))?(?:\\s*([^\\}]*))?\\}(.*)"); //$NON-NLS-1$
    581         // Detects <code>blah</code>
    582         Pattern p_code = Pattern.compile("<code>(.+?)</code>(.*)");                 //$NON-NLS-1$
    583         // Detects @blah@, used in hard-coded tooltip descriptors
    584         Pattern p_elem = Pattern.compile("@([\\w -]+)@(.*)");                       //$NON-NLS-1$
    585         // Detects a buffer that starts by @@ (request for a break)
    586         Pattern p_break = Pattern.compile("@@(.*)");                                //$NON-NLS-1$
    587         // Detects a buffer that starts by @ < or { (one that was not matched above)
    588         Pattern p_open = Pattern.compile("([@<\\{])(.*)");                          //$NON-NLS-1$
    589         // Detects everything till the next potential separator, i.e. @ < or {
    590         Pattern p_text = Pattern.compile("([^@<\\{]+)(.*)");                        //$NON-NLS-1$
    591 
    592         int currentLength = 0;
    593         String text = null;
    594 
    595         while(javadoc != null && javadoc.length() > 0) {
    596             Matcher m;
    597             String s = null;
    598             if ((m = p_code.matcher(javadoc)).matches()) {
    599                 spans.add(CODE);
    600                 spans.add(text = cleanupJavadocHtml(m.group(1))); // <code> text
    601                 javadoc = m.group(2);
    602                 if (text != null) {
    603                     currentLength += text.length();
    604                 }
    605             } else if ((m = p_link.matcher(javadoc)).matches()) {
    606                 spans.add(LINK);
    607                 spans.add(m.group(1)); // @link base
    608                 spans.add(m.group(2)); // @link anchor
    609                 spans.add(text = cleanupJavadocHtml(m.group(3))); // @link text
    610                 javadoc = m.group(4);
    611                 if (text != null) {
    612                     currentLength += text.length();
    613                 }
    614             } else if ((m = p_elem.matcher(javadoc)).matches()) {
    615                 spans.add(ELEM);
    616                 spans.add(text = cleanupJavadocHtml(m.group(1))); // @text@
    617                 javadoc = m.group(2);
    618                 if (text != null) {
    619                     currentLength += text.length() - 2;
    620                 }
    621             } else if ((m = p_break.matcher(javadoc)).matches()) {
    622                 spans.add(BREAK);
    623                 currentLength = 0;
    624                 javadoc = m.group(1);
    625             } else if ((m = p_open.matcher(javadoc)).matches()) {
    626                 s = m.group(1);
    627                 javadoc = m.group(2);
    628             } else if ((m = p_text.matcher(javadoc)).matches()) {
    629                 s = m.group(1);
    630                 javadoc = m.group(2);
    631             } else {
    632                 // This is not supposed to happen. In case of, just use everything.
    633                 s = javadoc;
    634                 javadoc = null;
    635             }
    636             if (s != null && s.length() > 0) {
    637                 s = cleanupJavadocHtml(s);
    638 
    639                 if (currentLength >= JAVADOC_BREAK_LENGTH) {
    640                     spans.add(BREAK);
    641                     currentLength = 0;
    642                 }
    643                 while (currentLength + s.length() > JAVADOC_BREAK_LENGTH) {
    644                     int pos = s.indexOf(' ', JAVADOC_BREAK_LENGTH - currentLength);
    645                     if (pos <= 0) {
    646                         break;
    647                     }
    648                     spans.add(s.substring(0, pos + 1));
    649                     spans.add(BREAK);
    650                     currentLength = 0;
    651                     s = s.substring(pos + 1);
    652                 }
    653 
    654                 spans.add(s);
    655                 currentLength += s.length();
    656             }
    657         }
    658 
    659         return spans;
    660     }
    661 
    662     /**
    663      * Remove anything that looks like HTML from a javadoc snippet, as it is supported
    664      * neither by FormText nor a standard text tooltip.
    665      */
    666     private static String cleanupJavadocHtml(String s) {
    667         if (s != null) {
    668             s = s.replaceAll(LT_ENTITY, "\"");     //$NON-NLS-1$ $NON-NLS-2$
    669             s = s.replaceAll(GT_ENTITY, "\"");     //$NON-NLS-1$ $NON-NLS-2$
    670             s = s.replaceAll("<[^>]+>", "");    //$NON-NLS-1$ $NON-NLS-2$
    671         }
    672         return s;
    673     }
    674 
    675     /**
    676      * Returns the basename for the given fully qualified class name. It is okay to pass
    677      * a basename to this method which will just be returned back.
    678      *
    679      * @param fqcn The fully qualified class name to convert
    680      * @return the basename of the class name
    681      */
    682     public static String getBasename(String fqcn) {
    683         String name = fqcn;
    684         int lastDot = name.lastIndexOf('.');
    685         if (lastDot != -1) {
    686             name = name.substring(lastDot + 1);
    687         }
    688 
    689         return name;
    690     }
    691 
    692     /**
    693      * Sets the default layout attributes for the a new UiElementNode.
    694      * <p/>
    695      * Note that ideally the node should already be part of a hierarchy so that its
    696      * parent layout and previous sibling can be determined, if any.
    697      * <p/>
    698      * This does not override attributes which are not empty.
    699      */
    700     public static void setDefaultLayoutAttributes(UiElementNode node, boolean updateLayout) {
    701         // if this ui_node is a layout and we're adding it to a document, use match_parent for
    702         // both W/H. Otherwise default to wrap_layout.
    703         ElementDescriptor descriptor = node.getDescriptor();
    704 
    705         String name = descriptor.getXmlLocalName();
    706         if (name.equals(REQUEST_FOCUS)) {
    707             // Don't add ids, widths and heights etc to <requestFocus>
    708             return;
    709         }
    710 
    711         // Width and height are mandatory in all layouts except GridLayout
    712         boolean setSize = !node.getUiParent().getDescriptor().getXmlName().equals(GRID_LAYOUT);
    713         if (setSize) {
    714             boolean fill = descriptor.hasChildren() &&
    715                            node.getUiParent() instanceof UiDocumentNode;
    716             node.setAttributeValue(
    717                     ATTR_LAYOUT_WIDTH,
    718                     ANDROID_URI,
    719                     fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT,
    720                     false /* override */);
    721             node.setAttributeValue(
    722                     ATTR_LAYOUT_HEIGHT,
    723                     ANDROID_URI,
    724                     fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT,
    725                     false /* override */);
    726         }
    727 
    728         if (needsDefaultId(node.getDescriptor())) {
    729             String freeId = getFreeWidgetId(node);
    730             if (freeId != null) {
    731                 node.setAttributeValue(
    732                         ATTR_ID,
    733                         ANDROID_URI,
    734                         freeId,
    735                         false /* override */);
    736             }
    737         }
    738 
    739         // Set a text attribute on textual widgets -- but only on those that define a text
    740         // attribute
    741         if (descriptor.definesAttribute(ANDROID_URI, ATTR_TEXT)
    742                 // Don't set default text value into edit texts - they typically start out blank
    743                 && !descriptor.getXmlLocalName().equals(EDIT_TEXT)) {
    744             String type = getBasename(descriptor.getUiName());
    745             node.setAttributeValue(
    746                 ATTR_TEXT,
    747                 ANDROID_URI,
    748                 type,
    749                 false /*override*/);
    750         }
    751 
    752         if (updateLayout) {
    753             UiElementNode parent = node.getUiParent();
    754             if (parent != null &&
    755                     parent.getDescriptor().getXmlLocalName().equals(
    756                             RELATIVE_LAYOUT)) {
    757                 UiElementNode previous = node.getUiPreviousSibling();
    758                 if (previous != null) {
    759                     String id = previous.getAttributeValue(ATTR_ID);
    760                     if (id != null && id.length() > 0) {
    761                         id = id.replace("@+", "@");                     //$NON-NLS-1$ //$NON-NLS-2$
    762                         node.setAttributeValue(
    763                                 ATTR_LAYOUT_BELOW,
    764                                 ANDROID_URI,
    765                                 id,
    766                                 false /* override */);
    767                     }
    768                 }
    769             }
    770         }
    771     }
    772 
    773     /**
    774      * Determines whether new views of the given type should be assigned a
    775      * default id.
    776      *
    777      * @param descriptor a descriptor describing the view to look up
    778      * @return true if new views of the given type should be assigned a default
    779      *         id
    780      */
    781     public static boolean needsDefaultId(ElementDescriptor descriptor) {
    782         // By default, layouts do not need ids.
    783         String tag = descriptor.getXmlLocalName();
    784         if (tag.endsWith("Layout")  //$NON-NLS-1$
    785                 || tag.equals(VIEW_INCLUDE)
    786                 || tag.equals(VIEW_MERGE)
    787                 || tag.equals(SPACE)
    788                 || tag.endsWith(SPACE) && tag.length() > SPACE.length() &&
    789                     tag.charAt(tag.length() - SPACE.length()) == '.') {
    790             return false;
    791         }
    792 
    793         return true;
    794     }
    795 
    796     /**
    797      * Given a UI node, returns the first available id that matches the
    798      * pattern "prefix%d".
    799      * <p/>TabWidget is a special case and the method will always return "@android:id/tabs".
    800      *
    801      * @param uiNode The UI node that gives the prefix to match.
    802      * @return A suitable generated id in the attribute form needed by the XML id tag
    803      * (e.g. "@+id/something")
    804      */
    805     public static String getFreeWidgetId(UiElementNode uiNode) {
    806         String name = getBasename(uiNode.getDescriptor().getXmlLocalName());
    807         return getFreeWidgetId(uiNode.getUiRoot(), name);
    808     }
    809 
    810     /**
    811      * Given a UI root node and a potential XML node name, returns the first available
    812      * id that matches the pattern "prefix%d".
    813      * <p/>TabWidget is a special case and the method will always return "@android:id/tabs".
    814      *
    815      * @param uiRoot The root UI node to search for name conflicts from
    816      * @param name The XML node prefix name to look for
    817      * @return A suitable generated id in the attribute form needed by the XML id tag
    818      * (e.g. "@+id/something")
    819      */
    820     public static String getFreeWidgetId(UiElementNode uiRoot, String name) {
    821         if ("TabWidget".equals(name)) {                        //$NON-NLS-1$
    822             return "@android:id/tabs";                         //$NON-NLS-1$
    823         }
    824 
    825         return NEW_ID_PREFIX + getFreeWidgetId(uiRoot,
    826                 new Object[] { name, null, null, null });
    827     }
    828 
    829     /**
    830      * Given a UI root node, returns the first available id that matches the
    831      * pattern "prefix%d".
    832      *
    833      * For recursion purposes, a "context" is given. Since Java doesn't have in-out parameters
    834      * in methods and we're not going to do a dedicated type, we just use an object array which
    835      * must contain one initial item and several are built on the fly just for internal storage:
    836      * <ul>
    837      * <li> prefix(String): The prefix of the generated id, i.e. "widget". Cannot be null.
    838      * <li> index(Integer): The minimum index of the generated id. Must start with null.
    839      * <li> generated(String): The generated widget currently being searched. Must start with null.
    840      * <li> map(Set<String>): A set of the ids collected so far when walking through the widget
    841      *                        hierarchy. Must start with null.
    842      * </ul>
    843      *
    844      * @param uiRoot The Ui root node where to start searching recursively. For the initial call
    845      *               you want to pass the document root.
    846      * @param params An in-out context of parameters used during recursion, as explained above.
    847      * @return A suitable generated id
    848      */
    849     @SuppressWarnings("unchecked")
    850     private static String getFreeWidgetId(UiElementNode uiRoot,
    851             Object[] params) {
    852 
    853         Set<String> map = (Set<String>)params[3];
    854         if (map == null) {
    855             params[3] = map = new HashSet<String>();
    856         }
    857 
    858         int num = params[1] == null ? 0 : ((Integer)params[1]).intValue();
    859 
    860         String generated = (String) params[2];
    861         String prefix = (String) params[0];
    862         if (generated == null) {
    863             int pos = prefix.indexOf('.');
    864             if (pos >= 0) {
    865                 prefix = prefix.substring(pos + 1);
    866             }
    867             pos = prefix.indexOf('$');
    868             if (pos >= 0) {
    869                 prefix = prefix.substring(pos + 1);
    870             }
    871             prefix = prefix.replaceAll("[^a-zA-Z]", "");                //$NON-NLS-1$ $NON-NLS-2$
    872             if (prefix.length() == 0) {
    873                 prefix = DEFAULT_WIDGET_PREFIX;
    874             } else {
    875                 // Lowercase initial character
    876                 prefix = Character.toLowerCase(prefix.charAt(0)) + prefix.substring(1);
    877             }
    878 
    879             // Note that we perform locale-independent lowercase checks; in "Image" we
    880             // want the lowercase version to be "image", not "?mage" where ? is
    881             // the char LATIN SMALL LETTER DOTLESS I.
    882             do {
    883                 num++;
    884                 generated = String.format("%1$s%2$d", prefix, num);   //$NON-NLS-1$
    885             } while (map.contains(generated.toLowerCase(Locale.US)));
    886 
    887             params[0] = prefix;
    888             params[1] = num;
    889             params[2] = generated;
    890         }
    891 
    892         String id = uiRoot.getAttributeValue(ATTR_ID);
    893         if (id != null) {
    894             id = id.replace(NEW_ID_PREFIX, "");                            //$NON-NLS-1$
    895             id = id.replace(ID_PREFIX, "");                                //$NON-NLS-1$
    896             if (map.add(id.toLowerCase(Locale.US))
    897                     && map.contains(generated.toLowerCase(Locale.US))) {
    898 
    899                 do {
    900                     num++;
    901                     generated = String.format("%1$s%2$d", prefix, num);   //$NON-NLS-1$
    902                 } while (map.contains(generated.toLowerCase(Locale.US)));
    903 
    904                 params[1] = num;
    905                 params[2] = generated;
    906             }
    907         }
    908 
    909         for (UiElementNode uiChild : uiRoot.getUiChildren()) {
    910             getFreeWidgetId(uiChild, params);
    911         }
    912 
    913         // Note: return params[2] (not "generated") since it could have changed during recursion.
    914         return (String) params[2];
    915     }
    916 
    917     /**
    918      * Returns true if the given descriptor represents a view that not only can have
    919      * children but which allows us to <b>insert</b> children. Some views, such as
    920      * ListView (and in general all AdapterViews), disallow children to be inserted except
    921      * through the dedicated AdapterView interface to do it.
    922      *
    923      * @param descriptor the descriptor for the view in question
    924      * @param viewObject an actual instance of the view, or null if not available
    925      * @return true if the descriptor describes a view which allows insertion of child
    926      *         views
    927      */
    928     public static boolean canInsertChildren(ElementDescriptor descriptor, Object viewObject) {
    929         if (descriptor.hasChildren()) {
    930             if (viewObject != null) {
    931                 // We have a view object; see if it derives from an AdapterView
    932                 Class<?> clz = viewObject.getClass();
    933                 while (clz != null) {
    934                     if (clz.getName().equals(FQCN_ADAPTER_VIEW)) {
    935                         return false;
    936                     }
    937                     clz = clz.getSuperclass();
    938                 }
    939             } else {
    940                 // No view object, so we can't easily look up the class and determine
    941                 // whether it's an AdapterView; instead, look at the fixed list of builtin
    942                 // concrete subclasses of AdapterView
    943                 String viewName = descriptor.getXmlLocalName();
    944                 if (viewName.equals(LIST_VIEW) || viewName.equals(EXPANDABLE_LIST_VIEW)
    945                         || viewName.equals(GALLERY) || viewName.equals(GRID_VIEW)) {
    946 
    947                     // We should really also enforce that
    948                     // XmlUtils.ANDROID_URI.equals(descriptor.getNameSpace())
    949                     // here and if not, return true, but it turns out the getNameSpace()
    950                     // for elements are often "".
    951 
    952                     return false;
    953                 }
    954             }
    955 
    956             return true;
    957         }
    958 
    959         return false;
    960     }
    961 }
    962