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