/*
* 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.ide.common.api.IViewMetadata.FillPreference.NONE;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
import com.android.ide.common.api.IViewMetadata.FillPreference;
import com.android.ide.eclipse.adt.AdtPlugin;
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.util.Pair;
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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
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.setIgnoringElementContentWhitespace(true);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(is);
} catch (Exception e) {
AdtPlugin.log(e, "Parsing palette file failed");
return null;
}
}
/**
* 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 */
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.setIgnoringElementContentWhitespace(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(), 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;
String fqcn = child.getAttribute("class"); //$NON-NLS-1$
String fill = child.getAttribute("fill"); //$NON-NLS-1$
FillPreference fillPreference = null;
if (fill.length() > 0) {
fillPreference = fillTypes.get(fill);
}
if (fillPreference == null) {
fillPreference = NONE;
}
String skip = child.getAttribute("skip"); //$NON-NLS-1$
RenderMode mode = RenderMode.NORMAL;
String render = child.getAttribute("render"); //$NON-NLS-1$
if (render.length() > 0) {
mode = RenderMode.get(render);
}
ViewData view = new ViewData(category, fqcn, fillPreference,
skip.length() == 0 ? false : Boolean.valueOf(skip),
mode);
category.addView(view);
}
}
mCategories.add(category);
}
}
}
} catch (Exception e) {
AdtPlugin.log(e, "Invalid palette metadata"); //$NON-NLS-1$
}
}
return mCategories;
}
/**
* 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>>>();
final Map<String, ViewData> viewMap = getClassToView();
Map<CategoryData, List<ViewElementDescriptor>> categories =
new TreeMap<CategoryData, List<ViewElementDescriptor>>();
// Locate the "Other" category
CategoryData other = null;
for (CategoryData category : getCategories()) {
if (category.getViewCount() == 0) {
other = category;
break;
}
}
List<List<ViewElementDescriptor>> lists = new ArrayList<List<ViewElementDescriptor>>(2);
LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors();
lists.add(layoutDescriptors.getViewDescriptors());
lists.add(layoutDescriptors.getLayoutDescriptors());
for (List<ViewElementDescriptor> list : lists) {
for (ViewElementDescriptor view : list) {
ViewData viewData = getClassToView().get(view.getFullClassName());
CategoryData category = other;
if (viewData != null) {
if (viewData.getSkip()) {
continue;
}
category = viewData.getCategory();
}
List<ViewElementDescriptor> viewList = categories.get(category);
if (viewList == null) {
viewList = new ArrayList<ViewElementDescriptor>();
categories.put(category, viewList);
}
viewList.add(view);
}
}
if (!createCategories) {
// Squash all categories into a single one, "Views"
Map<CategoryData, List<ViewElementDescriptor>> singleCategory =
new HashMap<CategoryData, List<ViewElementDescriptor>>();
List<ViewElementDescriptor> items = new ArrayList<ViewElementDescriptor>(100);
for (Map.Entry<CategoryData, List<ViewElementDescriptor>> entry : categories.entrySet()) {
items.addAll(entry.getValue());
}
singleCategory.put(new CategoryData("Views"), items);
categories = singleCategory;
}
for (Map.Entry<CategoryData, List<ViewElementDescriptor>> entry : categories.entrySet()) {
String name = entry.getKey().getName();
List<ViewElementDescriptor> items = entry.getValue();
if (items == null) {
continue; // empty category
}
// Natural sort of the descriptors
if (alphabetical) {
Collections.sort(items);
} else {
Collections.sort(items, new Comparator<ViewElementDescriptor>() {
public int compare(ViewElementDescriptor v1, ViewElementDescriptor v2) {
String fqcn1 = v1.getFullClassName();
String fqcn2 = v2.getFullClassName();
if (fqcn1 == null) {
// <view> and <merge> tags etc
fqcn1 = v1.getUiName();
}
if (fqcn2 == null) {
fqcn2 = v2.getUiName();
}
ViewData d1 = viewMap.get(fqcn1);
ViewData d2 = viewMap.get(fqcn2);
// Use natural sorting order of the view data
// Sort unknown views to the end (and alphabetically among themselves)
if (d1 != null) {
if (d2 != null) {
return d1.getOrdinal() - d2.getOrdinal();
} else {
return 1;
}
} else {
if (d2 == null) {
return v1.getUiName().compareTo(v2.getUiName());
} else {
return -1;
}
}
}
});
}
result.add(Pair.of(name, items));
}
return result;
}
/**
* 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;
}
public int getViewCount() {
return mViews.size();
}
// Implements Iterable<ViewData> such that we can use for-each on the category to
// enumerate its views
public Iterator<ViewData> iterator() {
return mViews.iterator();
}
// Implements Comparable<CategoryData> such that categories can be naturally sorted
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;
/** The category that the view belongs to */
private final CategoryData mCategory;
/** Skip this item in the palette? */
private final boolean mSkip;
/** Must this item be rendered alone? skipped? etc */
private final RenderMode mRenderMode;
/** The relative rank of the view for natural ordering */
private final int mOrdinal = sNextOrdinal++;
/** Constructs a new view data for the given class */
private ViewData(CategoryData category, String fqcn,
FillPreference fillPreference, boolean skip, RenderMode renderMode) {
super();
mCategory = category;
mFqcn = fqcn;
mFillPreference = fillPreference;
mSkip = skip;
mRenderMode = renderMode;
}
/** Returns the category for views of this type */
private CategoryData getCategory() {
return mCategory;
}
/** 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;
}
/** Relative rank of this view type */
private int getOrdinal() {
return mOrdinal;
}
// Implements Comparable<ViewData> such that views can be sorted naturally
public int compareTo(ViewData other) {
return mOrdinal - other.mOrdinal;
}
public RenderMode getRenderMode() {
return mRenderMode;
}
public boolean getSkip() {
return mSkip;
}
}
/**
* 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.ALONE;
}
/**
* 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;
}
/** 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;
public static RenderMode get(String render) {
if ("alone".equals(render)) {
return ALONE;
} else if ("skip".equals(render)) {
return SKIP;
} else {
return NORMAL;
}
}
}
}