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