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.descriptors; 18 19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 20 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; 25 import static com.android.ide.common.layout.LayoutConstants.EDIT_TEXT; 26 import static com.android.ide.common.layout.LayoutConstants.EXPANDABLE_LIST_VIEW; 27 import static com.android.ide.common.layout.LayoutConstants.FQCN_ADAPTER_VIEW; 28 import static com.android.ide.common.layout.LayoutConstants.GALLERY; 29 import static com.android.ide.common.layout.LayoutConstants.GRID_LAYOUT; 30 import static com.android.ide.common.layout.LayoutConstants.GRID_VIEW; 31 import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; 32 import static com.android.ide.common.layout.LayoutConstants.LIST_VIEW; 33 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; 34 import static com.android.ide.common.layout.LayoutConstants.RELATIVE_LAYOUT; 35 import static com.android.ide.common.layout.LayoutConstants.SPACE; 36 import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; 37 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; 38 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.REQUEST_FOCUS; 39 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT; 40 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_INCLUDE; 41 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE; 42 43 import com.android.annotations.NonNull; 44 import com.android.ide.common.api.IAttributeInfo.Format; 45 import com.android.ide.common.resources.platform.AttributeInfo; 46 import com.android.ide.eclipse.adt.AdtConstants; 47 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 48 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 49 import com.android.resources.ResourceType; 50 import com.android.sdklib.SdkConstants; 51 52 import org.eclipse.swt.graphics.Image; 53 54 import java.util.ArrayList; 55 import java.util.EnumSet; 56 import java.util.HashSet; 57 import java.util.Locale; 58 import java.util.Map; 59 import java.util.Map.Entry; 60 import java.util.Set; 61 import java.util.regex.Matcher; 62 import java.util.regex.Pattern; 63 64 65 /** 66 * Utility methods related to descriptors handling. 67 */ 68 public final class DescriptorsUtils { 69 private static final String DEFAULT_WIDGET_PREFIX = "widget"; 70 71 private static final int JAVADOC_BREAK_LENGTH = 60; 72 73 /** 74 * The path in the online documentation for the manifest description. 75 * <p/> 76 * This is NOT a complete URL. To be used, it needs to be appended 77 * to {@link AdtConstants#CODESITE_BASE_URL} or to the local SDK 78 * documentation. 79 */ 80 public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#"; //$NON-NLS-1$ 81 82 public static final String IMAGE_KEY = "image"; //$NON-NLS-1$ 83 84 private static final String CODE = "$code"; //$NON-NLS-1$ 85 private static final String LINK = "$link"; //$NON-NLS-1$ 86 private static final String ELEM = "$elem"; //$NON-NLS-1$ 87 private static final String BREAK = "$break"; //$NON-NLS-1$ 88 89 /** 90 * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}. 91 * 92 * @param attributes The list of {@link AttributeDescriptor} to append to 93 * @param elementXmlName Optional XML local name of the element to which attributes are 94 * being added. When not null, this is used to filter overrides. 95 * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. 96 * See {@link SdkConstants#NS_RESOURCES} for a common value. 97 * @param infos The array of {@link AttributeInfo} to read and append to attributes 98 * @param requiredAttributes An optional set of attributes to mark as "required" (i.e. append 99 * a "*" to their UI name as a hint for the user.) If not null, must contains 100 * entries in the form "elem-name/attr-name". Elem-name can be "*". 101 * @param overrides A map [attribute name => ITextAttributeCreator creator]. 102 */ appendAttributes(ArrayList<AttributeDescriptor> attributes, String elementXmlName, String nsUri, AttributeInfo[] infos, Set<String> requiredAttributes, Map<String, ITextAttributeCreator> overrides)103 public static void appendAttributes(ArrayList<AttributeDescriptor> attributes, 104 String elementXmlName, 105 String nsUri, AttributeInfo[] infos, 106 Set<String> requiredAttributes, 107 Map<String, ITextAttributeCreator> overrides) { 108 for (AttributeInfo info : infos) { 109 boolean required = false; 110 if (requiredAttributes != null) { 111 String attr_name = info.getName(); 112 if (requiredAttributes.contains("*/" + attr_name) || 113 requiredAttributes.contains(elementXmlName + "/" + attr_name)) { 114 required = true; 115 } 116 } 117 appendAttribute(attributes, elementXmlName, nsUri, info, required, overrides); 118 } 119 } 120 121 /** 122 * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}. 123 * 124 * @param attributes The list of {@link AttributeDescriptor} to append to 125 * @param elementXmlName Optional XML local name of the element to which attributes are 126 * being added. When not null, this is used to filter overrides. 127 * @param info The {@link AttributeInfo} to append to attributes 128 * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. 129 * See {@link SdkConstants#NS_RESOURCES} for a common value. 130 * @param required True if the attribute is to be marked as "required" (i.e. append 131 * a "*" to its UI name as a hint for the user.) 132 * @param overrides A map [attribute name => ITextAttributeCreator creator]. 133 */ appendAttribute(ArrayList<AttributeDescriptor> attributes, String elementXmlName, String nsUri, AttributeInfo info, boolean required, Map<String, ITextAttributeCreator> overrides)134 public static void appendAttribute(ArrayList<AttributeDescriptor> attributes, 135 String elementXmlName, 136 String nsUri, 137 AttributeInfo info, boolean required, 138 Map<String, ITextAttributeCreator> overrides) { 139 TextAttributeDescriptor attr = null; 140 141 String xmlLocalName = info.getName(); 142 143 // Add the known types to the tooltip 144 EnumSet<Format> formats_set = info.getFormats(); 145 int flen = formats_set.size(); 146 if (flen > 0) { 147 // Create a specialized attribute if we can 148 if (overrides != null) { 149 for (Entry<String, ITextAttributeCreator> entry: overrides.entrySet()) { 150 // The override key can have the following formats: 151 // */xmlLocalName 152 // element/xmlLocalName 153 // element1,element2,...,elementN/xmlLocalName 154 String key = entry.getKey(); 155 String elements[] = key.split("/"); //$NON-NLS-1$ 156 String overrideAttrLocalName = null; 157 if (elements.length < 1) { 158 continue; 159 } else if (elements.length == 1) { 160 overrideAttrLocalName = elements[0]; 161 elements = null; 162 } else { 163 overrideAttrLocalName = elements[elements.length - 1]; 164 elements = elements[0].split(","); //$NON-NLS-1$ 165 } 166 167 if (overrideAttrLocalName == null || 168 !overrideAttrLocalName.equals(xmlLocalName)) { 169 continue; 170 } 171 172 boolean ok_element = elements != null && elements.length < 1; 173 if (!ok_element && elements != null) { 174 for (String element : elements) { 175 if (element.equals("*") //$NON-NLS-1$ 176 || element.equals(elementXmlName)) { 177 ok_element = true; 178 break; 179 } 180 } 181 } 182 183 if (!ok_element) { 184 continue; 185 } 186 187 ITextAttributeCreator override = entry.getValue(); 188 if (override != null) { 189 attr = override.create(xmlLocalName, nsUri, info); 190 } 191 } 192 } // if overrides 193 194 // Create a specialized descriptor if we can, based on type 195 if (attr == null) { 196 if (formats_set.contains(Format.REFERENCE)) { 197 // This is either a multi-type reference or a generic reference. 198 attr = new ReferenceAttributeDescriptor( 199 xmlLocalName, nsUri, info); 200 } else if (formats_set.contains(Format.ENUM)) { 201 attr = new ListAttributeDescriptor( 202 xmlLocalName, nsUri, info); 203 } else if (formats_set.contains(Format.FLAG)) { 204 attr = new FlagAttributeDescriptor( 205 xmlLocalName, nsUri, info); 206 } else if (formats_set.contains(Format.BOOLEAN)) { 207 attr = new BooleanAttributeDescriptor( 208 xmlLocalName, nsUri, info); 209 } else if (formats_set.contains(Format.STRING)) { 210 attr = new ReferenceAttributeDescriptor( 211 ResourceType.STRING, xmlLocalName, nsUri, info); 212 } 213 } 214 } 215 216 // By default a simple text field is used 217 if (attr == null) { 218 attr = new TextAttributeDescriptor(xmlLocalName, nsUri, info); 219 } 220 221 if (required) { 222 attr.setRequired(true); 223 } 224 225 attributes.add(attr); 226 } 227 228 /** 229 * Indicates the the given {@link AttributeInfo} already exists in the ArrayList of 230 * {@link AttributeDescriptor}. This test for the presence of a descriptor with the same 231 * XML name. 232 * 233 * @param attributes The list of {@link AttributeDescriptor} to compare to. 234 * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. 235 * See {@link SdkConstants#NS_RESOURCES} for a common value. 236 * @param info The {@link AttributeInfo} to know whether it is included in the above list. 237 * @return True if this {@link AttributeInfo} is already present in 238 * the {@link AttributeDescriptor} list. 239 */ 240 public static boolean containsAttribute(ArrayList<AttributeDescriptor> attributes, 241 String nsUri, 242 AttributeInfo info) { 243 String xmlLocalName = info.getName(); 244 for (AttributeDescriptor desc : attributes) { 245 if (desc.getXmlLocalName().equals(xmlLocalName)) { 246 if (nsUri == desc.getNamespaceUri() || 247 (nsUri != null && nsUri.equals(desc.getNamespaceUri()))) { 248 return true; 249 } 250 } 251 } 252 return false; 253 } 254 255 /** 256 * Create a pretty attribute UI name from an XML name. 257 * <p/> 258 * The original xml name starts with a lower case and is camel-case, 259 * e.g. "maxWidthForView". The pretty name starts with an upper case 260 * and has space separators, e.g. "Max width for view". 261 */ 262 public static String prettyAttributeUiName(String name) { 263 if (name.length() < 1) { 264 return name; 265 } 266 StringBuilder buf = new StringBuilder(2 * name.length()); 267 268 char c = name.charAt(0); 269 // Use upper case initial letter 270 buf.append(Character.toUpperCase(c)); 271 int len = name.length(); 272 for (int i = 1; i < len; i++) { 273 c = name.charAt(i); 274 if (Character.isUpperCase(c)) { 275 // Break camel case into separate words 276 buf.append(' '); 277 // Use a lower case initial letter for the next word, except if the 278 // word is solely X, Y or Z. 279 if (c >= 'X' && c <= 'Z' && 280 (i == len-1 || 281 (i < len-1 && Character.isUpperCase(name.charAt(i+1))))) { 282 buf.append(c); 283 } else { 284 buf.append(Character.toLowerCase(c)); 285 } 286 } else if (c == '_') { 287 buf.append(' '); 288 } else { 289 buf.append(c); 290 } 291 } 292 293 name = buf.toString(); 294 295 // Replace these acronyms by upper-case versions 296 // - (?<=^| ) means "if preceded by a space or beginning of string" 297 // - (?=$| ) means "if followed by a space or end of string" 298 if (name.contains("sdk") || name.startsWith("Sdk")) { 299 name = name.replaceAll("(?<=^| )[sS]dk(?=$| )", "SDK"); 300 } 301 if (name.contains("uri") || name.startsWith("Uri")) { 302 name = name.replaceAll("(?<=^| )[uU]ri(?=$| )", "URI"); 303 } 304 if (name.contains("ime") || name.startsWith("Ime")) { 305 name = name.replaceAll("(?<=^| )[iI]me(?=$| )", "IME"); 306 } 307 308 return name; 309 } 310 311 /** 312 * Similar to {@link #prettyAttributeUiName(String)}, but it will capitalize 313 * all words, not just the first one. 314 * <p/> 315 * The original xml name starts with a lower case and is camel-case, e.g. 316 * "maxWidthForView". The corresponding return value is 317 * "Max Width For View". 318 * 319 * @param name the attribute name, which should be a camel case name, e.g. 320 * "maxWidth" 321 * @return the corresponding display name, e.g. "Max Width" 322 */ 323 @NonNull 324 public static String capitalize(@NonNull String name) { 325 if (name.isEmpty()) { 326 return name; 327 } 328 StringBuilder buf = new StringBuilder(2 * name.length()); 329 330 char c = name.charAt(0); 331 // Use upper case initial letter 332 buf.append(Character.toUpperCase(c)); 333 int len = name.length(); 334 for (int i = 1; i < len; i++) { 335 c = name.charAt(i); 336 if (Character.isUpperCase(c)) { 337 // Break camel case into separate words 338 buf.append(' '); 339 // Use a lower case initial letter for the next word, except if the 340 // word is solely X, Y or Z. 341 buf.append(c); 342 } else if (c == '_') { 343 buf.append(' '); 344 if (i < len -1 && Character.isLowerCase(name.charAt(i + 1))) { 345 buf.append(Character.toUpperCase(name.charAt(i + 1))); 346 i++; 347 } 348 } else { 349 buf.append(c); 350 } 351 } 352 353 name = buf.toString(); 354 355 // Replace these acronyms by upper-case versions 356 // - (?<=^| ) means "if preceded by a space or beginning of string" 357 // - (?=$| ) means "if followed by a space or end of string" 358 if (name.contains("Sdk")) { 359 name = name.replaceAll("(?<=^| )Sdk(?=$| )", "SDK"); 360 } 361 if (name.contains("Uri")) { 362 name = name.replaceAll("(?<=^| )Uri(?=$| )", "URI"); 363 } 364 if (name.contains("Ime")) { 365 name = name.replaceAll("(?<=^| )Ime(?=$| )", "IME"); 366 } 367 368 return name; 369 } 370 371 /** 372 * Formats the javadoc tooltip to be usable in a tooltip. 373 */ 374 public static String formatTooltip(String javadoc) { 375 ArrayList<String> spans = scanJavadoc(javadoc); 376 377 StringBuilder sb = new StringBuilder(); 378 boolean needBreak = false; 379 380 for (int n = spans.size(), i = 0; i < n; ++i) { 381 String s = spans.get(i); 382 if (CODE.equals(s)) { 383 s = spans.get(++i); 384 if (s != null) { 385 sb.append('"').append(s).append('"'); 386 } 387 } else if (LINK.equals(s)) { 388 String base = spans.get(++i); 389 String anchor = spans.get(++i); 390 String text = spans.get(++i); 391 392 if (base != null) { 393 base = base.trim(); 394 } 395 if (anchor != null) { 396 anchor = anchor.trim(); 397 } 398 if (text != null) { 399 text = text.trim(); 400 } 401 402 // If there's no text, use the anchor if there's one 403 if (text == null || text.length() == 0) { 404 text = anchor; 405 } 406 407 if (base != null && base.length() > 0) { 408 if (text == null || text.length() == 0) { 409 // If we still have no text, use the base as text 410 text = base; 411 } 412 } 413 414 if (text != null) { 415 sb.append(text); 416 } 417 418 } else if (ELEM.equals(s)) { 419 s = spans.get(++i); 420 if (s != null) { 421 sb.append(s); 422 } 423 } else if (BREAK.equals(s)) { 424 needBreak = true; 425 } else if (s != null) { 426 if (needBreak && s.trim().length() > 0) { 427 sb.append('\n'); 428 } 429 sb.append(s); 430 needBreak = false; 431 } 432 } 433 434 return sb.toString(); 435 } 436 437 /** 438 * Formats the javadoc tooltip to be usable in a FormText. 439 * <p/> 440 * If the descriptor can provide an icon, the caller should provide 441 * elementsDescriptor.getIcon() as "image" to FormText, e.g.: 442 * <code>formText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());</code> 443 * 444 * @param javadoc The javadoc to format. Cannot be null. 445 * @param elementDescriptor The element descriptor parent of the javadoc. Cannot be null. 446 * @param androidDocBaseUrl The base URL for the documentation. Cannot be null. Should be 447 * <code>FrameworkResourceManager.getInstance().getDocumentationBaseUrl()</code> 448 */ 449 public static String formatFormText(String javadoc, 450 ElementDescriptor elementDescriptor, 451 String androidDocBaseUrl) { 452 ArrayList<String> spans = scanJavadoc(javadoc); 453 454 String fullSdkUrl = androidDocBaseUrl + MANIFEST_SDK_URL; 455 String sdkUrl = elementDescriptor.getSdkUrl(); 456 if (sdkUrl != null && sdkUrl.startsWith(MANIFEST_SDK_URL)) { 457 fullSdkUrl = androidDocBaseUrl + sdkUrl; 458 } 459 460 StringBuilder sb = new StringBuilder(); 461 462 Image icon = elementDescriptor.getCustomizedIcon(); 463 if (icon != null) { 464 sb.append("<form><li style=\"image\" value=\"" + //$NON-NLS-1$ 465 IMAGE_KEY + "\">"); //$NON-NLS-1$ 466 } else { 467 sb.append("<form><p>"); //$NON-NLS-1$ 468 } 469 470 for (int n = spans.size(), i = 0; i < n; ++i) { 471 String s = spans.get(i); 472 if (CODE.equals(s)) { 473 s = spans.get(++i); 474 if (elementDescriptor.getXmlName().equals(s) && fullSdkUrl != null) { 475 sb.append("<a href=\""); //$NON-NLS-1$ 476 sb.append(fullSdkUrl); 477 sb.append("\">"); //$NON-NLS-1$ 478 sb.append(s); 479 sb.append("</a>"); //$NON-NLS-1$ 480 } else if (s != null) { 481 sb.append('"').append(s).append('"'); 482 } 483 } else if (LINK.equals(s)) { 484 String base = spans.get(++i); 485 String anchor = spans.get(++i); 486 String text = spans.get(++i); 487 488 if (base != null) { 489 base = base.trim(); 490 } 491 if (anchor != null) { 492 anchor = anchor.trim(); 493 } 494 if (text != null) { 495 text = text.trim(); 496 } 497 498 // If there's no text, use the anchor if there's one 499 if (text == null || text.length() == 0) { 500 text = anchor; 501 } 502 503 // TODO specialize with a base URL for views, menus & other resources 504 // Base is empty for a local page anchor, in which case we'll replace it 505 // by the element SDK URL if it exists. 506 if ((base == null || base.length() == 0) && fullSdkUrl != null) { 507 base = fullSdkUrl; 508 } 509 510 String url = null; 511 if (base != null && base.length() > 0) { 512 if (base.startsWith("http")) { //$NON-NLS-1$ 513 // If base looks an URL, use it, with the optional anchor 514 url = base; 515 if (anchor != null && anchor.length() > 0) { 516 // If the base URL already has an anchor, it needs to be 517 // removed first. If there's no anchor, we need to add "#" 518 int pos = url.lastIndexOf('#'); 519 if (pos < 0) { 520 url += "#"; //$NON-NLS-1$ 521 } else if (pos < url.length() - 1) { 522 url = url.substring(0, pos + 1); 523 } 524 525 url += anchor; 526 } 527 } else if (text == null || text.length() == 0) { 528 // If we still have no text, use the base as text 529 text = base; 530 } 531 } 532 533 if (url != null && text != null) { 534 sb.append("<a href=\""); //$NON-NLS-1$ 535 sb.append(url); 536 sb.append("\">"); //$NON-NLS-1$ 537 sb.append(text); 538 sb.append("</a>"); //$NON-NLS-1$ 539 } else if (text != null) { 540 sb.append("<b>").append(text).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$ 541 } 542 543 } else if (ELEM.equals(s)) { 544 s = spans.get(++i); 545 if (sdkUrl != null && s != null) { 546 sb.append("<a href=\""); //$NON-NLS-1$ 547 sb.append(sdkUrl); 548 sb.append("\">"); //$NON-NLS-1$ 549 sb.append(s); 550 sb.append("</a>"); //$NON-NLS-1$ 551 } else if (s != null) { 552 sb.append("<b>").append(s).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$ 553 } 554 } else if (BREAK.equals(s)) { 555 // ignore line breaks in pseudo-HTML rendering 556 } else if (s != null) { 557 sb.append(s); 558 } 559 } 560 561 if (icon != null) { 562 sb.append("</li></form>"); //$NON-NLS-1$ 563 } else { 564 sb.append("</p></form>"); //$NON-NLS-1$ 565 } 566 return sb.toString(); 567 } 568 569 private static ArrayList<String> scanJavadoc(String javadoc) { 570 ArrayList<String> spans = new ArrayList<String>(); 571 572 // Standardize all whitespace in the javadoc to single spaces. 573 if (javadoc != null) { 574 javadoc = javadoc.replaceAll("[ \t\f\r\n]+", " "); //$NON-NLS-1$ //$NON-NLS-2$ 575 } 576 577 // Detects {@link <base>#<name> <text>} where all 3 are optional 578 Pattern p_link = Pattern.compile("\\{@link\\s+([^#\\}\\s]*)(?:#([^\\s\\}]*))?(?:\\s*([^\\}]*))?\\}(.*)"); //$NON-NLS-1$ 579 // Detects <code>blah</code> 580 Pattern p_code = Pattern.compile("<code>(.+?)</code>(.*)"); //$NON-NLS-1$ 581 // Detects @blah@, used in hard-coded tooltip descriptors 582 Pattern p_elem = Pattern.compile("@([\\w -]+)@(.*)"); //$NON-NLS-1$ 583 // Detects a buffer that starts by @@ (request for a break) 584 Pattern p_break = Pattern.compile("@@(.*)"); //$NON-NLS-1$ 585 // Detects a buffer that starts by @ < or { (one that was not matched above) 586 Pattern p_open = Pattern.compile("([@<\\{])(.*)"); //$NON-NLS-1$ 587 // Detects everything till the next potential separator, i.e. @ < or { 588 Pattern p_text = Pattern.compile("([^@<\\{]+)(.*)"); //$NON-NLS-1$ 589 590 int currentLength = 0; 591 String text = null; 592 593 while(javadoc != null && javadoc.length() > 0) { 594 Matcher m; 595 String s = null; 596 if ((m = p_code.matcher(javadoc)).matches()) { 597 spans.add(CODE); 598 spans.add(text = cleanupJavadocHtml(m.group(1))); // <code> text 599 javadoc = m.group(2); 600 if (text != null) { 601 currentLength += text.length(); 602 } 603 } else if ((m = p_link.matcher(javadoc)).matches()) { 604 spans.add(LINK); 605 spans.add(m.group(1)); // @link base 606 spans.add(m.group(2)); // @link anchor 607 spans.add(text = cleanupJavadocHtml(m.group(3))); // @link text 608 javadoc = m.group(4); 609 if (text != null) { 610 currentLength += text.length(); 611 } 612 } else if ((m = p_elem.matcher(javadoc)).matches()) { 613 spans.add(ELEM); 614 spans.add(text = cleanupJavadocHtml(m.group(1))); // @text@ 615 javadoc = m.group(2); 616 if (text != null) { 617 currentLength += text.length() - 2; 618 } 619 } else if ((m = p_break.matcher(javadoc)).matches()) { 620 spans.add(BREAK); 621 currentLength = 0; 622 javadoc = m.group(1); 623 } else if ((m = p_open.matcher(javadoc)).matches()) { 624 s = m.group(1); 625 javadoc = m.group(2); 626 } else if ((m = p_text.matcher(javadoc)).matches()) { 627 s = m.group(1); 628 javadoc = m.group(2); 629 } else { 630 // This is not supposed to happen. In case of, just use everything. 631 s = javadoc; 632 javadoc = null; 633 } 634 if (s != null && s.length() > 0) { 635 s = cleanupJavadocHtml(s); 636 637 if (currentLength >= JAVADOC_BREAK_LENGTH) { 638 spans.add(BREAK); 639 currentLength = 0; 640 } 641 while (currentLength + s.length() > JAVADOC_BREAK_LENGTH) { 642 int pos = s.indexOf(' ', JAVADOC_BREAK_LENGTH - currentLength); 643 if (pos <= 0) { 644 break; 645 } 646 spans.add(s.substring(0, pos + 1)); 647 spans.add(BREAK); 648 currentLength = 0; 649 s = s.substring(pos + 1); 650 } 651 652 spans.add(s); 653 currentLength += s.length(); 654 } 655 } 656 657 return spans; 658 } 659 660 /** 661 * Remove anything that looks like HTML from a javadoc snippet, as it is supported 662 * neither by FormText nor a standard text tooltip. 663 */ 664 private static String cleanupJavadocHtml(String s) { 665 if (s != null) { 666 s = s.replaceAll("<", "\""); //$NON-NLS-1$ $NON-NLS-2$ 667 s = s.replaceAll(">", "\""); //$NON-NLS-1$ $NON-NLS-2$ 668 s = s.replaceAll("<[^>]+>", ""); //$NON-NLS-1$ $NON-NLS-2$ 669 } 670 return s; 671 } 672 673 /** 674 * Returns the basename for the given fully qualified class name. It is okay to pass 675 * a basename to this method which will just be returned back. 676 * 677 * @param fqcn The fully qualified class name to convert 678 * @return the basename of the class name 679 */ 680 public static String getBasename(String fqcn) { 681 String name = fqcn; 682 int lastDot = name.lastIndexOf('.'); 683 if (lastDot != -1) { 684 name = name.substring(lastDot + 1); 685 } 686 687 return name; 688 } 689 690 /** 691 * Sets the default layout attributes for the a new UiElementNode. 692 * <p/> 693 * Note that ideally the node should already be part of a hierarchy so that its 694 * parent layout and previous sibling can be determined, if any. 695 * <p/> 696 * This does not override attributes which are not empty. 697 */ 698 public static void setDefaultLayoutAttributes(UiElementNode node, boolean updateLayout) { 699 // if this ui_node is a layout and we're adding it to a document, use match_parent for 700 // both W/H. Otherwise default to wrap_layout. 701 ElementDescriptor descriptor = node.getDescriptor(); 702 703 String name = descriptor.getXmlLocalName(); 704 if (name.equals(REQUEST_FOCUS)) { 705 // Don't add ids, widths and heights etc to <requestFocus> 706 return; 707 } 708 709 // Width and height are mandatory in all layouts except GridLayout 710 boolean setSize = !node.getUiParent().getDescriptor().getXmlName().equals(GRID_LAYOUT); 711 if (setSize) { 712 boolean fill = descriptor.hasChildren() && 713 node.getUiParent() instanceof UiDocumentNode; 714 node.setAttributeValue( 715 ATTR_LAYOUT_WIDTH, 716 SdkConstants.NS_RESOURCES, 717 fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT, 718 false /* override */); 719 node.setAttributeValue( 720 ATTR_LAYOUT_HEIGHT, 721 SdkConstants.NS_RESOURCES, 722 fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT, 723 false /* override */); 724 } 725 726 if (needsDefaultId(node.getDescriptor())) { 727 String freeId = getFreeWidgetId(node); 728 if (freeId != null) { 729 node.setAttributeValue( 730 ATTR_ID, 731 SdkConstants.NS_RESOURCES, 732 freeId, 733 false /* override */); 734 } 735 } 736 737 // Set a text attribute on textual widgets -- but only on those that define a text 738 // attribute 739 if (descriptor.definesAttribute(ANDROID_URI, ATTR_TEXT) 740 // Don't set default text value into edit texts - they typically start out blank 741 && !descriptor.getXmlLocalName().equals(EDIT_TEXT)) { 742 String type = getBasename(descriptor.getUiName()); 743 node.setAttributeValue( 744 ATTR_TEXT, 745 SdkConstants.NS_RESOURCES, 746 type, 747 false /*override*/); 748 } 749 750 if (updateLayout) { 751 UiElementNode parent = node.getUiParent(); 752 if (parent != null && 753 parent.getDescriptor().getXmlLocalName().equals( 754 RELATIVE_LAYOUT)) { 755 UiElementNode previous = node.getUiPreviousSibling(); 756 if (previous != null) { 757 String id = previous.getAttributeValue(ATTR_ID); 758 if (id != null && id.length() > 0) { 759 id = id.replace("@+", "@"); //$NON-NLS-1$ //$NON-NLS-2$ 760 node.setAttributeValue( 761 ATTR_LAYOUT_BELOW, 762 SdkConstants.NS_RESOURCES, 763 id, 764 false /* override */); 765 } 766 } 767 } 768 } 769 } 770 771 /** 772 * Determines whether new views of the given type should be assigned a 773 * default id. 774 * 775 * @param descriptor a descriptor describing the view to look up 776 * @return true if new views of the given type should be assigned a default 777 * id 778 */ 779 public static boolean needsDefaultId(ElementDescriptor descriptor) { 780 // By default, layouts do not need ids. 781 String tag = descriptor.getXmlLocalName(); 782 if (tag.endsWith("Layout") //$NON-NLS-1$ 783 || tag.equals(VIEW_FRAGMENT) 784 || tag.equals(VIEW_INCLUDE) 785 || tag.equals(VIEW_MERGE) 786 || tag.equals(SPACE) 787 || tag.endsWith(SPACE) && tag.length() > SPACE.length() && 788 tag.charAt(tag.length() - SPACE.length()) == '.') { 789 return false; 790 } 791 792 return true; 793 } 794 795 /** 796 * Given a UI node, returns the first available id that matches the 797 * pattern "prefix%d". 798 * <p/>TabWidget is a special case and the method will always return "@android:id/tabs". 799 * 800 * @param uiNode The UI node that gives the prefix to match. 801 * @return A suitable generated id in the attribute form needed by the XML id tag 802 * (e.g. "@+id/something") 803 */ 804 public static String getFreeWidgetId(UiElementNode uiNode) { 805 String name = getBasename(uiNode.getDescriptor().getXmlLocalName()); 806 return getFreeWidgetId(uiNode.getUiRoot(), name); 807 } 808 809 /** 810 * Given a UI root node and a potential XML node name, returns the first available 811 * id that matches the pattern "prefix%d". 812 * <p/>TabWidget is a special case and the method will always return "@android:id/tabs". 813 * 814 * @param uiRoot The root UI node to search for name conflicts from 815 * @param name The XML node prefix name to look for 816 * @return A suitable generated id in the attribute form needed by the XML id tag 817 * (e.g. "@+id/something") 818 */ 819 public static String getFreeWidgetId(UiElementNode uiRoot, String name) { 820 if ("TabWidget".equals(name)) { //$NON-NLS-1$ 821 return "@android:id/tabs"; //$NON-NLS-1$ 822 } 823 824 return NEW_ID_PREFIX + getFreeWidgetId(uiRoot, 825 new Object[] { name, null, null, null }); 826 } 827 828 /** 829 * Given a UI root node, returns the first available id that matches the 830 * pattern "prefix%d". 831 * 832 * For recursion purposes, a "context" is given. Since Java doesn't have in-out parameters 833 * in methods and we're not going to do a dedicated type, we just use an object array which 834 * must contain one initial item and several are built on the fly just for internal storage: 835 * <ul> 836 * <li> prefix(String): The prefix of the generated id, i.e. "widget". Cannot be null. 837 * <li> index(Integer): The minimum index of the generated id. Must start with null. 838 * <li> generated(String): The generated widget currently being searched. Must start with null. 839 * <li> map(Set<String>): A set of the ids collected so far when walking through the widget 840 * hierarchy. Must start with null. 841 * </ul> 842 * 843 * @param uiRoot The Ui root node where to start searching recursively. For the initial call 844 * you want to pass the document root. 845 * @param params An in-out context of parameters used during recursion, as explained above. 846 * @return A suitable generated id 847 */ 848 @SuppressWarnings("unchecked") 849 private static String getFreeWidgetId(UiElementNode uiRoot, 850 Object[] params) { 851 852 Set<String> map = (Set<String>)params[3]; 853 if (map == null) { 854 params[3] = map = new HashSet<String>(); 855 } 856 857 int num = params[1] == null ? 0 : ((Integer)params[1]).intValue(); 858 859 String generated = (String) params[2]; 860 String prefix = (String) params[0]; 861 if (generated == null) { 862 int pos = prefix.indexOf('.'); 863 if (pos >= 0) { 864 prefix = prefix.substring(pos + 1); 865 } 866 pos = prefix.indexOf('$'); 867 if (pos >= 0) { 868 prefix = prefix.substring(pos + 1); 869 } 870 prefix = prefix.replaceAll("[^a-zA-Z]", ""); //$NON-NLS-1$ $NON-NLS-2$ 871 if (prefix.length() == 0) { 872 prefix = DEFAULT_WIDGET_PREFIX; 873 } else { 874 // Lowercase initial character 875 prefix = Character.toLowerCase(prefix.charAt(0)) + prefix.substring(1); 876 } 877 878 // Note that we perform locale-independent lowercase checks; in "Image" we 879 // want the lowercase version to be "image", not "?mage" where ? is 880 // the char LATIN SMALL LETTER DOTLESS I. 881 do { 882 num++; 883 generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$ 884 } while (map.contains(generated.toLowerCase(Locale.US))); 885 886 params[0] = prefix; 887 params[1] = num; 888 params[2] = generated; 889 } 890 891 String id = uiRoot.getAttributeValue(ATTR_ID); 892 if (id != null) { 893 id = id.replace(NEW_ID_PREFIX, ""); //$NON-NLS-1$ 894 id = id.replace(ID_PREFIX, ""); //$NON-NLS-1$ 895 if (map.add(id.toLowerCase(Locale.US)) 896 && map.contains(generated.toLowerCase(Locale.US))) { 897 898 do { 899 num++; 900 generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$ 901 } while (map.contains(generated.toLowerCase(Locale.US))); 902 903 params[1] = num; 904 params[2] = generated; 905 } 906 } 907 908 for (UiElementNode uiChild : uiRoot.getUiChildren()) { 909 getFreeWidgetId(uiChild, params); 910 } 911 912 // Note: return params[2] (not "generated") since it could have changed during recursion. 913 return (String) params[2]; 914 } 915 916 /** 917 * Returns true if the given descriptor represents a view that not only can have 918 * children but which allows us to <b>insert</b> children. Some views, such as 919 * ListView (and in general all AdapterViews), disallow children to be inserted except 920 * through the dedicated AdapterView interface to do it. 921 * 922 * @param descriptor the descriptor for the view in question 923 * @param viewObject an actual instance of the view, or null if not available 924 * @return true if the descriptor describes a view which allows insertion of child 925 * views 926 */ canInsertChildren(ElementDescriptor descriptor, Object viewObject)927 public static boolean canInsertChildren(ElementDescriptor descriptor, Object viewObject) { 928 if (descriptor.hasChildren()) { 929 if (viewObject != null) { 930 // We have a view object; see if it derives from an AdapterView 931 Class<?> clz = viewObject.getClass(); 932 while (clz != null) { 933 if (clz.getName().equals(FQCN_ADAPTER_VIEW)) { 934 return false; 935 } 936 clz = clz.getSuperclass(); 937 } 938 } else { 939 // No view object, so we can't easily look up the class and determine 940 // whether it's an AdapterView; instead, look at the fixed list of builtin 941 // concrete subclasses of AdapterView 942 String viewName = descriptor.getXmlLocalName(); 943 if (viewName.equals(LIST_VIEW) || viewName.equals(EXPANDABLE_LIST_VIEW) 944 || viewName.equals(GALLERY) || viewName.equals(GRID_VIEW)) { 945 946 // We should really also enforce that 947 // LayoutConstants.ANDROID_URI.equals(descriptor.getNameSpace()) 948 // here and if not, return true, but it turns out the getNameSpace() 949 // for elements are often "". 950 951 return false; 952 } 953 } 954 955 return true; 956 } 957 958 return false; 959 } 960 } 961