| /* |
| * 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 com.android.ide.eclipse.adt.AndroidConstants; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; |
| 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.layoutlib.api.IXmlPullParser; |
| import com.android.layoutlib.api.IDensityBasedResourceValue.Density; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.sdklib.SdkConstants; |
| |
| 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.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * {@link IXmlPullParser} 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. |
| */ |
| public final class UiElementPullParser extends BasePullParser { |
| private final static String ATTR_PADDING = "padding"; //$NON-NLS-1$ |
| private final static Pattern sFloatPattern = 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 List<ElementDescriptor> mLayoutDescriptors; |
| private final int mDensityValue; |
| private final float mXdpi; |
| private final String mDefaultPaddingValue; |
| |
| public UiElementPullParser(UiElementNode top, boolean explodeRendering, int densityValue, |
| float xdpi, IProject project) { |
| super(); |
| mRoot = top; |
| mExplodedRendering = explodeRendering; |
| mDensityValue = densityValue; |
| mXdpi = xdpi; |
| mDefaultPaddingValue = ExplodedRenderingHelper.PADDING_VALUE + "px"; //$NON-NLS-1$ |
| if (mExplodedRendering) { |
| // get the layout descriptor |
| IAndroidTarget target = Sdk.getCurrent().getTarget(project); |
| AndroidTargetData data = Sdk.getCurrent().getTargetData(target); |
| LayoutDescriptors descriptors = data.getLayoutDescriptors(); |
| mLayoutDescriptors = descriptors.getLayoutDescriptors(); |
| } |
| push(mRoot); |
| } |
| |
| private 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(); |
| for (ElementDescriptor descriptor : mLayoutDescriptors) { |
| if (xml.equals(descriptor.getXmlLocalName())) { |
| NamedNodeMap attributes = node.getXmlNode().getAttributes(); |
| Node padding = attributes.getNamedItemNS(SdkConstants.NS_RESOURCES, "padding"); |
| if (padding == null) { |
| // we'll return an extra padding |
| mZeroAttributeIsPadding = true; |
| } else { |
| mIncreaseExistingPadding = true; |
| } |
| |
| break; |
| } |
| } |
| } |
| } |
| |
| private UiElementNode pop() { |
| return mNodeStack.remove(mNodeStack.size()-1); |
| } |
| |
| // ------------- IXmlPullParser -------- |
| |
| /** |
| * {@inheritDoc} |
| * |
| * This implementation returns the underlying DOM node. |
| */ |
| public Object getViewKey() { |
| return getCurrentNode(); |
| } |
| |
| // ------------- XmlPullParser -------- |
| |
| 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. |
| */ |
| public int getAttributeCount() { |
| UiElementNode node = getCurrentNode(); |
| |
| if (node != null) { |
| Collection<UiAttributeNode> attributes = node.getUiAttributes(); |
| 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. |
| */ |
| 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. |
| */ |
| public String getAttributeNamespace(int i) { |
| if (mZeroAttributeIsPadding) { |
| if (i == 0) { |
| return SdkConstants.NS_RESOURCES; |
| } 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. |
| */ |
| 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(AndroidConstants.NS_CUSTOM_RESOURCES); |
| } 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. |
| */ |
| public String getAttributeValue(int i) { |
| if (mZeroAttributeIsPadding) { |
| if (i == 0) { |
| return mDefaultPaddingValue; |
| } else { |
| i--; |
| } |
| } |
| |
| Node attribute = getAttribute(i); |
| if (attribute != null) { |
| String value = attribute.getNodeValue(); |
| if (mIncreaseExistingPadding && ATTR_PADDING.equals(attribute.getLocalName()) && |
| SdkConstants.NS_RESOURCES.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. |
| */ |
| public String getAttributeValue(String namespace, String localName) { |
| if (mZeroAttributeIsPadding && ATTR_PADDING.equals(localName) && |
| SdkConstants.NS_RESOURCES.equals(namespace)) { |
| return mDefaultPaddingValue; |
| } |
| |
| // get the current uiNode |
| UiElementNode uiNode = getCurrentNode(); |
| |
| // get its xml node |
| Node xmlNode = uiNode.getXmlNode(); |
| |
| if (xmlNode != null) { |
| Node attribute = xmlNode.getAttributes().getNamedItemNS(namespace, localName); |
| if (attribute != null) { |
| String value = attribute.getNodeValue(); |
| if (mIncreaseExistingPadding && ATTR_PADDING.equals(localName) && |
| SdkConstants.NS_RESOURCES.equals(namespace)) { |
| // add the padding and return the value |
| return addPaddingToValue(value); |
| } |
| return value; |
| } |
| } |
| |
| return null; |
| } |
| |
| public int getDepth() { |
| return mNodeStack.size(); |
| } |
| |
| public String getName() { |
| if (mParsingState == START_TAG || mParsingState == END_TAG) { |
| return getCurrentNode().getDescriptor().getXmlLocalName(); |
| } |
| |
| return null; |
| } |
| |
| public String getNamespace() { |
| if (mParsingState == START_TAG || mParsingState == END_TAG) { |
| return getCurrentNode().getDescriptor().getNamespace(); |
| } |
| |
| return null; |
| } |
| |
| public String getPrefix() { |
| if (mParsingState == START_TAG || mParsingState == END_TAG) { |
| Document doc = mRoot.getXmlDocument(); |
| return doc.lookupPrefix(getCurrentNode().getDescriptor().getNamespace()); |
| } |
| |
| return null; |
| } |
| |
| 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. */ |
| public static final int COMPLEX_UNIT_PX = 0; |
| /** {@link DimensionEntry} complex unit: Value is Device Independent |
| * Pixels. */ |
| public static final int COMPLEX_UNIT_DIP = 1; |
| /** {@link DimensionEntry} complex unit: Value is a scaled pixel. */ |
| public static final int COMPLEX_UNIT_SP = 2; |
| /** {@link DimensionEntry} complex unit: Value is in points. */ |
| public static final int COMPLEX_UNIT_PT = 3; |
| /** {@link DimensionEntry} complex unit: Value is in inches. */ |
| public static final int COMPLEX_UNIT_IN = 4; |
| /** {@link DimensionEntry} complex unit: Value is in millimeters. */ |
| public static final int COMPLEX_UNIT_MM = 5; |
| |
| private final static DimensionEntry[] sDimensions = new DimensionEntry[] { |
| new DimensionEntry("px", COMPLEX_UNIT_PX), |
| new DimensionEntry("dip", COMPLEX_UNIT_DIP), |
| new DimensionEntry("dp", COMPLEX_UNIT_DIP), |
| new DimensionEntry("sp", COMPLEX_UNIT_SP), |
| new DimensionEntry("pt", COMPLEX_UNIT_PT), |
| new DimensionEntry("in", COMPLEX_UNIT_IN), |
| new DimensionEntry("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 + "px"; //$NON-NLS-1$ |
| } |
| |
| /** |
| * 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.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 = sFloatPattern.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)mDensityValue / Density.DEFAULT_DENSITY; |
| break; |
| case COMPLEX_UNIT_PT: |
| f *= mXdpi * (1.0f / 72); |
| break; |
| case COMPLEX_UNIT_IN: |
| f *= mXdpi; |
| break; |
| case COMPLEX_UNIT_MM: |
| f *= mXdpi * (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; |
| } |
| } |