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