2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.editors.descriptors;
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;
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;
45 import org.eclipse.swt.graphics.Image;
47 import java.util.ArrayList;
48 import java.util.HashSet;
51 import java.util.Map.Entry;
52 import java.util.regex.Matcher;
53 import java.util.regex.Pattern;
57 * Utility methods related to descriptors handling.
59 public final class DescriptorsUtils {
61 private static final String DEFAULT_WIDGET_PREFIX = "widget";
63 private static final int JAVADOC_BREAK_LENGTH = 60;
66 * The path in the online documentation for the manifest description.
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
72 public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#"; //$NON-NLS-1$
74 public static final String IMAGE_KEY = "image"; //$NON-NLS-1$
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$
82 * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
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].
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)) {
109 appendAttribute(attributes, elementXmlName, nsUri, info, required, overrides);
114 * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
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].
126 public static void appendAttribute(ArrayList<AttributeDescriptor> attributes,
127 String elementXmlName,
129 AttributeInfo info, boolean required,
130 Map<String, ITextAttributeCreator> overrides) {
131 AttributeDescriptor attr = null;
133 String xmlLocalName = info.getName();
134 String uiName = prettyAttributeUiName(info.getName()); // ui_name
136 uiName += "*"; //$NON-NLS-1$
139 String tooltip = null;
140 String rawTooltip = info.getJavaDoc();
141 if (rawTooltip == null) {
145 String deprecated = info.getDeprecatedDoc();
146 if (deprecated != null) {
147 if (rawTooltip.length() > 0) {
148 rawTooltip += "@@"; //$NON-NLS-1$ insert a break
150 rawTooltip += "* Deprecated";
151 if (deprecated.length() != 0) {
152 rawTooltip += ": " + deprecated; //$NON-NLS-1$
154 if (deprecated.length() == 0 || !deprecated.endsWith(".")) { //$NON-NLS-1$
155 rawTooltip += "."; //$NON-NLS-1$
159 // Add the known types to the tooltip
160 Format[] formats_list = info.getFormats();
161 int flen = formats_list.length;
163 // Fill the formats in a set for faster access
164 HashSet<Format> formats_set = new HashSet<Format>();
166 StringBuilder sb = new StringBuilder();
167 if (rawTooltip != null && rawTooltip.length() > 0) {
168 sb.append(rawTooltip);
169 sb.append(" "); //$NON-NLS-1$
171 if (sb.length() > 0) {
172 sb.append("@@"); //$NON-NLS-1$ @@ inserts a break before the types
174 sb.append("["); //$NON-NLS-1$
175 for (int i = 0; i < flen; i++) {
176 Format f = formats_list[i];
179 sb.append(f.toString().toLowerCase());
181 sb.append(", "); //$NON-NLS-1$
184 // The extra space at the end makes the tooltip more readable on Windows.
185 sb.append("]"); //$NON-NLS-1$
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.");
193 // The extra space at the end makes the tooltip more readable on Windows.
194 sb.append(" "); //$NON-NLS-1$
196 rawTooltip = sb.toString();
197 tooltip = formatTooltip(rawTooltip);
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:
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) {
211 } else if (elements.length == 1) {
212 overrideAttrLocalName = elements[0];
215 overrideAttrLocalName = elements[elements.length - 1];
216 elements = elements[0].split(","); //$NON-NLS-1$
219 if (overrideAttrLocalName == null ||
220 !overrideAttrLocalName.equals(xmlLocalName)) {
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)) {
239 ITextAttributeCreator override = entry.getValue();
240 if (override != null) {
241 attr = override.create(xmlLocalName, uiName, nsUri, tooltip, info);
246 // Create a specialized descriptor if we can, based on type
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);
268 // By default a simple text field is used
270 if (tooltip == null) {
271 tooltip = formatTooltip(rawTooltip);
273 attr = new TextAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip, info);
275 attributes.add(attr);
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
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.
290 public static boolean containsAttribute(ArrayList<AttributeDescriptor> attributes,
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()))) {
306 * Create a pretty attribute UI name from an XML name.
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".
312 public static String prettyAttributeUiName(String name) {
313 if (name.length() < 1) {
316 StringBuffer buf = new StringBuffer();
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++) {
324 if (c >= 'A' && c <= 'Z') {
325 // Break camel case into separate words
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' &&
331 (i < len-1 && name.charAt(i+1) >= 'A' && name.charAt(i+1) <= 'Z'))) {
334 buf.append((char)(c - 'A' + 'a'));
336 } else if (c == '_') {
343 name = buf.toString();
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");
355 * Formats the javadoc tooltip to be usable in a tooltip.
357 public static String formatTooltip(String javadoc) {
358 ArrayList<String> spans = scanJavadoc(javadoc);
360 StringBuilder sb = new StringBuilder();
361 boolean needBreak = false;
363 for (int n = spans.size(), i = 0; i < n; ++i) {
364 String s = spans.get(i);
365 if (CODE.equals(s)) {
368 sb.append('"').append(s).append('"');
370 } else if (LINK.equals(s)) {
371 String base = spans.get(++i);
372 String anchor = spans.get(++i);
373 String text = spans.get(++i);
378 if (anchor != null) {
379 anchor = anchor.trim();
385 // If there's no text, use the anchor if there's one
386 if (text == null || text.length() == 0) {
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
401 } else if (ELEM.equals(s)) {
406 } else if (BREAK.equals(s)) {
408 } else if (s != null) {
409 if (needBreak && s.trim().length() > 0) {
417 return sb.toString();
421 * Formats the javadoc tooltip to be usable in a FormText.
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>
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>
432 public static String formatFormText(String javadoc,
433 ElementDescriptor elementDescriptor,
434 String androidDocBaseUrl) {
435 ArrayList<String> spans = scanJavadoc(javadoc);
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;
443 StringBuilder sb = new StringBuilder();
445 Image icon = elementDescriptor.getCustomizedIcon();
447 sb.append("<form><li style=\"image\" value=\"" + //$NON-NLS-1$
448 IMAGE_KEY + "\">"); //$NON-NLS-1$
450 sb.append("<form><p>"); //$NON-NLS-1$
453 for (int n = spans.size(), i = 0; i < n; ++i) {
454 String s = spans.get(i);
455 if (CODE.equals(s)) {
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$
462 sb.append("</a>"); //$NON-NLS-1$
463 } else if (s != null) {
464 sb.append('"').append(s).append('"');
466 } else if (LINK.equals(s)) {
467 String base = spans.get(++i);
468 String anchor = spans.get(++i);
469 String text = spans.get(++i);
474 if (anchor != null) {
475 anchor = anchor.trim();
481 // If there's no text, use the anchor if there's one
482 if (text == null || text.length() == 0) {
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) {
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
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('#');
503 url += "#"; //$NON-NLS-1$
504 } else if (pos < url.length() - 1) {
505 url = url.substring(0, pos + 1);
510 } else if (text == null || text.length() == 0) {
511 // If we still have no text, use the base as text
516 if (url != null && text != null) {
517 sb.append("<a href=\""); //$NON-NLS-1$
519 sb.append("\">"); //$NON-NLS-1$
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$
526 } else if (ELEM.equals(s)) {
528 if (sdkUrl != null && s != null) {
529 sb.append("<a href=\""); //$NON-NLS-1$
531 sb.append("\">"); //$NON-NLS-1$
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$
537 } else if (BREAK.equals(s)) {
538 // ignore line breaks in pseudo-HTML rendering
539 } else if (s != null) {
545 sb.append("</li></form>"); //$NON-NLS-1$
547 sb.append("</p></form>"); //$NON-NLS-1$
549 return sb.toString();
552 private static ArrayList<String> scanJavadoc(String javadoc) {
553 ArrayList<String> spans = new ArrayList<String>();
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$
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$
573 int currentLength = 0;
576 while(javadoc != null && javadoc.length() > 0) {
579 if ((m = p_code.matcher(javadoc)).matches()) {
581 spans.add(text = cleanupJavadocHtml(m.group(1))); // <code> text
582 javadoc = m.group(2);
584 currentLength += text.length();
586 } else if ((m = p_link.matcher(javadoc)).matches()) {
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);
593 currentLength += text.length();
595 } else if ((m = p_elem.matcher(javadoc)).matches()) {
597 spans.add(text = cleanupJavadocHtml(m.group(1))); // @text@
598 javadoc = m.group(2);
600 currentLength += text.length() - 2;
602 } else if ((m = p_break.matcher(javadoc)).matches()) {
605 javadoc = m.group(1);
606 } else if ((m = p_open.matcher(javadoc)).matches()) {
608 javadoc = m.group(2);
609 } else if ((m = p_text.matcher(javadoc)).matches()) {
611 javadoc = m.group(2);
613 // This is not supposed to happen. In case of, just use everything.
617 if (s != null && s.length() > 0) {
618 s = cleanupJavadocHtml(s);
620 if (currentLength >= JAVADOC_BREAK_LENGTH) {
624 while (currentLength + s.length() > JAVADOC_BREAK_LENGTH) {
625 int pos = s.indexOf(' ', JAVADOC_BREAK_LENGTH - currentLength);
629 spans.add(s.substring(0, pos + 1));
632 s = s.substring(pos + 1);
636 currentLength += s.length();
644 * Remove anything that looks like HTML from a javadoc snippet, as it is supported
645 * neither by FormText nor a standard text tooltip.
647 private static String cleanupJavadocHtml(String s) {
649 s = s.replaceAll("<", "\""); //$NON-NLS-1$ $NON-NLS-2$
650 s = s.replaceAll(">", "\""); //$NON-NLS-1$ $NON-NLS-2$
651 s = s.replaceAll("<[^>]+>", ""); //$NON-NLS-1$ $NON-NLS-2$
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.
660 * @param fqcn The fully qualified class name to convert
661 * @return the basename of the class name
663 public static String getBasename(String fqcn) {
665 int lastDot = name.lastIndexOf('.');
667 name = name.substring(lastDot + 1);
674 * Sets the default layout attributes for the a new UiElementNode.
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.
679 * This does not override attributes which are not empty.
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();
686 if (descriptor.getXmlLocalName().equals(REQUEST_FOCUS)) {
687 // Don't add ids etc to <requestFocus>
691 boolean fill = descriptor.hasChildren() &&
692 node.getUiParent() instanceof UiDocumentNode;
693 node.setAttributeValue(
695 SdkConstants.NS_RESOURCES,
696 fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT,
697 false /* override */);
698 node.setAttributeValue(
700 SdkConstants.NS_RESOURCES,
701 fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT,
702 false /* override */);
704 String freeId = getFreeWidgetId(node);
705 if (freeId != null) {
706 node.setAttributeValue(
708 SdkConstants.NS_RESOURCES,
710 false /* override */);
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(
718 SdkConstants.NS_RESOURCES,
724 UiElementNode parent = node.getUiParent();
725 if (parent != null &&
726 parent.getDescriptor().getXmlLocalName().equals(
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(
735 SdkConstants.NS_RESOURCES,
737 false /* override */);
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".
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")
753 public static String getFreeWidgetId(UiElementNode uiNode) {
754 String name = getBasename(uiNode.getDescriptor().getXmlLocalName());
755 return getFreeWidgetId(uiNode.getUiRoot(), name);
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".
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")
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$
773 return NEW_ID_PREFIX + getFreeWidgetId(uiRoot,
774 new Object[] { name, null, null, null });
778 * Given a UI root node, returns the first available id that matches the
779 * pattern "prefix%d".
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:
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.
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
797 @SuppressWarnings("unchecked")
798 private static String getFreeWidgetId(UiElementNode uiRoot,
801 Set<String> map = (Set<String>)params[3];
803 params[3] = map = new HashSet<String>();
806 int num = params[1] == null ? 0 : ((Integer)params[1]).intValue();
808 String generated = (String) params[2];
809 String prefix = (String) params[0];
810 if (generated == null) {
811 int pos = prefix.indexOf('.');
813 prefix = prefix.substring(pos + 1);
815 pos = prefix.indexOf('$');
817 prefix = prefix.substring(pos + 1);
819 prefix = prefix.replaceAll("[^a-zA-Z]", ""); //$NON-NLS-1$ $NON-NLS-2$
820 if (prefix.length() == 0) {
821 prefix = DEFAULT_WIDGET_PREFIX;
823 // Lowercase initial character
824 prefix = Character.toLowerCase(prefix.charAt(0)) + prefix.substring(1);
829 generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
830 } while (map.contains(generated.toLowerCase()));
834 params[2] = generated;
837 String id = uiRoot.getAttributeValue(ATTR_ID);
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())) {
845 generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
846 } while (map.contains(generated.toLowerCase()));
849 params[2] = generated;
853 for (UiElementNode uiChild : uiRoot.getUiChildren()) {
854 getFreeWidgetId(uiChild, params);
857 // Note: return params[2] (not "generated") since it could have changed during recursion.
858 return (String) params[2];
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.
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
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)) {
881 clz = clz.getSuperclass();
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)) {
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 "".