blob: 858156884b4f501b218a0f510eb6e4959610b045 [file] [log] [blame]
/*
* 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.layout;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.UNIT_DIP;
import static com.android.SdkConstants.UNIT_DP;
import static com.android.SdkConstants.UNIT_IN;
import static com.android.SdkConstants.UNIT_MM;
import static com.android.SdkConstants.UNIT_PT;
import static com.android.SdkConstants.UNIT_PX;
import static com.android.SdkConstants.UNIT_SP;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import static com.android.SdkConstants.VIEW_FRAGMENT;
import static com.android.SdkConstants.VIEW_INCLUDE;
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.common.res2.ValueXmlHelper;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.FragmentMenu;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.resources.Density;
import com.android.sdklib.IAndroidTarget;
import org.eclipse.core.resources.IProject;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xmlpull.v1.XmlPullParserException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* {@link ILayoutPullParser} implementation on top of {@link UiElementNode}.
* <p/>
* It's designed to work on layout files, and will most likely not work on other resource files.
* <p/>
* This pull parser generates {@link ViewInfo}s which key is a {@link UiElementNode}.
*/
public class UiElementPullParser extends BasePullParser {
private final static Pattern FLOAT_PATTERN = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); //$NON-NLS-1$
private final int[] sIntOut = new int[1];
private final ArrayList<UiElementNode> mNodeStack = new ArrayList<UiElementNode>();
private UiElementNode mRoot;
private final boolean mExplodedRendering;
private boolean mZeroAttributeIsPadding = false;
private boolean mIncreaseExistingPadding = false;
private LayoutDescriptors mDescriptors;
private final Density mDensity;
/**
* Number of pixels to pad views with in exploded-rendering mode.
*/
private static final String DEFAULT_PADDING_VALUE =
ExplodedRenderingHelper.PADDING_VALUE + UNIT_PX;
/**
* Number of pixels to pad exploded individual views with. (This is HALF the width of the
* rectangle since padding is repeated on both sides of the empty content.)
*/
private static final String FIXED_PADDING_VALUE = "20px"; //$NON-NLS-1$
/**
* Set of nodes that we want to auto-pad using {@link #FIXED_PADDING_VALUE} as the padding
* attribute value. Can be null, which is the case when we don't want to perform any
* <b>individual</b> node exploding.
*/
private final Set<UiElementNode> mExplodeNodes;
/**
* Constructs a new {@link UiElementPullParser}, a parser dedicated to the special case of
* parsing a layout resource files, and handling "exploded rendering" - adding padding on views
* to make them easier to see and operate on.
*
* @param top The {@link UiElementNode} for the root node.
* @param explodeRendering When true, add padding to <b>all</b> nodes in the hierarchy. This
* will add rather than replace padding of a node.
* @param explodeNodes A set of individual nodes that should be assigned a fixed amount of
* padding ({@link #FIXED_PADDING_VALUE}). This is intended for use with nodes that
* (without padding) would be invisible. This parameter can be null, in which case
* nodes are not individually exploded (but they may all be exploded with the
* explodeRendering parameter.
* @param density the density factor for the screen.
* @param project Project containing this layout.
*/
public UiElementPullParser(UiElementNode top, boolean explodeRendering,
Set<UiElementNode> explodeNodes,
Density density, IProject project) {
super();
mRoot = top;
mExplodedRendering = explodeRendering;
mExplodeNodes = explodeNodes;
mDensity = density;
if (mExplodedRendering) {
// get the layout descriptor
IAndroidTarget target = Sdk.getCurrent().getTarget(project);
AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
mDescriptors = data.getLayoutDescriptors();
}
push(mRoot);
}
protected UiElementNode getCurrentNode() {
if (mNodeStack.size() > 0) {
return mNodeStack.get(mNodeStack.size()-1);
}
return null;
}
private Node getAttribute(int i) {
if (mParsingState != START_TAG) {
throw new IndexOutOfBoundsException();
}
// get the current uiNode
UiElementNode uiNode = getCurrentNode();
// get its xml node
Node xmlNode = uiNode.getXmlNode();
if (xmlNode != null) {
return xmlNode.getAttributes().item(i);
}
return null;
}
private void push(UiElementNode node) {
mNodeStack.add(node);
mZeroAttributeIsPadding = false;
mIncreaseExistingPadding = false;
if (mExplodedRendering) {
// first get the node name
String xml = node.getDescriptor().getXmlLocalName();
ViewElementDescriptor descriptor = mDescriptors.findDescriptorByTag(xml);
if (descriptor != null) {
NamedNodeMap attributes = node.getXmlNode().getAttributes();
Node padding = attributes.getNamedItemNS(ANDROID_URI, ATTR_PADDING);
if (padding == null) {
// we'll return an extra padding
mZeroAttributeIsPadding = true;
} else {
mIncreaseExistingPadding = true;
}
}
}
}
private UiElementNode pop() {
return mNodeStack.remove(mNodeStack.size()-1);
}
// ------------- IXmlPullParser --------
/**
* {@inheritDoc}
* <p/>
* This implementation returns the underlying DOM node of type {@link UiElementNode}.
* Note that the link between the GLE and the parsing code depends on this being the actual
* type returned, so you can't just randomly change it here.
* <p/>
* Currently used by:
* - private method GraphicalLayoutEditor#updateNodeWithBounds(ILayoutViewInfo).
* - private constructor of LayoutCanvas.CanvasViewInfo.
*/
@Override
public Object getViewCookie() {
return getCurrentNode();
}
/**
* Legacy method required by {@link com.android.layoutlib.api.IXmlPullParser}
*/
@Override
public Object getViewKey() {
return getViewCookie();
}
/**
* This implementation does nothing for now as all the embedded XML will use a normal KXML
* parser.
*/
@Override
public ILayoutPullParser getParser(String layoutName) {
return null;
}
// ------------- XmlPullParser --------
@Override
public String getPositionDescription() {
return "XML DOM element depth:" + mNodeStack.size();
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Override
public int getAttributeCount() {
UiElementNode node = getCurrentNode();
if (node != null) {
Collection<UiAttributeNode> attributes = node.getAllUiAttributes();
int count = attributes.size();
return count + (mZeroAttributeIsPadding ? 1 : 0);
}
return 0;
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Override
public String getAttributeName(int i) {
if (mZeroAttributeIsPadding) {
if (i == 0) {
return ATTR_PADDING;
} else {
i--;
}
}
Node attribute = getAttribute(i);
if (attribute != null) {
return attribute.getLocalName();
}
return null;
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Override
public String getAttributeNamespace(int i) {
if (mZeroAttributeIsPadding) {
if (i == 0) {
return ANDROID_URI;
} else {
i--;
}
}
Node attribute = getAttribute(i);
if (attribute != null) {
return attribute.getNamespaceURI();
}
return ""; //$NON-NLS-1$
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Override
public String getAttributePrefix(int i) {
if (mZeroAttributeIsPadding) {
if (i == 0) {
// figure out the prefix associated with the android namespace.
Document doc = mRoot.getXmlDocument();
return doc.lookupPrefix(ANDROID_URI);
} else {
i--;
}
}
Node attribute = getAttribute(i);
if (attribute != null) {
return attribute.getPrefix();
}
return null;
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Override
public String getAttributeValue(int i) {
if (mZeroAttributeIsPadding) {
if (i == 0) {
return DEFAULT_PADDING_VALUE;
} else {
i--;
}
}
Node attribute = getAttribute(i);
if (attribute != null) {
String value = attribute.getNodeValue();
if (mIncreaseExistingPadding && ATTR_PADDING.equals(attribute.getLocalName()) &&
ANDROID_URI.equals(attribute.getNamespaceURI())) {
// add the padding and return the value
return addPaddingToValue(value);
}
return value;
}
return null;
}
/*
* This is the main method used by the LayoutInflater to query for attributes.
*/
@Override
public String getAttributeValue(String namespace, String localName) {
if (mExplodeNodes != null && ATTR_PADDING.equals(localName) &&
ANDROID_URI.equals(namespace)) {
UiElementNode node = getCurrentNode();
if (node != null && mExplodeNodes.contains(node)) {
return FIXED_PADDING_VALUE;
}
}
if (mZeroAttributeIsPadding && ATTR_PADDING.equals(localName) &&
ANDROID_URI.equals(namespace)) {
return DEFAULT_PADDING_VALUE;
}
// get the current uiNode
UiElementNode uiNode = getCurrentNode();
// get its xml node
Node xmlNode = uiNode.getXmlNode();
if (xmlNode != null) {
if (ATTR_LAYOUT.equals(localName) && VIEW_FRAGMENT.equals(xmlNode.getNodeName())) {
String layout = FragmentMenu.getFragmentLayout(xmlNode);
if (layout != null) {
return layout;
}
}
Node attribute = xmlNode.getAttributes().getNamedItemNS(namespace, localName);
// Auto-convert http://schemas.android.com/apk/res-auto resources. The lookup
// will be for the current application's resource package, e.g.
// http://schemas.android.com/apk/res/foo.bar, but the XML document will
// be using http://schemas.android.com/apk/res-auto in library projects:
if (attribute == null && namespace != null && !namespace.equals(ANDROID_URI)) {
attribute = xmlNode.getAttributes().getNamedItemNS(AUTO_URI, localName);
}
if (attribute != null) {
String value = attribute.getNodeValue();
if (mIncreaseExistingPadding && ATTR_PADDING.equals(localName) &&
ANDROID_URI.equals(namespace)) {
// add the padding and return the value
return addPaddingToValue(value);
}
// on the fly convert match_parent to fill_parent for compatibility with older
// platforms.
if (VALUE_MATCH_PARENT.equals(value) &&
(ATTR_LAYOUT_WIDTH.equals(localName) ||
ATTR_LAYOUT_HEIGHT.equals(localName)) &&
ANDROID_URI.equals(namespace)) {
return VALUE_FILL_PARENT;
}
// Handle unicode escapes etc
value = ValueXmlHelper.unescapeResourceString(value, false, false);
return value;
}
}
return null;
}
@Override
public int getDepth() {
return mNodeStack.size();
}
@Override
public String getName() {
if (mParsingState == START_TAG || mParsingState == END_TAG) {
String name = getCurrentNode().getDescriptor().getXmlLocalName();
if (name.equals(VIEW_FRAGMENT)) {
// Temporarily translate <fragment> to <include> (and in getAttribute
// we will also provide a layout-attribute for the corresponding
// fragment name attribute)
String layout = FragmentMenu.getFragmentLayout(getCurrentNode().getXmlNode());
if (layout != null) {
return VIEW_INCLUDE;
}
}
return name;
}
return null;
}
@Override
public String getNamespace() {
if (mParsingState == START_TAG || mParsingState == END_TAG) {
return getCurrentNode().getDescriptor().getNamespace();
}
return null;
}
@Override
public String getPrefix() {
if (mParsingState == START_TAG || mParsingState == END_TAG) {
Document doc = mRoot.getXmlDocument();
return doc.lookupPrefix(getCurrentNode().getDescriptor().getNamespace());
}
return null;
}
@Override
public boolean isEmptyElementTag() throws XmlPullParserException {
if (mParsingState == START_TAG) {
return getCurrentNode().getUiChildren().size() == 0;
}
throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG",
this, null);
}
@Override
public void onNextFromStartDocument() {
onNextFromStartTag();
}
@Override
public void onNextFromStartTag() {
// get the current node, and look for text or children (children first)
UiElementNode node = getCurrentNode();
List<UiElementNode> children = node.getUiChildren();
if (children.size() > 0) {
// move to the new child, and don't change the state.
push(children.get(0));
// in case the current state is CURRENT_DOC, we set the proper state.
mParsingState = START_TAG;
} else {
if (mParsingState == START_DOCUMENT) {
// this handles the case where there's no node.
mParsingState = END_DOCUMENT;
} else {
mParsingState = END_TAG;
}
}
}
@Override
public void onNextFromEndTag() {
// look for a sibling. if no sibling, go back to the parent
UiElementNode node = getCurrentNode();
node = node.getUiNextSibling();
if (node != null) {
// to go to the sibling, we need to remove the current node,
pop();
// and add its sibling.
push(node);
mParsingState = START_TAG;
} else {
// move back to the parent
pop();
// we have only one element left (mRoot), then we're done with the document.
if (mNodeStack.size() == 1) {
mParsingState = END_DOCUMENT;
} else {
mParsingState = END_TAG;
}
}
}
// ------- TypedValue stuff
// This is adapted from com.android.layoutlib.bridge.ResourceHelper
// (but modified to directly take the parsed value and convert it into pixel instead of
// storing it into a TypedValue)
// this was originally taken from platform/frameworks/base/libs/utils/ResourceTypes.cpp
private static final class DimensionEntry {
String name;
int type;
DimensionEntry(String name, int unit) {
this.name = name;
this.type = unit;
}
}
/** {@link DimensionEntry} complex unit: Value is raw pixels. */
private static final int COMPLEX_UNIT_PX = 0;
/** {@link DimensionEntry} complex unit: Value is Device Independent
* Pixels. */
private static final int COMPLEX_UNIT_DIP = 1;
/** {@link DimensionEntry} complex unit: Value is a scaled pixel. */
private static final int COMPLEX_UNIT_SP = 2;
/** {@link DimensionEntry} complex unit: Value is in points. */
private static final int COMPLEX_UNIT_PT = 3;
/** {@link DimensionEntry} complex unit: Value is in inches. */
private static final int COMPLEX_UNIT_IN = 4;
/** {@link DimensionEntry} complex unit: Value is in millimeters. */
private static final int COMPLEX_UNIT_MM = 5;
private final static DimensionEntry[] sDimensions = new DimensionEntry[] {
new DimensionEntry(UNIT_PX, COMPLEX_UNIT_PX),
new DimensionEntry(UNIT_DIP, COMPLEX_UNIT_DIP),
new DimensionEntry(UNIT_DP, COMPLEX_UNIT_DIP),
new DimensionEntry(UNIT_SP, COMPLEX_UNIT_SP),
new DimensionEntry(UNIT_PT, COMPLEX_UNIT_PT),
new DimensionEntry(UNIT_IN, COMPLEX_UNIT_IN),
new DimensionEntry(UNIT_MM, COMPLEX_UNIT_MM),
};
/**
* Adds padding to an existing dimension.
* <p/>This will resolve the attribute value (which can be px, dip, dp, sp, pt, in, mm) to
* a pixel value, add the padding value ({@link ExplodedRenderingHelper#PADDING_VALUE}),
* and then return a string with the new value as a px string ("42px");
* If the conversion fails, only the special padding is returned.
*/
private String addPaddingToValue(String s) {
int padding = ExplodedRenderingHelper.PADDING_VALUE;
if (stringToPixel(s)) {
padding += sIntOut[0];
}
return padding + UNIT_PX;
}
/**
* Convert the string into a pixel value, and puts it in {@link #sIntOut}
* @param s the dimension value from an XML attribute
* @return true if success.
*/
private boolean stringToPixel(String s) {
// remove the space before and after
s = s.trim();
int len = s.length();
if (len <= 0) {
return false;
}
// check that there's no non ASCII characters.
char[] buf = s.toCharArray();
for (int i = 0 ; i < len ; i++) {
if (buf[i] > 255) {
return false;
}
}
// check the first character
if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.') {
return false;
}
// now look for the string that is after the float...
Matcher m = FLOAT_PATTERN.matcher(s);
if (m.matches()) {
String f_str = m.group(1);
String end = m.group(2);
float f;
try {
f = Float.parseFloat(f_str);
} catch (NumberFormatException e) {
// this shouldn't happen with the regexp above.
return false;
}
if (end.length() > 0 && end.charAt(0) != ' ') {
// We only support dimension-type values, so try to parse the unit for dimension
DimensionEntry dimension = parseDimension(end);
if (dimension != null) {
// convert the value into pixel based on the dimention type
// This is similar to TypedValue.applyDimension()
switch (dimension.type) {
case COMPLEX_UNIT_PX:
// do nothing, value is already in px
break;
case COMPLEX_UNIT_DIP:
case COMPLEX_UNIT_SP: // intended fall-through since we don't
// adjust for font size
f *= (float)mDensity.getDpiValue() / Density.DEFAULT_DENSITY;
break;
case COMPLEX_UNIT_PT:
f *= mDensity.getDpiValue() * (1.0f / 72);
break;
case COMPLEX_UNIT_IN:
f *= mDensity.getDpiValue();
break;
case COMPLEX_UNIT_MM:
f *= mDensity.getDpiValue() * (1.0f / 25.4f);
break;
}
// store result (converted to int)
sIntOut[0] = (int) (f + 0.5);
return true;
}
}
}
return false;
}
private static DimensionEntry parseDimension(String str) {
str = str.trim();
for (DimensionEntry d : sDimensions) {
if (d.name.equals(str)) {
return d;
}
}
return null;
}
}