/* * 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. *
* 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(ListformText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());
*
* @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
* FrameworkResourceManager.getInstance().getDocumentationBaseUrl()
*/
public static String formatFormText(String javadoc,
ElementDescriptor elementDescriptor,
String androidDocBaseUrl) {
ArrayListblah
Pattern p_code = Pattern.compile("(.+?)
(.*)"); //$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))); // 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.
*
* 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.
*
* 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
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".
* 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".
* 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:
*
* - prefix(String): The prefix of the generated id, i.e. "widget". Cannot be null.
*
- index(Integer): The minimum index of the generated id. Must start with null.
*
- generated(String): The generated widget currently being searched. Must start with null.
*
- map(Set
): A set of the ids collected so far when walking through the widget
* hierarchy. Must start with null.
*
*
* @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 map = (Set)params[3];
if (map == null) {
params[3] = map = new HashSet();
}
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 insert 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;
}
}