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