1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.gre; 18 19 import static com.android.SdkConstants.ANDROID_URI; 20 import static com.android.SdkConstants.ATTR_ID; 21 import static com.android.SdkConstants.FQCN_BUTTON; 22 import static com.android.SdkConstants.FQCN_SPINNER; 23 import static com.android.SdkConstants.FQCN_TOGGLE_BUTTON; 24 import static com.android.SdkConstants.ID_PREFIX; 25 import static com.android.SdkConstants.NEW_ID_PREFIX; 26 import static com.android.SdkConstants.VIEW_FRAGMENT; 27 import static com.android.SdkConstants.VIEW_INCLUDE; 28 29 import com.android.annotations.VisibleForTesting; 30 import com.android.ide.common.api.IViewMetadata.FillPreference; 31 import com.android.ide.common.api.Margins; 32 import com.android.ide.common.api.ResizePolicy; 33 import com.android.ide.eclipse.adt.AdtPlugin; 34 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 35 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 36 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 37 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 38 import com.android.resources.Density; 39 import com.android.utils.Pair; 40 41 import org.w3c.dom.Document; 42 import org.w3c.dom.Element; 43 import org.w3c.dom.Node; 44 import org.w3c.dom.NodeList; 45 import org.xml.sax.InputSource; 46 47 import java.io.BufferedInputStream; 48 import java.io.InputStream; 49 import java.util.ArrayList; 50 import java.util.Collection; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.Iterator; 55 import java.util.List; 56 import java.util.Locale; 57 import java.util.Map; 58 import java.util.Set; 59 60 import javax.xml.parsers.DocumentBuilder; 61 import javax.xml.parsers.DocumentBuilderFactory; 62 63 /** 64 * The {@link ViewMetadataRepository} contains additional metadata for Android view 65 * classes 66 */ 67 public class ViewMetadataRepository { 68 private static final String PREVIEW_CONFIG_FILENAME = "rendering-configs.xml"; //$NON-NLS-1$ 69 private static final String METADATA_FILENAME = "extra-view-metadata.xml"; //$NON-NLS-1$ 70 71 /** Singleton instance */ 72 private static ViewMetadataRepository sInstance = new ViewMetadataRepository(); 73 74 /** 75 * Returns the singleton instance 76 * 77 * @return the {@link ViewMetadataRepository} 78 */ 79 public static ViewMetadataRepository get() { 80 return sInstance; 81 } 82 83 /** 84 * Ever increasing counter used to assign natural ordering numbers to views and 85 * categories 86 */ 87 private static int sNextOrdinal = 0; 88 89 /** 90 * List of categories (which contain views); constructed lazily so use 91 * {@link #getCategories()} 92 */ 93 private List<CategoryData> mCategories; 94 95 /** 96 * Map from class names to view data objects; constructed lazily so use 97 * {@link #getClassToView} 98 */ 99 private Map<String, ViewData> mClassToView; 100 101 /** Hidden constructor: Create via factory {@link #get()} instead */ 102 private ViewMetadataRepository() { 103 } 104 105 /** Returns a map from class fully qualified names to {@link ViewData} objects */ 106 private Map<String, ViewData> getClassToView() { 107 if (mClassToView == null) { 108 int initialSize = 75; 109 mClassToView = new HashMap<String, ViewData>(initialSize); 110 List<CategoryData> categories = getCategories(); 111 for (CategoryData category : categories) { 112 for (ViewData view : category) { 113 mClassToView.put(view.getFcqn(), view); 114 } 115 } 116 assert mClassToView.size() <= initialSize; 117 } 118 119 return mClassToView; 120 } 121 122 /** 123 * Returns an XML document containing rendering configurations for the various Android 124 * views. The FQN of each view can be obtained via the 125 * {@link #getFullClassName(Element)} method 126 * 127 * @return an XML document containing rendering elements 128 */ 129 public Document getRenderingConfigDoc() { 130 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 131 Class<ViewMetadataRepository> clz = ViewMetadataRepository.class; 132 InputStream paletteStream = clz.getResourceAsStream(PREVIEW_CONFIG_FILENAME); 133 InputSource is = new InputSource(paletteStream); 134 try { 135 factory.setNamespaceAware(true); 136 factory.setValidating(false); 137 factory.setIgnoringComments(true); 138 DocumentBuilder builder = factory.newDocumentBuilder(); 139 return builder.parse(is); 140 } catch (Exception e) { 141 AdtPlugin.log(e, "Parsing palette file failed"); 142 return null; 143 } 144 } 145 146 /** 147 * Returns a fully qualified class name for an element in the rendering document 148 * returned by {@link #getRenderingConfigDoc()} 149 * 150 * @param element the element to look up the fqcn for 151 * @return the fqcn of the view the element represents a preview for 152 */ 153 public String getFullClassName(Element element) { 154 // We don't use the element tag name, because in some cases we have 155 // an outer element to render some interesting inner element, such as a tab widget 156 // (which must be rendered inside a tab host). 157 // 158 // Therefore, we instead use the convention that the id is the fully qualified 159 // class name, with .'s replaced with _'s. 160 161 // Special case: for tab host we aren't allowed to mess with the id 162 String id = element.getAttributeNS(ANDROID_URI, ATTR_ID); 163 164 if ("@android:id/tabhost".equals(id)) { 165 // Special case to distinguish TabHost and TabWidget 166 NodeList children = element.getChildNodes(); 167 if (children.getLength() > 1 && (children.item(1) instanceof Element)) { 168 Element child = (Element) children.item(1); 169 String childId = child.getAttributeNS(ANDROID_URI, ATTR_ID); 170 if ("@+id/android_widget_TabWidget".equals(childId)) { 171 return "android.widget.TabWidget"; // TODO: Tab widget! 172 } 173 } 174 return "android.widget.TabHost"; // TODO: Tab widget! 175 } 176 177 StringBuilder sb = new StringBuilder(); 178 int i = 0; 179 if (id.startsWith(NEW_ID_PREFIX)) { 180 i = NEW_ID_PREFIX.length(); 181 } else if (id.startsWith(ID_PREFIX)) { 182 i = ID_PREFIX.length(); 183 } 184 185 for (; i < id.length(); i++) { 186 char c = id.charAt(i); 187 if (c == '_') { 188 sb.append('.'); 189 } else { 190 sb.append(c); 191 } 192 } 193 194 return sb.toString(); 195 } 196 197 /** Returns an ordered list of categories and views, parsed from a metadata file */ 198 private List<CategoryData> getCategories() { 199 if (mCategories == null) { 200 mCategories = new ArrayList<CategoryData>(); 201 202 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 203 Class<ViewMetadataRepository> clz = ViewMetadataRepository.class; 204 InputStream inputStream = clz.getResourceAsStream(METADATA_FILENAME); 205 InputSource is = new InputSource(new BufferedInputStream(inputStream)); 206 try { 207 factory.setNamespaceAware(true); 208 factory.setValidating(false); 209 factory.setIgnoringComments(true); 210 DocumentBuilder builder = factory.newDocumentBuilder(); 211 Document document = builder.parse(is); 212 Map<String, FillPreference> fillTypes = new HashMap<String, FillPreference>(); 213 for (FillPreference pref : FillPreference.values()) { 214 fillTypes.put(pref.toString().toLowerCase(Locale.US), pref); 215 } 216 217 NodeList categoryNodes = document.getDocumentElement().getChildNodes(); 218 for (int i = 0, n = categoryNodes.getLength(); i < n; i++) { 219 Node node = categoryNodes.item(i); 220 if (node.getNodeType() == Node.ELEMENT_NODE) { 221 Element element = (Element) node; 222 if (element.getNodeName().equals("category")) { //$NON-NLS-1$ 223 String name = element.getAttribute("name"); //$NON-NLS-1$ 224 CategoryData category = new CategoryData(name); 225 NodeList children = element.getChildNodes(); 226 for (int j = 0, m = children.getLength(); j < m; j++) { 227 Node childNode = children.item(j); 228 if (childNode.getNodeType() == Node.ELEMENT_NODE) { 229 Element child = (Element) childNode; 230 ViewData view = createViewData(fillTypes, child, 231 null, FillPreference.NONE, RenderMode.NORMAL, null); 232 category.addView(view); 233 } 234 } 235 mCategories.add(category); 236 } 237 } 238 } 239 } catch (Exception e) { 240 AdtPlugin.log(e, "Invalid palette metadata"); //$NON-NLS-1$ 241 } 242 } 243 244 return mCategories; 245 } 246 247 private ViewData createViewData(Map<String, FillPreference> fillTypes, 248 Element child, String defaultFqcn, FillPreference defaultFill, 249 RenderMode defaultRender, String defaultSize) { 250 String fqcn = child.getAttribute("class"); //$NON-NLS-1$ 251 if (fqcn.length() == 0) { 252 fqcn = defaultFqcn; 253 } 254 String fill = child.getAttribute("fill"); //$NON-NLS-1$ 255 FillPreference fillPreference = null; 256 if (fill.length() > 0) { 257 fillPreference = fillTypes.get(fill); 258 } 259 if (fillPreference == null) { 260 fillPreference = defaultFill; 261 } 262 String skip = child.getAttribute("skip"); //$NON-NLS-1$ 263 RenderMode renderMode = defaultRender; 264 String render = child.getAttribute("render"); //$NON-NLS-1$ 265 if (render.length() > 0) { 266 renderMode = RenderMode.get(render); 267 } 268 String displayName = child.getAttribute("name"); //$NON-NLS-1$ 269 if (displayName.length() == 0) { 270 displayName = null; 271 } 272 273 String relatedTo = child.getAttribute("relatedTo"); //$NON-NLS-1$ 274 String topAttrs = child.getAttribute("topAttrs"); //$NON-NLS-1$ 275 String resize = child.getAttribute("resize"); //$NON-NLS-1$ 276 ViewData view = new ViewData(fqcn, displayName, fillPreference, 277 skip.length() == 0 ? false : Boolean.valueOf(skip), 278 renderMode, relatedTo, resize, topAttrs); 279 280 String init = child.getAttribute("init"); //$NON-NLS-1$ 281 String icon = child.getAttribute("icon"); //$NON-NLS-1$ 282 283 view.setInitString(init); 284 if (icon.length() > 0) { 285 view.setIconName(icon); 286 } 287 288 // Nested variations? 289 if (child.hasChildNodes()) { 290 // Palette variations 291 NodeList childNodes = child.getChildNodes(); 292 for (int k = 0, kl = childNodes.getLength(); k < kl; k++) { 293 Node variationNode = childNodes.item(k); 294 if (variationNode.getNodeType() == Node.ELEMENT_NODE) { 295 Element variation = (Element) variationNode; 296 ViewData variationView = createViewData(fillTypes, variation, 297 fqcn, fillPreference, renderMode, resize); 298 view.addVariation(variationView); 299 } 300 } 301 } 302 303 return view; 304 } 305 306 /** 307 * Computes the palette entries for the given {@link AndroidTargetData}, looking up the 308 * available node descriptors, categorizing and sorting them. 309 * 310 * @param targetData the target data for which to compute palette entries 311 * @param alphabetical if true, sort all items in alphabetical order 312 * @param createCategories if true, organize the items into categories 313 * @return a list of pairs where each pair contains of the category label and an 314 * ordered list of elements to be included in that category 315 */ 316 public List<Pair<String, List<ViewElementDescriptor>>> getPaletteEntries( 317 AndroidTargetData targetData, boolean alphabetical, boolean createCategories) { 318 List<Pair<String, List<ViewElementDescriptor>>> result = 319 new ArrayList<Pair<String, List<ViewElementDescriptor>>>(); 320 321 List<List<ViewElementDescriptor>> lists = new ArrayList<List<ViewElementDescriptor>>(2); 322 LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors(); 323 lists.add(layoutDescriptors.getViewDescriptors()); 324 lists.add(layoutDescriptors.getLayoutDescriptors()); 325 326 // First record map of FQCN to ViewElementDescriptor such that we can quickly 327 // determine if a particular palette entry is available 328 Map<String, ViewElementDescriptor> fqcnToDescriptor = 329 new HashMap<String, ViewElementDescriptor>(); 330 for (List<ViewElementDescriptor> list : lists) { 331 for (ViewElementDescriptor view : list) { 332 String fqcn = view.getFullClassName(); 333 if (fqcn == null) { 334 // <view> and <merge> tags etc 335 fqcn = view.getUiName(); 336 } 337 fqcnToDescriptor.put(fqcn, view); 338 } 339 } 340 341 Set<ViewElementDescriptor> remaining = new HashSet<ViewElementDescriptor>( 342 layoutDescriptors.getViewDescriptors().size() 343 + layoutDescriptors.getLayoutDescriptors().size()); 344 remaining.addAll(layoutDescriptors.getViewDescriptors()); 345 remaining.addAll(layoutDescriptors.getLayoutDescriptors()); 346 347 // Now iterate in palette metadata order over the items in the palette and include 348 // any that also appear as a descriptor 349 List<ViewElementDescriptor> categoryItems = new ArrayList<ViewElementDescriptor>(); 350 for (CategoryData category : getCategories()) { 351 if (createCategories) { 352 categoryItems = new ArrayList<ViewElementDescriptor>(); 353 } 354 for (ViewData view : category) { 355 String fqcn = view.getFcqn(); 356 ViewElementDescriptor descriptor = fqcnToDescriptor.get(fqcn); 357 if (descriptor != null) { 358 remaining.remove(descriptor); 359 if (view.getSkip()) { 360 continue; 361 } 362 363 if (view.getDisplayName() != null || view.getInitString().length() > 0) { 364 categoryItems.add(new PaletteMetadataDescriptor(descriptor, 365 view.getDisplayName(), view.getInitString(), view.getIconName())); 366 } else { 367 categoryItems.add(descriptor); 368 } 369 370 if (view.hasVariations()) { 371 for (ViewData variation : view.getVariations()) { 372 String init = variation.getInitString(); 373 String icon = variation.getIconName(); 374 ViewElementDescriptor desc = new PaletteMetadataDescriptor(descriptor, 375 variation.getDisplayName(), init, icon); 376 categoryItems.add(desc); 377 } 378 } 379 } 380 } 381 382 if (createCategories && categoryItems.size() > 0) { 383 if (alphabetical) { 384 Collections.sort(categoryItems); 385 } 386 result.add(Pair.of(category.getName(), categoryItems)); 387 } 388 } 389 390 if (remaining.size() > 0) { 391 List<ViewElementDescriptor> otherItems = 392 new ArrayList<ViewElementDescriptor>(remaining); 393 // Always sorted, we don't have a natural order for these unknowns 394 Collections.sort(otherItems); 395 if (createCategories) { 396 result.add(Pair.of("Other", otherItems)); 397 } else { 398 categoryItems.addAll(otherItems); 399 } 400 } 401 402 if (!createCategories) { 403 if (alphabetical) { 404 Collections.sort(categoryItems); 405 } 406 result.add(Pair.of("Views", categoryItems)); 407 } 408 409 return result; 410 } 411 412 @VisibleForTesting 413 Collection<String> getAllFqcns() { 414 return getClassToView().keySet(); 415 } 416 417 /** 418 * Metadata holder for a particular category - contains the name of the category, its 419 * ordinal (for natural/logical sorting order) and views contained in the category 420 */ 421 private static class CategoryData implements Iterable<ViewData>, Comparable<CategoryData> { 422 /** Category name */ 423 private final String mName; 424 /** Views included in this category */ 425 private final List<ViewData> mViews = new ArrayList<ViewData>(); 426 /** Natural ordering rank */ 427 private final int mOrdinal = sNextOrdinal++; 428 429 /** Constructs a new category with the given name */ 430 private CategoryData(String name) { 431 super(); 432 mName = name; 433 } 434 435 /** Adds a new view into this category */ 436 private void addView(ViewData view) { 437 mViews.add(view); 438 } 439 440 private String getName() { 441 return mName; 442 } 443 444 // Implements Iterable<ViewData> such that we can use for-each on the category to 445 // enumerate its views 446 @Override 447 public Iterator<ViewData> iterator() { 448 return mViews.iterator(); 449 } 450 451 // Implements Comparable<CategoryData> such that categories can be naturally sorted 452 @Override 453 public int compareTo(CategoryData other) { 454 return mOrdinal - other.mOrdinal; 455 } 456 } 457 458 /** Metadata holder for a view of a given fully qualified class name */ 459 private static class ViewData implements Comparable<ViewData> { 460 /** The fully qualified class name of the view */ 461 private final String mFqcn; 462 /** Fill preference of the view */ 463 private final FillPreference mFillPreference; 464 /** Skip this item in the palette? */ 465 private final boolean mSkip; 466 /** Must this item be rendered alone? skipped? etc */ 467 private final RenderMode mRenderMode; 468 /** Related views */ 469 private final String mRelatedTo; 470 /** The relative rank of the view for natural ordering */ 471 private final int mOrdinal = sNextOrdinal++; 472 /** List of optional variations */ 473 private List<ViewData> mVariations; 474 /** Display name. Can be null. */ 475 private String mDisplayName; 476 /** 477 * Optional initialization string - a comma separate set of name/value pairs to 478 * initialize the element with 479 */ 480 private String mInitString; 481 /** The name of an icon (known to the {@link IconFactory} to show for this view */ 482 private String mIconName; 483 /** The resize preference of this view */ 484 private String mResize; 485 /** The most commonly set attributes of this view */ 486 private String mTopAttrs; 487 488 /** Constructs a new view data for the given class */ 489 private ViewData(String fqcn, String displayName, 490 FillPreference fillPreference, boolean skip, RenderMode renderMode, 491 String relatedTo, String resize, String topAttrs) { 492 super(); 493 mFqcn = fqcn; 494 mDisplayName = displayName; 495 mFillPreference = fillPreference; 496 mSkip = skip; 497 mRenderMode = renderMode; 498 mRelatedTo = relatedTo; 499 mResize = resize; 500 mTopAttrs = topAttrs; 501 } 502 503 /** Returns the {@link FillPreference} for views of this type */ 504 private FillPreference getFillPreference() { 505 return mFillPreference; 506 } 507 508 /** Fully qualified class name of views of this type */ 509 private String getFcqn() { 510 return mFqcn; 511 } 512 513 private String getDisplayName() { 514 return mDisplayName; 515 } 516 517 private String getResize() { 518 return mResize; 519 } 520 521 // Implements Comparable<ViewData> such that views can be sorted naturally 522 @Override 523 public int compareTo(ViewData other) { 524 return mOrdinal - other.mOrdinal; 525 } 526 527 public RenderMode getRenderMode() { 528 return mRenderMode; 529 } 530 531 public boolean getSkip() { 532 return mSkip; 533 } 534 535 public List<String> getRelatedTo() { 536 if (mRelatedTo == null || mRelatedTo.length() == 0) { 537 return Collections.emptyList(); 538 } else { 539 String[] basenames = mRelatedTo.split(","); //$NON-NLS-1$ 540 List<String> result = new ArrayList<String>(); 541 ViewMetadataRepository repository = ViewMetadataRepository.get(); 542 Map<String, ViewData> classToView = repository.getClassToView(); 543 544 List<String> fqns = new ArrayList<String>(classToView.keySet()); 545 for (String basename : basenames) { 546 boolean found = false; 547 for (String fqcn : fqns) { 548 String suffix = '.' + basename; 549 if (fqcn.endsWith(suffix)) { 550 result.add(fqcn); 551 found = true; 552 break; 553 } 554 } 555 if (basename.equals(VIEW_FRAGMENT) || basename.equals(VIEW_INCLUDE)) { 556 result.add(basename); 557 } else { 558 assert found : basename; 559 } 560 } 561 562 return result; 563 } 564 } 565 566 public List<String> getTopAttributes() { 567 // "id" is a top attribute for all views, so it is not included in the XML, we just 568 // add it in dynamically here 569 if (mTopAttrs == null || mTopAttrs.length() == 0) { 570 return Collections.singletonList(ATTR_ID); 571 } else { 572 String[] split = mTopAttrs.split(","); //$NON-NLS-1$ 573 List<String> topAttributes = new ArrayList<String>(split.length + 1); 574 topAttributes.add(ATTR_ID); 575 for (int i = 0, n = split.length; i < n; i++) { 576 topAttributes.add(split[i]); 577 } 578 return Collections.<String>unmodifiableList(topAttributes); 579 } 580 } 581 582 void addVariation(ViewData variation) { 583 if (mVariations == null) { 584 mVariations = new ArrayList<ViewData>(4); 585 } 586 mVariations.add(variation); 587 } 588 589 List<ViewData> getVariations() { 590 return mVariations; 591 } 592 593 boolean hasVariations() { 594 return mVariations != null && mVariations.size() > 0; 595 } 596 597 private void setInitString(String initString) { 598 this.mInitString = initString; 599 } 600 601 private String getInitString() { 602 return mInitString; 603 } 604 605 private void setIconName(String iconName) { 606 this.mIconName = iconName; 607 } 608 609 private String getIconName() { 610 return mIconName; 611 } 612 } 613 614 /** 615 * Returns the {@link FillPreference} for classes with the given fully qualified class 616 * name 617 * 618 * @param fqcn the fully qualified class name of the view 619 * @return a suitable {@link FillPreference} for the given view type 620 */ 621 public FillPreference getFillPreference(String fqcn) { 622 ViewData view = getClassToView().get(fqcn); 623 if (view != null) { 624 return view.getFillPreference(); 625 } 626 627 return FillPreference.NONE; 628 } 629 630 /** 631 * Returns the {@link RenderMode} for classes with the given fully qualified class 632 * name 633 * 634 * @param fqcn the fully qualified class name 635 * @return the {@link RenderMode} to use for previews of the given view type 636 */ 637 public RenderMode getRenderMode(String fqcn) { 638 ViewData view = getClassToView().get(fqcn); 639 if (view != null) { 640 return view.getRenderMode(); 641 } 642 643 return RenderMode.NORMAL; 644 } 645 646 /** 647 * Returns the {@link ResizePolicy} for the given class. 648 * 649 * @param fqcn the fully qualified class name of the target widget 650 * @return the {@link ResizePolicy} for the widget, which will never be null (but may 651 * be the default of {@link ResizePolicy#full()} if no metadata is found for 652 * the given widget) 653 */ 654 public ResizePolicy getResizePolicy(String fqcn) { 655 ViewData view = getClassToView().get(fqcn); 656 if (view != null) { 657 String resize = view.getResize(); 658 if (resize != null && resize.length() > 0) { 659 if ("full".equals(resize)) { //$NON-NLS-1$ 660 return ResizePolicy.full(); 661 } else if ("none".equals(resize)) { //$NON-NLS-1$ 662 return ResizePolicy.none(); 663 } else if ("horizontal".equals(resize)) { //$NON-NLS-1$ 664 return ResizePolicy.horizontal(); 665 } else if ("vertical".equals(resize)) { //$NON-NLS-1$ 666 return ResizePolicy.vertical(); 667 } else if ("scaled".equals(resize)) { //$NON-NLS-1$ 668 return ResizePolicy.scaled(); 669 } else { 670 assert false : resize; 671 } 672 } 673 } 674 675 return ResizePolicy.full(); 676 } 677 678 /** 679 * Returns true if classes with the given fully qualified class name should be hidden 680 * or skipped from the palette 681 * 682 * @param fqcn the fully qualified class name 683 * @return true if views of the given type should be hidden from the palette 684 */ 685 public boolean getSkip(String fqcn) { 686 ViewData view = getClassToView().get(fqcn); 687 if (view != null) { 688 return view.getSkip(); 689 } 690 691 return false; 692 } 693 694 /** 695 * Returns a list of the top (most commonly set) attributes of the given 696 * view. 697 * 698 * @param fqcn the fully qualified class name 699 * @return a list, never null but possibly empty, of popular attribute names 700 * (not including a namespace prefix) 701 */ 702 public List<String> getTopAttributes(String fqcn) { 703 ViewData view = getClassToView().get(fqcn); 704 if (view != null) { 705 return view.getTopAttributes(); 706 } 707 708 return Collections.singletonList(ATTR_ID); 709 } 710 711 /** 712 * Returns a set of fully qualified names for views that are closely related to the 713 * given view 714 * 715 * @param fqcn the fully qualified class name 716 * @return a list, never null but possibly empty, of views that are related to the 717 * view of the given type 718 */ 719 public List<String> getRelatedTo(String fqcn) { 720 ViewData view = getClassToView().get(fqcn); 721 if (view != null) { 722 return view.getRelatedTo(); 723 } 724 725 return Collections.emptyList(); 726 } 727 728 /** Render mode for palette preview */ 729 public enum RenderMode { 730 /** 731 * Render previews, and it can be rendered as a sibling of many other views in a 732 * big linear layout 733 */ 734 NORMAL, 735 /** This view needs to be rendered alone */ 736 ALONE, 737 /** 738 * Skip this element; it doesn't work or does not produce any visible artifacts 739 * (such as the basic layouts) 740 */ 741 SKIP; 742 743 /** 744 * Returns the {@link RenderMode} for the given render XML attribute 745 * value 746 * 747 * @param render the attribute value in the metadata XML file 748 * @return a corresponding {@link RenderMode}, never null 749 */ 750 public static RenderMode get(String render) { 751 if ("alone".equals(render)) { //$NON-NLS-1$ 752 return ALONE; 753 } else if ("skip".equals(render)) { //$NON-NLS-1$ 754 return SKIP; 755 } else { 756 return NORMAL; 757 } 758 } 759 } 760 761 /** 762 * Are insets supported yet? This flag indicates whether the {@link #getInsets} method 763 * can return valid data, such that clients can avoid doing any work computing the 764 * current theme or density if there's no chance that valid insets will be returned 765 */ 766 public static final boolean INSETS_SUPPORTED = false; 767 768 /** 769 * Returns the insets of widgets with the given fully qualified name, in the given 770 * theme and the given screen density. 771 * 772 * @param fqcn the fully qualified name of the view 773 * @param density the screen density 774 * @param theme the theme name 775 * @return the insets of the visual bounds relative to the view info bounds, or null 776 * if not known or if there are no insets 777 */ 778 public static Margins getInsets(String fqcn, Density density, String theme) { 779 if (INSETS_SUPPORTED) { 780 // Some sample data measured manually for common themes and widgets. 781 if (fqcn.equals(FQCN_BUTTON)) { 782 if (density == Density.HIGH) { 783 if (theme.startsWith(HOLO_PREFIX)) { 784 // Theme.Holo, Theme.Holo.Light, WVGA 785 return new Margins(5, 5, 5, 5); 786 } else { 787 // Theme.Light, WVGA 788 return new Margins(4, 4, 0, 7); 789 } 790 } else if (density == Density.MEDIUM) { 791 if (theme.startsWith(HOLO_PREFIX)) { 792 // Theme.Holo, Theme.Holo.Light, WVGA 793 return new Margins(3, 3, 3, 3); 794 } else { 795 // Theme.Light, HVGA 796 return new Margins(2, 2, 0, 4); 797 } 798 } else if (density == Density.LOW) { 799 if (theme.startsWith(HOLO_PREFIX)) { 800 // Theme.Holo, Theme.Holo.Light, QVGA 801 return new Margins(2, 2, 2, 2); 802 } else { 803 // Theme.Light, QVGA 804 return new Margins(1, 3, 0, 4); 805 } 806 } 807 } else if (fqcn.equals(FQCN_TOGGLE_BUTTON)) { 808 if (density == Density.HIGH) { 809 if (theme.startsWith(HOLO_PREFIX)) { 810 // Theme.Holo, Theme.Holo.Light, WVGA 811 return new Margins(5, 5, 5, 5); 812 } else { 813 // Theme.Light, WVGA 814 return new Margins(2, 2, 0, 5); 815 } 816 } else if (density == Density.MEDIUM) { 817 if (theme.startsWith(HOLO_PREFIX)) { 818 // Theme.Holo, Theme.Holo.Light, WVGA 819 return new Margins(3, 3, 3, 3); 820 } else { 821 // Theme.Light, HVGA 822 return new Margins(0, 1, 0, 3); 823 } 824 } else if (density == Density.LOW) { 825 if (theme.startsWith(HOLO_PREFIX)) { 826 // Theme.Holo, Theme.Holo.Light, QVGA 827 return new Margins(2, 2, 2, 2); 828 } else { 829 // Theme.Light, QVGA 830 return new Margins(2, 2, 0, 4); 831 } 832 } 833 } else if (fqcn.equals(FQCN_SPINNER)) { 834 if (density == Density.HIGH) { 835 if (!theme.startsWith(HOLO_PREFIX)) { 836 // Theme.Light, WVGA 837 return new Margins(3, 4, 2, 8); 838 } // Doesn't render on Holo! 839 } else if (density == Density.MEDIUM) { 840 if (!theme.startsWith(HOLO_PREFIX)) { 841 // Theme.Light, HVGA 842 return new Margins(1, 1, 0, 4); 843 } 844 } 845 } 846 } 847 848 return null; 849 } 850 851 private static final String HOLO_PREFIX = "Theme.Holo"; //$NON-NLS-1$ 852 } 853