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