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