aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java961
1 files changed, 961 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
new file mode 100644
index 000000000..da3a1856c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
@@ -0,0 +1,961 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.descriptors;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.EDIT_TEXT;
+import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_ADAPTER_VIEW;
+import static com.android.SdkConstants.GALLERY;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.GRID_VIEW;
+import static com.android.SdkConstants.GT_ENTITY;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.LIST_VIEW;
+import static com.android.SdkConstants.LT_ENTITY;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.RELATIVE_LAYOUT;
+import static com.android.SdkConstants.REQUEST_FOCUS;
+import static com.android.SdkConstants.SPACE;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.resources.ResourceType;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * Utility methods related to descriptors handling.
+ */
+public final class DescriptorsUtils {
+ private static final String DEFAULT_WIDGET_PREFIX = "widget";
+
+ private static final int JAVADOC_BREAK_LENGTH = 60;
+
+ /**
+ * The path in the online documentation for the manifest description.
+ * <p/>
+ * This is NOT a complete URL. To be used, it needs to be appended
+ * to {@link AdtConstants#CODESITE_BASE_URL} or to the local SDK
+ * documentation.
+ */
+ public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#"; //$NON-NLS-1$
+
+ public static final String IMAGE_KEY = "image"; //$NON-NLS-1$
+
+ private static final String CODE = "$code"; //$NON-NLS-1$
+ private static final String LINK = "$link"; //$NON-NLS-1$
+ private static final String ELEM = "$elem"; //$NON-NLS-1$
+ private static final String BREAK = "$break"; //$NON-NLS-1$
+
+ /**
+ * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to append to
+ * @param elementXmlName Optional XML local name of the element to which attributes are
+ * being added. When not null, this is used to filter overrides.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param infos The array of {@link AttributeInfo} to read and append to attributes
+ * @param requiredAttributes An optional set of attributes to mark as "required" (i.e. append
+ * a "*" to their UI name as a hint for the user.) If not null, must contains
+ * entries in the form "elem-name/attr-name". Elem-name can be "*".
+ * @param overrides A map [attribute name => ITextAttributeCreator creator].
+ */
+ public static void appendAttributes(List<AttributeDescriptor> attributes,
+ String elementXmlName,
+ String nsUri, AttributeInfo[] infos,
+ Set<String> requiredAttributes,
+ Map<String, ITextAttributeCreator> overrides) {
+ for (AttributeInfo info : infos) {
+ boolean required = false;
+ if (requiredAttributes != null) {
+ String attr_name = info.getName();
+ if (requiredAttributes.contains("*/" + attr_name) ||
+ requiredAttributes.contains(elementXmlName + "/" + attr_name)) {
+ required = true;
+ }
+ }
+ appendAttribute(attributes, elementXmlName, nsUri, info, required, overrides);
+ }
+ }
+
+ /**
+ * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to append to
+ * @param elementXmlName Optional XML local name of the element to which attributes are
+ * being added. When not null, this is used to filter overrides.
+ * @param info The {@link AttributeInfo} to append to attributes
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param required True if the attribute is to be marked as "required" (i.e. append
+ * a "*" to its UI name as a hint for the user.)
+ * @param overrides A map [attribute name => ITextAttributeCreator creator].
+ */
+ public static void appendAttribute(List<AttributeDescriptor> attributes,
+ String elementXmlName,
+ String nsUri,
+ AttributeInfo info, boolean required,
+ Map<String, ITextAttributeCreator> overrides) {
+ TextAttributeDescriptor attr = null;
+
+ String xmlLocalName = info.getName();
+
+ // Add the known types to the tooltip
+ EnumSet<Format> formats_set = info.getFormats();
+ int flen = formats_set.size();
+ if (flen > 0) {
+ // Create a specialized attribute if we can
+ if (overrides != null) {
+ for (Entry<String, ITextAttributeCreator> entry: overrides.entrySet()) {
+ // The override key can have the following formats:
+ // */xmlLocalName
+ // element/xmlLocalName
+ // element1,element2,...,elementN/xmlLocalName
+ String key = entry.getKey();
+ String elements[] = key.split("/"); //$NON-NLS-1$
+ String overrideAttrLocalName = null;
+ if (elements.length < 1) {
+ continue;
+ } else if (elements.length == 1) {
+ overrideAttrLocalName = elements[0];
+ elements = null;
+ } else {
+ overrideAttrLocalName = elements[elements.length - 1];
+ elements = elements[0].split(","); //$NON-NLS-1$
+ }
+
+ if (overrideAttrLocalName == null ||
+ !overrideAttrLocalName.equals(xmlLocalName)) {
+ continue;
+ }
+
+ boolean ok_element = elements != null && elements.length < 1;
+ if (!ok_element && elements != null) {
+ for (String element : elements) {
+ if (element.equals("*") //$NON-NLS-1$
+ || element.equals(elementXmlName)) {
+ ok_element = true;
+ break;
+ }
+ }
+ }
+
+ if (!ok_element) {
+ continue;
+ }
+
+ ITextAttributeCreator override = entry.getValue();
+ if (override != null) {
+ attr = override.create(xmlLocalName, nsUri, info);
+ }
+ }
+ } // if overrides
+
+ // Create a specialized descriptor if we can, based on type
+ if (attr == null) {
+ if (formats_set.contains(Format.REFERENCE)) {
+ // This is either a multi-type reference or a generic reference.
+ attr = new ReferenceAttributeDescriptor(
+ xmlLocalName, nsUri, info);
+ } else if (formats_set.contains(Format.ENUM)) {
+ attr = new ListAttributeDescriptor(
+ xmlLocalName, nsUri, info);
+ } else if (formats_set.contains(Format.FLAG)) {
+ attr = new FlagAttributeDescriptor(
+ xmlLocalName, nsUri, info);
+ } else if (formats_set.contains(Format.BOOLEAN)) {
+ attr = new BooleanAttributeDescriptor(
+ xmlLocalName, nsUri, info);
+ } else if (formats_set.contains(Format.STRING)) {
+ attr = new ReferenceAttributeDescriptor(
+ ResourceType.STRING, xmlLocalName, nsUri, info);
+ }
+ }
+ }
+
+ // By default a simple text field is used
+ if (attr == null) {
+ attr = new TextAttributeDescriptor(xmlLocalName, nsUri, info);
+ }
+
+ if (required) {
+ attr.setRequired(true);
+ }
+
+ attributes.add(attr);
+ }
+
+ /**
+ * Indicates the the given {@link AttributeInfo} already exists in the ArrayList of
+ * {@link AttributeDescriptor}. This test for the presence of a descriptor with the same
+ * XML name.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to compare to.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param info The {@link AttributeInfo} to know whether it is included in the above list.
+ * @return True if this {@link AttributeInfo} is already present in
+ * the {@link AttributeDescriptor} list.
+ */
+ public static boolean containsAttribute(ArrayList<AttributeDescriptor> attributes,
+ String nsUri,
+ AttributeInfo info) {
+ String xmlLocalName = info.getName();
+ for (AttributeDescriptor desc : attributes) {
+ if (desc.getXmlLocalName().equals(xmlLocalName)) {
+ if (nsUri == desc.getNamespaceUri() ||
+ (nsUri != null && nsUri.equals(desc.getNamespaceUri()))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Create a pretty attribute UI name from an XML name.
+ * <p/>
+ * The original xml name starts with a lower case and is camel-case,
+ * e.g. "maxWidthForView". The pretty name starts with an upper case
+ * and has space separators, e.g. "Max width for view".
+ */
+ public static String prettyAttributeUiName(String name) {
+ if (name.length() < 1) {
+ return name;
+ }
+ StringBuilder buf = new StringBuilder(2 * name.length());
+
+ char c = name.charAt(0);
+ // Use upper case initial letter
+ buf.append(Character.toUpperCase(c));
+ int len = name.length();
+ for (int i = 1; i < len; i++) {
+ c = name.charAt(i);
+ if (Character.isUpperCase(c)) {
+ // Break camel case into separate words
+ buf.append(' ');
+ // Use a lower case initial letter for the next word, except if the
+ // word is solely X, Y or Z.
+ if (c >= 'X' && c <= 'Z' &&
+ (i == len-1 ||
+ (i < len-1 && Character.isUpperCase(name.charAt(i+1))))) {
+ buf.append(c);
+ } else {
+ buf.append(Character.toLowerCase(c));
+ }
+ } else if (c == '_') {
+ buf.append(' ');
+ } else {
+ buf.append(c);
+ }
+ }
+
+ name = buf.toString();
+
+ name = replaceAcronyms(name);
+
+ return name;
+ }
+
+ /**
+ * Similar to {@link #prettyAttributeUiName(String)}, but it will capitalize
+ * all words, not just the first one.
+ * <p/>
+ * The original xml name starts with a lower case and is camel-case, e.g.
+ * "maxWidthForView". The corresponding return value is
+ * "Max Width For View".
+ *
+ * @param name the attribute name, which should be a camel case name, e.g.
+ * "maxWidth"
+ * @return the corresponding display name, e.g. "Max Width"
+ */
+ @NonNull
+ public static String capitalize(@NonNull String name) {
+ if (name.isEmpty()) {
+ return name;
+ }
+ StringBuilder buf = new StringBuilder(2 * name.length());
+
+ char c = name.charAt(0);
+ // Use upper case initial letter
+ buf.append(Character.toUpperCase(c));
+ int len = name.length();
+ for (int i = 1; i < len; i++) {
+ c = name.charAt(i);
+ if (Character.isUpperCase(c)) {
+ // Break camel case into separate words
+ buf.append(' ');
+ // Use a lower case initial letter for the next word, except if the
+ // word is solely X, Y or Z.
+ buf.append(c);
+ } else if (c == '_') {
+ buf.append(' ');
+ if (i < len -1 && Character.isLowerCase(name.charAt(i + 1))) {
+ buf.append(Character.toUpperCase(name.charAt(i + 1)));
+ i++;
+ }
+ } else {
+ buf.append(c);
+ }
+ }
+
+ name = buf.toString();
+
+ name = replaceAcronyms(name);
+
+ return name;
+ }
+
+ private static String replaceAcronyms(String name) {
+ // Replace these acronyms by upper-case versions
+ // - (?<=^| ) means "if preceded by a space or beginning of string"
+ // - (?=$| ) means "if followed by a space or end of string"
+ if (name.contains("sdk") || name.contains("Sdk")) {
+ name = name.replaceAll("(?<=^| )[sS]dk(?=$| )", "SDK");
+ }
+ if (name.contains("uri") || name.contains("Uri")) {
+ name = name.replaceAll("(?<=^| )[uU]ri(?=$| )", "URI");
+ }
+ if (name.contains("ime") || name.contains("Ime")) {
+ name = name.replaceAll("(?<=^| )[iI]me(?=$| )", "IME");
+ }
+ if (name.contains("vm") || name.contains("Vm")) {
+ name = name.replaceAll("(?<=^| )[vV]m(?=$| )", "VM");
+ }
+ if (name.contains("ui") || name.contains("Ui")) {
+ name = name.replaceAll("(?<=^| )[uU]i(?=$| )", "UI");
+ }
+ return name;
+ }
+
+ /**
+ * Formats the javadoc tooltip to be usable in a tooltip.
+ */
+ public static String formatTooltip(String javadoc) {
+ ArrayList<String> spans = scanJavadoc(javadoc);
+
+ StringBuilder sb = new StringBuilder();
+ boolean needBreak = false;
+
+ for (int n = spans.size(), i = 0; i < n; ++i) {
+ String s = spans.get(i);
+ if (CODE.equals(s)) {
+ s = spans.get(++i);
+ if (s != null) {
+ sb.append('"').append(s).append('"');
+ }
+ } else if (LINK.equals(s)) {
+ String base = spans.get(++i);
+ String anchor = spans.get(++i);
+ String text = spans.get(++i);
+
+ if (base != null) {
+ base = base.trim();
+ }
+ if (anchor != null) {
+ anchor = anchor.trim();
+ }
+ if (text != null) {
+ text = text.trim();
+ }
+
+ // If there's no text, use the anchor if there's one
+ if (text == null || text.length() == 0) {
+ text = anchor;
+ }
+
+ if (base != null && base.length() > 0) {
+ if (text == null || text.length() == 0) {
+ // If we still have no text, use the base as text
+ text = base;
+ }
+ }
+
+ if (text != null) {
+ sb.append(text);
+ }
+
+ } else if (ELEM.equals(s)) {
+ s = spans.get(++i);
+ if (s != null) {
+ sb.append(s);
+ }
+ } else if (BREAK.equals(s)) {
+ needBreak = true;
+ } else if (s != null) {
+ if (needBreak && s.trim().length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(s);
+ needBreak = false;
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the javadoc tooltip to be usable in a FormText.
+ * <p/>
+ * If the descriptor can provide an icon, the caller should provide
+ * elementsDescriptor.getIcon() as "image" to FormText, e.g.:
+ * <code>formText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());</code>
+ *
+ * @param javadoc The javadoc to format. Cannot be null.
+ * @param elementDescriptor The element descriptor parent of the javadoc. Cannot be null.
+ * @param androidDocBaseUrl The base URL for the documentation. Cannot be null. Should be
+ * <code>FrameworkResourceManager.getInstance().getDocumentationBaseUrl()</code>
+ */
+ public static String formatFormText(String javadoc,
+ ElementDescriptor elementDescriptor,
+ String androidDocBaseUrl) {
+ ArrayList<String> spans = scanJavadoc(javadoc);
+
+ String fullSdkUrl = androidDocBaseUrl + MANIFEST_SDK_URL;
+ String sdkUrl = elementDescriptor.getSdkUrl();
+ if (sdkUrl != null && sdkUrl.startsWith(MANIFEST_SDK_URL)) {
+ fullSdkUrl = androidDocBaseUrl + sdkUrl;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ Image icon = elementDescriptor.getCustomizedIcon();
+ if (icon != null) {
+ sb.append("<form><li style=\"image\" value=\"" + //$NON-NLS-1$
+ IMAGE_KEY + "\">"); //$NON-NLS-1$
+ } else {
+ sb.append("<form><p>"); //$NON-NLS-1$
+ }
+
+ for (int n = spans.size(), i = 0; i < n; ++i) {
+ String s = spans.get(i);
+ if (CODE.equals(s)) {
+ s = spans.get(++i);
+ if (elementDescriptor.getXmlName().equals(s) && fullSdkUrl != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(fullSdkUrl);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(s);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (s != null) {
+ sb.append('"').append(s).append('"');
+ }
+ } else if (LINK.equals(s)) {
+ String base = spans.get(++i);
+ String anchor = spans.get(++i);
+ String text = spans.get(++i);
+
+ if (base != null) {
+ base = base.trim();
+ }
+ if (anchor != null) {
+ anchor = anchor.trim();
+ }
+ if (text != null) {
+ text = text.trim();
+ }
+
+ // If there's no text, use the anchor if there's one
+ if (text == null || text.length() == 0) {
+ text = anchor;
+ }
+
+ // TODO specialize with a base URL for views, menus & other resources
+ // Base is empty for a local page anchor, in which case we'll replace it
+ // by the element SDK URL if it exists.
+ if ((base == null || base.length() == 0) && fullSdkUrl != null) {
+ base = fullSdkUrl;
+ }
+
+ String url = null;
+ if (base != null && base.length() > 0) {
+ if (base.startsWith("http")) { //$NON-NLS-1$
+ // If base looks an URL, use it, with the optional anchor
+ url = base;
+ if (anchor != null && anchor.length() > 0) {
+ // If the base URL already has an anchor, it needs to be
+ // removed first. If there's no anchor, we need to add "#"
+ int pos = url.lastIndexOf('#');
+ if (pos < 0) {
+ url += "#"; //$NON-NLS-1$
+ } else if (pos < url.length() - 1) {
+ url = url.substring(0, pos + 1);
+ }
+
+ url += anchor;
+ }
+ } else if (text == null || text.length() == 0) {
+ // If we still have no text, use the base as text
+ text = base;
+ }
+ }
+
+ if (url != null && text != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(url);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(text);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (text != null) {
+ sb.append("<b>").append(text).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ } else if (ELEM.equals(s)) {
+ s = spans.get(++i);
+ if (sdkUrl != null && s != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(sdkUrl);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(s);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (s != null) {
+ sb.append("<b>").append(s).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ } else if (BREAK.equals(s)) {
+ // ignore line breaks in pseudo-HTML rendering
+ } else if (s != null) {
+ sb.append(s);
+ }
+ }
+
+ if (icon != null) {
+ sb.append("</li></form>"); //$NON-NLS-1$
+ } else {
+ sb.append("</p></form>"); //$NON-NLS-1$
+ }
+ return sb.toString();
+ }
+
+ private static ArrayList<String> scanJavadoc(String javadoc) {
+ ArrayList<String> spans = new ArrayList<String>();
+
+ // Standardize all whitespace in the javadoc to single spaces.
+ if (javadoc != null) {
+ javadoc = javadoc.replaceAll("[ \t\f\r\n]+", " "); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ // Detects {@link <base>#<name> <text>} where all 3 are optional
+ Pattern p_link = Pattern.compile("\\{@link\\s+([^#\\}\\s]*)(?:#([^\\s\\}]*))?(?:\\s*([^\\}]*))?\\}(.*)"); //$NON-NLS-1$
+ // Detects <code>blah</code>
+ Pattern p_code = Pattern.compile("<code>(.+?)</code>(.*)"); //$NON-NLS-1$
+ // Detects @blah@, used in hard-coded tooltip descriptors
+ Pattern p_elem = Pattern.compile("@([\\w -]+)@(.*)"); //$NON-NLS-1$
+ // Detects a buffer that starts by @@ (request for a break)
+ Pattern p_break = Pattern.compile("@@(.*)"); //$NON-NLS-1$
+ // Detects a buffer that starts by @ < or { (one that was not matched above)
+ Pattern p_open = Pattern.compile("([@<\\{])(.*)"); //$NON-NLS-1$
+ // Detects everything till the next potential separator, i.e. @ < or {
+ Pattern p_text = Pattern.compile("([^@<\\{]+)(.*)"); //$NON-NLS-1$
+
+ int currentLength = 0;
+ String text = null;
+
+ while(javadoc != null && javadoc.length() > 0) {
+ Matcher m;
+ String s = null;
+ if ((m = p_code.matcher(javadoc)).matches()) {
+ spans.add(CODE);
+ spans.add(text = cleanupJavadocHtml(m.group(1))); // <code> text
+ javadoc = m.group(2);
+ if (text != null) {
+ currentLength += text.length();
+ }
+ } else if ((m = p_link.matcher(javadoc)).matches()) {
+ spans.add(LINK);
+ spans.add(m.group(1)); // @link base
+ spans.add(m.group(2)); // @link anchor
+ spans.add(text = cleanupJavadocHtml(m.group(3))); // @link text
+ javadoc = m.group(4);
+ if (text != null) {
+ currentLength += text.length();
+ }
+ } else if ((m = p_elem.matcher(javadoc)).matches()) {
+ spans.add(ELEM);
+ spans.add(text = cleanupJavadocHtml(m.group(1))); // @text@
+ javadoc = m.group(2);
+ if (text != null) {
+ currentLength += text.length() - 2;
+ }
+ } else if ((m = p_break.matcher(javadoc)).matches()) {
+ spans.add(BREAK);
+ currentLength = 0;
+ javadoc = m.group(1);
+ } else if ((m = p_open.matcher(javadoc)).matches()) {
+ s = m.group(1);
+ javadoc = m.group(2);
+ } else if ((m = p_text.matcher(javadoc)).matches()) {
+ s = m.group(1);
+ javadoc = m.group(2);
+ } else {
+ // This is not supposed to happen. In case of, just use everything.
+ s = javadoc;
+ javadoc = null;
+ }
+ if (s != null && s.length() > 0) {
+ s = cleanupJavadocHtml(s);
+
+ if (currentLength >= JAVADOC_BREAK_LENGTH) {
+ spans.add(BREAK);
+ currentLength = 0;
+ }
+ while (currentLength + s.length() > JAVADOC_BREAK_LENGTH) {
+ int pos = s.indexOf(' ', JAVADOC_BREAK_LENGTH - currentLength);
+ if (pos <= 0) {
+ break;
+ }
+ spans.add(s.substring(0, pos + 1));
+ spans.add(BREAK);
+ currentLength = 0;
+ s = s.substring(pos + 1);
+ }
+
+ spans.add(s);
+ currentLength += s.length();
+ }
+ }
+
+ return spans;
+ }
+
+ /**
+ * Remove anything that looks like HTML from a javadoc snippet, as it is supported
+ * neither by FormText nor a standard text tooltip.
+ */
+ private static String cleanupJavadocHtml(String s) {
+ if (s != null) {
+ s = s.replaceAll(LT_ENTITY, "\""); //$NON-NLS-1$ $NON-NLS-2$
+ s = s.replaceAll(GT_ENTITY, "\""); //$NON-NLS-1$ $NON-NLS-2$
+ s = s.replaceAll("<[^>]+>", ""); //$NON-NLS-1$ $NON-NLS-2$
+ }
+ return s;
+ }
+
+ /**
+ * Returns the basename for the given fully qualified class name. It is okay to pass
+ * a basename to this method which will just be returned back.
+ *
+ * @param fqcn The fully qualified class name to convert
+ * @return the basename of the class name
+ */
+ public static String getBasename(String fqcn) {
+ String name = fqcn;
+ int lastDot = name.lastIndexOf('.');
+ if (lastDot != -1) {
+ name = name.substring(lastDot + 1);
+ }
+
+ return name;
+ }
+
+ /**
+ * Sets the default layout attributes for the a new UiElementNode.
+ * <p/>
+ * Note that ideally the node should already be part of a hierarchy so that its
+ * parent layout and previous sibling can be determined, if any.
+ * <p/>
+ * This does not override attributes which are not empty.
+ */
+ public static void setDefaultLayoutAttributes(UiElementNode node, boolean updateLayout) {
+ // if this ui_node is a layout and we're adding it to a document, use match_parent for
+ // both W/H. Otherwise default to wrap_layout.
+ ElementDescriptor descriptor = node.getDescriptor();
+
+ String name = descriptor.getXmlLocalName();
+ if (name.equals(REQUEST_FOCUS)) {
+ // Don't add ids, widths and heights etc to <requestFocus>
+ return;
+ }
+
+ // Width and height are mandatory in all layouts except GridLayout
+ boolean setSize = !node.getUiParent().getDescriptor().getXmlName().equals(GRID_LAYOUT);
+ if (setSize) {
+ boolean fill = descriptor.hasChildren() &&
+ node.getUiParent() instanceof UiDocumentNode;
+ node.setAttributeValue(
+ ATTR_LAYOUT_WIDTH,
+ ANDROID_URI,
+ fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT,
+ false /* override */);
+ node.setAttributeValue(
+ ATTR_LAYOUT_HEIGHT,
+ ANDROID_URI,
+ fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT,
+ false /* override */);
+ }
+
+ if (needsDefaultId(node.getDescriptor())) {
+ String freeId = getFreeWidgetId(node);
+ if (freeId != null) {
+ node.setAttributeValue(
+ ATTR_ID,
+ ANDROID_URI,
+ freeId,
+ false /* override */);
+ }
+ }
+
+ // Set a text attribute on textual widgets -- but only on those that define a text
+ // attribute
+ if (descriptor.definesAttribute(ANDROID_URI, ATTR_TEXT)
+ // Don't set default text value into edit texts - they typically start out blank
+ && !descriptor.getXmlLocalName().equals(EDIT_TEXT)) {
+ String type = getBasename(descriptor.getUiName());
+ node.setAttributeValue(
+ ATTR_TEXT,
+ ANDROID_URI,
+ type,
+ false /*override*/);
+ }
+
+ if (updateLayout) {
+ UiElementNode parent = node.getUiParent();
+ if (parent != null &&
+ parent.getDescriptor().getXmlLocalName().equals(
+ RELATIVE_LAYOUT)) {
+ UiElementNode previous = node.getUiPreviousSibling();
+ if (previous != null) {
+ String id = previous.getAttributeValue(ATTR_ID);
+ if (id != null && id.length() > 0) {
+ id = id.replace("@+", "@"); //$NON-NLS-1$ //$NON-NLS-2$
+ node.setAttributeValue(
+ ATTR_LAYOUT_BELOW,
+ ANDROID_URI,
+ id,
+ false /* override */);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Determines whether new views of the given type should be assigned a
+ * default id.
+ *
+ * @param descriptor a descriptor describing the view to look up
+ * @return true if new views of the given type should be assigned a default
+ * id
+ */
+ public static boolean needsDefaultId(ElementDescriptor descriptor) {
+ // By default, layouts do not need ids.
+ String tag = descriptor.getXmlLocalName();
+ if (tag.endsWith("Layout") //$NON-NLS-1$
+ || tag.equals(VIEW_INCLUDE)
+ || tag.equals(VIEW_MERGE)
+ || tag.equals(SPACE)
+ || tag.endsWith(SPACE) && tag.length() > SPACE.length() &&
+ tag.charAt(tag.length() - SPACE.length()) == '.') {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Given a UI node, returns the first available id that matches the
+ * pattern "prefix%d".
+ * <p/>TabWidget is a special case and the method will always return "@android:id/tabs".
+ *
+ * @param uiNode The UI node that gives the prefix to match.
+ * @return A suitable generated id in the attribute form needed by the XML id tag
+ * (e.g. "@+id/something")
+ */
+ public static String getFreeWidgetId(UiElementNode uiNode) {
+ String name = getBasename(uiNode.getDescriptor().getXmlLocalName());
+ return getFreeWidgetId(uiNode.getUiRoot(), name);
+ }
+
+ /**
+ * Given a UI root node and a potential XML node name, returns the first available
+ * id that matches the pattern "prefix%d".
+ * <p/>TabWidget is a special case and the method will always return "@android:id/tabs".
+ *
+ * @param uiRoot The root UI node to search for name conflicts from
+ * @param name The XML node prefix name to look for
+ * @return A suitable generated id in the attribute form needed by the XML id tag
+ * (e.g. "@+id/something")
+ */
+ public static String getFreeWidgetId(UiElementNode uiRoot, String name) {
+ if ("TabWidget".equals(name)) { //$NON-NLS-1$
+ return "@android:id/tabs"; //$NON-NLS-1$
+ }
+
+ return NEW_ID_PREFIX + getFreeWidgetId(uiRoot,
+ new Object[] { name, null, null, null });
+ }
+
+ /**
+ * Given a UI root node, returns the first available id that matches the
+ * pattern "prefix%d".
+ *
+ * For recursion purposes, a "context" is given. Since Java doesn't have in-out parameters
+ * in methods and we're not going to do a dedicated type, we just use an object array which
+ * must contain one initial item and several are built on the fly just for internal storage:
+ * <ul>
+ * <li> prefix(String): The prefix of the generated id, i.e. "widget". Cannot be null.
+ * <li> index(Integer): The minimum index of the generated id. Must start with null.
+ * <li> generated(String): The generated widget currently being searched. Must start with null.
+ * <li> map(Set<String>): A set of the ids collected so far when walking through the widget
+ * hierarchy. Must start with null.
+ * </ul>
+ *
+ * @param uiRoot The Ui root node where to start searching recursively. For the initial call
+ * you want to pass the document root.
+ * @param params An in-out context of parameters used during recursion, as explained above.
+ * @return A suitable generated id
+ */
+ @SuppressWarnings("unchecked")
+ private static String getFreeWidgetId(UiElementNode uiRoot,
+ Object[] params) {
+
+ Set<String> map = (Set<String>)params[3];
+ if (map == null) {
+ params[3] = map = new HashSet<String>();
+ }
+
+ int num = params[1] == null ? 0 : ((Integer)params[1]).intValue();
+
+ String generated = (String) params[2];
+ String prefix = (String) params[0];
+ if (generated == null) {
+ int pos = prefix.indexOf('.');
+ if (pos >= 0) {
+ prefix = prefix.substring(pos + 1);
+ }
+ pos = prefix.indexOf('$');
+ if (pos >= 0) {
+ prefix = prefix.substring(pos + 1);
+ }
+ prefix = prefix.replaceAll("[^a-zA-Z]", ""); //$NON-NLS-1$ $NON-NLS-2$
+ if (prefix.length() == 0) {
+ prefix = DEFAULT_WIDGET_PREFIX;
+ } else {
+ // Lowercase initial character
+ prefix = Character.toLowerCase(prefix.charAt(0)) + prefix.substring(1);
+ }
+
+ // Note that we perform locale-independent lowercase checks; in "Image" we
+ // want the lowercase version to be "image", not "?mage" where ? is
+ // the char LATIN SMALL LETTER DOTLESS I.
+ do {
+ num++;
+ generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
+ } while (map.contains(generated.toLowerCase(Locale.US)));
+
+ params[0] = prefix;
+ params[1] = num;
+ params[2] = generated;
+ }
+
+ String id = uiRoot.getAttributeValue(ATTR_ID);
+ if (id != null) {
+ id = id.replace(NEW_ID_PREFIX, ""); //$NON-NLS-1$
+ id = id.replace(ID_PREFIX, ""); //$NON-NLS-1$
+ if (map.add(id.toLowerCase(Locale.US))
+ && map.contains(generated.toLowerCase(Locale.US))) {
+
+ do {
+ num++;
+ generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
+ } while (map.contains(generated.toLowerCase(Locale.US)));
+
+ params[1] = num;
+ params[2] = generated;
+ }
+ }
+
+ for (UiElementNode uiChild : uiRoot.getUiChildren()) {
+ getFreeWidgetId(uiChild, params);
+ }
+
+ // Note: return params[2] (not "generated") since it could have changed during recursion.
+ return (String) params[2];
+ }
+
+ /**
+ * Returns true if the given descriptor represents a view that not only can have
+ * children but which allows us to <b>insert</b> children. Some views, such as
+ * ListView (and in general all AdapterViews), disallow children to be inserted except
+ * through the dedicated AdapterView interface to do it.
+ *
+ * @param descriptor the descriptor for the view in question
+ * @param viewObject an actual instance of the view, or null if not available
+ * @return true if the descriptor describes a view which allows insertion of child
+ * views
+ */
+ public static boolean canInsertChildren(ElementDescriptor descriptor, Object viewObject) {
+ if (descriptor.hasChildren()) {
+ if (viewObject != null) {
+ // We have a view object; see if it derives from an AdapterView
+ Class<?> clz = viewObject.getClass();
+ while (clz != null) {
+ if (clz.getName().equals(FQCN_ADAPTER_VIEW)) {
+ return false;
+ }
+ clz = clz.getSuperclass();
+ }
+ } else {
+ // No view object, so we can't easily look up the class and determine
+ // whether it's an AdapterView; instead, look at the fixed list of builtin
+ // concrete subclasses of AdapterView
+ String viewName = descriptor.getXmlLocalName();
+ if (viewName.equals(LIST_VIEW) || viewName.equals(EXPANDABLE_LIST_VIEW)
+ || viewName.equals(GALLERY) || viewName.equals(GRID_VIEW)) {
+
+ // We should really also enforce that
+ // XmlUtils.ANDROID_URI.equals(descriptor.getNameSpace())
+ // here and if not, return true, but it turns out the getNameSpace()
+ // for elements are often "".
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+}