• 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 
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      */
appendAttributes(ArrayList<AttributeDescriptor> attributes, String elementXmlName, String nsUri, AttributeInfo[] infos, Set<String> requiredAttributes, Map<String, ITextAttributeCreator> overrides)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      */
appendAttribute(ArrayList<AttributeDescriptor> attributes, String elementXmlName, String nsUri, AttributeInfo info, boolean required, Map<String, ITextAttributeCreator> overrides)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      */
cleanupJavadocHtml(String s)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      */
getBasename(String fqcn)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      */
setDefaultLayoutAttributes(UiElementNode node, boolean updateLayout)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      */
getFreeWidgetId(UiElementNode uiNode)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      */
getFreeWidgetId(UiElementNode uiRoot, String name)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")
getFreeWidgetId(UiElementNode uiRoot, Object[] params)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      */
canInsertChildren(ElementDescriptor descriptor, Object viewObject)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