1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 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 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 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. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout; 18 19 import static com.android.SdkConstants.ANDROID_URI; 20 import static com.android.SdkConstants.ATTR_LAYOUT; 21 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; 22 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; 23 import static com.android.SdkConstants.ATTR_PADDING; 24 import static com.android.SdkConstants.AUTO_URI; 25 import static com.android.SdkConstants.UNIT_DIP; 26 import static com.android.SdkConstants.UNIT_DP; 27 import static com.android.SdkConstants.UNIT_IN; 28 import static com.android.SdkConstants.UNIT_MM; 29 import static com.android.SdkConstants.UNIT_PT; 30 import static com.android.SdkConstants.UNIT_PX; 31 import static com.android.SdkConstants.UNIT_SP; 32 import static com.android.SdkConstants.VALUE_FILL_PARENT; 33 import static com.android.SdkConstants.VALUE_MATCH_PARENT; 34 import static com.android.SdkConstants.VIEW_FRAGMENT; 35 import static com.android.SdkConstants.VIEW_INCLUDE; 36 37 import com.android.ide.common.rendering.api.ILayoutPullParser; 38 import com.android.ide.common.rendering.api.ViewInfo; 39 import com.android.ide.common.res2.ValueXmlHelper; 40 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 41 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.FragmentMenu; 43 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; 44 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 45 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 46 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 47 import com.android.resources.Density; 48 import com.android.sdklib.IAndroidTarget; 49 50 import org.eclipse.core.resources.IProject; 51 import org.w3c.dom.Document; 52 import org.w3c.dom.NamedNodeMap; 53 import org.w3c.dom.Node; 54 import org.xmlpull.v1.XmlPullParserException; 55 56 import java.util.ArrayList; 57 import java.util.Collection; 58 import java.util.List; 59 import java.util.Set; 60 import java.util.regex.Matcher; 61 import java.util.regex.Pattern; 62 63 /** 64 * {@link ILayoutPullParser} implementation on top of {@link UiElementNode}. 65 * <p/> 66 * It's designed to work on layout files, and will most likely not work on other resource files. 67 * <p/> 68 * This pull parser generates {@link ViewInfo}s which key is a {@link UiElementNode}. 69 */ 70 public class UiElementPullParser extends BasePullParser { 71 private final static Pattern FLOAT_PATTERN = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); //$NON-NLS-1$ 72 73 private final int[] sIntOut = new int[1]; 74 75 private final ArrayList<UiElementNode> mNodeStack = new ArrayList<UiElementNode>(); 76 private UiElementNode mRoot; 77 private final boolean mExplodedRendering; 78 private boolean mZeroAttributeIsPadding = false; 79 private boolean mIncreaseExistingPadding = false; 80 private LayoutDescriptors mDescriptors; 81 private final Density mDensity; 82 83 /** 84 * Number of pixels to pad views with in exploded-rendering mode. 85 */ 86 private static final String DEFAULT_PADDING_VALUE = 87 ExplodedRenderingHelper.PADDING_VALUE + UNIT_PX; 88 89 /** 90 * Number of pixels to pad exploded individual views with. (This is HALF the width of the 91 * rectangle since padding is repeated on both sides of the empty content.) 92 */ 93 private static final String FIXED_PADDING_VALUE = "20px"; //$NON-NLS-1$ 94 95 /** 96 * Set of nodes that we want to auto-pad using {@link #FIXED_PADDING_VALUE} as the padding 97 * attribute value. Can be null, which is the case when we don't want to perform any 98 * <b>individual</b> node exploding. 99 */ 100 private final Set<UiElementNode> mExplodeNodes; 101 102 /** 103 * Constructs a new {@link UiElementPullParser}, a parser dedicated to the special case of 104 * parsing a layout resource files, and handling "exploded rendering" - adding padding on views 105 * to make them easier to see and operate on. 106 * 107 * @param top The {@link UiElementNode} for the root node. 108 * @param explodeRendering When true, add padding to <b>all</b> nodes in the hierarchy. This 109 * will add rather than replace padding of a node. 110 * @param explodeNodes A set of individual nodes that should be assigned a fixed amount of 111 * padding ({@link #FIXED_PADDING_VALUE}). This is intended for use with nodes that 112 * (without padding) would be invisible. This parameter can be null, in which case 113 * nodes are not individually exploded (but they may all be exploded with the 114 * explodeRendering parameter. 115 * @param density the density factor for the screen. 116 * @param project Project containing this layout. 117 */ UiElementPullParser(UiElementNode top, boolean explodeRendering, Set<UiElementNode> explodeNodes, Density density, IProject project)118 public UiElementPullParser(UiElementNode top, boolean explodeRendering, 119 Set<UiElementNode> explodeNodes, 120 Density density, IProject project) { 121 super(); 122 mRoot = top; 123 mExplodedRendering = explodeRendering; 124 mExplodeNodes = explodeNodes; 125 mDensity = density; 126 if (mExplodedRendering) { 127 // get the layout descriptor 128 IAndroidTarget target = Sdk.getCurrent().getTarget(project); 129 AndroidTargetData data = Sdk.getCurrent().getTargetData(target); 130 mDescriptors = data.getLayoutDescriptors(); 131 } 132 push(mRoot); 133 } 134 getCurrentNode()135 protected UiElementNode getCurrentNode() { 136 if (mNodeStack.size() > 0) { 137 return mNodeStack.get(mNodeStack.size()-1); 138 } 139 140 return null; 141 } 142 getAttribute(int i)143 private Node getAttribute(int i) { 144 if (mParsingState != START_TAG) { 145 throw new IndexOutOfBoundsException(); 146 } 147 148 // get the current uiNode 149 UiElementNode uiNode = getCurrentNode(); 150 151 // get its xml node 152 Node xmlNode = uiNode.getXmlNode(); 153 154 if (xmlNode != null) { 155 return xmlNode.getAttributes().item(i); 156 } 157 158 return null; 159 } 160 push(UiElementNode node)161 private void push(UiElementNode node) { 162 mNodeStack.add(node); 163 164 mZeroAttributeIsPadding = false; 165 mIncreaseExistingPadding = false; 166 167 if (mExplodedRendering) { 168 // first get the node name 169 String xml = node.getDescriptor().getXmlLocalName(); 170 ViewElementDescriptor descriptor = mDescriptors.findDescriptorByTag(xml); 171 if (descriptor != null) { 172 NamedNodeMap attributes = node.getXmlNode().getAttributes(); 173 Node padding = attributes.getNamedItemNS(ANDROID_URI, ATTR_PADDING); 174 if (padding == null) { 175 // we'll return an extra padding 176 mZeroAttributeIsPadding = true; 177 } else { 178 mIncreaseExistingPadding = true; 179 } 180 } 181 } 182 } 183 pop()184 private UiElementNode pop() { 185 return mNodeStack.remove(mNodeStack.size()-1); 186 } 187 188 // ------------- IXmlPullParser -------- 189 190 /** 191 * {@inheritDoc} 192 * <p/> 193 * This implementation returns the underlying DOM node of type {@link UiElementNode}. 194 * Note that the link between the GLE and the parsing code depends on this being the actual 195 * type returned, so you can't just randomly change it here. 196 * <p/> 197 * Currently used by: 198 * - private method GraphicalLayoutEditor#updateNodeWithBounds(ILayoutViewInfo). 199 * - private constructor of LayoutCanvas.CanvasViewInfo. 200 */ 201 @Override getViewCookie()202 public Object getViewCookie() { 203 return getCurrentNode(); 204 } 205 206 /** 207 * Legacy method required by {@link com.android.layoutlib.api.IXmlPullParser} 208 */ 209 @Override getViewKey()210 public Object getViewKey() { 211 return getViewCookie(); 212 } 213 214 /** 215 * This implementation does nothing for now as all the embedded XML will use a normal KXML 216 * parser. 217 */ 218 @Override getParser(String layoutName)219 public ILayoutPullParser getParser(String layoutName) { 220 return null; 221 } 222 223 // ------------- XmlPullParser -------- 224 225 @Override getPositionDescription()226 public String getPositionDescription() { 227 return "XML DOM element depth:" + mNodeStack.size(); 228 } 229 230 /* 231 * This does not seem to be called by the layoutlib, but we keep this (and maintain 232 * it) just in case. 233 */ 234 @Override getAttributeCount()235 public int getAttributeCount() { 236 UiElementNode node = getCurrentNode(); 237 238 if (node != null) { 239 Collection<UiAttributeNode> attributes = node.getAllUiAttributes(); 240 int count = attributes.size(); 241 242 return count + (mZeroAttributeIsPadding ? 1 : 0); 243 } 244 245 return 0; 246 } 247 248 /* 249 * This does not seem to be called by the layoutlib, but we keep this (and maintain 250 * it) just in case. 251 */ 252 @Override getAttributeName(int i)253 public String getAttributeName(int i) { 254 if (mZeroAttributeIsPadding) { 255 if (i == 0) { 256 return ATTR_PADDING; 257 } else { 258 i--; 259 } 260 } 261 262 Node attribute = getAttribute(i); 263 if (attribute != null) { 264 return attribute.getLocalName(); 265 } 266 267 return null; 268 } 269 270 /* 271 * This does not seem to be called by the layoutlib, but we keep this (and maintain 272 * it) just in case. 273 */ 274 @Override getAttributeNamespace(int i)275 public String getAttributeNamespace(int i) { 276 if (mZeroAttributeIsPadding) { 277 if (i == 0) { 278 return ANDROID_URI; 279 } else { 280 i--; 281 } 282 } 283 284 Node attribute = getAttribute(i); 285 if (attribute != null) { 286 return attribute.getNamespaceURI(); 287 } 288 return ""; //$NON-NLS-1$ 289 } 290 291 /* 292 * This does not seem to be called by the layoutlib, but we keep this (and maintain 293 * it) just in case. 294 */ 295 @Override getAttributePrefix(int i)296 public String getAttributePrefix(int i) { 297 if (mZeroAttributeIsPadding) { 298 if (i == 0) { 299 // figure out the prefix associated with the android namespace. 300 Document doc = mRoot.getXmlDocument(); 301 return doc.lookupPrefix(ANDROID_URI); 302 } else { 303 i--; 304 } 305 } 306 307 Node attribute = getAttribute(i); 308 if (attribute != null) { 309 return attribute.getPrefix(); 310 } 311 return null; 312 } 313 314 /* 315 * This does not seem to be called by the layoutlib, but we keep this (and maintain 316 * it) just in case. 317 */ 318 @Override getAttributeValue(int i)319 public String getAttributeValue(int i) { 320 if (mZeroAttributeIsPadding) { 321 if (i == 0) { 322 return DEFAULT_PADDING_VALUE; 323 } else { 324 i--; 325 } 326 } 327 328 Node attribute = getAttribute(i); 329 if (attribute != null) { 330 String value = attribute.getNodeValue(); 331 if (mIncreaseExistingPadding && ATTR_PADDING.equals(attribute.getLocalName()) && 332 ANDROID_URI.equals(attribute.getNamespaceURI())) { 333 // add the padding and return the value 334 return addPaddingToValue(value); 335 } 336 return value; 337 } 338 339 return null; 340 } 341 342 /* 343 * This is the main method used by the LayoutInflater to query for attributes. 344 */ 345 @Override getAttributeValue(String namespace, String localName)346 public String getAttributeValue(String namespace, String localName) { 347 if (mExplodeNodes != null && ATTR_PADDING.equals(localName) && 348 ANDROID_URI.equals(namespace)) { 349 UiElementNode node = getCurrentNode(); 350 if (node != null && mExplodeNodes.contains(node)) { 351 return FIXED_PADDING_VALUE; 352 } 353 } 354 355 if (mZeroAttributeIsPadding && ATTR_PADDING.equals(localName) && 356 ANDROID_URI.equals(namespace)) { 357 return DEFAULT_PADDING_VALUE; 358 } 359 360 // get the current uiNode 361 UiElementNode uiNode = getCurrentNode(); 362 363 // get its xml node 364 Node xmlNode = uiNode.getXmlNode(); 365 366 if (xmlNode != null) { 367 if (ATTR_LAYOUT.equals(localName) && VIEW_FRAGMENT.equals(xmlNode.getNodeName())) { 368 String layout = FragmentMenu.getFragmentLayout(xmlNode); 369 if (layout != null) { 370 return layout; 371 } 372 } 373 374 Node attribute = xmlNode.getAttributes().getNamedItemNS(namespace, localName); 375 376 // Auto-convert http://schemas.android.com/apk/res-auto resources. The lookup 377 // will be for the current application's resource package, e.g. 378 // http://schemas.android.com/apk/res/foo.bar, but the XML document will 379 // be using http://schemas.android.com/apk/res-auto in library projects: 380 if (attribute == null && namespace != null && !namespace.equals(ANDROID_URI)) { 381 attribute = xmlNode.getAttributes().getNamedItemNS(AUTO_URI, localName); 382 } 383 384 if (attribute != null) { 385 String value = attribute.getNodeValue(); 386 if (mIncreaseExistingPadding && ATTR_PADDING.equals(localName) && 387 ANDROID_URI.equals(namespace)) { 388 // add the padding and return the value 389 return addPaddingToValue(value); 390 } 391 392 // on the fly convert match_parent to fill_parent for compatibility with older 393 // platforms. 394 if (VALUE_MATCH_PARENT.equals(value) && 395 (ATTR_LAYOUT_WIDTH.equals(localName) || 396 ATTR_LAYOUT_HEIGHT.equals(localName)) && 397 ANDROID_URI.equals(namespace)) { 398 return VALUE_FILL_PARENT; 399 } 400 401 // Handle unicode escapes etc 402 value = ValueXmlHelper.unescapeResourceString(value, false, false); 403 404 return value; 405 } 406 } 407 408 return null; 409 } 410 411 @Override getDepth()412 public int getDepth() { 413 return mNodeStack.size(); 414 } 415 416 @Override getName()417 public String getName() { 418 if (mParsingState == START_TAG || mParsingState == END_TAG) { 419 String name = getCurrentNode().getDescriptor().getXmlLocalName(); 420 421 if (name.equals(VIEW_FRAGMENT)) { 422 // Temporarily translate <fragment> to <include> (and in getAttribute 423 // we will also provide a layout-attribute for the corresponding 424 // fragment name attribute) 425 String layout = FragmentMenu.getFragmentLayout(getCurrentNode().getXmlNode()); 426 if (layout != null) { 427 return VIEW_INCLUDE; 428 } 429 } 430 431 return name; 432 } 433 434 return null; 435 } 436 437 @Override getNamespace()438 public String getNamespace() { 439 if (mParsingState == START_TAG || mParsingState == END_TAG) { 440 return getCurrentNode().getDescriptor().getNamespace(); 441 } 442 443 return null; 444 } 445 446 @Override getPrefix()447 public String getPrefix() { 448 if (mParsingState == START_TAG || mParsingState == END_TAG) { 449 Document doc = mRoot.getXmlDocument(); 450 return doc.lookupPrefix(getCurrentNode().getDescriptor().getNamespace()); 451 } 452 453 return null; 454 } 455 456 @Override isEmptyElementTag()457 public boolean isEmptyElementTag() throws XmlPullParserException { 458 if (mParsingState == START_TAG) { 459 return getCurrentNode().getUiChildren().size() == 0; 460 } 461 462 throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG", 463 this, null); 464 } 465 466 @Override onNextFromStartDocument()467 public void onNextFromStartDocument() { 468 onNextFromStartTag(); 469 } 470 471 @Override onNextFromStartTag()472 public void onNextFromStartTag() { 473 // get the current node, and look for text or children (children first) 474 UiElementNode node = getCurrentNode(); 475 List<UiElementNode> children = node.getUiChildren(); 476 if (children.size() > 0) { 477 // move to the new child, and don't change the state. 478 push(children.get(0)); 479 480 // in case the current state is CURRENT_DOC, we set the proper state. 481 mParsingState = START_TAG; 482 } else { 483 if (mParsingState == START_DOCUMENT) { 484 // this handles the case where there's no node. 485 mParsingState = END_DOCUMENT; 486 } else { 487 mParsingState = END_TAG; 488 } 489 } 490 } 491 492 @Override onNextFromEndTag()493 public void onNextFromEndTag() { 494 // look for a sibling. if no sibling, go back to the parent 495 UiElementNode node = getCurrentNode(); 496 node = node.getUiNextSibling(); 497 if (node != null) { 498 // to go to the sibling, we need to remove the current node, 499 pop(); 500 // and add its sibling. 501 push(node); 502 mParsingState = START_TAG; 503 } else { 504 // move back to the parent 505 pop(); 506 507 // we have only one element left (mRoot), then we're done with the document. 508 if (mNodeStack.size() == 1) { 509 mParsingState = END_DOCUMENT; 510 } else { 511 mParsingState = END_TAG; 512 } 513 } 514 } 515 516 // ------- TypedValue stuff 517 // This is adapted from com.android.layoutlib.bridge.ResourceHelper 518 // (but modified to directly take the parsed value and convert it into pixel instead of 519 // storing it into a TypedValue) 520 // this was originally taken from platform/frameworks/base/libs/utils/ResourceTypes.cpp 521 522 private static final class DimensionEntry { 523 String name; 524 int type; 525 DimensionEntry(String name, int unit)526 DimensionEntry(String name, int unit) { 527 this.name = name; 528 this.type = unit; 529 } 530 } 531 532 /** {@link DimensionEntry} complex unit: Value is raw pixels. */ 533 private static final int COMPLEX_UNIT_PX = 0; 534 /** {@link DimensionEntry} complex unit: Value is Device Independent 535 * Pixels. */ 536 private static final int COMPLEX_UNIT_DIP = 1; 537 /** {@link DimensionEntry} complex unit: Value is a scaled pixel. */ 538 private static final int COMPLEX_UNIT_SP = 2; 539 /** {@link DimensionEntry} complex unit: Value is in points. */ 540 private static final int COMPLEX_UNIT_PT = 3; 541 /** {@link DimensionEntry} complex unit: Value is in inches. */ 542 private static final int COMPLEX_UNIT_IN = 4; 543 /** {@link DimensionEntry} complex unit: Value is in millimeters. */ 544 private static final int COMPLEX_UNIT_MM = 5; 545 546 private final static DimensionEntry[] sDimensions = new DimensionEntry[] { 547 new DimensionEntry(UNIT_PX, COMPLEX_UNIT_PX), 548 new DimensionEntry(UNIT_DIP, COMPLEX_UNIT_DIP), 549 new DimensionEntry(UNIT_DP, COMPLEX_UNIT_DIP), 550 new DimensionEntry(UNIT_SP, COMPLEX_UNIT_SP), 551 new DimensionEntry(UNIT_PT, COMPLEX_UNIT_PT), 552 new DimensionEntry(UNIT_IN, COMPLEX_UNIT_IN), 553 new DimensionEntry(UNIT_MM, COMPLEX_UNIT_MM), 554 }; 555 556 /** 557 * Adds padding to an existing dimension. 558 * <p/>This will resolve the attribute value (which can be px, dip, dp, sp, pt, in, mm) to 559 * a pixel value, add the padding value ({@link ExplodedRenderingHelper#PADDING_VALUE}), 560 * and then return a string with the new value as a px string ("42px"); 561 * If the conversion fails, only the special padding is returned. 562 */ addPaddingToValue(String s)563 private String addPaddingToValue(String s) { 564 int padding = ExplodedRenderingHelper.PADDING_VALUE; 565 if (stringToPixel(s)) { 566 padding += sIntOut[0]; 567 } 568 569 return padding + UNIT_PX; 570 } 571 572 /** 573 * Convert the string into a pixel value, and puts it in {@link #sIntOut} 574 * @param s the dimension value from an XML attribute 575 * @return true if success. 576 */ stringToPixel(String s)577 private boolean stringToPixel(String s) { 578 // remove the space before and after 579 s = s.trim(); 580 int len = s.length(); 581 582 if (len <= 0) { 583 return false; 584 } 585 586 // check that there's no non ASCII characters. 587 char[] buf = s.toCharArray(); 588 for (int i = 0 ; i < len ; i++) { 589 if (buf[i] > 255) { 590 return false; 591 } 592 } 593 594 // check the first character 595 if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.') { 596 return false; 597 } 598 599 // now look for the string that is after the float... 600 Matcher m = FLOAT_PATTERN.matcher(s); 601 if (m.matches()) { 602 String f_str = m.group(1); 603 String end = m.group(2); 604 605 float f; 606 try { 607 f = Float.parseFloat(f_str); 608 } catch (NumberFormatException e) { 609 // this shouldn't happen with the regexp above. 610 return false; 611 } 612 613 if (end.length() > 0 && end.charAt(0) != ' ') { 614 // We only support dimension-type values, so try to parse the unit for dimension 615 DimensionEntry dimension = parseDimension(end); 616 if (dimension != null) { 617 // convert the value into pixel based on the dimention type 618 // This is similar to TypedValue.applyDimension() 619 switch (dimension.type) { 620 case COMPLEX_UNIT_PX: 621 // do nothing, value is already in px 622 break; 623 case COMPLEX_UNIT_DIP: 624 case COMPLEX_UNIT_SP: // intended fall-through since we don't 625 // adjust for font size 626 f *= (float)mDensity.getDpiValue() / Density.DEFAULT_DENSITY; 627 break; 628 case COMPLEX_UNIT_PT: 629 f *= mDensity.getDpiValue() * (1.0f / 72); 630 break; 631 case COMPLEX_UNIT_IN: 632 f *= mDensity.getDpiValue(); 633 break; 634 case COMPLEX_UNIT_MM: 635 f *= mDensity.getDpiValue() * (1.0f / 25.4f); 636 break; 637 } 638 639 // store result (converted to int) 640 sIntOut[0] = (int) (f + 0.5); 641 642 return true; 643 } 644 } 645 } 646 647 return false; 648 } 649 parseDimension(String str)650 private static DimensionEntry parseDimension(String str) { 651 str = str.trim(); 652 653 for (DimensionEntry d : sDimensions) { 654 if (d.name.equals(str)) { 655 return d; 656 } 657 } 658 659 return null; 660 } 661 } 662