/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.gre; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.FQCN_BUTTON; import static com.android.SdkConstants.FQCN_SPINNER; import static com.android.SdkConstants.FQCN_TOGGLE_BUTTON; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.VIEW_FRAGMENT; import static com.android.SdkConstants.VIEW_INCLUDE; import com.android.annotations.VisibleForTesting; import com.android.ide.common.api.IViewMetadata.FillPreference; import com.android.ide.common.api.Margins; import com.android.ide.common.api.ResizePolicy; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.resources.Density; import com.android.utils.Pair; import com.google.common.base.Splitter; import com.google.common.io.Closeables; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import java.io.BufferedInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; /** * The {@link ViewMetadataRepository} contains additional metadata for Android view * classes */ public class ViewMetadataRepository { private static final String PREVIEW_CONFIG_FILENAME = "rendering-configs.xml"; //$NON-NLS-1$ private static final String METADATA_FILENAME = "extra-view-metadata.xml"; //$NON-NLS-1$ /** Singleton instance */ private static ViewMetadataRepository sInstance = new ViewMetadataRepository(); /** * Returns the singleton instance * * @return the {@link ViewMetadataRepository} */ public static ViewMetadataRepository get() { return sInstance; } /** * Ever increasing counter used to assign natural ordering numbers to views and * categories */ private static int sNextOrdinal = 0; /** * List of categories (which contain views); constructed lazily so use * {@link #getCategories()} */ private List<CategoryData> mCategories; /** * Map from class names to view data objects; constructed lazily so use * {@link #getClassToView} */ private Map<String, ViewData> mClassToView; /** Hidden constructor: Create via factory {@link #get()} instead */ private ViewMetadataRepository() { } /** Returns a map from class fully qualified names to {@link ViewData} objects */ private Map<String, ViewData> getClassToView() { if (mClassToView == null) { int initialSize = 75; mClassToView = new HashMap<String, ViewData>(initialSize); List<CategoryData> categories = getCategories(); for (CategoryData category : categories) { for (ViewData view : category) { mClassToView.put(view.getFcqn(), view); } } assert mClassToView.size() <= initialSize; } return mClassToView; } /** * Returns an XML document containing rendering configurations for the various Android * views. The FQN of each view can be obtained via the * {@link #getFullClassName(Element)} method * * @return an XML document containing rendering elements */ public Document getRenderingConfigDoc() { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Class<ViewMetadataRepository> clz = ViewMetadataRepository.class; InputStream paletteStream = clz.getResourceAsStream(PREVIEW_CONFIG_FILENAME); InputSource is = new InputSource(paletteStream); try { factory.setNamespaceAware(true); factory.setValidating(false); factory.setIgnoringComments(true); DocumentBuilder builder = factory.newDocumentBuilder(); return builder.parse(is); } catch (Exception e) { AdtPlugin.log(e, "Parsing palette file failed"); return null; } finally { Closeables.closeQuietly(paletteStream); } } /** * Returns a fully qualified class name for an element in the rendering document * returned by {@link #getRenderingConfigDoc()} * * @param element the element to look up the fqcn for * @return the fqcn of the view the element represents a preview for */ public String getFullClassName(Element element) { // We don't use the element tag name, because in some cases we have // an outer element to render some interesting inner element, such as a tab widget // (which must be rendered inside a tab host). // // Therefore, we instead use the convention that the id is the fully qualified // class name, with .'s replaced with _'s. // Special case: for tab host we aren't allowed to mess with the id String id = element.getAttributeNS(ANDROID_URI, ATTR_ID); if ("@android:id/tabhost".equals(id)) { // Special case to distinguish TabHost and TabWidget NodeList children = element.getChildNodes(); if (children.getLength() > 1 && (children.item(1) instanceof Element)) { Element child = (Element) children.item(1); String childId = child.getAttributeNS(ANDROID_URI, ATTR_ID); if ("@+id/android_widget_TabWidget".equals(childId)) { return "android.widget.TabWidget"; // TODO: Tab widget! } } return "android.widget.TabHost"; // TODO: Tab widget! } StringBuilder sb = new StringBuilder(); int i = 0; if (id.startsWith(NEW_ID_PREFIX)) { i = NEW_ID_PREFIX.length(); } else if (id.startsWith(ID_PREFIX)) { i = ID_PREFIX.length(); } for (; i < id.length(); i++) { char c = id.charAt(i); if (c == '_') { sb.append('.'); } else { sb.append(c); } } return sb.toString(); } /** Returns an ordered list of categories and views, parsed from a metadata file */ @SuppressWarnings("resource") // streams passed to parser InputSource closed by parser private List<CategoryData> getCategories() { if (mCategories == null) { mCategories = new ArrayList<CategoryData>(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Class<ViewMetadataRepository> clz = ViewMetadataRepository.class; InputStream inputStream = clz.getResourceAsStream(METADATA_FILENAME); InputSource is = new InputSource(new BufferedInputStream(inputStream)); try { factory.setNamespaceAware(true); factory.setValidating(false); factory.setIgnoringComments(true); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(is); Map<String, FillPreference> fillTypes = new HashMap<String, FillPreference>(); for (FillPreference pref : FillPreference.values()) { fillTypes.put(pref.toString().toLowerCase(Locale.US), pref); } NodeList categoryNodes = document.getDocumentElement().getChildNodes(); for (int i = 0, n = categoryNodes.getLength(); i < n; i++) { Node node = categoryNodes.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) node; if (element.getNodeName().equals("category")) { //$NON-NLS-1$ String name = element.getAttribute("name"); //$NON-NLS-1$ CategoryData category = new CategoryData(name); NodeList children = element.getChildNodes(); for (int j = 0, m = children.getLength(); j < m; j++) { Node childNode = children.item(j); if (childNode.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element) childNode; ViewData view = createViewData(fillTypes, child, null, FillPreference.NONE, RenderMode.NORMAL, null); category.addView(view); } } mCategories.add(category); } } } } catch (Exception e) { AdtPlugin.log(e, "Invalid palette metadata"); //$NON-NLS-1$ } } return mCategories; } private ViewData createViewData(Map<String, FillPreference> fillTypes, Element child, String defaultFqcn, FillPreference defaultFill, RenderMode defaultRender, String defaultSize) { String fqcn = child.getAttribute("class"); //$NON-NLS-1$ if (fqcn.length() == 0) { fqcn = defaultFqcn; } String fill = child.getAttribute("fill"); //$NON-NLS-1$ FillPreference fillPreference = null; if (fill.length() > 0) { fillPreference = fillTypes.get(fill); } if (fillPreference == null) { fillPreference = defaultFill; } String skip = child.getAttribute("skip"); //$NON-NLS-1$ RenderMode renderMode = defaultRender; String render = child.getAttribute("render"); //$NON-NLS-1$ if (render.length() > 0) { renderMode = RenderMode.get(render); } String displayName = child.getAttribute("name"); //$NON-NLS-1$ if (displayName.length() == 0) { displayName = null; } String relatedTo = child.getAttribute("relatedTo"); //$NON-NLS-1$ String topAttrs = child.getAttribute("topAttrs"); //$NON-NLS-1$ String resize = child.getAttribute("resize"); //$NON-NLS-1$ ViewData view = new ViewData(fqcn, displayName, fillPreference, skip.length() == 0 ? false : Boolean.valueOf(skip), renderMode, relatedTo, resize, topAttrs); String init = child.getAttribute("init"); //$NON-NLS-1$ String icon = child.getAttribute("icon"); //$NON-NLS-1$ view.setInitString(init); if (icon.length() > 0) { view.setIconName(icon); } // Nested variations? if (child.hasChildNodes()) { // Palette variations NodeList childNodes = child.getChildNodes(); for (int k = 0, kl = childNodes.getLength(); k < kl; k++) { Node variationNode = childNodes.item(k); if (variationNode.getNodeType() == Node.ELEMENT_NODE) { Element variation = (Element) variationNode; ViewData variationView = createViewData(fillTypes, variation, fqcn, fillPreference, renderMode, resize); view.addVariation(variationView); } } } return view; } /** * Computes the palette entries for the given {@link AndroidTargetData}, looking up the * available node descriptors, categorizing and sorting them. * * @param targetData the target data for which to compute palette entries * @param alphabetical if true, sort all items in alphabetical order * @param createCategories if true, organize the items into categories * @return a list of pairs where each pair contains of the category label and an * ordered list of elements to be included in that category */ public List<Pair<String, List<ViewElementDescriptor>>> getPaletteEntries( AndroidTargetData targetData, boolean alphabetical, boolean createCategories) { List<Pair<String, List<ViewElementDescriptor>>> result = new ArrayList<Pair<String, List<ViewElementDescriptor>>>(); List<List<ViewElementDescriptor>> lists = new ArrayList<List<ViewElementDescriptor>>(2); LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors(); lists.add(layoutDescriptors.getViewDescriptors()); lists.add(layoutDescriptors.getLayoutDescriptors()); // First record map of FQCN to ViewElementDescriptor such that we can quickly // determine if a particular palette entry is available Map<String, ViewElementDescriptor> fqcnToDescriptor = new HashMap<String, ViewElementDescriptor>(); for (List<ViewElementDescriptor> list : lists) { for (ViewElementDescriptor view : list) { String fqcn = view.getFullClassName(); if (fqcn == null) { // <view> and <merge> tags etc fqcn = view.getUiName(); } fqcnToDescriptor.put(fqcn, view); } } Set<ViewElementDescriptor> remaining = new HashSet<ViewElementDescriptor>( layoutDescriptors.getViewDescriptors().size() + layoutDescriptors.getLayoutDescriptors().size()); remaining.addAll(layoutDescriptors.getViewDescriptors()); remaining.addAll(layoutDescriptors.getLayoutDescriptors()); // Now iterate in palette metadata order over the items in the palette and include // any that also appear as a descriptor List<ViewElementDescriptor> categoryItems = new ArrayList<ViewElementDescriptor>(); for (CategoryData category : getCategories()) { if (createCategories) { categoryItems = new ArrayList<ViewElementDescriptor>(); } for (ViewData view : category) { String fqcn = view.getFcqn(); ViewElementDescriptor descriptor = fqcnToDescriptor.get(fqcn); if (descriptor != null) { remaining.remove(descriptor); if (view.getSkip()) { continue; } if (view.getDisplayName() != null || view.getInitString().length() > 0) { categoryItems.add(new PaletteMetadataDescriptor(descriptor, view.getDisplayName(), view.getInitString(), view.getIconName())); } else { categoryItems.add(descriptor); } if (view.hasVariations()) { for (ViewData variation : view.getVariations()) { String init = variation.getInitString(); String icon = variation.getIconName(); ViewElementDescriptor desc = new PaletteMetadataDescriptor(descriptor, variation.getDisplayName(), init, icon); categoryItems.add(desc); } } } } if (createCategories && categoryItems.size() > 0) { if (alphabetical) { Collections.sort(categoryItems); } result.add(Pair.of(category.getName(), categoryItems)); } } if (remaining.size() > 0) { List<ViewElementDescriptor> otherItems = new ArrayList<ViewElementDescriptor>(remaining); // Always sorted, we don't have a natural order for these unknowns Collections.sort(otherItems); if (createCategories) { result.add(Pair.of("Other", otherItems)); } else { categoryItems.addAll(otherItems); } } if (!createCategories) { if (alphabetical) { Collections.sort(categoryItems); } result.add(Pair.of("Views", categoryItems)); } return result; } @VisibleForTesting Collection<String> getAllFqcns() { return getClassToView().keySet(); } /** * Metadata holder for a particular category - contains the name of the category, its * ordinal (for natural/logical sorting order) and views contained in the category */ private static class CategoryData implements Iterable<ViewData>, Comparable<CategoryData> { /** Category name */ private final String mName; /** Views included in this category */ private final List<ViewData> mViews = new ArrayList<ViewData>(); /** Natural ordering rank */ private final int mOrdinal = sNextOrdinal++; /** Constructs a new category with the given name */ private CategoryData(String name) { super(); mName = name; } /** Adds a new view into this category */ private void addView(ViewData view) { mViews.add(view); } private String getName() { return mName; } // Implements Iterable<ViewData> such that we can use for-each on the category to // enumerate its views @Override public Iterator<ViewData> iterator() { return mViews.iterator(); } // Implements Comparable<CategoryData> such that categories can be naturally sorted @Override public int compareTo(CategoryData other) { return mOrdinal - other.mOrdinal; } } /** Metadata holder for a view of a given fully qualified class name */ private static class ViewData implements Comparable<ViewData> { /** The fully qualified class name of the view */ private final String mFqcn; /** Fill preference of the view */ private final FillPreference mFillPreference; /** Skip this item in the palette? */ private final boolean mSkip; /** Must this item be rendered alone? skipped? etc */ private final RenderMode mRenderMode; /** Related views */ private final String mRelatedTo; /** The relative rank of the view for natural ordering */ private final int mOrdinal = sNextOrdinal++; /** List of optional variations */ private List<ViewData> mVariations; /** Display name. Can be null. */ private String mDisplayName; /** * Optional initialization string - a comma separate set of name/value pairs to * initialize the element with */ private String mInitString; /** The name of an icon (known to the {@link IconFactory} to show for this view */ private String mIconName; /** The resize preference of this view */ private String mResize; /** The most commonly set attributes of this view */ private String mTopAttrs; /** Constructs a new view data for the given class */ private ViewData(String fqcn, String displayName, FillPreference fillPreference, boolean skip, RenderMode renderMode, String relatedTo, String resize, String topAttrs) { super(); mFqcn = fqcn; mDisplayName = displayName; mFillPreference = fillPreference; mSkip = skip; mRenderMode = renderMode; mRelatedTo = relatedTo; mResize = resize; mTopAttrs = topAttrs; } /** Returns the {@link FillPreference} for views of this type */ private FillPreference getFillPreference() { return mFillPreference; } /** Fully qualified class name of views of this type */ private String getFcqn() { return mFqcn; } private String getDisplayName() { return mDisplayName; } private String getResize() { return mResize; } // Implements Comparable<ViewData> such that views can be sorted naturally @Override public int compareTo(ViewData other) { return mOrdinal - other.mOrdinal; } public RenderMode getRenderMode() { return mRenderMode; } public boolean getSkip() { return mSkip; } public List<String> getRelatedTo() { if (mRelatedTo == null || mRelatedTo.length() == 0) { return Collections.emptyList(); } else { List<String> result = new ArrayList<String>(); ViewMetadataRepository repository = ViewMetadataRepository.get(); Map<String, ViewData> classToView = repository.getClassToView(); List<String> fqns = new ArrayList<String>(classToView.keySet()); for (String basename : Splitter.on(',').split(mRelatedTo)) { boolean found = false; for (String fqcn : fqns) { String suffix = '.' + basename; if (fqcn.endsWith(suffix)) { result.add(fqcn); found = true; break; } } if (basename.equals(VIEW_FRAGMENT) || basename.equals(VIEW_INCLUDE)) { result.add(basename); } else { assert found : basename; } } return result; } } public List<String> getTopAttributes() { // "id" is a top attribute for all views, so it is not included in the XML, we just // add it in dynamically here if (mTopAttrs == null || mTopAttrs.length() == 0) { return Collections.singletonList(ATTR_ID); } else { String[] split = mTopAttrs.split(","); //$NON-NLS-1$ List<String> topAttributes = new ArrayList<String>(split.length + 1); topAttributes.add(ATTR_ID); for (int i = 0, n = split.length; i < n; i++) { topAttributes.add(split[i]); } return Collections.<String>unmodifiableList(topAttributes); } } void addVariation(ViewData variation) { if (mVariations == null) { mVariations = new ArrayList<ViewData>(4); } mVariations.add(variation); } List<ViewData> getVariations() { return mVariations; } boolean hasVariations() { return mVariations != null && mVariations.size() > 0; } private void setInitString(String initString) { this.mInitString = initString; } private String getInitString() { return mInitString; } private void setIconName(String iconName) { this.mIconName = iconName; } private String getIconName() { return mIconName; } } /** * Returns the {@link FillPreference} for classes with the given fully qualified class * name * * @param fqcn the fully qualified class name of the view * @return a suitable {@link FillPreference} for the given view type */ public FillPreference getFillPreference(String fqcn) { ViewData view = getClassToView().get(fqcn); if (view != null) { return view.getFillPreference(); } return FillPreference.NONE; } /** * Returns the {@link RenderMode} for classes with the given fully qualified class * name * * @param fqcn the fully qualified class name * @return the {@link RenderMode} to use for previews of the given view type */ public RenderMode getRenderMode(String fqcn) { ViewData view = getClassToView().get(fqcn); if (view != null) { return view.getRenderMode(); } return RenderMode.NORMAL; } /** * Returns the {@link ResizePolicy} for the given class. * * @param fqcn the fully qualified class name of the target widget * @return the {@link ResizePolicy} for the widget, which will never be null (but may * be the default of {@link ResizePolicy#full()} if no metadata is found for * the given widget) */ public ResizePolicy getResizePolicy(String fqcn) { ViewData view = getClassToView().get(fqcn); if (view != null) { String resize = view.getResize(); if (resize != null && resize.length() > 0) { if ("full".equals(resize)) { //$NON-NLS-1$ return ResizePolicy.full(); } else if ("none".equals(resize)) { //$NON-NLS-1$ return ResizePolicy.none(); } else if ("horizontal".equals(resize)) { //$NON-NLS-1$ return ResizePolicy.horizontal(); } else if ("vertical".equals(resize)) { //$NON-NLS-1$ return ResizePolicy.vertical(); } else if ("scaled".equals(resize)) { //$NON-NLS-1$ return ResizePolicy.scaled(); } else { assert false : resize; } } } return ResizePolicy.full(); } /** * Returns true if classes with the given fully qualified class name should be hidden * or skipped from the palette * * @param fqcn the fully qualified class name * @return true if views of the given type should be hidden from the palette */ public boolean getSkip(String fqcn) { ViewData view = getClassToView().get(fqcn); if (view != null) { return view.getSkip(); } return false; } /** * Returns a list of the top (most commonly set) attributes of the given * view. * * @param fqcn the fully qualified class name * @return a list, never null but possibly empty, of popular attribute names * (not including a namespace prefix) */ public List<String> getTopAttributes(String fqcn) { ViewData view = getClassToView().get(fqcn); if (view != null) { return view.getTopAttributes(); } return Collections.singletonList(ATTR_ID); } /** * Returns a set of fully qualified names for views that are closely related to the * given view * * @param fqcn the fully qualified class name * @return a list, never null but possibly empty, of views that are related to the * view of the given type */ public List<String> getRelatedTo(String fqcn) { ViewData view = getClassToView().get(fqcn); if (view != null) { return view.getRelatedTo(); } return Collections.emptyList(); } /** Render mode for palette preview */ public enum RenderMode { /** * Render previews, and it can be rendered as a sibling of many other views in a * big linear layout */ NORMAL, /** This view needs to be rendered alone */ ALONE, /** * Skip this element; it doesn't work or does not produce any visible artifacts * (such as the basic layouts) */ SKIP; /** * Returns the {@link RenderMode} for the given render XML attribute * value * * @param render the attribute value in the metadata XML file * @return a corresponding {@link RenderMode}, never null */ public static RenderMode get(String render) { if ("alone".equals(render)) { //$NON-NLS-1$ return ALONE; } else if ("skip".equals(render)) { //$NON-NLS-1$ return SKIP; } else { return NORMAL; } } } /** * Are insets supported yet? This flag indicates whether the {@link #getInsets} method * can return valid data, such that clients can avoid doing any work computing the * current theme or density if there's no chance that valid insets will be returned */ public static final boolean INSETS_SUPPORTED = false; /** * Returns the insets of widgets with the given fully qualified name, in the given * theme and the given screen density. * * @param fqcn the fully qualified name of the view * @param density the screen density * @param theme the theme name * @return the insets of the visual bounds relative to the view info bounds, or null * if not known or if there are no insets */ public static Margins getInsets(String fqcn, Density density, String theme) { if (INSETS_SUPPORTED) { // Some sample data measured manually for common themes and widgets. if (fqcn.equals(FQCN_BUTTON)) { if (density == Density.HIGH) { if (theme.startsWith(HOLO_PREFIX)) { // Theme.Holo, Theme.Holo.Light, WVGA return new Margins(5, 5, 5, 5); } else { // Theme.Light, WVGA return new Margins(4, 4, 0, 7); } } else if (density == Density.MEDIUM) { if (theme.startsWith(HOLO_PREFIX)) { // Theme.Holo, Theme.Holo.Light, WVGA return new Margins(3, 3, 3, 3); } else { // Theme.Light, HVGA return new Margins(2, 2, 0, 4); } } else if (density == Density.LOW) { if (theme.startsWith(HOLO_PREFIX)) { // Theme.Holo, Theme.Holo.Light, QVGA return new Margins(2, 2, 2, 2); } else { // Theme.Light, QVGA return new Margins(1, 3, 0, 4); } } } else if (fqcn.equals(FQCN_TOGGLE_BUTTON)) { if (density == Density.HIGH) { if (theme.startsWith(HOLO_PREFIX)) { // Theme.Holo, Theme.Holo.Light, WVGA return new Margins(5, 5, 5, 5); } else { // Theme.Light, WVGA return new Margins(2, 2, 0, 5); } } else if (density == Density.MEDIUM) { if (theme.startsWith(HOLO_PREFIX)) { // Theme.Holo, Theme.Holo.Light, WVGA return new Margins(3, 3, 3, 3); } else { // Theme.Light, HVGA return new Margins(0, 1, 0, 3); } } else if (density == Density.LOW) { if (theme.startsWith(HOLO_PREFIX)) { // Theme.Holo, Theme.Holo.Light, QVGA return new Margins(2, 2, 2, 2); } else { // Theme.Light, QVGA return new Margins(2, 2, 0, 4); } } } else if (fqcn.equals(FQCN_SPINNER)) { if (density == Density.HIGH) { if (!theme.startsWith(HOLO_PREFIX)) { // Theme.Light, WVGA return new Margins(3, 4, 2, 8); } // Doesn't render on Holo! } else if (density == Density.MEDIUM) { if (!theme.startsWith(HOLO_PREFIX)) { // Theme.Light, HVGA return new Margins(1, 1, 0, 4); } } } } return null; } private static final String HOLO_PREFIX = "Theme.Holo"; //$NON-NLS-1$ }