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