OSDN Git Service

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