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